import { FastifyInstance } from "fastify";
import prisma from "../../config/database";
import { requirePermission } from "../../middleware/auth";
import { htmlToPdf } from "../../utils/html-to-pdf";
import { parseId, error } from "../../utils/response";
import {
  formatDate,
  formatNum,
  escapeHtml,
  cleanQuillHtml,
  buildQuillIndentCss,
} from "../../utils/pdf-shared";
import { parseBody } from "../../schemas/common";
import { z } from "zod";
import createDOMPurify from "dompurify";
import { JSDOM } from "jsdom";

const window = new JSDOM("").window;
const DOMPurify = createDOMPurify(window);

// Plain object (Zod strips unknown keys rather than rejecting) — the frontend
// may send extra item fields (id, position, item_description) we don't use.
const OrderPdfItemSchema = z.object({
  description: z.string().max(8000),
  quantity: z.number().min(0).finite(),
  unit: z.string().max(255),
  unit_price: z.number().min(0).finite(),
  is_included_in_total: z.boolean().optional(),
});

// `z.looseObject` is the Zod 4 replacement for the deprecated `.passthrough()`.
// `items` is strictly validated; `lang` is typed explicitly so the handler no
// longer reads it as an untyped passthrough key.
const OrderPdfBodySchema = z.looseObject({
  items: z.array(OrderPdfItemSchema).optional(),
  lang: z.string().max(10).optional(),
});

/* ── Helpers ─────────────────────────────────────────────────────── */

interface AddressResult {
  name: string;
  lines: string[];
}

function buildAddressLines(
  entity: Record<string, unknown> | null,
  isSupplier: boolean,
  tObj: Record<string, string>,
): AddressResult {
  if (!entity) return { name: "", lines: [] };

  const nameKey = isSupplier ? "company_name" : "name";
  const name = String(entity[nameKey] || "");

  let cfData: Array<{ name?: string; value?: string; showLabel?: boolean }> =
    [];
  let fieldOrder: string[] | null = null;
  const raw = entity.custom_fields;
  if (raw) {
    let parsed: unknown;
    try {
      parsed = typeof raw === "string" ? JSON.parse(raw) : raw;
    } catch {
      parsed = null;
    }
    if (parsed && typeof parsed === "object") {
      if ((parsed as Record<string, unknown>).fields) {
        cfData =
          ((parsed as Record<string, unknown>).fields as typeof cfData) || [];
        fieldOrder = ((parsed as Record<string, unknown>).field_order ||
          (parsed as Record<string, unknown>).fieldOrder) as string[] | null;
      } else if (Array.isArray(parsed)) {
        cfData = parsed;
      }
    }
  }

  if (Array.isArray(fieldOrder)) {
    const legacyMap: Record<string, string> = {
      Name: "name",
      CompanyName: "company_name",
      Street: "street",
      CityPostal: "city_postal",
      Country: "country",
      CompanyId: "company_id",
      VatId: "vat_id",
    };
    fieldOrder = fieldOrder.map((k) => legacyMap[k] || k);
  }

  const fieldMap: Record<string, string> = {};
  if (name) fieldMap[nameKey] = name;
  if (entity.street) fieldMap.street = String(entity.street);
  const cityParts = [entity.city || "", entity.postal_code || ""]
    .filter(Boolean)
    .map(String);
  const cityPostal = cityParts.join(" ").trim();
  if (cityPostal) fieldMap.city_postal = cityPostal;
  if (entity.country) fieldMap.country = String(entity.country);
  if (entity.company_id)
    fieldMap.company_id = `${tObj.ico}${entity.company_id}`;
  if (entity.vat_id) fieldMap.vat_id = `${tObj.dic}${entity.vat_id}`;

  cfData.forEach((cf, i) => {
    const cfName = (cf.name || "").trim();
    const cfValue = (cf.value || "").trim();
    const showLabel = cf.showLabel !== false;
    if (cfValue) {
      fieldMap[`custom_${i}`] =
        showLabel && cfName ? `${cfName}: ${cfValue}` : cfValue;
    }
  });

  const lines: string[] = [];
  if (Array.isArray(fieldOrder) && fieldOrder.length) {
    for (const key of fieldOrder) {
      if (key === nameKey) continue;
      if (fieldMap[key]) lines.push(fieldMap[key]);
    }
    for (const [key, line] of Object.entries(fieldMap)) {
      if (key === nameKey) continue;
      if (!fieldOrder!.includes(key)) lines.push(line);
    }
  } else {
    for (const [key, line] of Object.entries(fieldMap)) {
      if (key === nameKey) continue;
      lines.push(line);
    }
  }

  return { name, lines };
}

