import { jsPDF } from "jspdf";
import QRCode from "qrcode";
import {
  educationForAssessment,
  HEALTH_DISCLAIMER,
  PREVENTION_CHECKLIST,
  MAINTENANCE_REMINDERS,
  CALL_PROFESSIONAL,
} from "@/lib/mold-education";
import { COMPANY, REPORT_DISCLAIMER } from "./company";
import { LEGEND } from "./annotate";
import type { Level, ReportInput, ReportMedia } from "./report-types";

const BRAND: [number, number, number] = [16, 94, 66];
const ACCENT: [number, number, number] = [14, 116, 144];
const INK: [number, number, number] = [28, 32, 36];
const MUTED: [number, number, number] = [110, 118, 126];
const LINE: [number, number, number] = [222, 227, 231];

const LEVEL_COLOR: Record<string, [number, number, number]> = {
  Low: [22, 145, 88],
  Medium: [214, 158, 24],
  High: [200, 45, 45],
};

const M = 14;

/** Coerce anything into an array so .filter/.map/.forEach never throw. */
function arr<T>(value: unknown): T[] {
  if (Array.isArray(value)) return value as T[];
  if (value === null || value === undefined || value === "") return [];
  return [value as T];
}

function str(value: unknown): string {
  if (typeof value === "string") return value;
  if (value === null || value === undefined) return "";
  if (typeof value === "number" || typeof value === "boolean") return String(value);
  try {
    return JSON.stringify(value);
  } catch {
    return String(value);
  }
}

function num(value: unknown): number {
  const n = typeof value === "number" ? value : Number(value);
  return Number.isFinite(n) ? n : 0;
}

/** Normalize any value into a clean list of non-empty strings. */
function strList(value: unknown): string[] {
  return arr<unknown>(value)
    .map((v) => (v && typeof v === "object" && "label" in (v as object) ? str((v as { label: unknown }).label) : str(v)))
    .map((v) => v.trim())
    .filter(Boolean);
}

class Doc {
  doc: jsPDF;
  y = 0;
  w: number;
  h: number;
  reportNo: string;

  constructor(reportNo: string) {
    this.doc = new jsPDF({ unit: "mm", format: "a4", compress: true });
    this.w = this.doc.internal.pageSize.getWidth();
    this.h = this.doc.internal.pageSize.getHeight();
    this.reportNo = reportNo;
    this.y = M + 22;
  }

  get contentWidth() {
    return this.w - M * 2;
  }

  need(space: number) {
    if (this.y + space > this.h - 18) this.addPage();
  }

  addPage() {
    this.doc.addPage();
    this.y = M + 22;
  }

  text(value: string, size = 10, color = INK, style: "normal" | "bold" = "normal", indent = 0) {
    this.doc.setFont("helvetica", style);
    this.doc.setFontSize(size);
    this.doc.setTextColor(...color);
    const lines = arr<string>(this.doc.splitTextToSize(str(value), this.contentWidth - indent));
    lines.forEach((line) => {
      this.need(size * 0.5);
      this.doc.text(str(line), M + indent, this.y);
      this.y += size * 0.48 + 1.6;
    });
  }

  section(title: string) {
    this.need(18);
    this.y += 4;
    this.doc.setFillColor(...BRAND);
    this.doc.roundedRect(M, this.y - 4.6, this.contentWidth, 8.4, 1.6, 1.6, "F");
    this.doc.setFont("helvetica", "bold");
    this.doc.setFontSize(10.5);
    this.doc.setTextColor(255, 255, 255);
    this.doc.text(title.toUpperCase(), M + 3, this.y + 1.2);
    this.y += 10;
  }

  subheading(title: string) {
    this.need(10);
    this.y += 2;
    this.text(title, 10.5, ACCENT, "bold");
    this.y += 0.5;
  }

