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

import { createExternalSupabaseClient } from "@/lib/api-v1/auth";
import { canCompany, getCompanyContext, type CompanyContext } from "@/lib/company-context";
import { assinaturaDaEmpresa } from "@/lib/fiscal/servidor/assinatura-da-empresa";
import { createAdminClient } from "@/lib/supabase-server";

import { hashDaChave, lerEscopos, pareceChave, temMarca, type Escopo } from "./chave";
import { assistentesLiberadosAgora } from "./ajustes";
import { enderecosDoConector } from "./oauth/enderecos";
import { segredoDoServidor } from "./segredo";
import { sessaoDoUsuario } from "./sessao";
import { criarFreio, criarLembranca, enderecoDeQuemChama } from "./tentativas";

/**
 * Quem está do outro lado de um pedido do assistente.
 *
 * A resposta nunca vem do que o assistente diz. Vem da chave que chega no
 * cabeçalho, e a chave carrega três coisas amarradas na hora em que nasceu: a
 * pessoa, a empresa e o que ela pode fazer.
 *
 * A cada pedido, na ordem: quem chama não está errando chave em série; a chave
 * existe e vale; a empresa está liberada para usar assistentes; o limite por
 * minuto não estourou; a pessoa ainda pertence àquela empresa e ainda tem a
 * permissão; a assinatura da empresa está em dia. Qualquer "não" encerra ali,
 * antes de qualquer dado da empresa ser lido.
 */

export class ErroDeAcesso extends Error {
  status: number;
  codigo: string;

  constructor(status: number, codigo: string, mensagem: string) {
    super(mensagem);
    this.status = status;
    this.codigo = codigo;
  }
}

export type ChaveEmUso = {
  id: string;
  nome: string;
  companyId: string;
  usuarioId: string;
  escopos: Escopo[];
  /* "chave": criada e colada à mão. "conexao": nasceu do botão Conectar. */
  origem: "chave" | "conexao";
};

export type ContextoDoAssistente = {
  chave: ChaveEmUso;
  user: User;
  /* O banco visto pela pessoa: as travas das tabelas valem. É por aqui que as
     ferramentas leem e gravam. */
  supabase: SupabaseClient;
  /* A chave de serviço, que ignora as travas do banco. Serve ao registro dos
     assistentes, ao link público do orçamento (como na API) e às áreas cujas
     travas conferem uma marca que a sessão do assistente não carrega: o
     financeiro, a nota fiscal, os painéis, os relatórios e as regras leem e
     gravam com ela. Sempre DEPOIS de conferir a permissão da pessoa, e sempre
     com o filtro da empresa (`company_id`/`user_id` desta conta) em toda
     consulta — quem usar esta chave sem o filtro abre os dados de outra empresa. */
  admin: SupabaseClient;
  empresa: CompanyContext;
  tenantIds: string[];
  /* O id que os dados novos recebem em `user_id` ou `company_id`: o do dono. */
  donoDosDados: string;
  origemDoAdmin: string;
  cliente: string | null;
};

/* Pedidos por minuto, por chave. Uma conversa normal fica longe disso. */
export const LIMITE_POR_MINUTO = 120;
const MUITOS_PEDIDOS = "Muitos pedidos em pouco tempo com esta chave. Espere um minuto e continue.";

const USUARIO_VALE_POR_MS = 5 * 60 * 1000;

declare global {
  var __usuariosDosAssistentes: Map<string, { user: User; ate: number }> | undefined;
  var __freioDasChavesErradas: ReturnType<typeof criarFreio> | undefined;
  var __chavesBoasRecentes: ReturnType<typeof criarLembranca> | undefined;
  var __chavesNoLimite: ReturnType<typeof criarLembranca> | undefined;
  var __chavesQueNaoExistem: ReturnType<typeof criarLembranca> | undefined;
}

function freio() {
  globalThis.__freioDasChavesErradas ??= criarFreio();
  return globalThis.__freioDasChavesErradas;
}

function chavesBoas() {
  globalThis.__chavesBoasRecentes ??= criarLembranca();
  return globalThis.__chavesBoasRecentes;
}

/* As chaves que já passaram do limite neste minuto. Enquanto o minuto não
   vira, elas são recusadas aqui mesmo, sem gastar uma ida ao banco cada. */
function chavesNoLimite() {
  globalThis.__chavesNoLimite ??= criarLembranca(60_000);
  return globalThis.__chavesNoLimite;
}

/* Os acessos com a marca do servidor que o banco disse não conhecer. Um acesso
   trocado ou apagado não volta a valer; lembrar disso por dez minutos evita
   que a repetição dele vire uma consulta cada. */
function chavesQueNaoExistem() {
  globalThis.__chavesQueNaoExistem ??= criarLembranca();
  return globalThis.__chavesQueNaoExistem;
}

function usuariosLembrados() {
  globalThis.__usuariosDosAssistentes ??= new Map();
  return globalThis.__usuariosDosAssistentes;
}

