import fs from "node:fs";
import path from "node:path";
import PDFDocument from "pdfkit";

export class ReportValidationError extends Error {}

export interface ReportPayload {
  recipientEmails?: string[] | string;
  assessmentPurpose?: string;
  reportTitle?: string;
  subtitle?: string;
  generatedAt?: string;
  preparedBy?: string;
  reportNotes?: string;
  subjectProperty?: Record<string, unknown>;
  verdict?: Record<string, unknown>;
  snapshot?: unknown;
  whatILike?: unknown;
  whatIDontLike?: unknown;
  compView?: unknown;
  carryView?: unknown;
  risksAndDiligence?: unknown;
  photoReview?: Record<string, unknown>;
  publicRecords?: Record<string, unknown>;
  sourceLinks?: unknown;
}

export function isDecisionGradeReportPayload(payload: ReportPayload): boolean {
  const decision = String(payload.verdict?.decision || "").trim().toLowerCase();
  const fairValueRange = String(payload.verdict?.fairValueRange || "").trim().toLowerCase();
  const photoReviewStatus = String(payload.photoReview?.status || "").trim().toLowerCase();
  if (!decision || decision === "pending") return false;
  if (!fairValueRange || fairValueRange === "not established") return false;
  if (photoReviewStatus !== "completed") return false;
  return true;
}

function asStringArray(value: unknown): string[] {
  if (value == null) return [];
  if (typeof value === "string") {
    const text = value.trim();
    return text ? [text] : [];
  }
  if (typeof value === "number" || typeof value === "boolean") {
    return [String(value)];
  }
  if (Array.isArray(value)) {
    const out: string[] = [];
    for (const item of value) {
      out.push(...asStringArray(item));
    }
    return out;
  }
  if (typeof value === "object") {
    return Object.entries(value as Record<string, unknown>)
      .filter(([, item]) => item != null && item !== "")
      .map(([key, item]) => `${key}: ${item}`);
  }
  return [String(value)];
}

function currency(value: unknown): string {
  if (value == null || value === "") return "N/A";
  const num = Number(value);
  if (Number.isFinite(num)) return `$${num.toLocaleString("en-US", { maximumFractionDigits: 0 })}`;
  return String(value);
}

export function validateReportPayload(payload: ReportPayload): string[] {
  const recipients = asStringArray(payload.recipientEmails).map((item) => item.trim()).filter(Boolean);
  if (!recipients.length) {
    throw new ReportValidationError(
      "Missing target email. Stop and ask the user for target email address(es) before generating or sending the property assessment PDF."
    );
  }
  const address = payload.subjectProperty && typeof payload.subjectProperty.address === "string"
    ? payload.subjectProperty.address.trim()
    : "";
  if (!address) {
    throw new ReportValidationError("The report payload must include subjectProperty.address.");
  }
  if (!isDecisionGradeReportPayload(payload)) {
    throw new ReportValidationError(
      "The report payload is still preliminary. Stop and complete the decision-grade analysis, including subject-unit photo review, before generating or sending the property assessment PDF."
    );
  }
  return recipients;
}

function drawSectionHeader(doc: PDFKit.PDFDocument, title: string): void {
  const x = doc.page.margins.left;
  const y = doc.y;
  const width = doc.page.width - doc.page.margins.left - doc.page.margins.right;
  const height = 18;
  doc.save();
  doc.roundedRect(x, y, width, height, 3).fill("#123B5D");
  doc
    .fillColor("white")
    .font("Helvetica-Bold")
    .fontSize(12)
    .text(title, x + 8, y + 4, { width: width - 16 });
  doc.restore();
  doc.moveDown(1.2);
}

function drawBulletList(doc: PDFKit.PDFDocument, value: unknown, fallback = "Not provided."): void {
  const items = asStringArray(value);
  if (!items.length) {
    doc.fillColor("#1E2329").font("Helvetica").fontSize(10.5).text(fallback);
    doc.moveDown(0.6);
    return;
  }

  for (const item of items) {
    const startY = doc.y;
    doc.circle(doc.page.margins.left + 4, startY + 6, 1.5).fill("#123B5D");
    doc
      .fillColor("#1E2329")
      .font("Helvetica")
      .fontSize(10.5)
      .text(item, doc.page.margins.left + 14, startY, {
        width: doc.page.width - doc.page.margins.left - doc.page.margins.right - 14,
        lineGap: 2
      });
    doc.moveDown(0.35);
  }
  doc.moveDown(0.2);
}