/* ── Translations ────────────────────────────────────────────────── */

const translations: Record<string, Record<string, string>> = {
  cs: {
    title: "POTVRZENÍ PŘIJETÍ OBJEDNÁVKY",
    supplier: "Dodavatel",
    customer: "Odběratel",
    order_no: "Číslo objednávky:",
    po_no: "Číslo zakáz. objednávky:",
    date: "Datum:",
    payment_method: "Forma úhrady:",
    billing: "Potvrzujeme Vám následující položky:",
    col_no: "Č.",
    col_desc: "Popis",
    col_qty: "Množství",
    col_unit_price: "Jedn. cena",
    col_total: "Celkem",
    total_no_vat: "Celkem bez DPH",
    amounts_in: "Částky jsou uvedeny v",
    notes: "Poznámky",
    issued_by: "Vystavil:",
    received_by: "Převzal:",
    stamp: "Razítko:",
    ico: "IČ: ",
    dic: "DIČ: ",
  },
  en: {
    title: "ORDER CONFIRMATION",
    supplier: "Supplier",
    customer: "Customer",
    order_no: "Order No.:",
    po_no: "PO No.:",
    date: "Date:",
    payment_method: "Payment method:",
    billing: "We confirm the following items:",
    col_no: "No.",
    col_desc: "Description",
    col_qty: "Quantity",
    col_unit_price: "Unit price",
    col_total: "Total",
    total_no_vat: "Total excl. VAT",
    amounts_in: "Amounts are in",
    notes: "Notes",
    issued_by: "Issued by:",
    received_by: "Received by:",
    stamp: "Stamp:",
    ico: "Reg. No.: ",
    dic: "Tax ID: ",
  },
};

/* ── Template ────────────────────────────────────────────────────── */

export interface OrderConfirmationPdfItem {
  description: string;
  quantity: number;
  unit: string;
  unit_price: number;
  is_included_in_total: boolean;
}

interface OrderConfirmationPdfData {
  order_number: string | null;
  customer_order_number: string | null;
  created_at: Date | string | null;
  currency: string | null;
  notes: string | null;
  // Not a real orders column today — read defensively (PHP-era data carried it).
  payment_method?: string | null;
  customers?: Record<string, unknown> | null;
}

/**
 * Order-confirmation HTML. The confirmation is NOT a tax document — prices
 * are NET only: no VAT columns or VAT summary; the grand total is labeled
 * "Celkem bez DPH". Exported for tests.
 */
