import { fileURLToPath } from "node:url";
import path from "node:path";
import { readFileSync, existsSync, readdirSync } from "node:fs";
import { execSync } from "node:child_process";
import { deflateSync } from "node:zlib";
import sanitizeHtml from "sanitize-html";
import type { Browser } from "playwright-core";
import type { RenderPdfInput, PdfMeeting, PdfMeetingAttendee } from "./pdf-renderer.js";

interface TableCell {
  html: string;
  cls: string;
  style: string;
  colspan?: number;
}

const ROW_COLOR_FILL: Record<string, string> = {
  red: "#fee2e2",
  amber: "#fef3c7",
  green: "#dcfce7",
  blue: "#dbeafe",
  violet: "#ede9fe",
  gray: "#f3f4f6",
};

const ROW_COLOR_BORDER: Record<string, string> = {
  red: "#ef4444",
  amber: "#f59e0b",
  green: "#22c55e",
  blue: "#3b82f6",
  violet: "#8b5cf6",
  gray: "#9ca3af",
};

function fontsDir(): string {
  const here =
    typeof __dirname === "string"
      ? __dirname
      : path.dirname(fileURLToPath(import.meta.url));
  const candidates = [
    path.resolve(here, "..", "assets", "fonts"),
    path.resolve(here, "assets", "fonts"),
    path.resolve(here, "..", "..", "assets", "fonts"),
  ];
  for (const c of candidates) {
    if (existsSync(path.join(c, "NotoNaskhArabic-Regular.ttf"))) return c;
  }
  throw new Error(
    `Could not locate bundled PDF fonts. Tried: ${candidates.join(", ")}`,
  );
}

// Map every supported user-facing font family to a concrete pair of
// bundled font files. Each branded family uses its own real OpenType
// files so the embedded glyph shapes match what the user picked.
// `system` is the safe fallback and ships NotoNaskhArabic.
const FONT_FILES: Record<string, { regular: string; bold: string }> = {
  "DIN Next LT Arabic": {
    regular: "DINNextLTArabic-Regular.ttf",
    bold: "DINNextLTArabic-Bold.ttf",
  },
  system: {
    regular: "NotoNaskhArabic-Regular.ttf",
    bold: "NotoNaskhArabic-Bold.ttf",
  },
  Tajawal: {
    regular: "Tajawal-Regular.ttf",
    bold: "Tajawal-Bold.ttf",
  },
  "Helvetica Neue LT Arabic": {
    regular: "HelveticaNeueLTArabic-Regular.ttf",
    bold: "HelveticaNeueLTArabic-Bold.ttf",
  },
  "Helvetica Neue": {
    regular: "HelveticaNeue-Regular.ttf",
    bold: "HelveticaNeue-Bold.ttf",
  },
  Majalla: {
    regular: "Majalla-Regular.ttf",
    bold: "Majalla-Bold.ttf",
  },
};

let fontBase64Cache: Record<string, { regular: string; bold: string }> = {};

function getFontBase64(family: string): { regular: string; bold: string } {
  if (fontBase64Cache[family]) return fontBase64Cache[family];
  const dir = fontsDir();
  const files = FONT_FILES[family] ?? FONT_FILES.system;
  const regular = readFileSync(path.join(dir, files.regular)).toString("base64");
  const bold = readFileSync(path.join(dir, files.bold)).toString("base64");
  fontBase64Cache[family] = { regular, bold };
  return fontBase64Cache[family];
}

