import { randomUUID } from "node:crypto";
import { DB } from "./database.ts";
import { ensureApplicationPdf } from "./application-pdf.ts";
import { sendConfiguredEmail, type EmailInput } from "./email.ts";
import {
  familyConfirmationEmail,
  internalNotificationEmail,
  documentRequestEmail,
} from "../emails/templates.ts";
export type Job = {
  id: string;
  kind: string;
  resource_id: string;
  attempts: number;
  lock_token: string;
};
export async function claimJob(): Promise<Job | null> {
  const now = new Date().toISOString(),
    token = randomUUID(),
    until = new Date(Date.now() + 5 * 60000).toISOString();
  return DB.prepare(
    "UPDATE outbox_jobs SET status='PROCESSING',lock_token=?,locked_until=?,attempts=attempts+1,updated_at=? WHERE id=(SELECT id FROM outbox_jobs WHERE (status='PENDING' AND available_at<=?) OR (status='PROCESSING' AND locked_until<?) ORDER BY CASE kind WHEN 'GENERATE_APPLICATION_PDF' THEN 0 ELSE 1 END,created_at LIMIT 1) RETURNING id,kind,resource_id,attempts,lock_token",
  )
    .bind(token, until, now, now, now)
    .first<Job>();
}
export async function processJob(
  job: Job,
  send: (input: EmailInput) => Promise<unknown> = sendConfiguredEmail,
) {
  if (job.kind === "GENERATE_APPLICATION_PDF") {
    await ensureApplicationPdf(job.resource_id);
    return;
  }
  if (job.kind !== "SEND_APPLICATION_EMAIL") throw new Error("unsupported_job");
  const c = await DB.prepare(
    "SELECT m.*,a.public_number,a.deleted_at,c.full_name,s.name response_name FROM communications m JOIN applications a ON a.id=m.application_id JOIN children c ON c.application_id=a.id JOIN social_responses s ON s.id=a.social_response_id WHERE m.id=?",
  )
    .bind(job.resource_id)
    .first<{
      id: string;
      application_id: string;
      recipient: string;
      template_key: string;
      status: string;
      message_body: string | null;
      document_request_id: string | null;
      public_number: string;
      deleted_at: string | null;
      full_name: string;
      response_name: string;
    }>();
  if (!c || c.status === "SENT") return;
  if (c.deleted_at) throw new Error("application_archived");
  const input = {
    name: c.full_name,
    publicNumber: c.public_number,
    response: c.response_name,
  };
  let template: { subject: string; html: string },
    attachment: Uint8Array | undefined;
  if (c.template_key === "APPLICATION_CONFIRMATION") {
    attachment = await ensureApplicationPdf(c.application_id);
    template = familyConfirmationEmail(input);
  } else if (c.template_key === "APPLICATION_INTERNAL_NOTIFICATION")
    template = internalNotificationEmail({
      ...input,
      adminUrl: `${process.env.APP_URL || "http://localhost:3000"}/administracao/candidaturas/${c.application_id}`,
    });
  else if (c.template_key === "DOCUMENT_REQUEST")
    template = documentRequestEmail({
      ...input,
      message: c.message_body || "Contacte a secretaria.",
    });
  else throw new Error("unsupported_template");
  await send({
    to: c.recipient,
    ...template,
    attachment,
    messageId: `<${c.id}@${new URL(process.env.APP_URL || "http://localhost:3000").hostname}>`,
  });
  const now = new Date().toISOString();
  await DB.prepare(
    "UPDATE communications SET status='SENT',attempts=attempts+1,last_error_code=NULL,sent_at=?,updated_at=? WHERE id=?",
  )
    .bind(now, now, c.id)
    .run();
  if (c.document_request_id)
    await DB.prepare(
      "UPDATE document_requests SET status='SENT',sent_at=? WHERE id=?",
    )
      .bind(now, c.document_request_id)
      .run();
}
export async function runJobs(
  limit = 10,
  send?: (input: EmailInput) => Promise<unknown>,
) {
  let processed = 0,
    failed = 0;
  for (let i = 0; i < Math.min(50, Math.max(1, limit)); i++) {
    const job = await claimJob();
    if (!job) break;
    try {
      await processJob(job, send);
      await DB.prepare(
        "UPDATE outbox_jobs SET status='DONE',locked_until=NULL,lock_token=NULL,last_error_code=NULL,updated_at=? WHERE id=? AND lock_token=?",
      )
        .bind(new Date().toISOString(), job.id, job.lock_token)
        .run();
      processed++;
    } catch (error) {
      failed++;
      const code =
        (error as { code?: string }).code ||
        (error instanceof Error && /^[a-z_]+$/.test(error.message)
          ? error.message
          : "processing_failed");
      const final =
        job.attempts >= 5 ||
        code === "application_archived" ||
        code === "application_not_found";
      const now = new Date().toISOString(),
        available = new Date(
          Date.now() + Math.min(60, 2 ** job.attempts) * 60000,
        ).toISOString();
      await DB.prepare(
        "UPDATE outbox_jobs SET status=?,available_at=?,last_error_code=?,locked_until=NULL,lock_token=NULL,updated_at=? WHERE id=? AND lock_token=?",
      )
        .bind(
          final ? "FAILED" : "PENDING",
          available,
          code,
          now,
          job.id,
          job.lock_token,
        )
        .run();
      if (job.kind === "SEND_APPLICATION_EMAIL")
        await DB.prepare(
          "UPDATE communications SET status='FAILED',attempts=attempts+1,last_error_code=?,updated_at=? WHERE id=? AND status!='SENT'",
        )
          .bind(code, now, job.resource_id)
          .run();
    }
  }
  return { processed, failed };
}