  row(label: string, value: string) {
    this.need(8);
    this.doc.setDrawColor(...LINE);
    this.doc.line(M, this.y + 3.4, this.w - M, this.y + 3.4);
    this.doc.setFont("helvetica", "normal");
    this.doc.setFontSize(9.5);
    this.doc.setTextColor(...MUTED);
    this.doc.text(str(label), M + 1, this.y);
    this.doc.setFont("helvetica", "bold");
    this.doc.setTextColor(...INK);
    const lines = arr<string>(this.doc.splitTextToSize(str(value) || "—", this.contentWidth * 0.55));
    lines.forEach((line, i) => {
      this.doc.text(str(line), this.w - M - 1, this.y + i * 4.2, { align: "right" });
    });
    this.y += Math.max(1, lines.length) * 4.2 + 3.4;
  }

  levelRow(label: string, level?: unknown) {
    this.need(9);
    const value = str(level).trim() || "Not assessed";
    const color = LEVEL_COLOR[value] ?? [140, 146, 152];
    this.doc.setDrawColor(...LINE);
    this.doc.line(M, this.y + 3.4, this.w - M, this.y + 3.4);
    this.doc.setFont("helvetica", "normal");
    this.doc.setFontSize(9.5);
    this.doc.setTextColor(...MUTED);
    this.doc.text(str(label), M + 1, this.y);
    const chipW = 24;
    const x = this.w - M - chipW;
    this.doc.setFillColor(...color);
    this.doc.roundedRect(x, this.y - 3.4, chipW, 5.4, 2.4, 2.4, "F");
    this.doc.setFont("helvetica", "bold");
    this.doc.setFontSize(8);
    this.doc.setTextColor(255, 255, 255);
    this.doc.text(String(value), x + chipW / 2, this.y + 0.3, { align: "center" });
    this.y += 7.6;
  }

  bar(label: string, percent: number) {
    this.need(12);
    const value = Math.max(0, Math.min(100, Math.round(num(percent))));
    this.doc.setFont("helvetica", "normal");
    this.doc.setFontSize(9);
    this.doc.setTextColor(...MUTED);
    this.doc.text(label, M + 1, this.y);
    this.doc.text(`${value}%`, this.w - M - 1, this.y, { align: "right" });
    this.y += 2.2;
    this.doc.setFillColor(...LINE);
    this.doc.roundedRect(M, this.y, this.contentWidth, 3.2, 1.6, 1.6, "F");
    this.doc.setFillColor(...ACCENT);
    const filled = Math.max(2, (this.contentWidth * value) / 100);
    this.doc.roundedRect(M, this.y, filled, 3.2, 1.6, 1.6, "F");
    this.y += 8;
  }

  bullets(items: unknown) {
    strList(items).forEach((item) => {
      this.need(8);
      this.doc.setFillColor(...ACCENT);
      this.doc.circle(M + 1.4, this.y - 1.1, 0.9, "F");
      this.text(item, 9.5, INK, "normal", 5);
    });
  }

  image(dataUrl: string, caption: string, width = this.contentWidth) {
    try {
      if (typeof dataUrl !== "string" || !dataUrl) throw new Error("no image");
      const props = this.doc.getImageProperties(dataUrl);
      const ratio = props.height / props.width;
      const h = Math.min(width * ratio, 105);
      const w = h / ratio;
      this.need(h + 10);
      this.doc.addImage(dataUrl, "JPEG", M, this.y, w, h, undefined, "FAST");
      this.doc.setDrawColor(...LINE);
      this.doc.rect(M, this.y, w, h);
      this.y += h + 4;
      if (caption) this.text(caption, 8.5, MUTED);
      this.y += 2;
    } catch {
      this.text(`[${caption || "Image"} could not be embedded]`, 8.5, MUTED);
    }
  }