async function usuarioDaChave(admin: SupabaseClient, usuarioId: string): Promise<User> {
  const lembrado = usuariosLembrados().get(usuarioId);
  if (lembrado && lembrado.ate > Date.now()) return lembrado.user;

  const { data, error } = await admin.auth.admin.getUserById(usuarioId);
  if (error || !data?.user?.email) {
    throw new ErroDeAcesso(401, "chave_sem_dono", "A conta desta chave não existe mais.");
  }
  if (usuariosLembrados().size > 5000) usuariosLembrados().clear();
  usuariosLembrados().set(usuarioId, { user: data.user, ate: Date.now() + USUARIO_VALE_POR_MS });
  return data.user;
}

function chaveDoCabecalho(request: Request, quemChama: string | null) {
  const cabecalho = request.headers.get("authorization") || "";
  const [esquema, valor] = cabecalho.trim().split(/\s+/);
  if (esquema?.toLowerCase() !== "bearer" || !valor) {
    throw new ErroDeAcesso(
      401,
      "sem_chave",
      "Este conector pede acesso. Conecte pelo botão do assistente, ou envie uma chave de acesso no cabeçalho Authorization, no formato: Bearer ktl_... A chave é criada no painel, em Configurações, Integrações, Assistentes e Apps."
    );
  }
  if (!pareceChave(valor)) {
    freio().anotar(quemChama);
    throw new ErroDeAcesso(401, "chave_invalida", "Chave de acesso inválida.");
  }
  return valor;
}

