// LA PUERTA DE LA ESTACION FISCAL.
//
// La Estacion corre dentro del negocio (127.0.0.1), asi que la nube no la puede
// llamar: es ella la que hala. Por aca pregunta que ventas faltan por sellar,
// avisa cuando ya las sello, y manda su latido.
//
// Se identifica con la MISMA llave que le entrega la imprenta, en la cabecera.
// Si Gilberto le baja el breaker (cliente suspendido o llave revocada), todo
// esto deja de responder y la Estacion se entera sola.
interface Entorno {
  SUPABASE_URL: string;
  SUPABASE_SERVICE_KEY: string;
  /** Token de Mailtrap (el mismo del SMTP de activacion). Sin el, no se mandan correos y ya. */
  MAILTRAP_TOKEN?: string;
}

interface Peticion {
  accion?: string;
  rif?: string;
  datos?: Record<string, unknown>;
}

const ACCIONES = new Set([
  'pendientes',
  'confirmar',
  'error',
  // La anulacion viaja como solicitud: la Estacion la hala, emite la nota de
  // credito y responde. Gustito nunca escribe en el libro.
  'anulaciones',
  'anulacion_resultado',
  'latido',
  'rescatar',
  // El respaldo del libro fiscal, que llega cifrado: la imprenta guarda, no lee.
  'respaldo_hasta',
  'respaldo_guardar',
  'respaldo_bajar',
]);

