import { ESCOPOS, type Escopo } from "../chave.ts";
import { documentoConhecido } from "./documento.ts";
import { desafioValido } from "./pkce.ts";
import { voltaRegistrada } from "./redirecionamentos.ts";

/**
 * O pedido de permissão: o que chega no endereço da tela "Permitir".
 *
 * A regra que organiza tudo aqui é para onde vai o erro. Se o programa não é
 * conhecido, ou o endereço de retorno não é um dos que ele registrou, o erro
 * aparece NA TELA e a pessoa não é mandada a lugar nenhum: redirecionar para
 * um endereço que não conferimos seria fazer o trabalho do golpista. Só depois
 * de o programa e o retorno conferirem é que os outros erros voltam para o
 * programa, pelo endereço de retorno.
 *
 * Só regra, sem banco, para o teste ao lado rodar sozinho.
 */

export type ParametrosDoPedido = Record<string, string | undefined>;

export type PedidoDePermissao = {
  clientId: string;
  redirectUri: string;
  desafio: string;
  estado: string | null;
  /* O que o programa pediu. A pessoa escolhe na tela; isto só marca a opção. */
  escoposPedidos: Escopo[];
  recurso: string | null;
};

export type ErroParaOPrograma = {
  redirectUri: string;
  erro: "invalid_request" | "unsupported_response_type" | "invalid_scope" | "invalid_target" | "access_denied" | "server_error";
  descricao: string;
  estado: string | null;
};

export type LeituraDoPedido =
  | { ok: true; pedido: PedidoDePermissao }
  | { ok: false; naTela: string }
  | { ok: false; voltar: ErroParaOPrograma };

const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;

/**
 * Como o programa se identifica. São dois jeitos: o número que recebeu quando
 * se registrou aqui, ou o endereço da identidade publicada dele.
 */
export type IdentidadeDoPrograma = { tipo: "registro"; id: string } | { tipo: "documento"; endereco: string };

/** A identidade do programa, quando tem a forma de uma. É o que basta para ir buscá-lo. */
export function identidadeDoPrograma(clientId: unknown): IdentidadeDoPrograma | null {
  const valor = typeof clientId === "string" ? clientId.trim() : "";
  if (UUID.test(valor)) return { tipo: "registro", id: valor.toLowerCase() };
  if (documentoConhecido(valor)) return { tipo: "documento", endereco: valor };
  return null;
}

/** O `resource` do pedido aponta para o nosso conector? Sem `resource`, não há o que conferir. */
export function recursoAceito(recurso: string | undefined | null, origensAceitas: readonly string[]): boolean {
  if (!recurso) return true;
  try {
    return origensAceitas.includes(new URL(recurso).origin);
  } catch {
    return false;
  }
}

/**
 * Confere o pedido contra o programa registrado.
 * `origensAceitas` são as origens do nosso servidor MCP: o acesso só pode ser
 * pedido para ele.
 */
export function conferirPedido(
  parametros: ParametrosDoPedido,
  programa: { id: string; redirectUris: readonly string[] },
  origensAceitas: readonly string[]
): LeituraDoPedido {
  const pedida = String(parametros.redirect_uri ?? "");
  let redirectUri: string;
  if (pedida) {
    if (!voltaRegistrada(programa.redirectUris, pedida)) {
      return { ok: false, naTela: "O endereço de retorno deste pedido não é um dos que o programa registrou. Por segurança, a conexão foi interrompida." };
    }
    redirectUri = pedida;
  } else if (programa.redirectUris.length === 1) {
    redirectUri = programa.redirectUris[0];
  } else {
    return { ok: false, naTela: "O pedido chegou sem endereço de retorno. Comece a conexão de novo pelo assistente." };
  }

  const estadoBruto = parametros.state;
  if (estadoBruto !== undefined && estadoBruto.length > 2000) {
    return { ok: false, naTela: "O pedido chegou com dados grandes demais. Comece a conexão de novo pelo assistente." };
  }
  const estado = estadoBruto ?? null;
  const voltar = (erro: ErroParaOPrograma["erro"], descricao: string): LeituraDoPedido => ({ ok: false, voltar: { redirectUri, erro, descricao, estado } });

  if (parametros.response_type !== "code") {
    return voltar("unsupported_response_type", "Só response_type=code é atendido.");
  }

  const metodo = parametros.code_challenge_method;
  if (!parametros.code_challenge || metodo !== "S256") {
    return voltar("invalid_request", "PKCE é obrigatório: envie code_challenge com code_challenge_method=S256.");
  }
  if (!desafioValido(parametros.code_challenge)) {
    return voltar("invalid_request", "code_challenge inválido.");
  }

  const escoposPedidos = String(parametros.scope ?? "")
    .split(/\s+/)
    .filter((escopo): escopo is Escopo => (ESCOPOS as readonly string[]).includes(escopo));

  let recurso: string | null = null;
  if (parametros.resource) {
    /* O acesso é pedido para um servidor. Se o programa diz que é para outro
       que não o nosso, não é conosco. */
    if (!recursoAceito(parametros.resource, origensAceitas)) {
      return voltar("invalid_target", "O parâmetro resource não aponta para este servidor.");
    }
    recurso = parametros.resource.slice(0, 300);
  }

  return {
    ok: true,
    pedido: {
      clientId: programa.id,
      redirectUri,
      desafio: parametros.code_challenge,
      estado,
      escoposPedidos: escoposPedidos.includes("escrita") ? ["leitura", "escrita"] : escoposPedidos.length ? ["leitura"] : [],
      recurso,
    },
  };
}

/**
 * A explicação de um erro, do jeito que a norma aceita: só letras sem acento.
 * Quem lê é o programa (ou quem está depurando), não a pessoa.
 */
export function semAcentos(texto: string): string {
  return texto
    .normalize("NFD")
    .replace(/[\u0300-\u036f]/g, "")
    .replace(/[^\x20-\x7e]/g, "")
    .replace(/["\\]/g, "'");
}

/** Monta o endereço de volta ao programa, com o código ou com o erro. */
export function enderecoDeVolta(
  redirectUri: string,
  campos: { code?: string; error?: string; error_description?: string; state?: string | null; iss?: string }
): string {
  const url = new URL(redirectUri);
  for (const [nome, valor] of Object.entries(campos)) {
    if (valor === undefined || valor === null || valor === "") continue;
    url.searchParams.set(nome, nome === "error_description" ? semAcentos(valor) : valor);
  }
  return url.toString();
}