/** O nome do programa que chamou, tirado do cabeçalho User-Agent. */
export function clienteDoPedido(request: Request): string | null {
  const agente = (request.headers.get("user-agent") || "").trim();
  if (!agente) return null;
  const nome = agente.split(/[\s/(]/)[0];
  return nome ? nome.slice(0, 60) : null;
}

type ChaveDoBanco = {
  id?: unknown;
  company_id?: unknown;
  dono_id?: unknown;
  usuario_id?: unknown;
  nome?: unknown;
  escopos?: unknown;
  expira_em?: unknown;
  revogada_em?: unknown;
  origem?: unknown;
  acesso_vencido?: unknown;
  chamadas_no_minuto?: unknown;
};

/** Um acesso que acabou de ser entregue pela troca do código ou pela renovação: já é sabidamente bom. */
export function lembrarAcessoBom(acesso: string) {
  chavesBoas().lembrar(hashDaChave(acesso));
}

export async function autenticar(request: Request, clienteInformado?: string | null): Promise<ContextoDoAssistente> {
  const quemChama = enderecoDeQuemChama(request.headers);
  const chaveRecebida = chaveDoCabecalho(request, quemChama);
  const hash = hashDaChave(chaveRecebida);

  /* A marca do servidor separa o que saiu daqui do que alguém inventou. O
     Claude e o ChatGPT chegam pelos mesmos poucos endereços, com os acessos de
     todos os clientes deles; o freio por endereço, sozinho, deixaria um
     engraçadinho travar todo mundo que vem por ali. Então o que tem marca não
     passa pelo freio. O que não tem (chave antiga, ou inventada) passa. */
  const comMarca = temMarca(segredoDoServidor("ktalogu-assistentes-fichas"), chaveRecebida);

  /* Endereço que errou chave demais: é recusado sem ir ao banco. Só passa quem
     apresenta uma chave que funcionou há pouco, para ninguém pagar pelo vizinho
     de rede. */
  if (!comMarca && freio().bloqueado(quemChama) && !chavesBoas().conhecida(hash)) {
    throw new ErroDeAcesso(429, "muitas_tentativas", "Muitas tentativas com chave inválida vindas deste endereço. Espere um minuto e tente de novo.");
  }
  /* Com marca e desconhecida há pouco: é um acesso que já foi trocado por outro
     ou apagado. Não volta a valer, e não precisa de outra ida ao banco. */
  if (comMarca && chavesQueNaoExistem().conhecida(hash)) {
    throw new ErroDeAcesso(401, "chave_invalida", "Este acesso não vale mais. O programa renova sozinho; se não renovar, conecte de novo pelo assistente.");
  }
  if (chavesNoLimite().conhecida(hash)) {
    throw new ErroDeAcesso(429, "limite_por_minuto", MUITOS_PEDIDOS);
  }

  const admin = createAdminClient();
  const cliente = clienteInformado || clienteDoPedido(request);

  const { data, error } = await admin.rpc("usar_chave_de_acesso", {
    p_hash: hash,
    p_cliente: clienteInformado || null,
  });
  if (error) {
    console.error("[mcp] não foi possível conferir a chave:", error.message);
    throw new ErroDeAcesso(503, "indisponivel", "Não foi possível conferir a chave agora. Tente de novo em instantes.");
  }

  const linha = (data ?? null) as ChaveDoBanco | null;
  if (!linha || typeof linha.id !== "string") {
    if (comMarca) {
      chavesQueNaoExistem().lembrar(hash);
      throw new ErroDeAcesso(401, "chave_invalida", "Este acesso não vale mais. O programa renova sozinho; se não renovar, conecte de novo pelo assistente.");
    }
    freio().anotar(quemChama);
    throw new ErroDeAcesso(401, "chave_invalida", "Chave de acesso inválida.");
  }
  const origem = linha.origem === "conexao" ? "conexao" : "chave";
  if (linha.revogada_em) {
    chavesBoas().esquecer(hash);
    throw new ErroDeAcesso(
      401,
      "chave_revogada",
      origem === "conexao"
        ? "Esta conexão foi desligada. Conecte de novo pelo assistente."
        : "Esta chave foi revogada. Crie outra em Configurações, Integrações, Assistentes e Apps."
    );
  }
  /* O acesso de uma conexão vale uma hora. Vencido, o programa troca a
     renovação por um acesso novo sozinho; a recusa daqui é o aviso para isso. */
  if (linha.acesso_vencido === true) {
    chavesBoas().esquecer(hash);
    throw new ErroDeAcesso(401, "acesso_vencido", "O acesso desta conexão venceu. O programa renova sozinho; se não renovar, conecte de novo pelo assistente.");
  }
  if (typeof linha.expira_em === "string" && new Date(linha.expira_em).getTime() <= Date.now()) {
    chavesBoas().esquecer(hash);
    throw new ErroDeAcesso(401, "chave_vencida", "Esta chave venceu. Crie outra em Configurações, Integrações, Assistentes e Apps.");
  }
  /* Quem decide se a chave ainda vale é o relógio do banco, e ele só conta a
     chamada de chave válida. Nenhuma chamada contada quer dizer que o banco
     considerou a chave vencida, mesmo que o relógio deste servidor diga que
     ainda não. */
  if (Number(linha.chamadas_no_minuto ?? 0) < 1) {
    chavesBoas().esquecer(hash);
    throw new ErroDeAcesso(401, "chave_vencida", "Esta chave venceu. Crie outra em Configurações, Integrações, Assistentes e Apps.");
  }
  chavesBoas().lembrar(hash);

  const companyId = String(linha.company_id ?? "");
  const donoId = typeof linha.dono_id === "string" ? linha.dono_id : null;
  const usuarioId = String(linha.usuario_id ?? "");
  const escopos = lerEscopos(linha.escopos) ?? ["leitura"];

  if (!(await assistentesLiberadosAgora([companyId, donoId]))) {
    throw new ErroDeAcesso(403, "nao_liberado", "Assistentes e apps ainda não estão liberados para esta empresa.");
  }

  if (Number(linha.chamadas_no_minuto ?? 0) > LIMITE_POR_MINUTO) {
    const agora = Date.now();
    chavesNoLimite().lembrar(hash, agora, 60_000 - (agora % 60_000));
    throw new ErroDeAcesso(429, "limite_por_minuto", MUITOS_PEDIDOS);
  }

  const user = await usuarioDaChave(admin, usuarioId);

  let accessToken: string;
  try {
    accessToken = await sessaoDoUsuario(user.id);
  } catch (falha) {
    console.error("[mcp] não foi possível abrir a sessão da chave", linha.id, falha instanceof Error ? falha.message : falha);
    throw new ErroDeAcesso(503, "indisponivel", "Não foi possível abrir a sessão da conta agora. Tente de novo em instantes.");
  }
  const supabase = createExternalSupabaseClient(accessToken);

  const empresa = await getCompanyContext(supabase, user);
  const tenantIds = empresa.tenantIds?.length ? empresa.tenantIds : [user.id];

  /* A chave vale só na empresa em que nasceu: a empresa da pessoa, hoje, tem
     de ser exatamente aquela. Quem saiu, ou foi parar em outra empresa, leva
     uma chave que não abre mais nada. */
  if (empresa.companyId !== companyId || empresa.membership?.active === false) {
    throw new ErroDeAcesso(403, "sem_vinculo", "A pessoa desta chave não faz mais parte da empresa. A chave não vale mais.");
  }

  if (!canCompany(empresa, user.id, "company.assistants.manage")) {
    throw new ErroDeAcesso(403, "sem_permissao", "A pessoa desta chave não tem mais permissão para usar assistentes e apps.");
  }

  /* Mesma regra do painel e da API: empresa com a assinatura vencida não usa. */
  if (empresa.ownerId) {
    const assinatura = await assinaturaDaEmpresa(admin, empresa.ownerId);
    if (!assinatura.ativa && assinatura.situacao !== "sem-registro") {
      throw new ErroDeAcesso(402, "assinatura_inativa", "A assinatura do Ktalogu desta empresa está vencida ou inativa. Regularize em Assinatura para continuar.");
    }
  }

  return {
    chave: { id: linha.id, nome: String(linha.nome ?? ""), companyId, usuarioId, escopos, origem },
    user,
    supabase,
    admin,
    empresa,
    tenantIds,
    donoDosDados: empresa.ownerId ?? empresa.companyId,
    /* O pedido chega pelo endereço do conector; os links que saem daqui são
       montados a partir do endereço do painel. */
    /* Do X-Forwarded-Host só vale o último item (o que o Apache escreve): o
       primeiro é de quem chama, e daqui sai o endereço dos links e do aviso à
       loja. */
    origemDoAdmin: enderecosDoConector(request).painel,
    cliente,
  };
}