  chrome(logo?: string | null) {
    const pages = this.doc.getNumberOfPages();
    for (let p = 1; p <= pages; p += 1) {
      this.doc.setPage(p);
      this.doc.setFillColor(...BRAND);
      this.doc.rect(0, 0, this.w, 16, "F");
      if (logo) {
        try {
          this.doc.addImage(logo, "PNG", M, 3, 10, 10);
        } catch {
          /* logo optional */
        }
      }
      this.doc.setFont("helvetica", "bold");
      this.doc.setFontSize(10);
      this.doc.setTextColor(255, 255, 255);
      this.doc.text(COMPANY.name, logo ? M + 13 : M, 10);
      this.doc.setFont("helvetica", "normal");
      this.doc.setFontSize(8);
      this.doc.text(`Report ${this.reportNo}`, this.w - M, 10, { align: "right" });

      this.doc.setDrawColor(...LINE);
      this.doc.line(M, this.h - 12, this.w - M, this.h - 12);
      this.doc.setFontSize(7.5);
      this.doc.setTextColor(...MUTED);
      this.doc.text(`${COMPANY.email} · ${COMPANY.phone} · ${COMPANY.website}`, M, this.h - 8);
      this.doc.text(`Page ${p} of ${pages}`, this.w - M, this.h - 8, { align: "right" });
    }
  }
}

function fmtLevel(v?: unknown) {
  return str(v).trim() || "Not assessed";
}

function media(list?: unknown): ReportMedia[] {
  return arr<unknown>(list)
    .map((m) => {
      if (typeof m === "string") return { dataUrl: m, caption: "" } as ReportMedia;
      const item = (m ?? {}) as Partial<ReportMedia>;
      return { dataUrl: str(item.dataUrl), caption: str(item.caption) };
    })
    .filter((m) => Boolean(m.dataUrl));
}

/** Build the full professional AI inspection report as a PDF blob. */
export async function buildReportPdf(input: ReportInput): Promise<Blob> {
  try {
    return await renderReportPdf(input);
  } catch (error) {
    console.error("PDF generation failed", {
      error,
      images: input?.images,
      annotated: input?.annotated,
      detections: input?.assessment?.detections,
      factors: input?.assessment?.factors,
      recommendations: input?.assessment?.recommendations,
      leakageSigns: input?.assessment?.leakage?.signs,
      causes: input?.assessment?.causes,
      thermalImage: typeof input?.thermalImage,
      measurements: input?.measurements,
    });
    throw error instanceof Error ? error : new Error("The report could not be generated.");
  }
}

