import { mkdir, mkdtemp, readFile, writeFile } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { execFile } from "node:child_process";
import { promisify } from "node:util";

import { createCanvas, loadImage } from "@napi-rs/canvas";
import { PDFParse } from "pdf-parse";
import { getDocument } from "pdfjs-dist/legacy/build/pdf.mjs";

const execFileAsync = promisify(execFile);

export type PdfFileType = "text_pdf" | "scanned_pdf" | "cad_drawing_pdf";
export type PdfRenderer = "pdfjs" | "poppler" | "quicklook";

export interface PdfTextLayerInput {
  text: string;
  pageCount: number;
  fileSizeBytes: number;
  sourceFileName?: string;
}

export interface PdfProfile {
  source_file_name: string;
  file_type: PdfFileType;
  page_count: number;
  file_size_bytes: number;
  text_layer_char_count: number;
  text_layer_sample: string;
  requires_visual_route: boolean;
  diagnostics: string[];
}

export interface RenderedPdfPage {
  bytes: Buffer;
  dataUrl: string;
  pageNumber: number;
  scale: number;
  renderer: PdfRenderer;
  output_path?: string;
}

export interface ImageBounds {
  x: number;
  y: number;
  width: number;
  height: number;
}

export interface TrimmedImage {
  trimmedBytes: Buffer;
  dataUrl: string;
  source: { width: number; height: number };
  crop: ImageBounds;
  output_path?: string;
}

export interface ParseTextLayerResult {
  text: string;
  pageCount: number;
}

export interface BuildPdfDiagnosticArtifactsInput {
  pdfPath: string;
  sourceFileName: string;
  fileSizeBytes: number;
  outputDir?: string;
  previewScale?: number;
  detailScale?: number;
  parseTextLayer?: (pdfPath: string) => Promise<ParseTextLayerResult>;
  renderWithPdfJs?: (pdfPath: string, options: RenderPdfOptions) => Promise<Buffer>;
  renderWithPoppler?: (pdfPath: string, options: RenderPdfOptions) => Promise<Buffer>;
  renderWithQuickLook?: (pdfPath: string, options: RenderPdfOptions) => Promise<Buffer>;
}

export interface PdfDiagnosticArtifacts {
  profile: PdfProfile;
  renders: RenderedPdfPage[];
  trimmed_high_res: TrimmedImage;
}

interface RenderPdfOptions {
  pageNumber: number;
  scale: number;
  outputDir?: string;
}

interface TrimImageWhitespaceOptions {
  padding?: number;
  outputPath?: string;
}

function normalizeText(text: string): string {
  return text.replace(/\s+/g, " ").trim();
}

function toDataUrl(bytes: Buffer | Uint8Array, mimeType = "image/png"): string {
  return `data:${mimeType};base64,${Buffer.from(bytes).toString("base64")}`;
}

function classifyTextDensity(charCount: number, pageCount: number): PdfFileType {
  if (charCount === 0) {
    return "scanned_pdf";
  }

  const charsPerPage = charCount / Math.max(pageCount, 1);
  if (charCount < 80 || charsPerPage < 40) {
    return "cad_drawing_pdf";
  }

  return "text_pdf";
}

export function classifyPdfTextLayer(input: PdfTextLayerInput): PdfProfile {
  const normalizedText = normalizeText(input.text);
  const textLayerCharCount = normalizedText.length;
  const fileType = classifyTextDensity(textLayerCharCount, input.pageCount);
  const diagnostics: string[] = [];

  if (fileType === "scanned_pdf") {
    diagnostics.push("文本层为空，疑似扫描图片型 PDF，需要页面渲染和视觉识别。");
  } else if (fileType === "cad_drawing_pdf") {
    diagnostics.push("文本层较少，疑似 CAD/工程图纸型 PDF，主体信息可能在图纸、表格、尺寸标注和说明区。");
  } else {
    diagnostics.push("文本层较完整，可优先走文本/表格抽取，并保留渲染预览用于复核。");
  }

  return {
    source_file_name: input.sourceFileName ?? "",
    file_type: fileType,
    page_count: input.pageCount,
    file_size_bytes: input.fileSizeBytes,
    text_layer_char_count: textLayerCharCount,
    text_layer_sample: normalizedText.slice(0, 500),
    requires_visual_route: fileType !== "text_pdf",
    diagnostics,
  };
}

export async function parsePdfTextLayer(pdfPath: string): Promise<ParseTextLayerResult> {
  const parser = new PDFParse({ data: await readFile(pdfPath) });
  try {
    const result = await parser.getText();
    return {
      text: result.text,
      pageCount: typeof result.total === "number" ? result.total : 0,
    };
  } finally {
    await parser.destroy();
  }
}