function drawKeyValueTable(doc: PDFKit.PDFDocument, rows: Array<[string, string]>): void {
  const left = doc.page.margins.left;
  const totalWidth = doc.page.width - doc.page.margins.left - doc.page.margins.right;
  const keyWidth = 150;
  const valueWidth = totalWidth - keyWidth;
  for (const [key, value] of rows) {
    const startY = doc.y;
    const keyHeight = doc.heightOfString(key, { width: keyWidth - 12 });
    const valueHeight = doc.heightOfString(value, { width: valueWidth - 12 });
    const rowHeight = Math.max(keyHeight, valueHeight) + 12;

    doc.save();
    doc.rect(left, startY, keyWidth, rowHeight).fill("#EEF3F7");
    doc.rect(left + keyWidth, startY, valueWidth, rowHeight).fill("#FFFFFF");
    doc
      .lineWidth(0.5)
      .strokeColor("#C7D0D9")
      .rect(left, startY, totalWidth, rowHeight)
      .stroke();
    doc
      .moveTo(left + keyWidth, startY)
      .lineTo(left + keyWidth, startY + rowHeight)
      .stroke();
    doc.restore();

    doc.fillColor("#1E2329").font("Helvetica-Bold").fontSize(9.5).text(key, left + 6, startY + 6, { width: keyWidth - 12 });
    doc.fillColor("#1E2329").font("Helvetica").fontSize(9.5).text(value, left + keyWidth + 6, startY + 6, { width: valueWidth - 12 });
    doc.y = startY + rowHeight;
  }
  doc.moveDown(0.8);
}

function drawVerdictPanel(doc: PDFKit.PDFDocument, verdict: Record<string, unknown> | undefined): void {
  const decision = String(verdict?.decision || "pending").trim().toLowerCase();
  const badgeColor =
    decision === "buy" ? "#1E6B52" : decision === "pass" ? "#8B2E2E" : "#7A5D12";
  const left = doc.page.margins.left;
  const width = doc.page.width - doc.page.margins.left - doc.page.margins.right;
  const top = doc.y;
  const bodyText = String(
    verdict?.offerGuidance || "Offer guidance not provided."
  );
  const bodyHeight = doc.heightOfString(bodyText, { width: width - 20 }) + 16;

  doc.save();
  doc.roundedRect(left, top, width, 26, 4).fill(badgeColor);
  doc
    .fillColor("white")
    .font("Helvetica-Bold")
    .fontSize(12)
    .text(String(verdict?.decision || "N/A").toUpperCase(), left + 10, top + 7, {
      width: width - 20
    });

  doc
    .roundedRect(left, top + 26, width, bodyHeight, 4)
    .fillAndStroke("#F4F6F8", "#C7D0D9");
  doc
    .fillColor("#1E2329")
    .font("Helvetica")
    .fontSize(10.5)
    .text(bodyText, left + 10, top + 36, {
      width: width - 20,
      lineGap: 2
    });
  doc.restore();
  doc.y = top + 26 + bodyHeight + 10;
}

function drawLinks(doc: PDFKit.PDFDocument, value: unknown): void {
  const items = Array.isArray(value) ? value : [];
  if (!items.length) {
    drawBulletList(doc, [], "Not provided.");
    return;
  }
  for (const item of items as Array<Record<string, unknown>>) {
    const label = typeof item.label === "string" ? item.label : "Link";
    const url = typeof item.url === "string" ? item.url : "";
    const line = url ? `${label}: ${url}` : label;
    const startY = doc.y;
    doc.circle(doc.page.margins.left + 4, startY + 6, 1.5).fill("#123B5D");
    doc.fillColor("#1E2329").font("Helvetica").fontSize(10.5);
    if (url) {
      doc.text(line, doc.page.margins.left + 14, startY, {
        width: doc.page.width - doc.page.margins.left - doc.page.margins.right - 14,
        lineGap: 2,
        link: url,
        underline: true
      });
    } else {
      doc.text(line, doc.page.margins.left + 14, startY, {
        width: doc.page.width - doc.page.margins.left - doc.page.margins.right - 14,
        lineGap: 2
      });
    }
    doc.moveDown(0.35);
  }
  doc.moveDown(0.2);
}

