/**
 * ============================================================
 * document-extraction-llm.ts — 文档提取 LLM 函数
 * ============================================================
 * 功能：
 *   extractMaterialsFromOriginalFile() — 调用文档提取阶段 LLM 从原始文件提取采购物料
 * ============================================================
 */

import { existsSync, mkdirSync, writeFileSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";

import { PDFParse } from "pdf-parse";

import { createLlmClient, resolveStageLlmModel } from "../core/llm-client.ts";
import { runWithLlmDebugLog } from "../core/llm-debug-log.js";
import { resolveTextPrompt } from "../internal/prompt-runtime.js";
import {
  DOCUMENT_EXTRACTION_PROMPT_KEY,
  parseDocumentMaterialExtractionPayload,
  type DocumentMaterialExtractionItem,
} from "./document-extraction-contract.ts";

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const PROJECT_ROOT = path.resolve(__dirname, "..", "..");
const OUTPUT_DIR = path.join(PROJECT_ROOT, "output");
export const DOCUMENT_OCR_MODEL = "mimo-v2.5";
export const DOCUMENT_ORIGINAL_FILE_EXTRACTION_MODEL = "mimo-v2.5";

// ── 日志工具 ──────────────────────────────────────────────────

function ensureOutputDir(subDir: string): string {
  const dir = path.join(OUTPUT_DIR, subDir);
  if (!existsSync(dir)) {
    mkdirSync(dir, { recursive: true });
  }
  return dir;
}

function writeOutputFile(subDir: string, fileName: string, content: string): string {
  const dir = ensureOutputDir(subDir);
  const filePath = path.join(dir, fileName);
  writeFileSync(filePath, content, "utf-8");
  return filePath;
}

// ── LLM 提取 ─────────────────────────────────────────────────

export interface DocumentMaterialExtractionResult {
  items: DocumentMaterialExtractionItem[];
  summary: string;
  warnings: string[];
  model: string;
  provider: string;
}

export interface ExtractMaterialsFromOriginalFileInput {
  fileBuffer: Buffer;
  mimeType?: string | null;
  batchId: string;
  sourceFileName: string;
}

interface LlmRuntimeConfigLike {
  provider: string;
  baseURL: string;
}

async function resolveDocumentExtractionSystemPrompt(provider: string, model: string): Promise<string> {
  const prompt = await resolveTextPrompt(DOCUMENT_EXTRACTION_PROMPT_KEY, {
    provider: provider || process.env.PROMPT_PROVIDER || process.env.LLM_PROVIDER || "*",
    model: model || process.env.LLM_MODEL || "*",
  });

  return prompt.text;
}

export function resolveDocumentOcrModel(fallbackModel?: string | null): string {
  return fallbackModel === undefined || fallbackModel === null
    ? resolveStageLlmModel("document_ocr")
    : resolveStageLlmModel("document_ocr", { fallbackModel });
}

export function resolveDocumentExtractionModel(fallbackModel?: string | null): string {
  return fallbackModel === undefined || fallbackModel === null
    ? resolveStageLlmModel("document_extraction")
    : resolveStageLlmModel("document_extraction", { fallbackModel });
}

function normalizeMimeType(mimeType: string | undefined | null, sourceFileName: string): string {
  const normalized = mimeType?.trim().toLowerCase();
  if (normalized) {
    return normalized;
  }

  const ext = path.extname(sourceFileName).toLowerCase();
  if (ext === ".pdf") return "application/pdf";
  if (ext === ".doc") return "application/msword";
  if (ext === ".docx") return "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
  if (ext === ".ppt") return "application/vnd.ms-powerpoint";
  if (ext === ".pptx") return "application/vnd.openxmlformats-officedocument.presentationml.presentation";
  if (ext === ".png") return "image/png";
  if (ext === ".jpg" || ext === ".jpeg") return "image/jpeg";
  if (ext === ".webp") return "image/webp";
  return "application/octet-stream";
}

function isImageMimeType(mimeType: string): boolean {
  return mimeType.startsWith("image/");
}

function isPdfMimeType(mimeType: string): boolean {
  return mimeType === "application/pdf";
}

/**
 * 判断 OCR 返回的文本是否是模型"未收到图片"的拒绝回复。
 * 当视觉模型不支持图片输入、或图片数据未正确传达时会出现此类回复。
 */
function isOcrModelRefusal(text: string): boolean {
  const patterns = [
    "未提供图片内容",
    "没有提供图片",
    "您未提供",
    "无法进行 OCR 转写",
    "无法进行OCR转写",
    "请上传图片",
    "请提供图片",
    "没有图片",
    "未收到图片",
    "图片未上传",
  ];
  return patterns.some((p) => text.includes(p));
}

/**
 * 判断当前 Provider 是否支持 OpenAI type:"file" 格式的文件内容块。
 * GLM（智谱 AI）不支持此格式，会返回 400 类型错误。
 */
function supportsNativeFileUpload(config: LlmRuntimeConfigLike): boolean {
  const provider = config.provider.trim().toLowerCase();
  // 已知不支持 type:"file" 内容块的 Provider
  const unsupportedProviders = new Set(["glm", "deepseek", "gemini"]);
  return !unsupportedProviders.has(provider);
}

/**
 * 从 PDF Buffer 提取纯文本（用于不支持原始文件上传的 Provider）。
 */
async function extractTextFromPdfBuffer(fileBuffer: Buffer): Promise<string> {
  const parser = new PDFParse({ data: fileBuffer });
  try {
    const result = await parser.getText();
    return result.text;
  } finally {
    await parser.destroy();
  }
}

function toDataUrl(fileBuffer: Buffer, mimeType: string): string {
  return `data:${mimeType};base64,${fileBuffer.toString("base64")}`;
}

function isOfficialDeepSeekTextOnlyApi(config: LlmRuntimeConfigLike): boolean {
  return config.provider.trim().toLowerCase() === "deepseek"
    && config.baseURL.trim().toLowerCase().replace(/\/+$/, "") === "https://api.deepseek.com";
}

function isMimoProvider(config: Pick<LlmRuntimeConfigLike, "provider">): boolean {
  return config.provider.trim().toLowerCase() === "mimo";
}

function assertSupportsOriginalFileInput(
  config: LlmRuntimeConfigLike,
  envName: "DOCUMENT_OCR_LLM_PROVIDER" | "DOCUMENT_EXTRACTION_LLM_PROVIDER",
  inputKind: "图片输入" | "原始文件输入",
): void {
  if (!isOfficialDeepSeekTextOnlyApi(config)) {
    return;
  }

  throw new Error(
    `${envName}=deepseek 使用的是 DeepSeek 官方开放平台 API (${config.baseURL})，当前 API 不支持${inputKind}；` +
      "网页聊天窗的文件识别能力尚未开放到该 API。请改用支持图片/附件输入的 OCR Provider，或配置已验证的 DeepSeek 多模态 API 端点。",
  );
}

function buildOriginalFileUserContent(input: Required<Pick<ExtractMaterialsFromOriginalFileInput, "fileBuffer" | "batchId" | "sourceFileName">> & { mimeType: string }) {
  const instruction = [
    `批次: ${input.batchId}`,
    `文件名: ${input.sourceFileName}`,
    "请直接读取附件中的采购清单内容，按系统提示的固定 JSON schema 返回。",
  ].join("\n");

  if (isImageMimeType(input.mimeType)) {
    return [
      { type: "text", text: instruction },
      {
        type: "image_url",
        image_url: {
          url: toDataUrl(input.fileBuffer, input.mimeType),
        },
      },
    ];
  }

  return [
    { type: "text", text: instruction },
    {
      type: "file",
      file: {
        filename: input.sourceFileName,
        file_data: toDataUrl(input.fileBuffer, input.mimeType),
      },
    },
  ];
}

function buildImageOcrUserContent(input: Required<Pick<ExtractMaterialsFromOriginalFileInput, "fileBuffer" | "batchId" | "sourceFileName">> & { mimeType: string }) {
  return [
    {
      type: "text",
      text: [
        `批次: ${input.batchId}`,
        `文件名: ${input.sourceFileName}`,
        "请对这张采购资料图片做 OCR，尽量保留表格行列、物料名称、规格、数量、单位、技术要求和备注。",
        "只返回 OCR 文本，不要提取结构化 JSON。",
      ].join("\n"),
    },
    {
      type: "image_url",
      image_url: {
        url: toDataUrl(input.fileBuffer, input.mimeType),
      },
    },
  ];
}

function buildOcrTextExtractionUserContent(input: Pick<ExtractMaterialsFromOriginalFileInput, "batchId" | "sourceFileName"> & { ocrText: string }): string {
  return [
    `批次: ${input.batchId}`,
    `文件名: ${input.sourceFileName}`,
    "下面是图片 OCR 文本。请基于 OCR 文本按系统提示的固定 JSON schema 返回采购物料，不要补充 OCR 文本中没有的物料。",
    "",
    "OCR 文本:",
    input.ocrText,
  ].join("\n");
}

async function extractTextFromImage(
  input: Required<Pick<ExtractMaterialsFromOriginalFileInput, "fileBuffer" | "batchId" | "sourceFileName">> & { mimeType: string },
): Promise<{ text: string; model: string; provider: string }> {
  const { client, config } = await createLlmClient({ maxRetries: 2, stage: "document_ocr" });
  assertSupportsOriginalFileInput(config, "DOCUMENT_OCR_LLM_PROVIDER", "图片输入");
  const model = resolveDocumentOcrModel(config.model);
  const sizeKB = Math.round(input.fileBuffer.byteLength / 1024);
  const startedAt = Date.now();
  console.log(`[文档 OCR] 开始识别图片: file=${input.sourceFileName}, batch=${input.batchId}, model=${model}, provider=${config.provider}, size=${sizeKB}KB, mimeType=${input.mimeType}`);

  const request = {
    model,
    messages: [
      {
        role: "system",
        content: "你是采购资料 OCR 助手。请忠实转写图片内容，保留表格结构和关键字段，不要输出 JSON。",
      },
      {
        role: "user",
        content: buildImageOcrUserContent(input),
      },
    ],
    temperature: 0.1,
    ...(isMimoProvider(config) ? { max_completion_tokens: 2048 } : {}),
  };
  const response = await runWithLlmDebugLog(
    {
      scope: "automation",
      stage: "document_ocr",
      label: "extractTextFromImage",
      provider: config.provider,
      model,
    },
    request,
    () => client.chat.completions.create(request as never),
  );

  const text = response.choices[0]?.message?.content ?? "";
  writeOutputFile(input.batchId, `${input.sourceFileName}.ocr-response.txt`, text);

  if (!text.trim()) {
    throw new Error("文档 OCR 返回内容为空");
  }

  // 检测模型是否实际处理了图片（部分模型不支持视觉输入时会返回拒绝文本）
  if (isOcrModelRefusal(text)) {
    const snippet = text.slice(0, 120).replace(/\n/g, " ");
    const elapsed = ((Date.now() - startedAt) / 1000).toFixed(1);
    console.error(
      `[文档 OCR] 模型未能识别图片内容: file=${input.sourceFileName}, batch=${input.batchId}, model=${model}, provider=${config.provider}, elapsed=${elapsed}s, 模型回复="${snippet}"`,
    );
    throw new Error(
      `OCR 模型未能识别图片内容（模型可能不支持视觉输入，或图片数据未正确传达）。` +
      `请检查 DOCUMENT_OCR_LLM_MODEL/PROVIDER 配置，确保使用支持视觉输入的模型。` +
      `模型回复：${snippet}`,
    );
  }

  const elapsed = ((Date.now() - startedAt) / 1000).toFixed(1);
  console.log(`[文档 OCR] 完成: file=${input.sourceFileName}, batch=${input.batchId}, model=${model}, provider=${config.provider}, elapsed=${elapsed}s, ocrLength=${text.length}`);
  return { text, model, provider: config.provider };
}

/**
 * 调用文档提取阶段模型从原始文件中提取采购物料列表。
 */
export async function extractMaterialsFromOriginalFile(
  input: ExtractMaterialsFromOriginalFileInput,
): Promise<DocumentMaterialExtractionResult> {
  const mimeType = normalizeMimeType(input.mimeType, input.sourceFileName);
  const sizeKB = Math.round(input.fileBuffer.byteLength / 1024);
  let ocrText: string | null = null;

  if (isImageMimeType(mimeType)) {
    const ocrResult = await extractTextFromImage({
      fileBuffer: input.fileBuffer,
      mimeType,
      batchId: input.batchId,
      sourceFileName: input.sourceFileName,
    });
    ocrText = ocrResult.text;
  }

  const { client, config } = await createLlmClient({ maxRetries: 2, stage: "document_extraction" });
  const model = resolveDocumentExtractionModel(config.model);

  // 对不支持 type:"file" 的 Provider（如 GLM），PDF 文件改走本地文本提取 → 文本提取路径
  if (!ocrText && isPdfMimeType(mimeType) && !supportsNativeFileUpload(config)) {
    const pdfStartedAt = Date.now();
    console.log(
      `[文档 OCR] 开始提取 PDF 文本(本地): file=${input.sourceFileName}, batch=${input.batchId}, provider=${config.provider}, size=${sizeKB}KB`,
    );
    const pdfText = await extractTextFromPdfBuffer(input.fileBuffer);
    const pdfElapsed = ((Date.now() - pdfStartedAt) / 1000).toFixed(1);
    if (!pdfText.trim()) {
      throw new Error(
        `PDF 文本提取结果为空（文件可能是扫描件或加密 PDF）。` +
        `provider=${config.provider} 不支持原始文件上传，请配置支持 type:"file" 内容块的 Provider，或使用扫描件转图片后上传。`,
      );
    }
    console.log(
      `[文档 OCR] 完成 PDF 文本提取: file=${input.sourceFileName}, batch=${input.batchId}, provider=${config.provider}, elapsed=${pdfElapsed}s, textLength=${pdfText.length}`,
    );
    ocrText = pdfText;
  }

  const systemPrompt = await resolveDocumentExtractionSystemPrompt(config.provider, model);
  const extractionPath = ocrText
    ? (isImageMimeType(mimeType) ? "ocr-image→text" : "pdf-local→text")
    : "native-file";
  const startedAt = Date.now();
  console.log(
    `[文档提取] 开始识别原始文件: file=${input.sourceFileName}, batch=${input.batchId}, model=${model}, provider=${config.provider}, size=${sizeKB}KB, mimeType=${mimeType}, path=${extractionPath}`,
  );

  try {
    if (!ocrText) {
      assertSupportsOriginalFileInput(config, "DOCUMENT_EXTRACTION_LLM_PROVIDER", "原始文件输入");
    }
    const request = {
      model,
      messages: [
        { role: "system", content: systemPrompt },
        {
          role: "user",
          content: ocrText
            ? buildOcrTextExtractionUserContent({
                batchId: input.batchId,
                sourceFileName: input.sourceFileName,
                ocrText,
              })
            : buildOriginalFileUserContent({
                fileBuffer: input.fileBuffer,
                mimeType,
                batchId: input.batchId,
                sourceFileName: input.sourceFileName,
              }),
        },
      ],
      temperature: 0.1,
      response_format: { type: "json_object" },
    };
    const response = await runWithLlmDebugLog(
      {
        scope: "automation",
        stage: "document_extraction",
        label: "extractMaterialsFromOriginalFile",
        provider: config.provider,
        model,
      },
      request,
      () => client.chat.completions.create(request as never),
    );

    const content = response.choices[0]?.message?.content ?? "";

    // 写 LLM 原始响应日志
    writeOutputFile(input.batchId, `${input.sourceFileName}.llm-response.json`, JSON.stringify({
      provider: config.provider,
      model,
      sourceFileName: input.sourceFileName,
      batchId: input.batchId,
      receivedAt: new Date().toISOString(),
      rawResponse: content,
    }, null, 2));

    if (!content.trim()) {
      throw new Error("LLM 返回内容为空");
    }

    const parsed = parseDocumentMaterialExtractionPayload(content);
    const { items, warnings } = parsed;
    const summary = parsed.extraction_summary;
    const elapsed = ((Date.now() - startedAt) / 1000).toFixed(1);
    console.log(`[文档提取] 完成: file=${input.sourceFileName}, batch=${input.batchId}, model=${model}, provider=${config.provider}, items=${items.length}, elapsed=${elapsed}s, path=${extractionPath}`);

    // 写提取结果日志
    writeOutputFile(input.batchId, `${input.sourceFileName}.extraction-result.json`, JSON.stringify({
      provider: config.provider,
      model,
      totalItems: items.length,
      summary,
      warnings,
      items,
    }, null, 2));

    return { items, summary, warnings, model, provider: config.provider };
  } catch (error: unknown) {
    const message = error instanceof Error ? error.message : String(error);
    const elapsed = ((Date.now() - startedAt) / 1000).toFixed(1);
    console.error(`[文档提取] 失败: file=${input.sourceFileName}, batch=${input.batchId}, model=${model}, provider=${config.provider}, elapsed=${elapsed}s, path=${extractionPath}`, message);

    // 写错误日志
    writeOutputFile(input.batchId, `${input.sourceFileName}.extraction-error.log`, [
      `提取时间: ${new Date().toISOString()}`,
      `原始文件: ${input.sourceFileName}`,
      `批次: ${input.batchId}`,
      `LLM: ${config.provider}/${model}`,
      `提取路径: ${extractionPath}`,
      `错误: ${message}`,
    ].join("\n"));

    throw new Error(`物料提取失败 (${input.sourceFileName}): ${message}`);
  }
}
