import prisma from "../config/database";
import { config } from "../config/env";
import { sendMail } from "./mailer";
import {
  localDateCzStr,
  localDateStr,
  utcMidnightOfLocalDay,
} from "../utils/date";
import { getSystemSettings } from "./system-settings";

interface AlertInvoice {
  id: number;
  type: "created" | "received";
  alert_type: "due" | "3days";
  number: string;
  counterparty: string;
  amount: string;
  currency: string;
  due_date: string;
  days_label: string;
}

function escapeHtml(str: string): string {
  return str
    .replace(/&/g, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;")
    .replace(/"/g, "&quot;")
    .replace(/'/g, "&#39;");
}

function formatAmount(n: number | { toNumber?: () => number }): string {
  const num = typeof n === "number" ? n : Number(n);
  return num.toLocaleString("cs-CZ", {
    minimumFractionDigits: 2,
    maximumFractionDigits: 2,
  });
}

/**
 * Alert window [today, today+3] for the due-date queries + the local date
 * strings the classifier matches against.
 *
 * due_date is @db.Date: Prisma truncates the filter Dates to their UTC date
 * part, so the window boundaries must be UTC midnights of the LOCAL calendar
 * day. A local midnight (22:00/23:00 UTC of the previous day) shifted the
 * whole window a day early — the "splatnost za 3 dny" advance alert
 * (due == today+3) could then never fire.
 *
 * Exported (with an injectable `now`) so the window math is unit-testable.
 */
export function computeAlertWindow(now: Date = new Date()) {
  const today = utcMidnightOfLocalDay(now);
  const todayStr = localDateStr(now);
  const in3days = new Date(today);
  in3days.setUTCDate(in3days.getUTCDate() + 3);
  const in3daysStr = localDateStr(
    new Date(now.getFullYear(), now.getMonth(), now.getDate() + 3),
  );
  return { today, todayStr, in3days, in3daysStr };
}

export async function checkInvoiceAlerts(): Promise<void> {
  const settings = await getSystemSettings();
  const alertEmail = settings.invoice_alert_email || config.email.invoiceAlert;
  if (!alertEmail) return;

  const { today, todayStr, in3days, in3daysStr } = computeAlertWindow();

  // Classify a due date into an alert type/label, or null if it doesn't match.
  const classify = (
    dueDate: Date,
  ): { alertType: "due" | "3days"; daysLabel: string } | null => {
    const dueDateStr = localDateStr(dueDate);
    if (dueDateStr === todayStr)
      return { alertType: "due", daysLabel: "splatnost dnes" };
    if (dueDateStr === in3daysStr)
      return { alertType: "3days", daysLabel: "splatnost za 3 dny" };
    return null;
  };

  // --- Gather candidates (created + received) before touching the alert log ---
  const [createdInvoices, receivedInvoices] = await Promise.all([
    prisma.invoices.findMany({
      where: {
        status: { in: ["issued", "overdue"] },
        due_date: { gte: today, lte: in3days },
      },
      select: {
        id: true,
        invoice_number: true,
        due_date: true,
        currency: true,
        apply_vat: true,
        vat_rate: true,
        customers: { select: { name: true } },
        invoice_items: {
          select: { quantity: true, unit_price: true, vat_rate: true },
        },
      },
    }),
    prisma.received_invoices.findMany({
      where: {
        status: "unpaid",
        due_date: { gte: today, lte: in3days },
      },
      select: {
        id: true,
        invoice_number: true,
        supplier_name: true,
        amount: true,
        currency: true,
        due_date: true,
      },
    }),
  ]);

  // Build the candidate set (matched-on-date, not yet de-duped against the log)
  // so we can batch-load the alert log in a single query.
  type Candidate = AlertInvoice;
  const createdCandidates: Candidate[] = [];
  for (const inv of createdInvoices) {
    if (!inv.due_date) continue;
    const c = classify(new Date(inv.due_date));
    if (!c) continue;

    // Gross total (subtotal + VAT) — consistent with the gross amounts shown
    // for received invoices. VAT is computed per line, rounded to 2 decimals.
    const subtotal = inv.invoice_items.reduce(
      (sum, item) => sum + Number(item.quantity) * Number(item.unit_price),
      0,
    );
    const vatTotal = inv.apply_vat
      ? inv.invoice_items.reduce((sum, item) => {
          const base = Number(item.quantity) * Number(item.unit_price);
          const rate = Number(item.vat_rate) || Number(inv.vat_rate) || 21;
          return sum + Math.round(base * (rate / 100) * 100) / 100;
        }, 0)
      : 0;
    const gross = Math.round((subtotal + vatTotal) * 100) / 100;

    createdCandidates.push({
      id: inv.id,
      type: "created",
      alert_type: c.alertType,
      number: inv.invoice_number || `#${inv.id}`,
      counterparty: inv.customers?.name || "—",
      amount: formatAmount(gross),
      currency: inv.currency || "CZK",
      due_date: localDateCzStr(new Date(inv.due_date)),
      days_label: c.daysLabel,
    });
  }

  const receivedCandidates: Candidate[] = [];
  for (const inv of receivedInvoices) {
    if (!inv.due_date) continue;
    const c = classify(new Date(inv.due_date));
    if (!c) continue;

    receivedCandidates.push({
      id: inv.id,
      type: "received",
      alert_type: c.alertType,
      number: inv.invoice_number || inv.supplier_name,
      counterparty: inv.supplier_name,
      // received_invoices.amount is GROSS (VAT-inclusive) by convention.
      amount: formatAmount(inv.amount),
      currency: inv.currency,
      due_date: localDateCzStr(new Date(inv.due_date)),
      days_label: c.daysLabel,
    });
  }

  const candidates = [...createdCandidates, ...receivedCandidates];
  if (candidates.length === 0) return;

  // Batch-load every already-sent log entry for the candidate set in ONE query
  // (instead of a findUnique per invoice).
  const sentLogs = await prisma.invoice_alert_log.findMany({
    where: {
      OR: candidates.map((c) => ({
        invoice_type: c.type,
        invoice_id: c.id,
        alert_type: c.alert_type,
      })),
    },
    select: { invoice_type: true, invoice_id: true, alert_type: true },
  });
  const sentKeys = new Set(
    sentLogs.map((l) => `${l.invoice_type}:${l.invoice_id}:${l.alert_type}`),
  );

  const alerts: AlertInvoice[] = candidates.filter(
    (c) => !sentKeys.has(`${c.type}:${c.id}:${c.alert_type}`),
  );

  if (alerts.length === 0) return;

  // --- Build summary email ---
  const createdAlerts = alerts.filter((a) => a.type === "created");
  const receivedAlerts = alerts.filter((a) => a.type === "received");

  const subject = `Upozornění na splatnost faktur (${alerts.length})`;

  const buildTable = (items: AlertInvoice[], title: string) => {
    if (items.length === 0) return "";
    const rows = items
      .map(
        (a) => `
        <tr>
          <td style="padding: 8px; border-bottom: 1px solid #ddd;">${escapeHtml(a.number)}</td>
          <td style="padding: 8px; border-bottom: 1px solid #ddd;">${escapeHtml(a.counterparty)}</td>
          <td style="padding: 8px; border-bottom: 1px solid #ddd; text-align: right;">${a.amount} ${a.currency}</td>
          <td style="padding: 8px; border-bottom: 1px solid #ddd;">${a.due_date}</td>
          <td style="padding: 8px; border-bottom: 1px solid #ddd; color: ${a.days_label.includes("dnes") ? "#dc3545" : "#e67e22"}; font-weight: bold;">${a.days_label}</td>
        </tr>`,
      )
      .join("");

    return `
      <h3 style="color: #333; margin-top: 24px;">${escapeHtml(title)} (${items.length})</h3>
      <table style="width: 100%; border-collapse: collapse; margin: 12px 0;">
        <thead>
          <tr style="background: #f5f5f5;">
            <th style="padding: 8px; text-align: left;">Číslo</th>
            <th style="padding: 8px; text-align: left;">Firma</th>
            <th style="padding: 8px; text-align: right;">Částka</th>
            <th style="padding: 8px; text-align: left;">Splatnost</th>
            <th style="padding: 8px; text-align: left;">Stav</th>
          </tr>
        </thead>
        <tbody>${rows}</tbody>
      </table>`;
  };

  const html = `
    <html>
    <body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
      <h2 style="color: #dc3545;">Upozornění na splatnost faktur</h2>
      <p>Následující faktury se blíží ke splatnosti nebo jsou dnes splatné:</p>
      ${buildTable(createdAlerts, "Vydané faktury (neuhrazené zákazníkem)")}
      ${buildTable(receivedAlerts, "Přijaté faktury (k úhradě)")}
      <hr style="margin: 30px 0; border: none; border-top: 1px solid #ddd;">
      <p style="font-size: 12px; color: #999;">
        Automatické upozornění vygenerováno ${localDateCzStr(today)}.
      </p>
    </body>
    </html>`;

  const sent = await sendMail(alertEmail, subject, html);
  if (!sent) {
    console.error(`InvoiceAlerts: Failed to send alert to ${alertEmail}`);
    return;
  }

  console.log(`InvoiceAlerts: Sent ${alerts.length} alert(s) to ${alertEmail}`);

  // Mark alerts as sent only after successful delivery. One INSERT with
  // skipDuplicates so a unique-constraint hit (concurrent run) on one row
  // doesn't abort logging the rest.
  try {
    await prisma.invoice_alert_log.createMany({
      data: alerts.map((a) => ({
        invoice_type: a.type,
        invoice_id: a.id,
        alert_type: a.alert_type,
      })),
      skipDuplicates: true,
    });
  } catch (err) {
    // Non-fatal: the email already went out. Log so a failed bookkeeping
    // write is visible (and may cause a duplicate alert on the next run).
    console.error("InvoiceAlerts: failed to record alert log:", err);
  }
}
