import { execFile } from "node:child_process";
import { constants } from "node:fs";
import { access, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { resolve } from "node:path";
import { promisify } from "node:util";
import {
  PDFCheckBox,
  PDFDocument,
  PDFDropdown,
  PDFOptionList,
  PDFRadioGroup,
  PDFTextField,
  StandardFonts,
  rgb,
  type PDFFont,
  type PDFPage,
} from "pdf-lib";

const execFileAsync = promisify(execFile);

const A4_WIDTH = 595.28;
const A4_HEIGHT = 841.89;
const PAGE_MARGIN = 44;
const TITLE_SIZE = 20;
const SUBTITLE_SIZE = 11;
const BODY_SIZE = 10;
const SECTION_SIZE = 12;
const LINE_HEIGHT = 14;

type TextSection = {
  heading: string;
  lines: string[];
};

export type FilledFormAuditEntry = {
  sectionTitle: string;
  label: string;
  pdfFieldName: string;
  value: string;
  rationale: string;
  sourceTitles: string[];
  evidenceExcerpt?: string | null;
  confidence?: number | null;
};

export type FilledFormCompletionSuggestions = {
  summary?: string | null;
  attorneyQuestions: string[];
  clientQuestions: string[];
  sponsorQuestions: string[];
  requestedDocuments: string[];
};

type PacketPdfItem = {
  kind: "form" | "document";
  title: string;
  mimeType: string;
  originalFileName: string;
  bytes: Buffer;
  fallbackText?: string | null;
};

function isTruthyValue(value: string) {
  const normalized = value.trim().toLowerCase();
  return ["1", "true", "yes", "y", "sim", "s", "checked", "x"].includes(normalized);
}

function looksLikePdf(input: { mimeType: string; originalFileName: string; bytes: Buffer }) {
  return (
    input.mimeType.includes("pdf") ||
    input.originalFileName.toLowerCase().endsWith(".pdf") ||
    input.bytes.subarray(0, 4).toString("utf8") === "%PDF"
  );
}

function looksLikePng(input: { mimeType: string; originalFileName: string; bytes: Buffer }) {
  return (
    input.mimeType === "image/png" ||
    input.originalFileName.toLowerCase().endsWith(".png") ||
    input.bytes.subarray(1, 4).toString("utf8") === "PNG"
  );
}

function looksLikeJpeg(input: { mimeType: string; originalFileName: string; bytes: Buffer }) {
  const signature = input.bytes.subarray(0, 2);
  return (
    input.mimeType === "image/jpeg" ||
    input.mimeType === "image/jpg" ||
    input.originalFileName.toLowerCase().endsWith(".jpg") ||
    input.originalFileName.toLowerCase().endsWith(".jpeg") ||
    (signature[0] === 0xff && signature[1] === 0xd8)
  );
}

async function resolveBundledToolPath(...segments: string[]) {
  const candidates = [
    resolve(process.cwd(), ...segments),
    resolve(process.cwd(), "apps", "api", ...segments),
    resolve(process.cwd(), "..", ...segments),
    resolve(process.cwd(), "..", "..", "apps", "api", ...segments),
  ];

  for (const candidate of candidates) {
    try {
      await access(candidate, constants.F_OK);
      return candidate;
    } catch {
      continue;
    }
  }

  return null;
}

async function tryBuildFilledFormPdfWithPyMuPdf(input: {
  fields: Array<{
    pdfFieldName: string;
    label: string;
    value: string | null;
  }>;
  basePdfBytes: Buffer;
}) {
  const helperScriptPath = await resolveBundledToolPath("tools", "fill_pdf_form.py");

  if (!helperScriptPath) {
    return null;
  }

  const tempDirectory = await mkdtemp(resolve(tmpdir(), "neurav2-form-fill-"));
  const inputPdfPath = resolve(tempDirectory, "source.pdf");
  const outputPdfPath = resolve(tempDirectory, "filled.pdf");
  const valuesPath = resolve(tempDirectory, "values.json");

  try {
    await writeFile(inputPdfPath, input.basePdfBytes);
    await writeFile(
      valuesPath,
      JSON.stringify(
        input.fields
          .filter((field) => field.value?.trim())
          .map((field) => ({
            pdfFieldName: field.pdfFieldName,
            value: field.value,
          })),
      ),
      "utf8",
    );

    await execFileAsync("python3", [
      helperScriptPath,
      "--input",
      inputPdfPath,
      "--output",
      outputPdfPath,
      "--values",
      valuesPath,
    ]);

    const filledBytes = await readFile(outputPdfPath);
    const document = await PDFDocument.load(filledBytes, {
      ignoreEncryption: true,
    });

    return {
      bytes: Buffer.from(filledBytes),
      pageCount: document.getPageCount(),
    };
  } catch {
    return null;
  } finally {
    await rm(tempDirectory, { recursive: true, force: true }).catch(() => undefined);
  }
}

function wrapText(font: PDFFont, text: string, maxWidth: number, size: number) {
  const normalized = text.replace(/\s+/g, " ").trim();

  if (!normalized) {
    return [""];
  }

  const words = normalized.split(" ");
  const lines: string[] = [];
  let currentLine = "";

  for (const word of words) {
    const nextLine = currentLine ? `${currentLine} ${word}` : word;

    if (font.widthOfTextAtSize(nextLine, size) <= maxWidth) {
      currentLine = nextLine;
      continue;
    }

    if (currentLine) {
      lines.push(currentLine);
    }

    if (font.widthOfTextAtSize(word, size) <= maxWidth) {
      currentLine = word;
      continue;
    }

    let partial = "";
    for (const character of word) {
      const nextPartial = `${partial}${character}`;
      if (font.widthOfTextAtSize(nextPartial, size) > maxWidth && partial) {
        lines.push(partial);
        partial = character;
      } else {
        partial = nextPartial;
      }
    }

    currentLine = partial;
  }

  if (currentLine) {
    lines.push(currentLine);
  }

  return lines;
}

function createTextPage(document: PDFDocument) {
  return document.addPage([A4_WIDTH, A4_HEIGHT]);
}

function fitWithinBox(input: {
  width: number;
  height: number;
  maxWidth: number;
  maxHeight: number;
}) {
  const widthScale = input.maxWidth / input.width;
  const heightScale = input.maxHeight / input.height;
  const scale = Math.min(widthScale, heightScale, 1);

  return {
    width: input.width * scale,
    height: input.height * scale,
  };
}

async function appendTextSections(input: {
  document: PDFDocument;
  title: string;
  subtitle?: string | null;
  sections: TextSection[];
}) {
  const regularFont = await input.document.embedFont(StandardFonts.Helvetica);
  const boldFont = await input.document.embedFont(StandardFonts.HelveticaBold);
  let page = createTextPage(input.document);
  let cursorY = A4_HEIGHT - PAGE_MARGIN;

  const maxWidth = A4_WIDTH - PAGE_MARGIN * 2;

  page.drawText(input.title, {
    x: PAGE_MARGIN,
    y: cursorY,
    size: TITLE_SIZE,
    font: boldFont,
  });
  cursorY -= TITLE_SIZE + 10;

  if (input.subtitle?.trim()) {
    for (const line of wrapText(regularFont, input.subtitle, maxWidth, SUBTITLE_SIZE)) {
      page.drawText(line, {
        x: PAGE_MARGIN,
        y: cursorY,
        size: SUBTITLE_SIZE,
        font: regularFont,
      });
      cursorY -= LINE_HEIGHT;
    }
    cursorY -= 10;
  }

  for (const section of input.sections) {
    const headingLines = wrapText(boldFont, section.heading, maxWidth, SECTION_SIZE);

    if (cursorY <= PAGE_MARGIN + headingLines.length * LINE_HEIGHT + LINE_HEIGHT * 2) {
      page = createTextPage(input.document);
      cursorY = A4_HEIGHT - PAGE_MARGIN;
    }

    for (const line of headingLines) {
      page.drawText(line, {
        x: PAGE_MARGIN,
        y: cursorY,
        size: SECTION_SIZE,
        font: boldFont,
      });
      cursorY -= LINE_HEIGHT;
    }

    cursorY -= 4;

    for (const sourceLine of section.lines) {
      const wrappedLines = wrapText(regularFont, sourceLine, maxWidth, BODY_SIZE);

      for (const line of wrappedLines) {
        if (cursorY <= PAGE_MARGIN + LINE_HEIGHT) {
          page = createTextPage(input.document);
          cursorY = A4_HEIGHT - PAGE_MARGIN;
        }

        page.drawText(line, {
          x: PAGE_MARGIN,
          y: cursorY,
          size: BODY_SIZE,
          font: regularFont,
        });
        cursorY -= LINE_HEIGHT;
      }
    }

    cursorY -= 12;
  }
}

async function buildTextPdf(input: {
  title: string;
  subtitle?: string | null;
  sections: TextSection[];
}) {
  const document = await PDFDocument.create();
  await appendTextSections({
    document,
    title: input.title,
    subtitle: input.subtitle,
    sections: input.sections,
  });

  return {
    bytes: Buffer.from(await document.save()),
    pageCount: document.getPageCount(),
  };
}

export async function buildFilledFormAuditPdf(input: {
  formName: string;
  clientName: string;
  filledCount: number;
  unresolvedCount: number;
  entries: FilledFormAuditEntry[];
  completionSuggestions?: FilledFormCompletionSuggestions | null;
}) {
  const groupedEntries = new Map<string, FilledFormAuditEntry[]>();

  for (const entry of input.entries) {
    const sectionTitle = entry.sectionTitle.trim() || "Other filled fields";
    const currentEntries = groupedEntries.get(sectionTitle) ?? [];
    currentEntries.push(entry);
    groupedEntries.set(sectionTitle, currentEntries);
  }

  const sections: TextSection[] = Array.from(groupedEntries.entries()).map(([heading, entries]) => ({
    heading,
    lines: entries.flatMap((entry, index) => {
      const lines = [
        `${index + 1}. ${entry.label || entry.pdfFieldName}`,
        `PDF field: ${entry.pdfFieldName}`,
        `Filled value: ${entry.value}`,
        `Why it was filled: ${entry.rationale || "Direct evidence supported this answer."}`,
      ];

      if (entry.sourceTitles.length > 0) {
        lines.push(`Reference document(s): ${entry.sourceTitles.join(", ")}`);
      }

      if (entry.evidenceExcerpt?.trim()) {
        lines.push(`Evidence excerpt: ${entry.evidenceExcerpt.trim()}`);
      }

      if (typeof entry.confidence === "number" && Number.isFinite(entry.confidence)) {
        lines.push(`Confidence: ${Math.round(Math.max(0, Math.min(1, entry.confidence)) * 100)}%`);
      }

      lines.push("");
      return lines;
    }),
  }));

  const suggestionSections: TextSection[] = [];

  if (input.completionSuggestions) {
    const summary = input.completionSuggestions.summary?.trim();
    if (summary) {
      suggestionSections.push({
        heading: "Completion summary",
        lines: [summary],
      });
    }

    if (input.completionSuggestions.attorneyQuestions.length > 0) {
      suggestionSections.push({
        heading: "Questions for the case attorney",
        lines: input.completionSuggestions.attorneyQuestions.map((item, index) => `${index + 1}. ${item}`),
      });
    }

    if (input.completionSuggestions.clientQuestions.length > 0) {
      suggestionSections.push({
        heading: "Questions for the client",
        lines: input.completionSuggestions.clientQuestions.map((item, index) => `${index + 1}. ${item}`),
      });
    }

    if (input.completionSuggestions.sponsorQuestions.length > 0) {
      suggestionSections.push({
        heading: "Questions for the sponsor",
        lines: input.completionSuggestions.sponsorQuestions.map((item, index) => `${index + 1}. ${item}`),
      });
    }

    if (input.completionSuggestions.requestedDocuments.length > 0) {
      suggestionSections.push({
        heading: "Documents to request",
        lines: input.completionSuggestions.requestedDocuments.map((item, index) => `${index + 1}. ${item}`),
      });
    }
  }

  return buildTextPdf({
    title: `${input.formName} field fill explanation`,
    subtitle: `${input.clientName} • ${input.filledCount} field(s) filled • ${input.unresolvedCount} left blank when evidence was missing.`,
    sections: [...sections, ...suggestionSections],
  });
}

export async function buildFilledFormPdf(input: {
  formName: string;
  fields: Array<{
    pdfFieldName: string;
    label: string;
    value: string | null;
  }>;
  unresolved: string[];
  basePdfBytes?: Buffer | null;
}) {
  if (input.basePdfBytes?.length) {
    try {
      const document = await PDFDocument.load(input.basePdfBytes, {
        ignoreEncryption: true,
      });
      const form = document.getForm();
      const availableFields = form.getFields();

      if (availableFields.length === 0) {
        throw new Error("The base PDF exposes no fillable fields to pdf-lib.");
      }

      const defaultFont = await document.embedFont(StandardFonts.Helvetica);
      const fieldsByName = new Map(availableFields.map((field) => [field.getName(), field]));

      for (const fieldValue of input.fields) {
        const value = fieldValue.value?.trim() ?? "";
        if (!value) {
          continue;
        }

        const field = fieldsByName.get(fieldValue.pdfFieldName);
        if (!field) {
          continue;
        }

        try {
          if (field instanceof PDFTextField) {
            field.setText(value);
            continue;
          }

          if (field instanceof PDFCheckBox) {
            if (isTruthyValue(value)) {
              field.check();
            } else {
              field.uncheck();
            }
            continue;
          }

          if (field instanceof PDFDropdown) {
            field.select(value);
            continue;
          }

          if (field instanceof PDFOptionList) {
            field.select([value]);
            continue;
          }

          if (field instanceof PDFRadioGroup) {
            field.select(value);
          }
        } catch {
          // Keep processing remaining fields even if one target does not accept the value.
        }
      }

      form.updateFieldAppearances(defaultFont);
      form.flatten();

      return {
        bytes: Buffer.from(await document.save()),
        pageCount: document.getPageCount(),
      };
    } catch {
      const pymupdfResult = await tryBuildFilledFormPdfWithPyMuPdf({
        fields: input.fields,
        basePdfBytes: input.basePdfBytes,
      });

      if (pymupdfResult) {
        return pymupdfResult;
      }

      // Fall back to a synthesized PDF when the base PDF cannot be rendered safely.
    }
  }

  const sections: TextSection[] = [
    {
      heading: "Campos preenchidos",
      lines: input.fields.map(
        (field) => `${field.label || field.pdfFieldName}: ${field.value?.trim() || "Sem valor"}`,
      ),
    },
  ];

  if (input.unresolved.length) {
    sections.push({
      heading: "Campos pendentes",
      lines: input.unresolved,
    });
  }

  return buildTextPdf({
    title: input.formName,
    subtitle: "Formulario convertido para PDF estruturado.",
    sections,
  });
}

async function appendImagePage(input: {
  document: PDFDocument;
  bytes: Buffer;
  kind: "png" | "jpeg";
}) {
  const page = input.document.addPage([A4_WIDTH, A4_HEIGHT]);
  const embedded =
    input.kind === "png"
      ? await input.document.embedPng(input.bytes)
      : await input.document.embedJpg(input.bytes);

  const size = fitWithinBox({
    width: embedded.width,
    height: embedded.height,
    maxWidth: A4_WIDTH - PAGE_MARGIN * 2,
    maxHeight: A4_HEIGHT - PAGE_MARGIN * 2,
  });

  page.drawImage(embedded, {
    x: (A4_WIDTH - size.width) / 2,
    y: (A4_HEIGHT - size.height) / 2,
    width: size.width,
    height: size.height,
  });
}

async function appendPdfPages(input: { document: PDFDocument; bytes: Buffer }) {
  const source = await PDFDocument.load(input.bytes, {
    ignoreEncryption: true,
  });
  const pages = await input.document.copyPages(source, source.getPageIndices());

  for (const page of pages) {
    input.document.addPage(page);
  }
}

function drawNoteOnPdfPage(input: {
  page: PDFPage;
  regularFont: PDFFont;
  boldFont: PDFFont;
  anchorX: number;
  anchorY: number;
  noteText: string;
  authorLabel?: string | null;
}) {
  const page = input.page;
  const regularFont = input.regularFont;
  const boldFont = input.boldFont;
  const { width, height } = page.getSize();

  const noteText = String(input.noteText ?? "").replace(/\s+/g, " ").trim();
  const noteTitle =
    String(input.authorLabel ?? "").trim() ? `Nota • ${String(input.authorLabel).trim()}` : "Nota";
  const padding = 8;
  const margin = 14;
  const titleSize = 8.5;
  const bodySize = 10.5;
  const lineHeight = 13;
  const maxNoteWidth = Math.min(240, width - margin * 2);
  const noteWidth = Math.max(120, Math.min(maxNoteWidth, width * 0.36));
  const maxTextWidth = Math.max(60, noteWidth - padding * 2);

  let lines = wrapText(regularFont, noteText, maxTextWidth, bodySize).filter(Boolean);
  if (!lines.length) {
    lines = [" "];
  }

  const maxLines = Math.max(1, Math.floor((height - margin * 2 - padding * 2 - titleSize - 8) / lineHeight));
  if (lines.length > maxLines) {
    lines = lines.slice(0, maxLines);
    const lastLine = lines[maxLines - 1] ?? "";
    lines[maxLines - 1] =
      lastLine.length > 3 ? `${lastLine.slice(0, Math.max(0, lastLine.length - 3)).trimEnd()}...` : "...";
  }

  const noteHeight = padding * 2 + titleSize + 8 + lines.length * lineHeight;
  const clickX = Math.min(1, Math.max(0, input.anchorX)) * width;
  const clickTop = Math.min(1, Math.max(0, input.anchorY)) * height;
  const x = Math.min(Math.max(margin, clickX), width - noteWidth - margin);
  const y = Math.min(Math.max(margin, height - clickTop - noteHeight), height - noteHeight - margin);

  page.drawRectangle({
    x,
    y,
    width: noteWidth,
    height: noteHeight,
    color: rgb(1, 0.976, 0.835),
    borderColor: rgb(0.91, 0.77, 0.23),
    borderWidth: 1,
    opacity: 0.96,
  });

  page.drawText(noteTitle, {
    x: x + padding,
    y: y + noteHeight - padding - titleSize,
    size: titleSize,
    font: boldFont,
    color: rgb(0.48, 0.34, 0.02),
  });

  let cursorY = y + noteHeight - padding - titleSize - 10;
  for (const line of lines) {
    page.drawText(line, {
      x: x + padding,
      y: cursorY,
      size: bodySize,
      font: regularFont,
      color: rgb(0.12, 0.12, 0.12),
      maxWidth: maxTextWidth,
      lineHeight,
    });
    cursorY -= lineHeight;
  }
}

export async function stampNotesOnPdf(input: {
  bytes: Buffer;
  notes: Array<{
    pageNumber: number;
    anchorX: number;
    anchorY: number;
    noteText: string;
    authorLabel?: string | null;
  }>;
}) {
  const document = await PDFDocument.load(input.bytes, {
    ignoreEncryption: true,
  });
  const pageCount = document.getPageCount();
  const regularFont = await document.embedFont(StandardFonts.Helvetica);
  const boldFont = await document.embedFont(StandardFonts.HelveticaBold);

  for (const note of input.notes) {
    const safePageNumber = Math.min(pageCount, Math.max(1, Math.floor(note.pageNumber || 1)));
    const page = document.getPage(safePageNumber - 1);

    drawNoteOnPdfPage({
      page,
      regularFont,
      boldFont,
      anchorX: note.anchorX,
      anchorY: note.anchorY,
      noteText: note.noteText,
      authorLabel: note.authorLabel,
    });
  }

  return {
    bytes: Buffer.from(await document.save()),
    pageCount,
  };
}

export async function stampNoteOnPdf(input: {
  bytes: Buffer;
  pageNumber: number;
  anchorX: number;
  anchorY: number;
  noteText: string;
  authorLabel?: string | null;
}) {
  const stamped = await stampNotesOnPdf({
    bytes: input.bytes,
    notes: [
      {
        pageNumber: input.pageNumber,
        anchorX: input.anchorX,
        anchorY: input.anchorY,
        noteText: input.noteText,
        authorLabel: input.authorLabel,
      },
    ],
  });

  return {
    bytes: stamped.bytes,
    pageCount: stamped.pageCount,
  };
}

export async function buildPacketPdf(input: {
  caseTitle: string;
  clientName: string;
  workflowName: string;
  items: PacketPdfItem[];
}) {
  const document = await PDFDocument.create();
  const coverSections: TextSection[] = [
    {
      heading: "Cliente",
      lines: [input.clientName || "Nao informado"],
    },
    {
      heading: "Workflow",
      lines: [input.workflowName || "Workflow padrao"],
    },
    {
      heading: "Itens incluidos",
      lines:
        input.items.length > 0
          ? input.items.map((item, index) => `${index + 1}. ${item.title} (${item.kind})`)
          : ["Nenhum item elegivel encontrado para este processo."],
    },
  ];

  await appendTextSections({
    document,
    title: input.caseTitle || "Processo",
    subtitle: `Gerado em ${new Date().toISOString()}`,
    sections: coverSections,
  });

  for (const item of input.items) {
    try {
      if (looksLikePdf(item)) {
        await appendPdfPages({
          document,
          bytes: item.bytes,
        });
        continue;
      }

      if (looksLikePng(item)) {
        await appendImagePage({
          document,
          bytes: item.bytes,
          kind: "png",
        });
        continue;
      }

      if (looksLikeJpeg(item)) {
        await appendImagePage({
          document,
          bytes: item.bytes,
          kind: "jpeg",
        });
        continue;
      }
    } catch {
      // Unsupported or corrupted source files fall back to a text page below.
    }

    await appendTextSections({
      document,
      title: item.title,
      subtitle: `Arquivo original: ${item.originalFileName}`,
      sections: [
        {
          heading: "Conteudo resumido",
          lines: item.fallbackText?.trim()
            ? item.fallbackText.split(/\r?\n/).filter(Boolean)
            : ["Nao foi possivel incorporar o binario diretamente. O arquivo segue representado por esta pagina."],
        },
      ],
    });
  }

  return {
    bytes: Buffer.from(await document.save()),
    pageCount: document.getPageCount(),
  };
}