export async function renderReportPdf(
  payload: ReportPayload,
  outputPath: string
): Promise<string> {
  const recipients = validateReportPayload(payload);
  await fs.promises.mkdir(path.dirname(outputPath), { recursive: true });

  const doc = new PDFDocument({
    size: "LETTER",
    margin: 50,
    info: {
      Title: payload.reportTitle || "Property Assessment Report",
      Author: String(payload.preparedBy || "OpenClaw property-assessor")
    }
  });

  const stream = fs.createWriteStream(outputPath);
  doc.pipe(stream);

  const generatedAt =
    payload.generatedAt ||
    new Date().toLocaleString("en-US", {
      dateStyle: "medium",
      timeStyle: "short"
    });
  const subject = payload.subjectProperty || {};
  const verdict = payload.verdict || {};
  const publicRecords = payload.publicRecords || {};

  doc.fillColor("#123B5D").font("Helvetica-Bold").fontSize(22).text(payload.reportTitle || "Property Assessment Report");
  doc.moveDown(0.2);
  doc.fillColor("#1E2329").font("Helvetica").fontSize(11).text(
    String(
      payload.subtitle ||
        "Decision-grade acquisition review with listing, public-record, comp, and risk analysis."
    )
  );
  doc.moveDown(0.4);
  doc.fillColor("#5A6570").font("Helvetica").fontSize(9);
  doc.text(`Prepared for: ${recipients.join(", ")}`);
  doc.text(`Generated: ${generatedAt}`);
  doc.moveDown(0.8);

  drawVerdictPanel(doc, verdict);

  drawKeyValueTable(doc, [
    ["Address", String(subject.address || "N/A")],
    ["Assessment Purpose", String(payload.assessmentPurpose || "N/A")],
    ["Ask / Last Price", currency(subject.listingPrice)],
    ["Type", String(subject.propertyType || "N/A")],
    ["Beds / Baths", `${subject.beds ?? "N/A"} / ${subject.baths ?? "N/A"}`],
    ["Sqft", String(subject.squareFeet ?? "N/A")],
    ["Year Built", String(subject.yearBuilt ?? "N/A")],
    ["Verdict", String(verdict.decision || "N/A")],
    ["Fair Value Range", String(verdict.fairValueRange || "N/A")],
    ["Public-Record Jurisdiction", String(publicRecords.jurisdiction || "N/A")],
    ["Assessed Total", currency(publicRecords.assessedTotalValue)]
  ]);

  const sections: Array<[string, unknown, "list" | "links"]> = [
    ["Snapshot", payload.snapshot, "list"],
    ["What I Like", payload.whatILike, "list"],
    ["What I Do Not Like", payload.whatIDontLike, "list"],
    ["Comp View", payload.compView, "list"],
    ["Underwriting / Carry View", payload.carryView, "list"],
    ["Risks and Diligence Items", payload.risksAndDiligence, "list"],
    [
      "Photo Review",
      [
        ...(asStringArray((payload.photoReview || {}).status ? [`Photo review: ${String((payload.photoReview || {}).status)}${(payload.photoReview || {}).source ? ` via ${String((payload.photoReview || {}).source)}` : ""}`] : [])),
        ...asStringArray((payload.photoReview || {}).attempts),
        ...asStringArray((payload.photoReview || {}).summary ? [`Condition read: ${String((payload.photoReview || {}).summary)}`] : [])
      ],
      "list"
    ],
    [
      "Public Records",
      [
        ...asStringArray({
          Jurisdiction: publicRecords.jurisdiction,
          "Account Number": publicRecords.accountNumber,
          "Owner Name": publicRecords.ownerName,
          "Land Value": publicRecords.landValue != null ? currency(publicRecords.landValue) : undefined,
          "Improvement Value":
            publicRecords.improvementValue != null ? currency(publicRecords.improvementValue) : undefined,
          "Assessed Total":
            publicRecords.assessedTotalValue != null ? currency(publicRecords.assessedTotalValue) : undefined,
          Exemptions: publicRecords.exemptions
        }),
        ...asStringArray((publicRecords.links || []).map((item: any) => `${item.label}: ${item.url}`))
      ],
      "list"
    ],
    ["Source Links", payload.sourceLinks, "links"]
  ];

  for (const [title, content, kind] of sections) {
    if (doc.y > 660) doc.addPage();
    drawSectionHeader(doc, title);
    if (kind === "links") {
      drawLinks(doc, content);
    } else {
      drawBulletList(doc, content);
    }
  }

  doc.addPage();
  drawSectionHeader(doc, "Report Notes");
  doc.fillColor("#1E2329").font("Helvetica").fontSize(10.5).text(
    String(
      payload.reportNotes ||
        "This report uses the property-assessor fixed PDF template. Listing data should be reconciled against official public records when available, and public-record links should be included in any delivered report."
    ),
    {
      lineGap: 3
    }
  );

  doc.end();

  await new Promise<void>((resolve, reject) => {
    stream.on("finish", () => resolve());
    stream.on("error", reject);
  });

  return outputPath;
}

export async function loadReportPayload(inputPath: string): Promise<ReportPayload> {
  return JSON.parse(await fs.promises.readFile(inputPath, "utf8")) as ReportPayload;
}
