import { getSupabaseClient } from "@/lib/supabase";

/*
  A leitura do orçamento, do lado de quem lê.

  Abre uma "leitura" por página: avisa a abertura, conta o tempo em que a
  pessoa está de fato com o orçamento na frente (aba visível e alguém mexendo
  nos últimos 2 minutos), mede até onde ela rolou a folha, anota as partes que
  apareceram na tela (itens, total, condições, observações) e os cliques que
  importam. Manda um sinal a cada 15 segundos e um último quando a aba some.

  Uma leitura por página, mesmo que o React monte o componente duas vezes: o
  Map abaixo guarda a leitura de cada link.

  Se a rota nova falhar, o contador antigo segue valendo pelo caminho antigo,
  para o número do painel nunca parar por causa disto.
*/

export type AcaoNaLeitura = "pdf" | "aprovar" | "aprovou" | "artes" | "acompanhar";

type Leitura = { acao: (acao: AcaoNaLeitura) => void };

const ENDERECO = "/api/orcamento/leitura";
const GUARDA_DO_LEITOR = "ktl:leitor-do-orcamento";
const SINAL_A_CADA_MS = 15000;
const PARADO_DEPOIS_DE_MS = 2 * 60 * 1000;
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;

const leituras = new Map<string, Leitura>();
const NADA: Leitura = { acao: () => {} };

function novoId() {
  if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") return crypto.randomUUID();
  const bytes = new Uint8Array(16);
  crypto.getRandomValues(bytes);
  bytes[6] = (bytes[6] & 0x0f) | 0x40;
  bytes[8] = (bytes[8] & 0x3f) | 0x80;
  const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
  return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
}

/* Quem lê: um número guardado no navegador. Some com a limpeza do navegador
   ou numa aba anônima, e aí a próxima abertura parece de outro navegador. */
function chaveDoLeitor() {
  for (const guarda of [() => window.localStorage, () => window.sessionStorage]) {
    try {
      const lugar = guarda();
      const guardada = lugar.getItem(GUARDA_DO_LEITOR);
      if (guardada && UUID.test(guardada)) return guardada;
      const nova = novoId();
      lugar.setItem(GUARDA_DO_LEITOR, nova);
      return nova;
    } catch {
      /* Armazenamento bloqueado: tenta o próximo. */
    }
  }
  return novoId();
}

/* O Chrome esconde o modelo do Android no user agent e só conta por aqui. */
async function dicasDoNavegador(): Promise<{ modelo?: string | null; versao?: string | null }> {
  const dados = (navigator as Navigator & {
    userAgentData?: { getHighEntropyValues?: (dicas: string[]) => Promise<{ model?: string; platformVersion?: string }> };
  }).userAgentData;
  if (!dados?.getHighEntropyValues) return {};
  try {
    const valores = await Promise.race([
      dados.getHighEntropyValues(["model", "platformVersion"]),
      new Promise<null>((resolve) => window.setTimeout(() => resolve(null), 400)),
    ]);
    return { modelo: valores?.model || null, versao: valores?.platformVersion || null };
  } catch {
    return {};
  }
}

function enviar(corpo: Record<string, unknown>, porBeacon = false): Promise<Record<string, unknown> | null> {
  const json = JSON.stringify(corpo);
  if (porBeacon && typeof navigator.sendBeacon === "function") {
    try {
      if (navigator.sendBeacon(ENDERECO, new Blob([json], { type: "application/json" }))) return Promise.resolve(null);
    } catch {
      /* Cai para o fetch. */
    }
  }
  return fetch(ENDERECO, {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: json,
    keepalive: true,
    credentials: "same-origin",
  }).then((resposta) => (resposta.ok ? resposta.json() : Promise.reject(new Error(String(resposta.status)))));
}

async function contarPeloCaminhoAntigo(token: string) {
  try {
    const supabase = getSupabaseClient();
    const {
      data: { user },
    } = await supabase.auth.getUser();
    if (user) return;
    await supabase.rpc("record_public_quote_view", { p_token: token });
  } catch {
    /* Sem rede: não há o que fazer. */
  }
}

