import type { LaneInfo, TimelineEvent } from "./shape-timeline";

// chart-export (t-paliad-177 Slice 2) — client-side export helpers for
// the Project Timeline / Chart page.
//
// Five formats land in Slice 2 (per design §7.1, m's pick on faraday-Q4
// to rule out server-side PDF via chromedp):
//
//   SVG  — XMLSerializer of the live SVG element
//   PNG  — SVG → <img> → <canvas> at 2× HiDPI, toBlob("image/png")
//   PDF  — window.print() with @media print stylesheet (browser handles
//          the PDF engine; no chromedp dep on Dokploy)
//   CSV  — flat tabular dump of TimelineEvent[] (UTF-8 BOM for Excel-DE)
//   JSON — wire envelope verbatim + export-metadata header
//
// iCal lands in a follow-up commit (C5) and goes via a server-side
// endpoint that reuses internal/services/caldav_ical.go (faraday-Q6).
//
// Design ref: docs/design-project-chart-2026-05-09.md §7.

export interface ExportContext {
  projectId: string;
  projectTitle: string;
  svgEl: SVGSVGElement;
  events: ReadonlyArray<TimelineEvent>;
  lanes: ReadonlyArray<LaneInfo>;
}

// ---------------------------------------------------------------------------
// Public surface
// ---------------------------------------------------------------------------

export async function exportSVG(ctx: ExportContext): Promise<void> {
  const svgString = serialiseSVG(ctx.svgEl);
  const blob = new Blob([svgString], { type: "image/svg+xml;charset=utf-8" });
  triggerDownload(blob, filename(ctx, "svg"));
}

export async function exportPNG(ctx: ExportContext): Promise<void> {
  const svgString = serialiseSVG(ctx.svgEl);
  const blob = await rasterise(svgString, ctx.svgEl);
  if (!blob) {
    throw new Error("PNG raster failed");
  }
  triggerDownload(blob, filename(ctx, "png"));
}

export function exportCSV(ctx: ExportContext): void {
  const rows: string[][] = [csvHeader()];
  for (const event of ctx.events) {
    rows.push(csvRow(event, ctx));
  }
  // UTF-8 BOM keeps Excel-DE from mis-detecting ANSI; ISO-8601 dates
  // round-trip correctly into German Excel as text.
  const text = "﻿" + rows.map(csvLine).join("\r\n") + "\r\n";
  const blob = new Blob([text], { type: "text/csv;charset=utf-8" });
  triggerDownload(blob, filename(ctx, "csv"));
}

export function exportJSON(ctx: ExportContext): void {
  const envelope = {
    project_id: ctx.projectId,
    project_title: ctx.projectTitle,
    exported_at: new Date().toISOString(),
    events: ctx.events,
    lanes: ctx.lanes,
  };
  const text = JSON.stringify(envelope, null, 2) + "\n";
  const blob = new Blob([text], { type: "application/json;charset=utf-8" });
  triggerDownload(blob, filename(ctx, "json"));
}

export function exportPrint(): void {
  // The @media print stylesheet in global.css does the layout work;
  // we just invoke the browser's print dialog. User picks "Save as PDF"
  // (Chrome/Edge), "Drucken in Datei" (Firefox), etc.
  window.print();
}

// ---------------------------------------------------------------------------
// SVG / PNG plumbing
// ---------------------------------------------------------------------------

function serialiseSVG(svgEl: SVGSVGElement): string {
  // Clone so we can inline computed styles without polluting the live DOM.
  // For a true cross-environment-portable SVG, we'd compute every used
  // CSS-var into a literal value. v1 keeps it light: the receiver inherits
  // colours via document context when opened standalone, and the rendered
  // bars still work because palette tokens fall through to the .smart-
  // timeline-chart root selector via inline class. Add a fallback width /
  // height attribute so headless viewers don't render 0×0.
  const clone = svgEl.cloneNode(true) as SVGSVGElement;
  if (!clone.getAttribute("width") && svgEl.getAttribute("width")) {
    clone.setAttribute("width", svgEl.getAttribute("width") || "1000");
  }
  if (!clone.getAttribute("height") && svgEl.getAttribute("height")) {
    clone.setAttribute("height", svgEl.getAttribute("height") || "400");
  }
  clone.setAttribute("xmlns", "http://www.w3.org/2000/svg");
  clone.setAttribute("xmlns:xlink", "http://www.w3.org/1999/xlink");

  // Inline the chart's computed palette tokens so the standalone SVG
  // paints the same way when opened in an image viewer (which has no
  // document.css). Read every --chart-* property off the live element.
  const computed = window.getComputedStyle(svgEl);
  const styleLines: string[] = [];
  for (const prop of [
    "--chart-mark-deadline",
    "--chart-mark-appointment",
    "--chart-mark-milestone",
    "--chart-mark-projected",
    "--chart-mark-overdue",
    "--chart-mark-done",
    "--chart-today-rule",
    "--chart-grid-line",
    "--chart-lane-label",
    "--chart-tick-label",
    "--chart-bg",
  ]) {
    const val = computed.getPropertyValue(prop).trim();
    if (val) styleLines.push(`${prop}: ${val};`);
  }
  if (styleLines.length > 0) {
    const existing = clone.getAttribute("style") || "";
    clone.setAttribute("style", existing + styleLines.join(" "));
  }

  return new XMLSerializer().serializeToString(clone);
}