export async function renderPdfPageToPngWithPdfJs(
  pdfPath: string,
  options: RenderPdfOptions,
): Promise<Buffer> {
  const pdfBytes = await readFile(pdfPath);
  const loadingTask = getDocument({
    data: new Uint8Array(pdfBytes),
    disableFontFace: true,
    isEvalSupported: false,
    useSystemFonts: true,
  });

  const pdf = await loadingTask.promise;
  const page = await pdf.getPage(options.pageNumber);
  const viewport = page.getViewport({ scale: options.scale });
  const canvas = createCanvas(Math.ceil(viewport.width), Math.ceil(viewport.height));
  const canvasContext = canvas.getContext("2d");

  await page.render({
    canvasContext,
    viewport,
  }).promise;

  return canvas.toBuffer("image/png");
}

export async function renderPdfPageToPngWithQuickLook(
  pdfPath: string,
  options: RenderPdfOptions,
): Promise<Buffer> {
  const outputDir = options.outputDir ?? await mkdtemp(path.join(os.tmpdir(), "procurement-pdf-preview-"));
  await mkdir(outputDir, { recursive: true });
  await execFileAsync("qlmanage", ["-t", "-s", "3000", "-o", outputDir, pdfPath]);

  const previewPath = path.join(outputDir, `${path.basename(pdfPath)}.png`);
  return readFile(previewPath);
}

export async function renderPdfPageToPngWithPoppler(
  pdfPath: string,
  options: RenderPdfOptions,
): Promise<Buffer> {
  const outputDir = options.outputDir ?? await mkdtemp(path.join(os.tmpdir(), "procurement-pdf-poppler-"));
  await mkdir(outputDir, { recursive: true });
  const outputPrefix = path.join(outputDir, `page-${options.pageNumber}-poppler`);
  const dpi = Math.max(72, Math.round(options.scale * 72));

  await execFileAsync("pdftoppm", [
    "-png",
    "-r",
    String(dpi),
    "-f",
    String(options.pageNumber),
    "-l",
    String(options.pageNumber),
    "-singlefile",
    pdfPath,
    outputPrefix,
  ]);

  return readFile(`${outputPrefix}.png`);
}

export async function renderPdfFileFirstPageToPng(
  pdfPath: string,
  options: Partial<RenderPdfOptions> & {
    renderWithPdfJs?: (pdfPath: string, options: RenderPdfOptions) => Promise<Buffer>;
    renderWithPoppler?: (pdfPath: string, options: RenderPdfOptions) => Promise<Buffer>;
    renderWithQuickLook?: (pdfPath: string, options: RenderPdfOptions) => Promise<Buffer>;
  } = {},
): Promise<RenderedPdfPage> {
  const renderOptions: RenderPdfOptions = {
    pageNumber: options.pageNumber ?? 1,
    scale: options.scale ?? 2,
    outputDir: options.outputDir,
  };
  const renderWithPdfJs = options.renderWithPdfJs ?? renderPdfPageToPngWithPdfJs;
  const renderWithPoppler = options.renderWithPoppler ?? renderPdfPageToPngWithPoppler;
  const renderWithQuickLook = options.renderWithQuickLook ?? renderPdfPageToPngWithQuickLook;

  try {
    const bytes = await renderWithPdfJs(pdfPath, renderOptions);
    return {
      bytes,
      dataUrl: toDataUrl(bytes),
      pageNumber: renderOptions.pageNumber,
      scale: renderOptions.scale,
      renderer: "pdfjs",
    };
  } catch (pdfJsError) {
    try {
      const bytes = await renderWithPoppler(pdfPath, renderOptions);
      return {
        bytes,
        dataUrl: toDataUrl(bytes),
        pageNumber: renderOptions.pageNumber,
        scale: renderOptions.scale,
        renderer: "poppler",
      };
    } catch (popplerError) {
      try {
        const bytes = await renderWithQuickLook(pdfPath, renderOptions);
        return {
          bytes,
          dataUrl: toDataUrl(bytes),
          pageNumber: renderOptions.pageNumber,
          scale: renderOptions.scale,
          renderer: "quicklook",
        };
      } catch (quickLookError) {
        throw new Error(
          `PDF 页面渲染失败：pdfjs=${String(pdfJsError)}; poppler=${String(popplerError)}; quicklook=${String(quickLookError)}`,
        );
      }
    }
  }
}

