import type { SupabaseClient } from "@supabase/supabase-js";

import type { CompanyPermission } from "@/lib/company-context";

import {
  ACOES_POR_REGRA_POR_VOLTA,
  conferirAcao,
  conferirGatilho,
  conferirRegra,
  ESTOQUE_LEMBRA_A_CADA_DIAS,
  fraseDaRegra,
  opcoesDeEtapa,
  REGRAS_POR_EMPRESA,
  ROTULO_DA_ACAO,
  ROTULO_DO_GATILHO,
  TIPOS_DE_ACAO,
  TIPOS_DE_GATILHO,
  type Acao,
  type Gatilho,
  type NomesDaFrase,
  type RegraConferida,
} from "../regras";
import { alvosDaRegra, chavesJaFeitas, ErroDaConsulta, etapasDaEmpresa, nomesParaAFrase, pessoasDaEmpresa, type Empresa } from "./avaliar";

/**
 * O que a tela de Regras e o assistente fazem com as regras: listar, criar,
 * alterar, ligar e desligar, excluir, testar e ver o histórico.
 *
 * Grava e lê as regras pela sessão da pessoa (`supabase`): as travas da tabela
 * (company.settings) valem. A chave de serviço (`admin`) só lê a lista da
 * equipe, para a pessoa escolher quem recebe o aviso.
 */

export type AcessoAsRegras = {
  supabase: SupabaseClient;
  admin: SupabaseClient;
  empresa: Empresa;
  usuarioId: string;
  /* As permissões de quem chama (canCompany do contexto dela): uma regra de
     conta a pagar ou a receber mostra valores do financeiro nos avisos. */
  pode: (permissao: CompanyPermission) => boolean;
};

/** Erro que pode ir para a tela e para o assistente: diz o que corrigir. */
export class ErroDaRegra extends Error {}

/* Quem cria, altera ou testa uma regra de financeiro precisa poder ver aquilo
   que ela vai avisar: a regra roda com a chave do servidor e mandaria a
   descrição e o valor das contas a quem não tem acesso a elas. */
const PERMISSOES_DO_GATILHO: Partial<Record<Gatilho["tipo"], { permissoes: CompanyPermission[]; oQue: string }>> = {
  conta_a_pagar_vencendo: { permissoes: ["financial.payables.read", "financial.amounts.see"], oQue: "ver as contas a pagar e os valores do financeiro" },
  conta_a_receber_vencida: { permissoes: ["financial.receivables.read", "financial.amounts.see"], oQue: "ver as contas a receber e os valores do financeiro" },
};

function conferirPermissaoDoGatilho(acesso: AcessoAsRegras, gatilho: Gatilho) {
  const exigido = PERMISSOES_DO_GATILHO[gatilho.tipo];
  if (exigido && !exigido.permissoes.every((permissao) => acesso.pode(permissao))) {
    throw new ErroDaRegra(`Esta regra é sobre o financeiro, e esta pessoa não tem permissão para ${exigido.oQue}. Quem libera é o dono da empresa, em Funcionários.`);
  }
}

type LinhaDaRegra = {
  id: string;
  nome: string;
  ativa: boolean;
  gatilho: unknown;
  acoes: unknown;
  criada_em: string;
  atualizada_em: string;
  ultima_verificacao: string | null;
  ultimo_erro: string | null;
  vezes: number | null;
};

type LinhaDaExecucao = {
  id: string;
  regra_id: string;
  alvo_tipo: string;
  alvo_titulo: string | null;
  ok: boolean | null;
  detalhe: string | null;
  criada_em: string;
};

const CAMPOS = "id,nome,ativa,gatilho,acoes,criada_em,atualizada_em,ultima_verificacao,ultimo_erro,vezes";

function falhou(oQue: string, erro: { message?: string } | null): never {
  console.error(`[regras] não foi possível ${oQue}:`, erro?.message ?? erro);
  throw new ErroDaRegra(`Não foi possível ${oQue} agora. Tente de novo em instantes.`);
}