async function rasterise(svgString: string, svgEl: SVGSVGElement): Promise<Blob | null> {
  const widthAttr = svgEl.getAttribute("width") || "1000";
  const heightAttr = svgEl.getAttribute("height") || "400";
  const width = Number(widthAttr) || 1000;
  const height = Number(heightAttr) || 400;
  // 2× device pixel ratio for HiDPI exports (design §7.1 "PNG, 2× HiDPI").
  const scale = 2;

  const blob = new Blob([svgString], { type: "image/svg+xml;charset=utf-8" });
  const url = URL.createObjectURL(blob);

  try {
    const img = await loadImage(url);
    const canvas = document.createElement("canvas");
    canvas.width = Math.round(width * scale);
    canvas.height = Math.round(height * scale);
    const ctx = canvas.getContext("2d");
    if (!ctx) return null;
    ctx.fillStyle = "#ffffff";
    ctx.fillRect(0, 0, canvas.width, canvas.height);
    ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
    return await new Promise<Blob | null>((resolve) => {
      canvas.toBlob((b) => resolve(b), "image/png");
    });
  } finally {
    URL.revokeObjectURL(url);
  }
}

function loadImage(src: string): Promise<HTMLImageElement> {
  return new Promise((resolve, reject) => {
    const img = new Image();
    img.onload = () => resolve(img);
    img.onerror = () => reject(new Error("Image load failed"));
    img.src = src;
  });
}

// ---------------------------------------------------------------------------
// CSV plumbing
// ---------------------------------------------------------------------------

const CSV_COLUMNS = [
  "project_id",
  "project_title",
  "kind",
  "status",
  "track",
  "lane_id",
  "date",
  "title",
  "description",
  "rule_code",
  "depends_on_rule_code",
  "depends_on_date",
  "depends_on_rule_name",
  "sub_project_id",
  "sub_project_title",
  "bubble_up",
  "deadline_id",
  "appointment_id",
  "project_event_id",
  "project_event_type",
] as const;

function csvHeader(): string[] {
  return [...CSV_COLUMNS];
}

function csvRow(event: TimelineEvent, ctx: ExportContext): string[] {
  return [
    ctx.projectId,
    ctx.projectTitle,
    event.kind,
    event.status,
    event.track,
    event.lane_id ?? "",
    isoOnly(event.date),
    event.title,
    event.description ?? "",
    event.rule_code ?? "",
    event.depends_on_rule_code ?? "",
    isoOnly(event.depends_on_date),
    event.depends_on_rule_name ?? "",
    event.sub_project_id ?? "",
    event.sub_project_title ?? "",
    event.bubble_up ? "true" : "false",
    event.deadline_id ?? "",
    event.appointment_id ?? "",
    event.project_event_id ?? "",
    event.project_event_type ?? "",
  ];
}

function csvLine(fields: string[]): string {
  return fields.map(csvEscape).join(",");
}

/** RFC 4180 quoting: double quotes inside the field are doubled; wrap
 *  the whole field in quotes if it contains comma / quote / newline. */
function csvEscape(value: string): string {
  if (/[,"\r\n]/.test(value)) {
    return '"' + value.replace(/"/g, '""') + '"';
  }
  return value;
}

function isoOnly(date: string | null | undefined): string {
  if (!date) return "";
  return date.slice(0, 10);
}

// ---------------------------------------------------------------------------
// Download trigger
// ---------------------------------------------------------------------------

function triggerDownload(blob: Blob, name: string): void {
  const url = URL.createObjectURL(blob);
  const a = document.createElement("a");
  a.href = url;
  a.download = name;
  // Some browsers (Safari < 14) ignore the download attribute unless
  // the link is in the document tree. Inserting + removing is cheap.
  a.style.display = "none";
  document.body.appendChild(a);
  a.click();
  document.body.removeChild(a);
  // Give the browser a tick to start the download before we revoke.
  setTimeout(() => URL.revokeObjectURL(url), 1000);
}

function filename(ctx: ExportContext, ext: string): string {
  // Keep filenames diff-friendly + filesystem-safe. Replace anything that
  // isn't ASCII alnum/dot/hyphen with "_". Truncate the title to 60 chars.
  const safeTitle = (ctx.projectTitle || "timeline")
    .normalize("NFKD")
    .replace(/[^\x20-\x7e]/g, "")
    .replace(/[^A-Za-z0-9.-]+/g, "_")
    .replace(/_+/g, "_")
    .replace(/^_|_$/g, "")
    .slice(0, 60) || "timeline";
  const dateStr = new Date().toISOString().slice(0, 10);
  return `paliad-${safeTitle}-${dateStr}.${ext}`;
}