export async function trimImageWhitespace(
  pngBytes: Buffer | Uint8Array,
  options: TrimImageWhitespaceOptions = {},
): Promise<TrimmedImage> {
  const padding = options.padding ?? 24;
  const image = await loadImage(Buffer.from(pngBytes));
  const sourceCanvas = createCanvas(image.width, image.height);
  const sourceContext = sourceCanvas.getContext("2d");
  sourceContext.drawImage(image, 0, 0);

  const pixels = sourceContext.getImageData(0, 0, image.width, image.height).data;
  const rowInkCounts = new Uint32Array(image.height);
  const columnInkCounts = new Uint32Array(image.width);

  for (let y = 0; y < image.height; y += 1) {
    for (let x = 0; x < image.width; x += 1) {
      const offset = (y * image.width + x) * 4;
      if (isRelevantInk(
        pixels[offset] ?? 0,
        pixels[offset + 1] ?? 0,
        pixels[offset + 2] ?? 0,
        pixels[offset + 3] ?? 0,
      )) {
        rowInkCounts[y] += 1;
        columnInkCounts[x] += 1;
      }
    }
  }

  const rowThreshold = Math.max(4, Math.floor(image.width * 0.006));
  const columnThreshold = Math.max(4, Math.floor(image.height * 0.006));
  const minY = firstIndexAboveThreshold(rowInkCounts, rowThreshold);
  const maxY = lastIndexAboveThreshold(rowInkCounts, rowThreshold);
  const minX = firstIndexAboveThreshold(columnInkCounts, columnThreshold);
  const maxX = lastIndexAboveThreshold(columnInkCounts, columnThreshold);

  if (maxX < minX || maxY < minY) {
    const bytes = Buffer.from(pngBytes);
    if (options.outputPath) {
      await writeFile(options.outputPath, bytes);
    }
    return {
      trimmedBytes: bytes,
      dataUrl: toDataUrl(bytes),
      source: { width: image.width, height: image.height },
      crop: { x: 0, y: 0, width: image.width, height: image.height },
      output_path: options.outputPath,
    };
  }

  const cropX = Math.max(0, minX - padding);
  const cropY = Math.max(0, minY - padding);
  const cropWidth = Math.min(image.width - cropX, maxX - minX + 1 + padding * 2);
  const cropHeight = Math.min(image.height - cropY, maxY - minY + 1 + padding * 2);
  const outputCanvas = createCanvas(cropWidth, cropHeight);
  const outputContext = outputCanvas.getContext("2d");

  outputContext.drawImage(
    sourceCanvas,
    cropX,
    cropY,
    cropWidth,
    cropHeight,
    0,
    0,
    cropWidth,
    cropHeight,
  );

  const trimmedBytes = outputCanvas.toBuffer("image/png");
  if (options.outputPath) {
    await writeFile(options.outputPath, trimmedBytes);
  }

  return {
    trimmedBytes,
    dataUrl: toDataUrl(trimmedBytes),
    source: { width: image.width, height: image.height },
    crop: { x: cropX, y: cropY, width: cropWidth, height: cropHeight },
    output_path: options.outputPath,
  };
}

export async function buildPdfDiagnosticArtifacts(
  input: BuildPdfDiagnosticArtifactsInput,
): Promise<PdfDiagnosticArtifacts> {
  const outputDir = input.outputDir ?? await mkdtemp(path.join(os.tmpdir(), "procurement-pdf-diagnostics-"));
  await mkdir(outputDir, { recursive: true });

  const parseTextLayer = input.parseTextLayer ?? parsePdfTextLayer;
  const textLayer = await parseTextLayer(input.pdfPath);
  const profile = classifyPdfTextLayer({
    text: textLayer.text,
    pageCount: textLayer.pageCount,
    fileSizeBytes: input.fileSizeBytes,
    sourceFileName: input.sourceFileName,
  });

  const renderScales = [input.previewScale ?? 1.5, input.detailScale ?? 4];
  const renders: RenderedPdfPage[] = [];

  for (const [index, scale] of renderScales.entries()) {
    const rendered = await renderPdfFileFirstPageToPng(input.pdfPath, {
      pageNumber: 1,
      scale,
      outputDir,
      renderWithPdfJs: input.renderWithPdfJs,
      renderWithPoppler: input.renderWithPoppler,
      renderWithQuickLook: input.renderWithQuickLook,
    });
    const outputPath = path.join(outputDir, `page-1-${index === 0 ? "preview" : "detail"}.png`);
    await writeFile(outputPath, rendered.bytes);
    renders.push({ ...rendered, output_path: outputPath });
  }

  const trimmedHighResPath = path.join(outputDir, "page-1-detail-trimmed.png");
  const trimmedHighRes = await trimImageWhitespace(renders[1]?.bytes ?? renders[0]?.bytes ?? Buffer.alloc(0), {
    outputPath: trimmedHighResPath,
  });

  return {
    profile,
    renders,
    trimmed_high_res: trimmedHighRes,
  };
}

function firstIndexAboveThreshold(counts: Uint32Array, threshold: number): number {
  for (let index = 0; index < counts.length; index += 1) {
    if ((counts[index] ?? 0) >= threshold) {
      return index;
    }
  }
  return counts.length;
}

function lastIndexAboveThreshold(counts: Uint32Array, threshold: number): number {
  for (let index = counts.length - 1; index >= 0; index -= 1) {
    if ((counts[index] ?? 0) >= threshold) {
      return index;
    }
  }
  return -1;
}

function isRelevantInk(red: number, green: number, blue: number, alpha: number): boolean {
  if (alpha < 16) {
    return false;
  }

  const isNearWhite = red > 246 && green > 246 && blue > 246;
  if (isNearWhite) {
    return false;
  }

  const isExternalMagenta = red > 210 && green < 120 && blue > 180;
  return !isExternalMagenta;
}
