// API DEL PANEL DE LA IMPRENTA.
//
// Todo pasa por aca: entrar, ver el tablero, dar de alta clientes, generar y
// revocar llaves, y sacar el reporte mensual del SENIAT.
//
// La sesion va en una galleta firmada (HMAC) que el navegador no puede leer ni
// falsificar. Sin sesion valida, ninguna accion pasa.
interface Entorno {
  SUPABASE_URL: string;
  SUPABASE_SERVICE_KEY: string;
  PANEL_SECRET: string;
}

const HORAS_SESION = 8;

function json(datos: unknown, estado = 200, galleta?: string): Response {
  const cabeceras: Record<string, string> = {
    'content-type': 'application/json; charset=utf-8',
    'cache-control': 'no-store',
  };
  if (galleta) cabeceras['set-cookie'] = galleta;
  return new Response(JSON.stringify(datos), { status: estado, headers: cabeceras });
}

async function firmar(texto: string, secreto: string): Promise<string> {
  const llave = await crypto.subtle.importKey(
    'raw',
    new TextEncoder().encode(secreto),
    { name: 'HMAC', hash: 'SHA-256' },
    false,
    ['sign'],
  );
  const firma = await crypto.subtle.sign('HMAC', llave, new TextEncoder().encode(texto));
  return [...new Uint8Array(firma)].map((b) => b.toString(16).padStart(2, '0')).join('');
}

/**
 * Arma la galleta de sesion: quien entro y hasta cuando. Lleva el nombre (para
 * mostrar y para firmar los movimientos) y el usuario (que es el unico, y hace
 * falta para que nadie se apague a si mismo).
 */
async function armarSesion(nombre: string, usuario: string, secreto: string): Promise<string> {
  const hasta = Date.now() + HORAS_SESION * 3600_000;
  const cuerpo = btoa(JSON.stringify({ nombre, usuario, hasta }));
  const firma = await firmar(cuerpo, secreto);
  return `gxf_panel=${cuerpo}.${firma}; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=${HORAS_SESION * 3600}`;
}

/** Revisa la galleta. Devuelve quien entro, o null. */
async function verSesion(req: Request, secreto: string): Promise<{ nombre: string; usuario: string } | null> {
  const galletas = req.headers.get('cookie') ?? '';
  const cruda = galletas.split(';').map((c) => c.trim()).find((c) => c.startsWith('gxf_panel='));
  if (!cruda) return null;
  const [cuerpo, firma] = cruda.slice('gxf_panel='.length).split('.');
  if (!cuerpo || !firma) return null;
  if ((await firmar(cuerpo, secreto)) !== firma) return null;
  try {
    const datos = JSON.parse(atob(cuerpo)) as { nombre: string; usuario?: string; hasta: number };
    if (Date.now() > datos.hasta) return null;
    return { nombre: datos.nombre, usuario: datos.usuario ?? '' };
  } catch {
    return null;
  }
}

/**
 * Llama al panel en la base. `quien` y `desde` los pone ESTA funcion desde la
 * sesion firmada, nunca el navegador: si vinieran del cuerpo, cualquiera podria
 * firmar sus movimientos con el nombre de otro.
 */
async function llamarBase(
  env: Entorno,
  accion: string,
  datos: unknown,
  quien?: string | null,
  desde?: string | null,
  yo?: string | null,
): Promise<unknown> {
  const r = await fetch(`${env.SUPABASE_URL}/rest/v1/rpc/fn_panel_imprenta`, {
    method: 'POST',
    headers: {
      apikey: env.SUPABASE_SERVICE_KEY,
      Authorization: `Bearer ${env.SUPABASE_SERVICE_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      p_accion: accion,
      p_datos: datos ?? {},
      p_quien: quien ?? null,
      p_desde: desde ?? null,
      p_yo: yo ?? null,
    }),
  });
  if (!r.ok) {
    const detalle = (await r.json().catch(() => ({}))) as { message?: string };
    throw new Error(detalle.message ?? `Error consultando (${r.status})`);
  }
  return r.json();
}

/** Acciones que NO necesitan sesion (solo entrar). */
const ABIERTAS = new Set(['entrar']);

export const onRequestPost: PagesFunction<Entorno> = async (ctx) => {
  let peticion: { accion?: string; datos?: Record<string, unknown> };
  try {
    peticion = (await ctx.request.json()) as typeof peticion;
  } catch {
    return json({ error: 'Peticion invalida' }, 400);
  }
  const accion = peticion.accion ?? '';
  const datos = peticion.datos ?? {};
  const desde = ctx.request.headers.get('cf-connecting-ip');
  let sesion: { nombre: string; usuario: string } | null = null;

  try {
    if (accion === 'salir') {
      return json({ ok: true }, 200, 'gxf_panel=; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=0');
    }

    if (!ABIERTAS.has(accion)) {
      sesion = await verSesion(ctx.request, ctx.env.PANEL_SECRET);
      if (!sesion) return json({ error: 'Sesion vencida' }, 401);
      // El navegador no decide quien hizo las cosas: si mando un `quien`, se
      // bota. El bueno es el de la sesion y lo pone llamarBase.
      delete datos.quien;
    }

    if (accion === 'entrar') {
      // La direccion la pone Cloudflare, nunca el navegador: si se aceptara del
      // cuerpo, el que ataca se inventaria una distinta en cada intento y el
      // freno por direccion no serviria de nada.
      datos.desde = desde ?? null;
      const r = (await llamarBase(ctx.env, 'entrar', datos)) as {
        entro: boolean;
        nombre: string | null;
        espera_seg: number | null;
      };
      if (r?.espera_seg) {
        const min = Math.max(1, Math.ceil(r.espera_seg / 60));
        return json({ error: `Demasiados intentos fallidos. Espera ${min} minuto${min === 1 ? '' : 's'}.` }, 429);
      }
      if (!r?.entro) return json({ error: 'Usuario o clave incorrectos' }, 401);
      const usuario = String(datos.usuario ?? '').trim().toLowerCase();
      return json(
        { ok: true, nombre: r.nombre },
        200,
        await armarSesion(r.nombre ?? 'operador', usuario, ctx.env.PANEL_SECRET),
      );
    }

    return json({
      ok: true,
      datos: await llamarBase(ctx.env, accion, datos, sesion?.nombre, desde, sesion?.usuario),
    });
  } catch (e) {
    return json({ error: (e as Error).message }, 400);
  }
};

/** Saber si hay sesion abierta, para que la pantalla sepa que mostrar. */
export const onRequestGet: PagesFunction<Entorno> = async (ctx) => {
  const quien = await verSesion(ctx.request, ctx.env.PANEL_SECRET);
  return json({ sesion: !!quien, nombre: quien });
};