export function renderOrderConfirmationHtml(
  order: OrderConfirmationPdfData,
  items: OrderConfirmationPdfItem[],
  settings: Record<string, unknown> | null,
  lang: "cs" | "en",
  userName: string,
): string {
  const t = translations[lang];

  let logoImg = "";
  if (settings?.logo_data) {
    const buf = Buffer.from(settings.logo_data as Buffer);
    let mime = "image/png";
    if (buf[0] === 0xff && buf[1] === 0xd8) mime = "image/jpeg";
    else if (buf[0] === 0x47 && buf[1] === 0x49) mime = "image/gif";
    else if (buf[0] === 0x52 && buf[1] === 0x49) mime = "image/webp";
    const b64 = buf.toString("base64");
    logoImg = `<img src="data:${escapeHtml(mime)};base64,${b64}" class="logo" />`;
  }

  const currency = order.currency || "CZK";

  // NET-only total over the included lines — no VAT math anywhere.
  let total = 0;
  for (const item of items) {
    if (item.is_included_in_total) total += item.quantity * item.unit_price;
  }
  total = Math.round(total * 100) / 100;

  const supp = buildAddressLines(settings, true, t);
  const cust = buildAddressLines(
    (order.customers as Record<string, unknown>) || null,
    false,
    t,
  );

  const suppLinesHtml = supp.lines
    .map((l) => `<div class="address-line">${escapeHtml(l)}</div>`)
    .join("");
  const custLinesHtml = cust.lines
    .map((l) => `<div class="address-line">${escapeHtml(l)}</div>`)
    .join("");

  const orderNumber = escapeHtml(order.order_number || "");
  const poNumber = escapeHtml(order.customer_order_number || "");
  const orderDateStr = formatDate(order.created_at);

  const itemsHtml = items
    .map((item, i) => {
      const lineTotal = item.quantity * item.unit_price;
      const qtyDecimals = Math.floor(item.quantity) === item.quantity ? 0 : 2;
      return `<tr>
            <td class="row-num">${i + 1}</td>
            <td class="desc">${escapeHtml(item.description)}</td>
            <td class="center">${formatNum(item.quantity, qtyDecimals)}${item.unit ? ` / ${escapeHtml(item.unit)}` : ""}</td>
            <td class="right">${formatNum(item.unit_price)}</td>
            <td class="right total-cell">${formatNum(lineTotal)}</td>
        </tr>`;
    })
    .join("");

  const paymentMethod =
    String(order.payment_method || "") ||
    (lang === "cs" ? "převodem" : "Bank transfer");

  const notesRaw = order.notes ?? "";
  const notesStripped = notesRaw.replace(/<[^>]*>/g, "").trim();
  const notesHtml = notesStripped
    ? `
  <div class="invoice-notes">
    <div class="invoice-notes-label">${escapeHtml(t.notes)}</div>
    <div class="invoice-notes-content">${cleanQuillHtml(DOMPurify.sanitize(notesRaw))}</div>
  </div>
  `
    : "";

  // Quill indent CSS
  const indentCSS = buildQuillIndentCss();

  return `<!DOCTYPE html>
<html lang="${escapeHtml(lang)}">
<head>
<meta charset="utf-8" />
<title>${escapeHtml(t.title)} ${orderNumber}</title>
<style>
  @page {
    size: A4;
    margin: 8mm 12mm 10mm 12mm;
  }
  * { margin: 0; padding: 0; box-sizing: border-box; }
  html, body {
    font-family: "Segoe UI", Tahoma, Arial, sans-serif;
    font-size: 10pt;
    color: #1a1a1a;
    width: 186mm;
  }

  .invoice-page {
    display: flex;
    flex-direction: column;
    min-height: calc(297mm - 27mm);
  }
  .invoice-content { flex: 1 1 auto; }
  .invoice-footer {
    flex-shrink: 0;
  }

  .accent { color: #de3a3a; }

  /* ── Hlavicka ── */
  .invoice-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 1mm;
    padding-bottom: 1mm;
    border-bottom: 2pt solid #de3a3a;
  }
  .invoice-header .left {
    display: flex;
    align-items: center;
    gap: 3mm;
  }
  .logo-header { text-align: left; }
  .company-title {
    font-size: 12pt;
    font-weight: 700;
  }
  .invoice-title {
    font-size: 13pt;
    font-weight: 700;
    color: #de3a3a;
    text-align: right;
    letter-spacing: 0.03em;
  }

  .logo {
    max-width: 42mm;
    max-height: 22mm;
    object-fit: contain;
  }

  /* ── Adresy ── */
  .header-grid {
    border: 0.5pt solid #d0d0d0;
    border-collapse: collapse;
    width: 100%;
    margin-bottom: 1mm;
  }
  .header-grid td {
    padding: 2mm 3mm;
    border: 0.5pt solid #d0d0d0;
    vertical-align: top;
    width: 50%;
  }
  .header-grid td.addr-customer {
    background: #f5f5f5;
  }
  .header-grid td.details-bank {
    background: #f5f5f5;
  }
  .address-label {
    font-size: 8pt;
    font-weight: 700;
    color: #de3a3a;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    margin-bottom: 1mm;
  }
  .address-name {
    font-size: 10pt;
    font-weight: 700;
    color: #1a1a1a;
    line-height: 1.3;
    margin-bottom: 1mm;
  }
  .address-line {
    font-size: 9pt;
    color: #444;
    line-height: 1.5;
  }

  /* ── Detaily (banka + datumy) — inside header-grid ── */

  .info-row {
    display: flex;
    align-items: baseline;
    font-size: 9pt;
    padding: 1mm 0;
    border-bottom: 0.5pt solid #f0f0f0;
  }
  .info-row:last-child { border-bottom: none; }
  .info-row .lbl {
    color: #666;
    font-weight: 400;
    flex-shrink: 0;
    white-space: nowrap;
    margin-right: 3mm;
  }
  .info-row .val {
    font-weight: 600;
    color: #1a1a1a;
    text-align: right;
    margin-left: auto;
  }

  /* VS/KS blok */
  .vs-block {
    font-size: 9pt;
    line-height: 1.4;
    padding-top: 2mm;
  }

  /* ── Polozky ── */
  .billing-label {
    font-weight: 700;
    color: #1a1a1a;
    font-size: 10pt;
    padding: 2mm 0 1mm 0;
    border-bottom: 1.5pt solid #de3a3a;
    margin-bottom: 0;
    text-transform: uppercase;
    letter-spacing: 0.03em;
  }

  table.items {
    width: 100%;
    table-layout: fixed;
    border-collapse: collapse;
    font-size: 9pt;
    margin-bottom: 2mm;
  }
  table.items thead th {
    font-size: 8.5pt;
    font-weight: 600;
    color: #646464;
    padding: 4px 4px;
    text-align: left;
    border-bottom: 0.5pt solid #d0d0d0;
    white-space: nowrap;
  }
  table.items thead th.center { text-align: center; }
  table.items thead th.right { text-align: right; }
  table.items tbody td {
    padding: 4px 4px;
    border-bottom: 0.5pt solid #e0e0e0;
    vertical-align: middle;
    color: #1a1a1a;
  }
  table.items tbody tr:nth-child(even) { background: #f8f9fa; }
  table.items tbody td.center { text-align: center; white-space: nowrap; }
  table.items tbody td.right { text-align: right; }
  table.items tbody td.row-num {
    text-align: center;
    color: #969696;
    font-size: 9pt;
  }
  table.items tbody td.desc {
    font-size: 9pt;
    font-weight: 600;
    color: #1a1a1a;
  }
  table.items tbody td.total-cell { font-weight: 700; }

  /* Soucet + total - styl z nabidek */
  .totals-wrapper {
    display: flex;
    justify-content: flex-end;
    margin-top: 2mm;
  }
  .totals {
    width: 80mm;
  }
  .totals .detail-rows {
    margin-bottom: 3mm;
  }
  .totals .row {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    font-size: 9.5pt;
    color: #1a1a1a;
    margin-bottom: 2mm;
  }
  .totals .grand {
    border-top: 0.5pt solid #e0e0e0;
    padding-top: 4mm;
    display: flex;
    justify-content: space-between;
    align-items: baseline;
  }
  .totals .grand .label {
    font-size: 10.5pt;
    font-weight: 400;
    color: #1a1a1a;
    align-self: center;
  }
  .totals .grand .value {
    font-size: 14pt;
    font-weight: 600;
    color: #1a1a1a;
    border-bottom: 2.5pt solid #de3a3a;
    padding-bottom: 1mm;
  }
  .totals .currency-note {
    text-align: right;
    font-size: 8pt;
    color: #1a1a1a;
    margin-top: 2mm;
  }

  /* Vystavil */
  .issued-by {
    font-size: 9pt;
    margin: 2mm 0;
    line-height: 1.4;
  }
  .issued-by .lbl { font-weight: 600; }

  /* Upozorneni */
  .notice {
    font-size: 8pt;
    color: #1a1a1a;
    margin: 2mm 0;
    line-height: 1.3;
  }

  /* Prevzal / razitko */
  .footer-row {
    display: flex;
    justify-content: space-between;
    margin-top: 4mm;
    font-size: 9pt;
    border-top: 0.5pt solid #aaa;
    padding-top: 2mm;
    min-height: 15mm;
  }
  .footer-row .col {
    font-weight: 600;
    color: #555;
  }

  /* Poznamky */
  .invoice-notes {
    margin-top: 4mm;
    font-size: 10pt;
    line-height: 1.5;
    color: #1a1a1a;
  }
  .invoice-notes-label {
    font-weight: 600;
    font-size: 9pt;
    text-transform: uppercase;
    color: #555;
    margin-bottom: 1mm;
  }
  .invoice-notes-content p { margin: 0 0 0.4em 0; }
  .invoice-notes-content ul, .invoice-notes-content ol { margin: 0 0 0.4em 1.5em; }
  .invoice-notes-content li { margin-bottom: 0.2em; }
  .invoice-notes-content,
  .invoice-notes-content * {
    font-family: Tahoma, sans-serif !important;
  }
  .invoice-notes-content { font-size: 14px; }
  .invoice-notes-content h1 { font-size: 20px; }
  .invoice-notes-content h2 { font-size: 18px; }
  .invoice-notes-content h3 { font-size: 16px; }
  .invoice-notes-content h4 { font-size: 15px; }

  /* Quill fonty – v PDF vynuceno Tahoma */
  [class*="ql-font-"] { font-family: Tahoma, sans-serif !important; }
  .ql-align-center  { text-align: center; }
  .ql-align-right   { text-align: right; }
  .ql-align-justify { text-align: justify; }
${indentCSS}

  @media print {
    body { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
  }
  @media screen {
    html { background: #525659; }
    body {
      width: 100vw !important;
      margin: 0;
      padding: 30px 0;
      background: transparent;
      display: flex;
      flex-direction: column;
      align-items: center;
      min-height: 100vh;
      overflow-x: hidden;
    }
    .invoice-page {
      width: 210mm;
      min-height: 297mm;
      padding: 15mm;
      background: white;
      box-shadow: 0 4px 20px rgba(0,0,0,0.3);
      box-sizing: border-box;
      border-radius: 2px;
    }
  }
</style>
</head>
<body>
<div class="invoice-page">
<div class="invoice-content">

  <!-- Hlavicka -->
  <div class="invoice-header">
    <div class="left">
      ${logoImg ? `<div class="logo-header">${logoImg}</div>` : ""}
    </div>
    <div class="invoice-title">${escapeHtml(t.title)}</div>
  </div>

  <!-- Dodavatel / Odberatel + Detaily -->
  <table class="header-grid" cellspacing="0">
    <tr>
      <td>
        <div class="address-label">${escapeHtml(t.supplier)}</div>
        <div class="address-name">${escapeHtml(supp.name)}</div>
        ${suppLinesHtml}
      </td>
      <td class="addr-customer">
        <div class="address-label">${escapeHtml(t.customer)}</div>
        <div class="address-name">${escapeHtml(cust.name)}</div>
        ${custLinesHtml}
      </td>
    </tr>
    <tr>
      <td class="details-bank">
        <div class="info-row"><span class="lbl">${escapeHtml(t.order_no)}</span> <span class="val">${orderNumber}</span></div>
        ${poNumber ? `<div class="info-row"><span class="lbl">${escapeHtml(t.po_no)}</span> <span class="val">${poNumber}</span></div>` : ""}
        <div class="info-row"><span class="lbl">${escapeHtml(t.payment_method)}</span> <span class="val">${escapeHtml(paymentMethod)}</span></div>
      </td>
      <td>
        <div class="info-row"><span class="lbl">${escapeHtml(t.date)}</span> <span class="val">${escapeHtml(orderDateStr)}</span></div>
      </td>
    </tr>
  </table>

  <!-- Polozky -->
  <div class="billing-label">${escapeHtml(t.billing)}</div>
  <table class="items">
    <thead>
      <tr>
        <th class="center" style="width:3%">${escapeHtml(t.col_no)}</th>
        <th style="width:56%">${escapeHtml(t.col_desc)}</th>
        <th class="center" style="width:10%">${escapeHtml(t.col_qty)}</th>
        <th class="right" style="width:10%">${escapeHtml(t.col_unit_price)}</th>
        <th class="right" style="width:21%">${escapeHtml(t.col_total)}</th>
      </tr>
    </thead>
    <tbody>
      ${itemsHtml}
    </tbody>
  </table>

  <!-- Soucty (jen souhrnny radek bez DPH - mezisoucet by ho jen opakoval) -->
  <div class="totals-wrapper">
    <div class="totals">
      <div class="grand">
        <span class="label">${escapeHtml(t.total_no_vat)}</span>
        <span class="value">${formatNum(total)} ${escapeHtml(currency)}</span>
      </div>
      <div class="currency-note">${escapeHtml(t.amounts_in)} ${escapeHtml(currency)}</div>
    </div>
  </div>

  ${notesHtml}

</div><!-- /.invoice-content -->
<div class="invoice-footer">

  <!-- Vystavil -->
  <div class="issued-by">
    <span class="lbl">${escapeHtml(t.issued_by)}</span> ${escapeHtml(userName)}
  </div>

  <!-- Prevzal / razitko -->
  <div class="footer-row">
    <div class="col">${escapeHtml(t.received_by)}</div>
    <div class="col" style="text-align:right">${escapeHtml(t.stamp)}</div>
  </div>

</div><!-- /.invoice-footer -->
</div><!-- /.invoice-page -->

</body>
</html>`;
}