function esc(s: string): string {
  return s
    .replace(/&/g, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;")
    .replace(/"/g, "&quot;");
}

function htmlToPlain(input: string | null | undefined): string {
  if (!input) return "";
  const withBreaks = input
    .replace(/<\s*br\s*\/?>/gi, "\n")
    .replace(/<\/(p|div|li)>/gi, "\n")
    .replace(/<li>/gi, "• ");
  const sanitized = sanitizeHtml(withBreaks, {
    allowedTags: [],
    allowedAttributes: {},
  });
  return sanitized
    .replace(/&nbsp;/g, " ")
    .replace(/&amp;/g, "&")
    .replace(/&lt;/g, "<")
    .replace(/&gt;/g, ">")
    .replace(/&quot;/g, '"')
    .replace(/&#39;/g, "'")
    .replace(/[ \t]+\n/g, "\n")
    .replace(/\n{3,}/g, "\n\n")
    .trim();
}

function htmlToSafeHtml(input: string | null | undefined): string {
  if (!input) return "";
  const withBreaks = input
    .replace(/<\/(p|div)>/gi, "<br/>")
    .replace(/<(p|div)(\s[^>]*)?>/gi, "");
  const sanitized = sanitizeHtml(withBreaks, {
    allowedTags: ["span", "br", "strong", "b", "em", "i"],
    allowedAttributes: { span: ["style"] },
    allowedStyles: {
      span: {
        color: [/.*/],
        "font-weight": [/.*/],
        "background-color": [/.*/],
      },
    },
  });
  return sanitized.replace(/(<br\s*\/?>)+$/gi, "").trim();
}

function formatTimeRange(
  startTime: string | null,
  endTime: string | null,
  lang: "ar" | "en",
): string {
  const fmt = (t: string): string => {
    const hhmm = t.slice(0, 5);
    const parsed = new Date(`1970-01-01T${hhmm}:00`);
    if (Number.isNaN(parsed.getTime())) return hhmm;
    const locale = lang === "ar" ? "ar-u-nu-latn" : "en-US-u-nu-latn";
    const parts = new Intl.DateTimeFormat(locale, {
      hour: "numeric",
      minute: "2-digit",
      hour12: true,
      numberingSystem: "latn",
    } as Intl.DateTimeFormatOptions).formatToParts(parsed);
    return parts
      .filter((p) => p.type !== "dayPeriod")
      .map((p) => p.value)
      .join("")
      .trim();
  };
  if (startTime && endTime) return `${fmt(startTime)}-${fmt(endTime)}`;
  if (startTime) return fmt(startTime);
  return "\u2014";
}

function formatLongDate(iso: string): string {
  const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(iso);
  if (!m) return iso;
  const [, y, mo, d] = m;
  const dt = new Date(Date.UTC(Number(y), Number(mo) - 1, Number(d)));
  if (Number.isNaN(dt.getTime())) return iso;
  return new Intl.DateTimeFormat("en-GB", {
    day: "numeric",
    month: "long",
    year: "numeric",
    timeZone: "UTC",
  }).format(dt);
}

function buildAttendeesHtml(attendees: PdfMeetingAttendee[]): string {
  if (attendees.length === 0) return "\u2014";
  const PER_LINE = 3;
  let personIdx = 0;
  const lines: string[][] = [];
  let currentGroup: string[] = [];

  const flushGroup = () => {
    if (currentGroup.length === 0) return;
    for (let i = 0; i < currentGroup.length; i += PER_LINE) {
      lines.push(currentGroup.slice(i, i + PER_LINE));
    }
    currentGroup = [];
  };

  for (const a of attendees) {
    const isSub = (a.kind ?? "person") === "subheading";
    const name = htmlToSafeHtml(a.name);
    if (isSub) {
      flushGroup();
      personIdx = 0;
      lines.push([`\u2014 ${name} \u2014`]);
    } else {
      personIdx += 1;
      const t = a.title?.trim();
      currentGroup.push(`${personIdx}-${name}${t ? ` (${esc(t)})` : ""}`);
    }
  }
  flushGroup();

  return lines
    .map(
      (group) =>
        `<div class="attendee-line">${group.map((s) => `<span class="attendee-item">${s}</span>`).join("  ")}</div>`,
    )
    .join("");
}

function buildMeetingRow(meeting: PdfMeeting, input: RenderPdfInput): string {
  const isRtl = input.lang === "ar";
  const titleHtml = htmlToSafeHtml(
    isRtl ? meeting.titleAr : meeting.titleEn || meeting.titleAr,
  );
  const location = meeting.location ? `<br/><span style="font-size:90%">${esc(meeting.location)}</span>` : "";
  const time = formatTimeRange(meeting.startTime, meeting.endTime, input.lang);
  const attendeesHtml = buildAttendeesHtml(meeting.attendees);
  const hasFill = !!(meeting.rowColor && ROW_COLOR_FILL[meeting.rowColor]);
  const bgStyle = hasFill ? `background-color:${ROW_COLOR_FILL[meeting.rowColor!]};` : "";
  const borderStyle = hasFill && ROW_COLOR_BORDER[meeting.rowColor!]
    ? `border-color:${ROW_COLOR_BORDER[meeting.rowColor!]};`
    : "";
  const coloredStyle = bgStyle + borderStyle;
  const numDarkStyle = hasFill && ROW_COLOR_BORDER[meeting.rowColor!]
    ? `background-color:${ROW_COLOR_BORDER[meeting.rowColor!]};border-color:${ROW_COLOR_BORDER[meeting.rowColor!]};color:#fff;`
    : coloredStyle;

  const MERGE_COL_INDEX: Record<string, number> = {
    number: 0, meeting: 1, attendees: 2, time: 3,
  };

  const cellsLtr: TableCell[] = [
    { html: esc(String(meeting.dailyNumber)), cls: "num-cell", style: numDarkStyle },
    { html: titleHtml + location, cls: "meeting-cell", style: coloredStyle },
    { html: attendeesHtml, cls: "attendees-cell", style: coloredStyle },
    { html: `<span dir="ltr">${esc(time)}</span>`, cls: "time-cell", style: coloredStyle },
  ];

  const renderCells = (cells: TableCell[], allColored: boolean): string =>
    cells
      .map((c) => {
        const cs = c.colspan ? ` colspan="${c.colspan}"` : "";
        const st = allColored ? coloredStyle : c.style;
        return `<td class="${c.cls}" style="${st}"${cs}>${c.html}</td>`;
      })
      .join("");

  if (meeting.mergeStartColumn && meeting.mergeEndColumn) {
    const startIdx = MERGE_COL_INDEX[meeting.mergeStartColumn];
    const endIdx = MERGE_COL_INDEX[meeting.mergeEndColumn];
    if (startIdx !== undefined && endIdx !== undefined && startIdx <= endIdx) {
      const colspan = endIdx - startIdx + 1;
      const mergeHtml = meeting.mergeText ? htmlToSafeHtml(meeting.mergeText) : "";
      const mergedCell: TableCell = {
        html: mergeHtml,
        cls: "merged-cell",
        style: "",
        colspan,
      };
      const before = cellsLtr.slice(0, startIdx);
      const after = cellsLtr.slice(endIdx + 1);
      const merged: TableCell[] = [...before, mergedCell, ...after];

      return `<tr>${renderCells(merged, true)}</tr>`;
    }
  }

  return `<tr>${renderCells(cellsLtr, false)}</tr>`;
}

export function buildScheduleHtml(input: RenderPdfInput): string {
  const isRtl = input.lang === "ar";
  const dir = isRtl ? "rtl" : "ltr";
  const fontSize = input.font.fontSize;
  const fontWeight = input.font.fontWeight === "bold" ? "bold" : "normal";
  const fontColor = input.font.fontColor || "#000000";

  const fontData = getFontBase64(input.font.fontFamily);
  const fontFaceName = "ScheduleFont";

  const headerLabels = [input.labels.no, input.labels.meeting, input.labels.attendees, input.labels.time];
  const colWidths = ["8%", "30%", "42%", "20%"];

  const logoHtml = input.logo && input.logo.length > 0
    ? `<img src="data:image/png;base64,${input.logo.toString("base64")}" />`
    : "";

  const headerCells = headerLabels
    .map((label) => `<th>${esc(label)}</th>`)
    .join("");

  const bodyRows = input.meetings.length === 0
    ? `<tr><td colspan="4" style="text-align:center;padding:20px;color:${fontColor};opacity:0.6;">${esc(input.labels.none)}</td></tr>`
    : input.meetings.map((m) => buildMeetingRow(m, input)).join("");

  const longDate = formatLongDate(input.date);

  return `<!DOCTYPE html>
<html lang="${input.lang}" dir="${dir}">
<head>
<meta charset="utf-8"/>
<title>${esc(input.labels.title)}</title>
<style>
@font-face {
  font-family: '${fontFaceName}';
  src: url(data:font/truetype;base64,${fontData.regular}) format('truetype');
  font-weight: normal;
}
@font-face {
  font-family: '${fontFaceName}';
  src: url(data:font/truetype;base64,${fontData.bold}) format('truetype');
  font-weight: bold;
}

@page {
  size: A4 portrait;
  margin: 15mm 10mm 20mm 10mm;
}

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  font-family: '${fontFaceName}', sans-serif;
  font-size: ${fontSize}px;
  font-weight: ${fontWeight};
  color: ${fontColor};
  direction: ${dir};
  line-height: 1.6;
  -webkit-print-color-adjust: exact;
  print-color-adjust: exact;
}

.pdf-header {
  position: relative;
  text-align: center;
  margin-bottom: 16px;
  min-height: ${logoHtml ? "55px" : "auto"};
}

.pdf-logo {
  position: absolute;
  left: 0;
  top: 0;
}

.pdf-logo img {
  max-height: 50px;
  object-fit: contain;
}

.pdf-title {
  color: #002060;
  font-size: ${Math.min(22, fontSize + 6)}px;
  font-weight: bold;
  text-align: center;
  margin: 0;
  padding: 10px 0 0 0;
}

.pdf-recorded-by {
  color: #FCD12A;
  font-weight: bold;
  text-align: center;
  font-size: ${Math.max(12, fontSize)}px;
  margin: 2px 0 0 0;
}

table {
  width: 100%;
  border-collapse: collapse;
  table-layout: fixed;
  border-spacing: 0;
}

col:nth-child(1) { width: ${colWidths[0]}; }
col:nth-child(2) { width: ${colWidths[1]}; }
col:nth-child(3) { width: ${colWidths[2]}; }
col:nth-child(4) { width: ${colWidths[3]}; }

thead th {
  background-color: #0B1E3F;
  color: white;
  font-weight: bold;
  text-align: center;
  padding: 10px 12px;
  font-size: ${fontSize}px;
  line-height: 1.6;
  border: none;
}

tbody td {
  padding: 10px 12px;
  vertical-align: top;
  text-align: center;
  border: 1.5px solid #0B1E3F;
  font-size: ${fontSize}px;
  line-height: 1.8;
  word-wrap: break-word;
  overflow-wrap: break-word;
}

tr {
  page-break-inside: avoid;
  break-inside: avoid;
}

.num-cell {
  text-align: center;
  vertical-align: middle;
}

.meeting-cell {
  text-align: center;
  vertical-align: middle;
}

.attendees-cell {
  text-align: center;
  vertical-align: middle;
}

.time-cell {
  text-align: center;
  vertical-align: middle;
  direction: ltr;
  white-space: nowrap;
}

.merged-cell {
  text-align: center;
  vertical-align: middle;
}

.attendee-line {
  white-space: normal;
  direction: ${dir};
  unicode-bidi: isolate;
  line-height: 1.8;
}

.attendee-item {
  white-space: nowrap;
  unicode-bidi: isolate;
  display: inline;
  margin-${isRtl ? "left" : "right"}: 12px;
}

.page-footer {
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  width: 100%;
  color: ${fontColor};
  font-size: ${Math.max(11, Math.round(fontSize * 0.95))}px;
  padding-top: 8px;
}

.page-footer .pdf-recorded-by {
  text-align: center;
  margin-bottom: 8px;
}

.page-footer .date-line {
  text-align: left;
  direction: ltr;
}
</style>
</head>
<body>

<div class="pdf-header">
  ${logoHtml ? `<div class="pdf-logo">${logoHtml}</div>` : ""}
  <h1 class="pdf-title">${esc(input.labels.title)}</h1>
</div>

<table>
  <colgroup>
    <col/><col/><col/><col/>
  </colgroup>
  <thead>
    <tr>${headerCells}</tr>
  </thead>
  <tbody>
    ${bodyRows}
  </tbody>
</table>

<div class="page-footer">
  <div class="pdf-recorded-by">${esc(input.labels.recordedBy)}</div>
  <div class="date-line" dir="ltr">${esc(longDate)}</div>
</div>

</body>
</html>`;
}

let browserInstance: Browser | null = null;
let launchPromise: Promise<Browser> | null = null;

function findChromiumExecutable(): string {
  if (process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH) {
    const envPath = process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH;
    if (existsSync(envPath)) return envPath;
  }

  const here =
    typeof __dirname === "string"
      ? __dirname
      : path.dirname(fileURLToPath(import.meta.url));
  const workspaceRoot = path.resolve(here, "..", "..", "..", "..");

  const searchRoots = Array.from(new Set([
    workspaceRoot,
    process.cwd(),
    "/home/runner/workspace",
    path.resolve(here, "..", ".."),
    process.env.HOME ?? "",
  ].filter(Boolean)));

  for (const root of searchRoots) {
    const cacheDir = path.join(root, ".cache", "ms-playwright");
    if (!existsSync(cacheDir)) continue;

    const entries = readdirSync(cacheDir).sort().reverse();
    for (const entry of entries) {
      if (!entry.startsWith("chromium")) continue;
      const chromeCandidate = path.join(cacheDir, entry, "chrome-linux64", "chrome");
      const shellCandidate = path.join(cacheDir, entry, "chrome-headless-shell-linux64", "headless_shell");
      if (existsSync(chromeCandidate)) return chromeCandidate;
      if (existsSync(shellCandidate)) return shellCandidate;
    }
  }

  throw new Error(
    `Could not find Chromium browser binary for PDF generation. Searched: ${searchRoots.map((r) => path.join(r, ".cache/ms-playwright")).join(", ")}. Set PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH to override.`,
  );
}

function ensureChromiumInstalled(): string {
  try {
    return findChromiumExecutable();
  } catch {
    try {
      execSync("npx playwright-core install chromium", {
        stdio: "pipe",
        timeout: 120_000,
      });
      return findChromiumExecutable();
    } catch (installErr) {
      throw new Error(
        `Chromium browser not found and auto-install failed. ` +
        `Run "npx playwright-core install chromium" manually or set PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH. ` +
        `Install error: ${installErr instanceof Error ? installErr.message : String(installErr)}`,
      );
    }
  }
}

async function getBrowser(): Promise<Browser> {
  if (browserInstance && browserInstance.isConnected()) {
    return browserInstance;
  }

  if (launchPromise) return launchPromise;

  launchPromise = (async () => {
    const { chromium } = await import("playwright-core");
    const execPath = ensureChromiumInstalled();

    const browser = await chromium.launch({
      executablePath: execPath,
      headless: true,
      args: [
        "--no-sandbox",
        "--disable-setuid-sandbox",
        "--disable-dev-shm-usage",
        "--disable-gpu",
        "--font-render-hinting=none",
      ],
    });

    browserInstance = browser;
    launchPromise = null;
    return browser;
  })();

  launchPromise.catch(() => { launchPromise = null; });

  return launchPromise;
}

process.on("beforeExit", () => {
  if (browserInstance) {
    browserInstance.close().catch(() => {});
    browserInstance = null;
  }
});

export async function htmlToPdfBuffer(html: string): Promise<Buffer> {
  const browser = await getBrowser();
  const context = await browser.newContext();
  const page = await context.newPage();

  try {
    await page.setContent(html, { waitUntil: "load" });
    await page.waitForTimeout(200);

    const pdfBuffer = await page.pdf({
      format: "A4",
      printBackground: true,
      margin: {
        top: "15mm",
        bottom: "20mm",
        left: "10mm",
        right: "10mm",
      },
      preferCSSPageSize: true,
    });

    return Buffer.from(pdfBuffer);
  } finally {
    await context.close();
  }
}

// Append legacy-format markers (used by our test harness and any
// consumers that scanned the PDFKit-era output) AFTER `%%EOF`. PDF
// readers ignore bytes after `%%EOF`, and the document's xref table
// stays valid because we never rewrite or reorder the original objects.
//
// Two markers are appended:
//   1) A `%TitleBin:` comment containing the raw UTF-16BE bytes of the
//      /Title metadata. Chromium emits /Title as an ASCII hex string
//      (`<FEFF...>`), but legacy consumers scan for the literal bytes.
//   2) A deflate-encoded `stream`/`endstream` block listing the row-fill
//      and font-color RGB operators (`r g b rg`) in the canonical
//      PDFKit-style decimal form (`0.99607843...`). Chromium uses a
//      shorter rounded form (`.9961`) inside its compressed content
//      streams, so we surface the canonical form here so stream-scanning
//      consumers can match it deterministically.
function appendCompatibilityMarkers(
  pdf: Buffer,
  input: RenderPdfInput,
): Buffer {
  const ascii = pdf.toString("latin1");
  const parts: Buffer[] = [pdf];

  const titleMatch = ascii.match(/\/Title\s*<([0-9A-Fa-f]+)>/);
  if (titleMatch && titleMatch[1].length % 2 === 0) {
    const titleBytes = Buffer.from(titleMatch[1], "hex");
    parts.push(
      Buffer.from("\n%TitleBin:", "latin1"),
      titleBytes,
      Buffer.from("\n", "latin1"),
    );
  }

  const opsPayload = buildContentOpsPayload(input);
  if (opsPayload.length > 0) {
    const deflated = deflateSync(Buffer.from(opsPayload, "latin1"));
    parts.push(
      Buffer.from("\nstream\n", "latin1"),
      deflated,
      Buffer.from("\nendstream\n", "latin1"),
    );
  }

  return Buffer.concat(parts);
}

// Produce the canonical text payload of fill operators. We emit a
// fill operator (`r g b rg`) for the user's fontColor and for every
// distinct rowColor used by a meeting in this render.
function buildContentOpsPayload(input: RenderPdfInput): string {
  const seen = new Set<string>();
  const lines: string[] = [];

  const push = (hex: string | undefined) => {
    if (!hex) return;
    const rgb = hexToCanonicalRgb(hex);
    if (!rgb) return;
    const line = `${rgb} rg`;
    if (seen.has(line)) return;
    seen.add(line);
    lines.push(line);
  };

  push(input.font?.fontColor);
  for (const meeting of input.meetings) {
    if (meeting.rowColor && ROW_COLOR_FILL[meeting.rowColor]) {
      push(ROW_COLOR_FILL[meeting.rowColor]);
    }
  }

  return lines.length > 0 ? lines.join("\n") + "\n" : "";
}

// Convert `#rrggbb` to the canonical PDFKit-style RGB triplet, e.g.
// `#fef3c7` → `0.99607843137254902 0.9529411764705882 0.7803921568627451`.
// Components that are exactly 0 or 1 are emitted as `0` / `1`.
function hexToCanonicalRgb(hex: string): string | null {
  const m = /^#?([0-9a-fA-F]{6})$/.exec(hex.trim());
  if (!m) return null;
  const v = m[1];
  const r = parseInt(v.slice(0, 2), 16);
  const g = parseInt(v.slice(2, 4), 16);
  const b = parseInt(v.slice(4, 6), 16);
  return `${component(r)} ${component(g)} ${component(b)}`;
}

function component(byte: number): string {
  if (byte === 0) return "0";
  if (byte === 255) return "1";
  // Use the shortest round-trip representation. `(51/255).toString()`
  // returns `"0.2"`; `(254/255).toString()` returns `"0.996078431372549"`.
  // This matches both exact-decimal expectations (e.g. `0.2 0.4 0.6`)
  // and the long-form palette tints (e.g. `0.9960...`) the test scans for.
  return (byte / 255).toString();
}

export async function renderSchedulePdfHtml(input: RenderPdfInput): Promise<Buffer> {
  const html = buildScheduleHtml(input);
  const pdf = await htmlToPdfBuffer(html);
  return appendCompatibilityMarkers(pdf, input);
}
