import { NextResponse } from "next/server";

import { lerCorpoComLimite } from "../corpo";
import { criarFreio } from "../tentativas";
import { semAcentos } from "./pedido";

/**
 * O que as portas do login por botão têm em comum: como respondem, como leem
 * o pedido e como freiam quem abusa.
 */

export const SEM_CACHE = { "Cache-Control": "no-store", Pragma: "no-cache" };

/**
 * Programas que rodam dentro do navegador (o MCP Inspector, por exemplo) só
 * conseguem chamar estas portas se elas disserem que aceitam ser chamadas de
 * outro site. Aqui pode: nenhuma delas usa a sessão do navegador. Quem chama
 * se identifica pelo que manda no pedido, e um site qualquer não tem o que
 * mandar. A tela de permitir e o "Permitir" em si ficam de fora: esses usam a
 * sessão, e só respondem ao próprio painel.
 */
export function cabecalhosDeCors(request: Request): Record<string, string> {
  return {
    "Access-Control-Allow-Origin": "*",
    "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
    "Access-Control-Allow-Headers": request.headers.get("access-control-request-headers") || "Authorization, Content-Type, MCP-Protocol-Version",
    "Access-Control-Expose-Headers": "WWW-Authenticate",
    "Access-Control-Max-Age": "600",
  };
}

export function respostaAoPreflight(request: Request) {
  return new NextResponse(null, { status: 204, headers: { ...SEM_CACHE, ...cabecalhosDeCors(request) } });
}

const ORIGEM_DO_COMPUTADOR = /^http:\/\/(localhost|127\.0\.0\.1|\[::1\])(:\d{1,5})?$/;

/**
 * O registro aceita ser chamado de outro site só quando o site é o próprio
 * computador da pessoa (o MCP Inspector, por exemplo). Um site qualquer na
 * internet não precisa registrar programa nenhum, e abrir a porta para ele
 * deixaria qualquer página disparar registros a partir do navegador de quem a
 * visita.
 */
export function cabecalhosDeCorsDoComputador(request: Request): Record<string, string> {
  const origem = request.headers.get("origin") || "";
  if (!ORIGEM_DO_COMPUTADOR.test(origem)) return { Vary: "Origin" };
  return {
    "Access-Control-Allow-Origin": origem,
    "Access-Control-Allow-Methods": "POST, OPTIONS",
    "Access-Control-Allow-Headers": "Content-Type",
    "Access-Control-Max-Age": "600",
    Vary: "Origin",
  };
}

export function json(request: Request, corpo: unknown, status = 200, cabecalhos: Record<string, string> = {}) {
  return NextResponse.json(corpo, { status, headers: { ...SEM_CACHE, ...cabecalhosDeCors(request), ...cabecalhos } });
}

/** A recusa no formato que os programas esperam: um código curto e a explicação. */
export function recusaOAuth(request: Request, status: number, erro: string, descricao: string, cabecalhos: Record<string, string> = {}) {
  return json(request, { error: erro, error_description: semAcentos(descricao) }, status, cabecalhos);
}

const TAMANHO_MAXIMO = 32 * 1024;

/**
 * Lê os campos do pedido. A norma manda formulário; alguns programas mandam
 * JSON. Os dois são aceitos. `null` = grande demais ou ilegível.
 */
export async function lerCampos(request: Request): Promise<Record<string, string> | null> {
  const bruto = await lerCorpoComLimite(request, TAMANHO_MAXIMO);
  if (bruto === null) return null;
  const tipo = (request.headers.get("content-type") || "").toLowerCase();
  const campos: Record<string, string> = {};
  if (tipo.includes("application/json")) {
    try {
      const objeto = JSON.parse(bruto || "{}") as unknown;
      if (!objeto || typeof objeto !== "object" || Array.isArray(objeto)) return null;
      for (const [nome, valor] of Object.entries(objeto as Record<string, unknown>)) {
        if (typeof valor === "string") campos[nome] = valor;
      }
      return campos;
    } catch {
      return null;
    }
  }
  for (const [nome, valor] of new URLSearchParams(bruto)) {
    if (!(nome in campos)) campos[nome] = valor;
  }
  return campos;
}

/** O corpo do registro, que é sempre JSON. `undefined` = grande demais ou ilegível. */
export async function lerJson(request: Request): Promise<unknown | undefined> {
  const bruto = await lerCorpoComLimite(request, TAMANHO_MAXIMO);
  if (bruto === null) return undefined;
  try {
    return JSON.parse(bruto);
  } catch {
    return undefined;
  }
}

/**
 * Como o programa se apresentou neste pedido: o identificador e, se tiver, o
 * segredo. Podem vir no cabeçalho (`Authorization: Basic`) ou nos campos; há
 * programa que se registra de um jeito e manda do outro, e por isso os dois
 * lugares são olhados sempre.
 */
export function credenciaisDoPrograma(request: Request, campos: Record<string, string>): { clientId: string | null; segredo: string | null; porCabecalho: boolean } {
  const cabecalho = request.headers.get("authorization") || "";
  const [esquema, valor] = cabecalho.trim().split(/\s+/);
  if (esquema?.toLowerCase() === "basic" && valor) {
    try {
      const decodificado = Buffer.from(valor, "base64").toString("utf8");
      const corte = decodificado.indexOf(":");
      if (corte > 0) {
        const decodificar = (parte: string) => {
          try {
            return decodeURIComponent(parte.replace(/\+/g, "%20"));
          } catch {
            return parte;
          }
        };
        return { clientId: decodificar(decodificado.slice(0, corte)), segredo: decodificar(decodificado.slice(corte + 1)) || null, porCabecalho: true };
      }
    } catch {
      /* cabeçalho ilegível: cai para os campos */
    }
  }
  return { clientId: campos.client_id || null, segredo: campos.client_secret || null, porCabecalho: false };
}

/* Os freios. Moram na memória do processo, como o das chaves erradas.

   Registro: os programas dos assistentes saem todos pelos mesmos poucos
   endereços (os servidores do Claude, os do ChatGPT), então o teto por
   endereço (por faixa, no IPv6) é folgado. O teto geral é por hora, e alto:
   quem segura o tamanho da tabela é a faxina dos registros que nunca foram
   usados, e um teto geral baixo viraria um jeito de fechar o registro para
   todo mundo. */
const REGISTROS_POR_HORA_POR_ENDERECO = 120;
const REGISTROS_POR_HORA = 2000;
const FALHAS_POR_MINUTO = 60;

declare global {
  var __freiosDoLoginPorBotao:
    | { registro: ReturnType<typeof criarFreio>; registroGeral: ReturnType<typeof criarFreio>; falhas: ReturnType<typeof criarFreio> }
    | undefined;
}

export function freios() {
  globalThis.__freiosDoLoginPorBotao ??= {
    registro: criarFreio(REGISTROS_POR_HORA_POR_ENDERECO, 60 * 60 * 1000),
    registroGeral: criarFreio(REGISTROS_POR_HORA, 60 * 60 * 1000),
    falhas: criarFreio(FALHAS_POR_MINUTO, 60 * 1000),
  };
  return globalThis.__freiosDoLoginPorBotao;
}