function lerRegra(linha: LinhaDaRegra): { gatilho: Gatilho | null; acoes: Acao[] } {
  const gatilho = conferirGatilho(linha.gatilho);
  const acoes = (Array.isArray(linha.acoes) ? linha.acoes : []).map(conferirAcao).flatMap((a) => (a.ok ? [a.valor] : []));
  return { gatilho: gatilho.ok ? gatilho.valor : null, acoes };
}

function pessoasDasAcoes(acoes: Acao[]) {
  return acoes.flatMap((a) =>
    a.tipo === "criar_compromisso" && a.parametros.responsavel_id
      ? [a.parametros.responsavel_id]
      : a.tipo === "avisar_pessoa" && a.parametros.usuario_id
        ? [a.parametros.usuario_id]
        : []
  );
}

/* ------------------------------------------------------------------- opções */

export async function opcoesDasRegras(acesso: AcessoAsRegras) {
  const { supabase, admin, empresa } = acesso;
  const [{ data: perfil }, pessoas, insumos, produtos] = await Promise.all([
    supabase.from("profiles").select("production_kanban_columns").eq("id", empresa.dono).maybeSingle(),
    pessoasDaEmpresa(admin, empresa),
    supabase.from("materials").select("id,name,unit,current_stock,min_stock_alert").in("user_id", empresa.tenantIds).eq("track_stock", true).order("name").limit(500),
    supabase.from("products").select("id,name,stock").in("user_id", empresa.tenantIds).eq("track_inventory", true).order("name").limit(500),
  ]);
  return {
    gatilhos: TIPOS_DE_GATILHO.map((tipo) => ({ tipo, rotulo: ROTULO_DO_GATILHO[tipo] })),
    acoes: TIPOS_DE_ACAO.map((tipo) => ({ tipo, rotulo: ROTULO_DA_ACAO[tipo] })),
    etapas: opcoesDeEtapa((perfil as { production_kanban_columns?: unknown } | null)?.production_kanban_columns),
    pessoas: pessoas.map((p) => ({ id: p.id, nome: p.nome })),
    insumos: ((insumos.data ?? []) as Array<{ id: string; name: string | null; unit: string | null; current_stock: number | null; min_stock_alert: number | null }>).map((m) => ({
      id: m.id,
      nome: m.name ?? "Insumo",
      unidade: m.unit,
      estoque: m.current_stock,
      minimo: m.min_stock_alert,
    })),
    produtos: ((produtos.data ?? []) as Array<{ id: string; name: string | null; stock: number | null }>).map((p) => ({ id: p.id, nome: p.name ?? "Produto", estoque: p.stock })),
    lembra_estoque_a_cada_dias: ESTOQUE_LEMBRA_A_CADA_DIAS,
  };
}

/* ----------------------------------------------------- referências da regra */

/** Confere o que a regra aponta: a etapa existe, o item é da empresa, a pessoa é da equipe. */
async function conferirReferencias(acesso: AcessoAsRegras, regra: RegraConferida) {
  const { supabase, admin, empresa } = acesso;
  const g = regra.gatilho;
  const etapa = g.tipo === "pedido_entrou_na_etapa" ? g.parametros.etapa : g.tipo === "pedido_parado_na_etapa" ? g.parametros.etapa : null;
  if (etapa && etapa.startsWith("stage-")) {
    const etapas = await etapasDaEmpresa(supabase, empresa);
    if (!etapas.some((e) => e.id === etapa)) throw new ErroDaRegra("Essa etapa não existe no quadro de produção desta empresa.");
  }
  if (g.tipo === "estoque_abaixo" && g.parametros.item_id) {
    const tabela = g.parametros.item_tipo === "insumo" ? "materials" : "products";
    const { data } = await supabase.from(tabela).select("id").eq("id", g.parametros.item_id).in("user_id", empresa.tenantIds).maybeSingle();
    if (!data) throw new ErroDaRegra(g.parametros.item_tipo === "insumo" ? "Insumo não encontrado nesta empresa." : "Produto não encontrado nesta empresa.");
  }
  const ids = pessoasDasAcoes(regra.acoes);
  if (ids.length) {
    const equipe = new Set((await pessoasDaEmpresa(admin, empresa)).map((p) => p.id));
    if (ids.some((id) => !equipe.has(id))) throw new ErroDaRegra("A pessoa escolhida não faz parte da equipe desta empresa.");
  }
}