/* ── Route ───────────────────────────────────────────────────────── */

export default async function ordersPdfRoutes(
  fastify: FastifyInstance,
): Promise<void> {
  fastify.post<{ Params: { id: string }; Body: Record<string, unknown> }>(
    "/:id/confirmation",
    { preHandler: requirePermission("orders.view") },
    async (request, reply) => {
      const id = parseId(request.params.id, reply);
      if (id === null) return;
      const parsed = parseBody(OrderPdfBodySchema, request.body || {});
      if ("error" in parsed) return error(reply, parsed.error, 400);
      const body = parsed.data;

      try {
        const lang = body.lang === "en" ? "en" : "cs";

        const order = await prisma.orders.findUnique({
          where: { id },
          // The confirmation PDF never renders the PO attachment — don't pull
          // the blob just to read the order header/items.
          omit: { attachment_data: true },
          include: {
            customers: true,
            order_items: { orderBy: { position: "asc" } },
          },
        });

        if (!order) {
          return reply
            .status(404)
            .type("text/html")
            .send("<html><body><h1>Objednávka nenalezena</h1></body></html>");
        }

        const settings = (await prisma.company_settings.findFirst()) as Record<
          string,
          unknown
        > | null;

        // The confirmation PDF can be rendered from client-supplied items (e.g.
        // a live preview of unsaved edits on the detail page) OR from the
        // stored order. Fabricating descriptions/prices that don't reflect the
        // stored order is an editing action, so the custom-items path requires
        // `orders.edit` (admins bypass). A view-only caller is silently served
        // the STORED order items instead — preventing a `orders.view` holder
        // from producing an "official" confirmation with invented figures.
        const authData = request.authData;
        const canUseCustomItems =
          authData?.roleName === "admin" ||
          !!authData?.permissions.includes("orders.edit");
        const customItemsRaw =
          canUseCustomItems && Array.isArray(body.items) ? body.items : null;

        let items: OrderConfirmationPdfItem[];
        if (customItemsRaw && customItemsRaw.length > 0) {
          items = customItemsRaw.map((it) => ({
            description: it.description,
            quantity: it.quantity,
            unit: it.unit,
            unit_price: it.unit_price,
            is_included_in_total: it.is_included_in_total !== false,
          }));
        } else {
          items = order.order_items.map((it) => ({
            description: it.description || "",
            quantity: Number(it.quantity) || 0,
            unit: it.unit || "",
            unit_price: Number(it.unit_price) || 0,
            is_included_in_total: !!it.is_included_in_total,
          }));
        }

        const userName = request.authData
          ? `${request.authData.firstName || ""} ${request.authData.lastName || ""}`.trim()
          : "";

        const html = renderOrderConfirmationHtml(
          order,
          items,
          settings,
          lang,
          userName,
        );

        const pdfBuffer = await htmlToPdf(html);
        const filename = `Potvrzeni-${order.order_number || String(id)}.pdf`;

        return reply
          .type("application/pdf")
          .header("Content-Disposition", `attachment; filename="${filename}"`)
          .send(pdfBuffer);
      } catch (err) {
        request.log.error(err, "PDF generation failed");
        return reply
          .status(500)
          .type("text/html")
          .send("<html><body><h1>Chyba při generování PDF</h1></body></html>");
      }
    },
  );
}
