import { lerDestinoDaVolta } from "./redirecionamentos.ts";

/**
 * O registro de um programa (o Claude, o ChatGPT, o Claude Code).
 *
 * Antes de mandar a pessoa para a tela de permissão, o programa se apresenta:
 * diz como se chama e para onde a pessoa deve voltar. Ninguém preenche isso à
 * mão; o programa faz sozinho, e por isso esta porta fica aberta para qualquer
 * um na internet. O que protege é a conferência: só é aceito quem volta para
 * um endereço da lista fechada (`redirecionamentos.ts`), e nada do que o
 * programa escreve sobre si mesmo é tomado como verdade.
 *
 * Só regra, sem banco, para o teste ao lado rodar sozinho.
 */

export const METODOS = ["none", "client_secret_post", "client_secret_basic"] as const;
export type MetodoDoPrograma = (typeof METODOS)[number];

export type ProgramaParaRegistrar = {
  nome: string;
  redirectUris: string[];
  metodo: MetodoDoPrograma;
  /* O nome veio da nossa lista de assistentes conhecidos (e não do que o programa escreveu). */
  conhecido: boolean;
  /* O que o programa disse de si, guardado só para consulta. */
  dados: Record<string, string>;
};

export type ErroDeRegistro = {
  erro: "invalid_redirect_uri" | "invalid_client_metadata";
  descricao: string;
};

const MAXIMO_DE_VOLTAS = 10;
/* O nome do programa vira o nome da conexão na lista do painel, que aceita até 80 letras. */
const TAMANHO_DO_NOME = 80;

/**
 * Tira o que não se vê. O nome que o programa escreve aparece na tela de
 * permissão, e caracteres de controle ou de direção do texto servem para
 * fazer um nome parecer outro.
 */
function semInvisiveis(valor: string) {
  let saida = "";
  for (const letra of valor) {
    const codigo = letra.codePointAt(0) ?? 0;
    const invisivel =
      codigo < 0x20 ||
      (codigo >= 0x7f && codigo <= 0x9f) ||
      (codigo >= 0x200b && codigo <= 0x200f) ||
      (codigo >= 0x2028 && codigo <= 0x202e) ||
      (codigo >= 0x2060 && codigo <= 0x2069) ||
      codigo === 0xfeff;
    saida += invisivel ? " " : letra;
  }
  return saida;
}

function texto(valor: unknown, maximo: number): string | null {
  if (typeof valor !== "string") return null;
  const limpo = semInvisiveis(valor).replace(/\s+/g, " ").trim();
  return limpo ? limpo.slice(0, maximo) : null;
}

export function lerRegistro(corpo: unknown): { ok: true; programa: ProgramaParaRegistrar } | ({ ok: false } & ErroDeRegistro) {
  if (!corpo || typeof corpo !== "object" || Array.isArray(corpo)) {
    return { ok: false, erro: "invalid_client_metadata", descricao: "O registro precisa ser um objeto JSON." };
  }
  const dadosBrutos = corpo as Record<string, unknown>;

  const voltas = dadosBrutos.redirect_uris;
  if (!Array.isArray(voltas) || voltas.length === 0) {
    return { ok: false, erro: "invalid_redirect_uri", descricao: "redirect_uris é obrigatório." };
  }
  if (voltas.length > MAXIMO_DE_VOLTAS) {
    return { ok: false, erro: "invalid_redirect_uri", descricao: `No máximo ${MAXIMO_DE_VOLTAS} endereços de retorno.` };
  }
  const redirectUris: string[] = [];
  let assistente: string | null = null;
  let todosConhecidos = true;
  for (const volta of voltas) {
    /* Retorno fora da lista é deixado de lado, sem derrubar o registro: há
       programa que manda vários de uma vez, e basta um aceito para ele
       funcionar. O que ficou de fora não vale na hora de pedir a permissão. */
    const destino = lerDestinoDaVolta(volta);
    if (!destino) continue;
    if (!redirectUris.includes(destino.endereco)) redirectUris.push(destino.endereco);
    if (destino.assistente) assistente ??= destino.assistente;
    else todosConhecidos = false;
  }
  if (!redirectUris.length) {
    return {
      ok: false,
      erro: "invalid_redirect_uri",
      descricao:
        "Endereço de retorno não aceito. Este servidor só registra programas que voltam para o próprio computador (http://localhost ou http://127.0.0.1) ou para os assistentes conhecidos. Outros programas se ligam pela chave de acesso.",
    };
  }

  const metodoPedido = dadosBrutos.token_endpoint_auth_method;
  /* Programa que não diz como se autentica é tratado como público: a prova de
     quem pediu o código (PKCE) é obrigatória para todos, e é ela que protege. */
  const metodo: MetodoDoPrograma = metodoPedido === undefined || metodoPedido === null ? "none" : (metodoPedido as MetodoDoPrograma);
  if (!(METODOS as readonly string[]).includes(metodo)) {
    return { ok: false, erro: "invalid_client_metadata", descricao: `token_endpoint_auth_method não atendido. Use um destes: ${METODOS.join(", ")}.` };
  }

  const concessoes = dadosBrutos.grant_types;
  if (concessoes !== undefined && (!Array.isArray(concessoes) || !concessoes.includes("authorization_code"))) {
    return { ok: false, erro: "invalid_client_metadata", descricao: "grant_types precisa incluir authorization_code." };
  }
  const respostas = dadosBrutos.response_types;
  if (respostas !== undefined && (!Array.isArray(respostas) || !respostas.includes("code"))) {
    return { ok: false, erro: "invalid_client_metadata", descricao: "response_types precisa incluir code." };
  }

  const dados: Record<string, string> = {};
  for (const campo of ["client_uri", "logo_uri", "software_id", "software_version", "scope"]) {
    const valor = texto(dadosBrutos[campo], 300);
    if (valor) dados[campo] = valor;
  }
  const nomeInformado = texto(dadosBrutos.client_name, TAMANHO_DO_NOME);
  if (nomeInformado) dados.client_name = nomeInformado;

  /* Para um assistente conhecido vale o nome da nossa lista, não o que o
     programa escreveu: é o endereço de retorno que diz quem ele é. Só quando
     TODOS os retornos são de assistentes conhecidos; um programa que mistura
     um endereço do Claude com um do próprio computador fica com o nome que
     ele mesmo deu, e a tela de permissão o trata como desconhecido. */
  const conhecido = Boolean(assistente) && todosConhecidos;

  return {
    ok: true,
    programa: {
      nome: (conhecido ? assistente : nomeInformado) ?? "Programa sem nome",
      redirectUris,
      metodo,
      conhecido,
      dados,
    },
  };
}