async function nomes(acesso: AcessoAsRegras, regras: Array<{ gatilho: Gatilho | null; acoes: Acao[] }>): Promise<NomesDaFrase> {
  return nomesParaAFrase(
    acesso.supabase,
    acesso.admin,
    acesso.empresa,
    regras.flatMap((r) => (r.gatilho ? [r.gatilho] : [])),
    regras.flatMap((r) => pessoasDasAcoes(r.acoes))
  );
}

function regraNaTela(linha: LinhaDaRegra, nomesDaFrase: NomesDaFrase, execucoes: LinhaDaExecucao[] = []) {
  const { gatilho, acoes } = lerRegra(linha);
  return {
    id: linha.id,
    nome: linha.nome,
    ativa: linha.ativa,
    frase: gatilho && acoes.length ? fraseDaRegra(gatilho, acoes, nomesDaFrase) : "Regra com formato antigo: abra e salve de novo.",
    gatilho,
    acoes,
    vezes: Number(linha.vezes ?? 0),
    criada_em: linha.criada_em,
    ultima_verificacao: linha.ultima_verificacao,
    ultimo_erro: linha.ultimo_erro,
    ultimas_execucoes: execucoes.map(execucaoNaTela),
  };
}

function execucaoNaTela(e: LinhaDaExecucao) {
  return {
    id: e.id,
    regra_id: e.regra_id,
    quando: e.criada_em,
    sobre: e.alvo_titulo,
    resultado: e.ok === null ? "em andamento" : e.ok ? "feito" : "falhou",
    detalhe: e.detalhe,
  };
}

/* -------------------------------------------------------------- consultas */

export async function listarRegras(acesso: AcessoAsRegras) {
  const { data, error } = await acesso.supabase
    .from("regras")
    .select(CAMPOS)
    .eq("company_id", acesso.empresa.dono)
    .order("criada_em", { ascending: true })
    .limit(REGRAS_POR_EMPRESA + 10);
  if (error) falhou("ler as regras", error);
  const linhas = (data ?? []) as LinhaDaRegra[];

  const { data: execs } = linhas.length
    ? await acesso.supabase
        .from("regras_execucoes")
        .select("id,regra_id,alvo_tipo,alvo_titulo,ok,detalhe,criada_em")
        .eq("company_id", acesso.empresa.dono)
        .order("criada_em", { ascending: false })
        .limit(300)
    : { data: [] };
  const porRegra = new Map<string, LinhaDaExecucao[]>();
  for (const e of (execs ?? []) as LinhaDaExecucao[]) {
    const lista = porRegra.get(e.regra_id) ?? [];
    if (lista.length < 5) lista.push(e);
    porRegra.set(e.regra_id, lista);
  }

  const nomesDaFrase = await nomes(acesso, linhas.map(lerRegra));
  return linhas.map((l) => regraNaTela(l, nomesDaFrase, porRegra.get(l.id) ?? []));
}

async function buscarRegra(acesso: AcessoAsRegras, id: string): Promise<LinhaDaRegra> {
  if (!/^[0-9a-f-]{36}$/i.test(id)) throw new ErroDaRegra("Regra não encontrada.");
  const { data, error } = await acesso.supabase.from("regras").select(CAMPOS).eq("id", id).eq("company_id", acesso.empresa.dono).maybeSingle();
  if (error) falhou("ler a regra", error);
  if (!data) throw new ErroDaRegra("Regra não encontrada. Use listar_regras para ver as regras desta empresa.");
  return data as LinhaDaRegra;
}