function json(datos: unknown, estado = 200): Response {
  return new Response(JSON.stringify(datos), {
    status: estado,
    headers: { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' },
  });
}

/**
 * Anota un intento fallido. Va desde ACA y no desde la base a proposito: alla
 * el `raise exception` revierte la transaccion, y con ella se iria el registro
 * (ese era el bug que hacia decorativo el freno anti fuerza bruta).
 */
function anotarFallo(env: Entorno, rif: string, motivo: string, desde: string | null): Promise<unknown> {
  return fetch(`${env.SUPABASE_URL}/rest/v1/rpc/fn_registrar_intento`, {
    method: 'POST',
    headers: {
      apikey: env.SUPABASE_SERVICE_KEY,
      Authorization: `Bearer ${env.SUPABASE_SERVICE_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ p_quien: rif, p_motivo: motivo, p_desde: desde, p_tipo: 'estacion' }),
  }).catch(() => null);
}

function rpc(env: Entorno, fn: string, cuerpo: Record<string, unknown>): Promise<Response> {
  return fetch(`${env.SUPABASE_URL}/rest/v1/rpc/${fn}`, {
    method: 'POST',
    headers: {
      apikey: env.SUPABASE_SERVICE_KEY,
      Authorization: `Bearer ${env.SUPABASE_SERVICE_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(cuerpo),
  });
}

interface CorreoPendiente {
  id: string;
  para: string;
  token: string;
  negocio: string;
  rif?: string | null;
  domicilio?: string | null;
  serie: string | null;
  numero: number | null;
  control: number | null;
  fecha: string | null;
  fecha_asignacion?: string | null;
  total_bs: number | null;
  /** El DocumentoFiscal completo tal como quedo sellado: con el se arma el PDF. */
  documento?: Record<string, unknown> | null;
  /** La cara del negocio, congelada al emitir. */
  marca?: { mensaje_pie?: string | null; logo_url?: string | null; color?: string | null } | null;
  imprenta?: { rif: string; razon_social: string; providencia?: string | null; providencia_fecha?: string | null } | null;
}

/** Uint8Array a base64 por pedazos: el spread de un PDF entero revienta la pila. */
function aBase64(bytes: Uint8Array): string {
  let binario = '';
  for (let i = 0; i < bytes.length; i += 8192) {
    binario += String.fromCharCode(...bytes.subarray(i, i + 8192));
  }
  return btoa(binario);
}

/**
 * Baja el logo del negocio para meterlo en el PDF. Con paracaidas: maximo
 * 400 KB, solo PNG o JPG, y cualquier falla devuelve null (la factura sale
 * sin logo, jamas se queda sin salir por un logo caido).
 */
async function bajarLogo(url?: string | null): Promise<{ bytes: Uint8Array; tipo: 'png' | 'jpg' } | null> {
  if (!url || !/^https:\/\//.test(url)) return null;
  try {
    const r = await fetch(url, { signal: AbortSignal.timeout(4000) });
    if (!r.ok) return null;
    const bytes = new Uint8Array(await r.arrayBuffer());
    if (bytes.length === 0 || bytes.length > 400_000) return null;
    if (bytes[0] === 0x89 && bytes[1] === 0x50) return { bytes, tipo: 'png' };
    if (bytes[0] === 0xff && bytes[1] === 0xd8) return { bytes, tipo: 'jpg' };
    return null;
  } catch {
    return null;
  }
}

/**
 * El PDF adjunto del correo. Si el documento completo no esta disponible
 * (correos viejos de la cola) o algo falla, devuelve null y el correo sale
 * igual con su enlace: el PDF es cortesia, el documento con valor es el
 * digital sellado.
 */
async function armarPdf(c: CorreoPendiente, enlace: string): Promise<string | null> {
  if (!c.documento || c.numero == null || c.control == null || !c.fecha) return null;
  try {
    const { pdfFactura } = await import('../lib/pdf_factura.js');
    const bytes = await pdfFactura({
      tipo: 'factura',
      serie: c.serie ?? 'A',
      numero: c.numero,
      control: c.control,
      fecha: c.fecha,
      fecha_asignacion: c.fecha_asignacion ?? null,
      documento: c.documento,
      marca: c.marca ?? null,
      logo: await bajarLogo(c.marca?.logo_url),
      imprenta: c.imprenta ?? null,
      enlace,
    });
    return aBase64(bytes);
  } catch {
    return null;
  }
}

/**
 * El cuerpo del correo: sobrio, con el enlace del sello y la marca del negocio
 * si la configuro (logo arriba, su color en el boton, su mensaje al pie). Sin
 * adornos que huelan a spam.
 */
function cuerpoCorreo(c: CorreoPendiente, enlace: string, conPdf: boolean): string {
  const numero = c.numero != null ? `${c.serie ? c.serie + '-' : ''}${String(c.numero).padStart(8, '0')}` : null;
  const color = c.marca?.color && /^#[0-9a-fA-F]{6}$/.test(c.marca.color) ? c.marca.color : '#0D0F1E';
  const logo = c.marca?.logo_url && /^https:\/\/\S+$/.test(c.marca.logo_url)
    ? `<img src="${c.marca.logo_url}" alt="" style="max-height:52px;max-width:220px;margin-bottom:14px" />`
    : '';
  const filas: string[] = [];
  if (numero) filas.push(`<tr><td style="padding:4px 12px 4px 0;color:#666">Factura</td><td style="padding:4px 0"><b>${numero}</b></td></tr>`);
  if (c.control != null) filas.push(`<tr><td style="padding:4px 12px 4px 0;color:#666">N&uacute;mero de control</td><td style="padding:4px 0"><b>${String(c.control).padStart(8, '0')}</b></td></tr>`);
  if (c.total_bs != null) filas.push(`<tr><td style="padding:4px 12px 4px 0;color:#666">Total</td><td style="padding:4px 0"><b>Bs ${Number(c.total_bs).toFixed(2)}</b></td></tr>`);
  const mensaje = c.marca?.mensaje_pie
    ? `<p style="margin:16px 0 0;font-size:12.5px;color:${color}">${String(c.marca.mensaje_pie).slice(0, 240).replace(/[<>&]/g, '')}</p>`
    : '';
  const notaPdf = conPdf
    ? `<p style="margin:16px 0 0;font-size:12px;color:#888">Tu factura va adjunta en PDF a este correo.</p>`
    : '';
  return `<!doctype html><html lang="es"><body style="margin:0;padding:24px;background:#f6f6f4;font-family:Arial,Helvetica,sans-serif;color:#1a1a1a">
  <div style="max-width:520px;margin:0 auto;background:#ffffff;border:1px solid #e5e5e2;border-radius:10px;padding:28px">
    ${logo}
    <p style="margin:0 0 4px;font-size:13px;color:#666">Documento fiscal de</p>
    <h2 style="margin:0 0 18px;font-size:20px">${c.negocio}</h2>
    <table style="border-collapse:collapse;font-size:14px;margin-bottom:20px">${filas.join('')}</table>
    <a href="${enlace}" style="display:inline-block;background:${color};color:#ffffff;text-decoration:none;padding:12px 22px;border-radius:8px;font-size:14px;font-weight:bold">Ver y verificar mi factura</a>
    ${notaPdf}
    ${mensaje}
    <p style="margin:14px 0 0;font-size:11px;color:#aaa">Este correo lo env&iacute;a la plataforma de facturaci&oacute;n que usa ${c.negocio}. Guarda el enlace: verifica tu documento cuando lo necesites.</p>
  </div>
</body></html>`;
}

/**
 * Drena la cola de correos de factura (pocos por ronda, sin bloquear a nadie).
 * Corre despues de responderle a la Estacion, en waitUntil: si Mailtrap esta
 * caido o el secreto no esta cargado, la cola espera y no se pierde nada.
 */
async function drenarCorreos(env: Entorno, origen: string): Promise<void> {
  if (!env.MAILTRAP_TOKEN) return;
  try {
    const r = await rpc(env, 'fn_correos_factura_pendientes', { p_limite: 5 });
    if (!r.ok) return;
    const pendientes = (await r.json()) as CorreoPendiente[];
    for (const c of pendientes) {
      const enlace = `https://sello.gustitoxpress.app/v/${c.token}`;
      const numero = c.numero != null ? ` N° ${c.serie ? c.serie + '-' : ''}${c.numero}` : '';
      try {
        // El PDF con la cara del negocio. Si no se puede armar, el correo sale
        // igual con su enlace: nunca se queda una factura sin llegar por el PDF.
        const pdf = await armarPdf(c, enlace);
        const envio = await fetch('https://send.api.mailtrap.io/api/send', {
          method: 'POST',
          headers: { 'Api-Token': env.MAILTRAP_TOKEN, 'Content-Type': 'application/json' },
          body: JSON.stringify({
            from: { email: 'no-reply@gustitoxpress.app', name: c.negocio },
            to: [{ email: c.para }],
            subject: `Tu factura de ${c.negocio}${numero}`,
            html: cuerpoCorreo(c, enlace, !!pdf),
            text: `Tu factura de ${c.negocio}${numero}. Vela y verificala aqui: ${enlace}`,
            category: 'factura',
            ...(pdf
              ? {
                  attachments: [{
                    content: pdf,
                    filename: `factura-${c.serie ?? 'A'}-${String(c.numero).padStart(8, '0')}.pdf`,
                    type: 'application/pdf',
                    disposition: 'attachment',
                  }],
                }
              : {}),
          }),
        });
        const detalle = envio.ok ? null : (await envio.text()).slice(0, 300);
        await rpc(env, 'fn_correo_factura_resultado', { p_id: c.id, p_ok: envio.ok, p_error: detalle });
      } catch (e) {
        await rpc(env, 'fn_correo_factura_resultado', { p_id: c.id, p_ok: false, p_error: `${origen}: ${(e as Error).message}`.slice(0, 300) });
      }
    }
  } catch {
    /* la cola espera a la proxima ronda */
  }
}

export const onRequestPost: PagesFunction<Entorno> = async (ctx) => {
  const llave = (ctx.request.headers.get('authorization') ?? '').replace(/^Bearer\s+/i, '').trim();
  if (!llave) return json({ error: 'Falta la llave de acceso' }, 401);

  let p: Peticion;
  try {
    p = (await ctx.request.json()) as Peticion;
  } catch {
    return json({ error: 'El cuerpo no es un JSON valido' }, 400);
  }

  if (!p.rif) return json({ error: 'Falta el rif' }, 400);
  if (!p.accion || !ACCIONES.has(p.accion)) {
    return json({ error: 'Accion invalida', validas: [...ACCIONES] }, 400);
  }

  const r = await fetch(`${ctx.env.SUPABASE_URL}/rest/v1/rpc/fn_estacion`, {
    method: 'POST',
    headers: {
      apikey: ctx.env.SUPABASE_SERVICE_KEY,
      Authorization: `Bearer ${ctx.env.SUPABASE_SERVICE_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      p_accion: p.accion,
      p_llave: llave,
      p_rif: p.rif,
      p_datos: p.datos ?? {},
    }),
  });

  if (!r.ok) {
    const detalle = (await r.json().catch(() => ({}))) as { message?: string };
    const msg = detalle.message ?? '';
    const desde = ctx.request.headers.get('cf-connecting-ip') ?? null;
    // Sin decir si fallo el RIF o la llave.
    if (/no autorizado/i.test(msg)) {
      ctx.waitUntil(anotarFallo(ctx.env, p.rif, `estacion: ${p.accion}`, desde));
      return json({ error: 'No autorizado' }, 401);
    }
    if (/demasiados intentos/i.test(msg)) return json({ error: 'Demasiados intentos fallidos' }, 429);
    return json({ error: msg || 'No se pudo procesar' }, 400);
  }

  // Tras confirmar un sellado (o en el latido periodico) se aprovecha de mandar
  // los correos de factura que esten esperando. Fuera de la respuesta: la
  // Estacion no espera por Mailtrap.
  if (p.accion === 'confirmar' || p.accion === 'latido') {
    ctx.waitUntil(drenarCorreos(ctx.env, p.accion));
  }

  return json(await r.json());
};