function iniciar(token: string, leitura: { id: string | null; pendentes: AcaoNaLeitura[] }, enviarAcao: (acao: AcaoNaLeitura) => void) {
  const leitor = chaveDoLeitor();
  let ativo = 0;
  let ultimoMovimento = Date.now();
  let rolagem = 0;
  let rolagemEnviada = 0;
  const vistas = new Set<string>();
  const enviadas = new Set<string>();

  function medirRolagem() {
    const folha = document.querySelector<HTMLElement>(".documento");
    if (!folha || !folha.offsetHeight) return;
    const topo = folha.getBoundingClientRect().top + window.scrollY;
    const altura = folha.offsetHeight;
    const visto = Math.min(window.scrollY + window.innerHeight, topo + altura) - topo;
    const fracao = Math.max(0, Math.min(100, Math.round((visto / altura) * 100)));
    if (fracao > rolagem) rolagem = fracao;
    if (rolagem >= 95) vistas.add("fim");
  }

  function sinal(acao: AcaoNaLeitura | null, porBeacon: boolean) {
    if (!leitura.id) return;
    const novas = [...vistas].filter((secao) => !enviadas.has(secao));
    if (!acao && ativo <= 0 && rolagem <= rolagemEnviada && novas.length === 0) return;
    const segundos = ativo;
    const rolagemDaVez = rolagem;
    ativo = 0;
    rolagemEnviada = rolagem;
    novas.forEach((secao) => enviadas.add(secao));
    enviar(
      {
        tipo: acao ? "acao" : "sinal",
        token,
        leitor,
        id: leitura.id,
        ativo: segundos,
        rolagem: rolagemDaVez,
        secoes: novas,
        ...(acao ? { acao } : {}),
      },
      porBeacon
    ).catch(() => {
      /* Não chegou: o tempo e as partes voltam para o próximo sinal. A ação
         se perde (reenviar poderia duplicar). */
      ativo += segundos;
      novas.forEach((secao) => enviadas.delete(secao));
      rolagemEnviada = Math.min(rolagemEnviada, rolagemDaVez - 1);
    });
  }

  const marcarMovimento = () => {
    ultimoMovimento = Date.now();
  };
  let quadro = 0;
  const aoRolar = () => {
    marcarMovimento();
    if (quadro) return;
    quadro = window.requestAnimationFrame(() => {
      quadro = 0;
      medirRolagem();
    });
  };

  window.addEventListener("scroll", aoRolar, { passive: true });
  window.addEventListener("resize", aoRolar, { passive: true });
  for (const evento of ["pointerdown", "pointermove", "keydown", "touchstart", "wheel"]) {
    window.addEventListener(evento, marcarMovimento, { passive: true });
  }
  window.addEventListener("beforeprint", () => enviarAcao("pdf"));
  document.addEventListener("visibilitychange", () => {
    if (document.visibilityState === "hidden") sinal(null, true);
    else marcarMovimento();
  });
  window.addEventListener("pagehide", () => sinal(null, true));

  window.setInterval(() => {
    if (document.visibilityState !== "visible") return;
    if (Date.now() - ultimoMovimento > PARADO_DEPOIS_DE_MS) return;
    ativo += 1;
  }, 1000);
  window.setInterval(() => sinal(null, false), SINAL_A_CADA_MS);

  if (typeof IntersectionObserver === "function") {
    const observador = new IntersectionObserver(
      (entradas) => {
        for (const entrada of entradas) {
          if (!entrada.isIntersecting) continue;
          const secao = (entrada.target as HTMLElement).dataset.leitura;
          /* Parte baixa: metade dela na tela. Parte alta (uma lista longa de
             itens): um terço da tela ocupado por ela. */
          const precisa = Math.min(entrada.boundingClientRect.height * 0.5, window.innerHeight * 0.35);
          if (secao && entrada.intersectionRect.height >= precisa) vistas.add(secao);
        }
      },
      { threshold: [0, 0.25, 0.5, 0.75, 1] }
    );
    document.querySelectorAll<HTMLElement>("[data-leitura]").forEach((parte) => observador.observe(parte));
  }
  window.setTimeout(medirRolagem, 800);

  void (async () => {
    const dicas = await dicasDoNavegador();
    try {
      const resposta = await enviar({
        tipo: "abriu",
        token,
        leitor,
        tela: {
          largura: window.screen?.width,
          altura: window.screen?.height,
          densidade: window.devicePixelRatio,
          toques: navigator.maxTouchPoints,
        },
        fuso: Intl.DateTimeFormat().resolvedOptions().timeZone,
        idioma: navigator.language,
        origem: document.referrer,
        dicas,
      });
      if (resposta?.ok === true && typeof resposta.id === "string") {
        leitura.id = resposta.id;
        const pendentes = leitura.pendentes.splice(0);
        pendentes.forEach((acao) => sinal(acao, false));
      }
    } catch {
      void contarPeloCaminhoAntigo(token);
    }
  })();

  return (acao: AcaoNaLeitura) => sinal(acao, false);
}

export function leituraDoOrcamento(token: string): Leitura {
  if (typeof window === "undefined" || !token) return NADA;
  const existente = leituras.get(token);
  if (existente) return existente;

  const estado: { id: string | null; pendentes: AcaoNaLeitura[] } = { id: null, pendentes: [] };
  let mandarAcao: ((acao: AcaoNaLeitura) => void) | null = null;
  let ultimoPdf = 0;

  const leitura: Leitura = {
    acao(acao) {
      /* O botão "Baixar PDF" e a impressão avisam o mesmo clique. */
      if (acao === "pdf") {
        if (Date.now() - ultimoPdf < 5000) return;
        ultimoPdf = Date.now();
      }
      if (!mandarAcao || !estado.id) {
        if (estado.pendentes.length < 10) estado.pendentes.push(acao);
        return;
      }
      mandarAcao(acao);
    },
  };
  leituras.set(token, leitura);

  const comecar = () => {
    mandarAcao = iniciar(token, estado, (acao) => leitura.acao(acao));
  };
  /* Página pré-carregada pelo navegador (a pessoa ainda não a abriu) não é
     leitura: espera ela aparecer de verdade. */
  const pagina = document as Document & { prerendering?: boolean };
  if (pagina.prerendering) document.addEventListener("prerenderingchange", comecar, { once: true });
  else comecar();

  return leitura;
}