export async function verExecucoes(acesso: AcessoAsRegras, regraId: string | null, limite = 50) {
  let consulta = acesso.supabase
    .from("regras_execucoes")
    .select("id,regra_id,alvo_tipo,alvo_titulo,ok,detalhe,criada_em")
    .eq("company_id", acesso.empresa.dono)
    .order("criada_em", { ascending: false })
    .limit(Math.min(Math.max(limite, 1), 200));
  if (regraId) consulta = consulta.eq("regra_id", regraId);
  const { data, error } = await consulta;
  if (error) falhou("ler o histórico das regras", error);
  return ((data ?? []) as LinhaDaExecucao[]).map(execucaoNaTela);
}

/* ---------------------------------------------------------------- gravações */

export async function prepararRegra(acesso: AcessoAsRegras, entrada: unknown) {
  const conferida = conferirRegra(entrada);
  if (!conferida.ok) throw new ErroDaRegra(conferida.erro);
  conferirPermissaoDoGatilho(acesso, conferida.valor.gatilho);
  await conferirReferencias(acesso, conferida.valor);
  const nomesDaFrase = await nomes(acesso, [conferida.valor]);
  /* Sem nome dado, o nome é a frase do gatilho com os nomes certos (da etapa, do insumo). */
  const semNome = !(entrada && typeof entrada === "object" && typeof (entrada as { nome?: unknown }).nome === "string" && (entrada as { nome: string }).nome.trim());
  const comNomes = semNome ? conferirRegra(entrada, nomesDaFrase) : conferida;
  const regra = comNomes.ok ? comNomes.valor : conferida.valor;
  return { regra, frase: fraseDaRegra(regra.gatilho, regra.acoes, nomesDaFrase) };
}

export async function criarRegra(acesso: AcessoAsRegras, entrada: unknown, ativa = true) {
  const { count } = await acesso.supabase.from("regras").select("id", { count: "exact", head: true }).eq("company_id", acesso.empresa.dono);
  if ((count ?? 0) >= REGRAS_POR_EMPRESA) throw new ErroDaRegra(`Esta empresa já tem ${REGRAS_POR_EMPRESA} regras. Exclua uma que não usa mais para criar outra.`);
  const { regra, frase } = await prepararRegra(acesso, entrada);
  const { data, error } = await acesso.supabase
    .from("regras")
    .insert({ company_id: acesso.empresa.dono, nome: regra.nome, ativa, gatilho: regra.gatilho, acoes: regra.acoes, criada_por: acesso.usuarioId })
    .select("id")
    .maybeSingle();
  if (error || !data) falhou("criar a regra", error);
  return { id: (data as { id: string }).id, nome: regra.nome, frase, ativa };
}

export async function alterarRegra(acesso: AcessoAsRegras, id: string, mudancas: { nome?: unknown; gatilho?: unknown; acoes?: unknown; ativa?: unknown }) {
  const atual = await buscarRegra(acesso, id);
  const so = mudancas.nome === undefined && mudancas.gatilho === undefined && mudancas.acoes === undefined;
  if (so && typeof mudancas.ativa === "boolean") return ligarRegra(acesso, id, mudancas.ativa);

  const { regra, frase } = await prepararRegra(acesso, {
    nome: mudancas.nome !== undefined ? mudancas.nome : atual.nome,
    gatilho: mudancas.gatilho !== undefined ? mudancas.gatilho : atual.gatilho,
    acoes: mudancas.acoes !== undefined ? mudancas.acoes : atual.acoes,
  });
  const ativa = typeof mudancas.ativa === "boolean" ? mudancas.ativa : atual.ativa;
  const { error } = await acesso.supabase
    .from("regras")
    .update({ nome: regra.nome, gatilho: regra.gatilho, acoes: regra.acoes, ativa, ultimo_erro: null })
    .eq("id", id)
    .eq("company_id", acesso.empresa.dono);
  if (error) falhou("alterar a regra", error);
  return { id, nome: regra.nome, frase, ativa };
}