async function renderReportPdf(input: ReportInput): Promise<Blob> {
  const d = new Doc(input.reportNo);
  const a = (input.assessment ?? {}) as ReportInput["assessment"];

  // Cover block
  d.doc.setFillColor(245, 248, 246);
  d.doc.roundedRect(M, d.y - 6, d.contentWidth, 28, 2.5, 2.5, "F");
  d.text("AI MOLD INSPECTION REPORT", 15, BRAND, "bold");
  d.text(COMPANY.tagline, 9, MUTED);
  d.y += 8;

  let qr: string | null = null;
  try {
    qr = await QRCode.toDataURL(input.verifyUrl, { margin: 0, width: 240 });
  } catch {
    qr = null;
  }
  if (qr) {
    try {
      d.doc.addImage(qr, "PNG", d.w - M - 24, M + 20, 22, 22);
      d.doc.setFontSize(7);
      d.doc.setTextColor(...MUTED);
      d.doc.text("Scan to verify", d.w - M - 13, M + 45, { align: "center" });
    } catch {
      /* optional */
    }
  }

  d.section("Property inspection summary");
  d.row("Report ID", input.reportNo);
  d.row(
    "Inspection date & time",
    input.createdAt instanceof Date ? input.createdAt.toLocaleString() : str(input.createdAt),
  );
  d.row("Customer name", input.customerName || "Not provided");
  d.row("Property address", input.propertyAddress || "Not provided");
  d.row("Inspector", "Emma AI Assistant (preliminary assessment)");
  d.row("Analysis type", input.analysisType);

  d.section("Mold detection results");
  d.row("Overall verdict", str(a.verdict) || "Not determined");
  d.row("Severity", fmtLevel(a.severity));
  d.bar("AI confidence", num(a.confidence));
  d.text(str(a.summary), 9.5, INK);

  const detections = arr<NonNullable<typeof a.detections>[number]>(a.detections).filter(
    (det) => det && typeof det === "object",
  );
  if (detections.length) {
    d.subheading("Detected areas");
    detections.forEach((det, i) => {
      d.need(24);
      d.text(`Area ${i + 1}`, 9.5, ACCENT, "bold");
      d.row("Mold detected", det.moldDetected ?? "Possible");
      d.row("Severity", fmtLevel(det.severity));
      d.row("Confidence score", `${Math.round(num(det.confidence))}%`);
      d.row("Affected surface", det.surface ?? "Not determined");
      d.row("Estimated affected area", det.areaEstimate ?? "Not determined");
    });
  }

  const factors = strList(a.factors);
  if (factors.length) {
    d.subheading("Contributing factors observed");
    d.bullets(factors);
  }

  // Media
  const annotated = media(input.annotated);
  const originals = media(input.images);
  if (annotated.length || originals.length) {
    d.section("Uploaded media & annotated images");
    d.subheading("Legend");
    arr<{ color: string; label: string }>(LEGEND).forEach((l) => {
      d.need(7);
      const rgb = hexToRgb(l.color);
      d.doc.setDrawColor(rgb[0], rgb[1], rgb[2]);
      d.doc.setLineWidth(0.7);
      d.doc.rect(M + 1, d.y - 2.6, 5, 3.4);
      d.doc.setLineWidth(0.2);
      d.text(l.label, 9, INK, "normal", 9);
    });
    d.y += 2;
    (annotated.length ? annotated : originals).forEach((m) => d.image(m.dataUrl, m.caption));
    if (annotated.length && originals.length) {
      d.subheading("Original uploaded media");
      originals.forEach((m) => d.image(m.dataUrl, m.caption));
    }
  }

  d.section("Environmental assessment");
  d.levelRow("Poor ventilation", a.environmental?.ventilation);
  d.levelRow("High humidity", a.environmental?.humidity);
  d.levelRow("Condensation risk", a.environmental?.condensation);
  d.levelRow("Water leakage indicators", a.environmental?.leakage);
  d.levelRow("Dampness level", a.environmental?.dampness);
  d.levelRow("Air circulation rating", a.environmental?.airCirculation);

  const ms = input.measurements ?? {};
  const has = (v: unknown) => v !== undefined && v !== null && v !== "";
  const NR = "Not recorded";

  d.section("Inspector measurements");

  d.subheading("Temperature");
  d.row("Measured value", has(ms.temperatureValue) ? `${ms.temperatureValue} °C` : NR);
  d.row("Status", ms.temperatureLevel || NR);
  d.row("Score", ms.temperatureScore != null ? `${ms.temperatureScore}%` : NR);
  if (ms.temperatureScore != null) d.bar("Temperature score", ms.temperatureScore);

  d.subheading("Humidity");
  d.row("Measured value", has(ms.humidityValue) ? `${ms.humidityValue}% RH` : NR);
  d.row("Status", ms.humidityLevel || NR);
  d.row("Score", ms.humidityScore != null ? `${ms.humidityScore}%` : NR);
  if (ms.humidityScore != null) d.bar("Humidity score", ms.humidityScore);

  d.subheading("Moisture reading");
  d.row("Moisture meter reading", ms.moisture || NR);
  d.row("Moisture percentage", ms.moistureScore != null ? `${ms.moistureScore}%` : NR);
  if (ms.moistureScore != null) d.bar("Moisture score", ms.moistureScore);

  d.subheading("Water detector reading");
  d.row("Detector result", ms.detector || NR);

  d.subheading("Overall environmental risk");
  d.row(
    "Risk level",
    [ms.overallRisk || NR, ms.overallRiskScore != null ? `${ms.overallRiskScore}%` : ""].filter(Boolean).join(" · "),
  );
  if (ms.overallRiskScore != null) d.bar("Overall risk score", ms.overallRiskScore);
  if (ms.overallRiskNote) d.text(ms.overallRiskNote, 9, MUTED);

  d.subheading("Inspector comments");
  d.text(ms.inspectorComments && ms.inspectorComments.trim() ? ms.inspectorComments : NR, 9.5);

  d.section("Possible water leakage detection");
  d.row("Verdict", str(a.leakage?.verdict) || "No obvious signs detected");
  const leakSigns = strList(a.leakage?.signs);
  if (leakSigns.length) d.bullets(leakSigns);
  const sources: [string, string][] = [
    ["ceiling", "Ceiling leak indicators"],
    ["wall", "Wall moisture indicators"],
    ["roof", "Roof leak indicators"],
    ["pipe", "Pipe leak indicators"],
    ["window", "Window infiltration indicators"],
    ["floor", "Floor moisture indicators"],
  ];
  sources.forEach(([key, label]) => {
    const s = a.leakSources?.[key as keyof NonNullable<typeof a.leakSources>];
    d.row(label, `${str(s?.indicator) || "None observed"} (${Math.round(num(s?.confidence))}% confidence)`);
  });

  const causes = arr<NonNullable<typeof a.causes>[number]>(a.causes).filter((c) => c && typeof c === "object");
  if (causes.length) {
    d.section("Likely causes of mold");
    causes.forEach((c) => {
      d.need(16);
      d.levelRow(str(c.cause), c.likelihood);
      if (c.reason) d.text(str(c.reason), 9, MUTED, "normal", 3);
    });
  }

  d.section("Risk assessment");
  d.levelRow("Health risk", a.risks?.health);
  d.levelRow("Structural risk", a.risks?.structural);
  d.levelRow("Moisture risk", a.risks?.moisture);
  d.levelRow("Mold growth risk", a.risks?.moldGrowth);
  d.levelRow("Need for urgent inspection", a.risks?.urgentInspection);

  const recommendations = strList(a.recommendations);
  if (recommendations.length) {
    d.section("Recommended actions");
    d.bullets(recommendations);
  }

  const thermalImages = arr<unknown>(input.thermalImage).map(str).filter(Boolean);
  if (thermalImages.length) {
    d.section("Thermal inspection");
    arr<unknown>(input.thermalNormal)
      .map(str)
      .filter(Boolean)
      .forEach((img) => d.image(img, "Normal image of the same area"));
    thermalImages.forEach((img) => d.image(img, "Thermal image / heat map"));
    d.row("Cold spots", str(a.thermal?.coldSpots) || "Not assessed");
    d.row("Moisture intrusion", str(a.thermal?.moistureIntrusion) || "Not assessed");
    if (a.thermal?.notes) d.text(str(a.thermal.notes), 9.5, INK);
    if (a.thermal?.findings) d.text(str(a.thermal.findings), 9.5, INK);
    if (ms?.thermalNotes) d.text(`Inspector notes: ${str(ms.thermalNotes)}`, 9, MUTED);
  }

  if (input.analysisType === "Video") {
    d.section("Video analysis summary");
    d.row("Video duration", input.video?.duration ? `${Math.round(num(input.video.duration))} seconds` : "Not available");
    d.row("Frames analysed", String(input.video?.framesAnalyzed ?? originals.length));
    const areas = strList(a.video?.areasDetected);
    if (areas.length) {
      d.subheading("Areas detected across frames");
      d.bullets(areas);
    }
    d.row("Mold progression", str(a.video?.progression) || "Not observed");
    d.row("Moisture observations", str(a.video?.moisture) || "Not observed");
    if (a.video?.combined) d.text(str(a.video.combined), 9.5, INK);
  }

  const before = media(input.before);
  const after = media(input.after);
  if (before.length || after.length) {
    d.section("Before and after comparison");
    if (before.length) {
      d.subheading("Before treatment");
      before.forEach((m) => d.image(m.dataUrl, m.caption));
    }
    if (after.length) {
      d.subheading("After treatment");
      after.forEach((m) => d.image(m.dataUrl, m.caption));
    }
    if (input.improvement) d.row("Improvement assessment", input.improvement);
    if (input.remainingRisks) d.row("Remaining risk areas", input.remainingRisks);
  }

  const edu = educationForAssessment(a);
  d.section("Mold education and prevention");
  d.row("Most likely mold type", `${edu.issue.name}${edu.issue.scientific ? ` (${edu.issue.scientific})` : ""}`);
  d.row("Indicative risk level", edu.issue.risk);
  d.row("Species to consider", edu.species.map((s) => s.name).join(", "));
  d.subheading("Possible causes");
  d.text(`${edu.issue.moisture} ${edu.reason}`, 9.5, INK);
  d.subheading("Health considerations");
  d.text(edu.issue.healthRisks, 9.5, INK);
  d.text(HEALTH_DISCLAIMER, 8.5, MUTED);
  d.subheading("Prevention recommendations");
  PREVENTION_CHECKLIST.slice(0, 5).forEach((p) => d.text(`\u2022 ${p}`, 9.5, INK, "normal", 3));
  d.subheading("Maintenance checklist");
  MAINTENANCE_REMINDERS.forEach((r) => d.text(`\u2022 ${r.every}: ${r.task}`, 9.5, INK, "normal", 3));
  d.subheading("Professional treatment guidance");
  d.text(edu.issue.treatment, 9.5, INK);
  d.text(`Call a professional when: ${CALL_PROFESSIONAL.slice(0, 4).join("; ")}.`, 9, MUTED);

  d.section("Professional conclusion");
  d.row("Visible mold present", str(a.conclusion?.visibleMold) || str(a.verdict));
  d.row("Location", str(a.conclusion?.location) || "See detected areas");
  d.row("Most likely causes", str(a.conclusion?.likelyCauses) || str(causes[0]?.cause) || "Not determined");
  d.levelRow("Severity", a.conclusion?.severity ?? (a.severity as Level));
  d.row("Professional inspection", str(a.conclusion?.professionalInspection) || "Recommended");
  d.row("Mold treatment", str(a.conclusion?.treatment) || "Recommended");
  d.levelRow("Priority level", a.conclusion?.priority ?? (a.severity as Level));
  if (a.conclusion?.statement) d.text(str(a.conclusion.statement), 9.5, INK);

  d.section("Signatures");
  d.need(34);
  const half = (d.contentWidth - 8) / 2;
  d.doc.setDrawColor(...LINE);
  d.doc.roundedRect(M, d.y, half, 26, 2, 2);
  d.doc.roundedRect(M + half + 8, d.y, half, 26, 2, 2);
  d.doc.setFontSize(8.5);
  d.doc.setTextColor(...MUTED);
  d.doc.text("Inspector signature", M + 3, d.y + 21);
  d.doc.text("Customer signature", M + half + 11, d.y + 21);
  d.doc.line(M + 3, d.y + 17, M + half - 3, d.y + 17);
  d.doc.line(M + half + 11, d.y + 17, M + d.contentWidth - 3, d.y + 17);
  d.y += 32;

  d.section("Important disclaimer");
  d.text(REPORT_DISCLAIMER, 9, MUTED);
  d.y += 2;
  d.text(`${COMPANY.name} · ${COMPANY.address} · ${COMPANY.phone} · ${COMPANY.email} · ${COMPANY.website}`, 8.5, MUTED);

  d.chrome(input.logoDataUrl);
  return d.doc.output("blob");
}

function hexToRgb(hex: string): [number, number, number] {
  const n = parseInt(hex.replace("#", ""), 16);
  return [(n >> 16) & 255, (n >> 8) & 255, n & 255];
}