export async function ligarRegra(acesso: AcessoAsRegras, id: string, ativa: boolean) {
  const atual = await buscarRegra(acesso, id);
  /* Religar uma regra de financeiro volta a mandar os avisos: mesma régua de criar. */
  const gatilhoAtual = ativa ? lerRegra(atual).gatilho : null;
  if (gatilhoAtual) conferirPermissaoDoGatilho(acesso, gatilhoAtual);
  const { error } = await acesso.supabase.from("regras").update({ ativa }).eq("id", id).eq("company_id", acesso.empresa.dono);
  if (error) falhou(ativa ? "ligar a regra" : "desligar a regra", error);
  return { id, nome: atual.nome, ativa };
}

export async function excluirRegra(acesso: AcessoAsRegras, id: string) {
  const atual = await buscarRegra(acesso, id);
  const { error } = await acesso.supabase.from("regras").delete().eq("id", id).eq("company_id", acesso.empresa.dono);
  if (error) falhou("excluir a regra", error);
  return { id, nome: atual.nome };
}

export async function resumoDaRegra(acesso: AcessoAsRegras, id: string) {
  const linha = await buscarRegra(acesso, id);
  const nomesDaFrase = await nomes(acesso, [lerRegra(linha)]);
  return regraNaTela(linha, nomesDaFrase);
}

/* -------------------------------------------------------------------- teste */

/**
 * O que a regra pegaria agora, sem agir: os registros que disparam, e quais
 * deles a regra já tratou. Lê pela sessão da pessoa.
 */
export async function testarRegra(acesso: AcessoAsRegras, alvo: { id: string } | { regra: unknown }) {
  let regraId: string | null = null;
  let gatilho: Gatilho;
  let acoes: Acao[];
  let criadaEm = new Date().toISOString();
  let nome: string;
  if ("id" in alvo) {
    const linha = await buscarRegra(acesso, alvo.id);
    const lida = lerRegra(linha);
    if (!lida.gatilho || !lida.acoes.length) throw new ErroDaRegra("A regra está com um formato que não vale mais. Abra e salve de novo.");
    conferirPermissaoDoGatilho(acesso, lida.gatilho);
    regraId = linha.id;
    gatilho = lida.gatilho;
    acoes = lida.acoes;
    criadaEm = linha.criada_em;
    nome = linha.nome;
  } else {
    const { regra } = await prepararRegra(acesso, alvo.regra);
    gatilho = regra.gatilho;
    acoes = regra.acoes;
    nome = regra.nome;
    /* Regra ainda não salva: mostra como exemplo o que entrou na etapa na última semana. */
    criadaEm = new Date(Date.now() - 7 * 86_400_000).toISOString();
  }

  const nomesDaFrase = await nomes(acesso, [{ gatilho, acoes }]);
  let alvos;
  try {
    alvos = await alvosDaRegra(acesso.supabase, acesso.empresa, { gatilho, criada_em: criadaEm }, new Date(), nomesDaFrase);
  } catch (erro) {
    if (erro instanceof ErroDaConsulta) falhou("conferir a regra", { message: erro.message });
    throw erro;
  }
  const feitas = regraId ? await chavesJaFeitas(acesso.supabase, regraId, alvos) : new Set<string>();
  const novos = alvos.filter((a) => !feitas.has(a.chave));
  return {
    nome,
    frase: fraseDaRegra(gatilho, acoes, nomesDaFrase),
    total_agora: alvos.length,
    ainda_nao_tratados: novos.length,
    ja_tratados: alvos.length - novos.length,
    agiria_na_proxima_volta: Math.min(novos.length, ACOES_POR_REGRA_POR_VOLTA),
    ...(gatilho.tipo === "pedido_entrou_na_etapa" && !regraId
      ? { observacao: "Uma regra nova só vale para pedidos que entrarem na etapa depois de ela ser criada. A lista mostra os da última semana, só como exemplo." }
      : {}),
    itens: alvos.slice(0, 20).map((a) => ({ sobre: a.titulo, detalhe: a.detalhe, ja_tratado: feitas.has(a.chave) })),
  };
}
