import type {
  ProcurementListExtractionFileProfile,
  ProcurementListExtractionFields,
  ProcurementListExtractionPayload,
  ProcurementListPurchaseObject,
} from "./procurement-extraction-contract.ts";

type MergeableRole = "primary_item" | "accessory";
type IgnoredRole = Exclude<ProcurementListPurchaseObject["object_role"], MergeableRole>;
type IssueType = "missing" | "conflict" | "low_confidence" | "uncertain" | "manual_review";

export interface ProcurementListExtractionPayloadWithSource extends ProcurementListExtractionPayload {
  source_file_name?: string;
}

export interface ProcurementListPdfDiagnosticInput {
  source_file_name: string;
  file_type?: string;
  drawing_no?: string;
  diagnostics?: string[];
}

export interface MergeProcurementListObjectsInput {
  extractionPayloads: ProcurementListExtractionPayloadWithSource[];
  pdfDiagnostics?: ProcurementListPdfDiagnosticInput[];
}

export interface ProcurementListCanonicalItemDraft {
  status: "review_required";
  recommended_name: string;
  original_name: string;
  specification: string;
  quantity: number | null;
  unit: string;
  fields: Record<string, unknown>;
  evidence_refs: string[];
  missing_fields: string[];
  human_fix_hint: string;
  can_search_1688: boolean;
  confidence: number;
  manual_revision_status: "none";
}

export interface ProcurementListPurchaseCardDraft {
  recommended_name: string;
  original_names: string[];
  source_files: string[];
  fields: Record<string, unknown>;
  field_sources: Record<string, string[]>;
  pending_confirmations: string[];
  pdf_diagnostics: ProcurementListPdfDiagnosticInput[];
}

export interface ProcurementListConfirmationDraft {
  canonical_item_index: number;
  field_name: string;
  issue_type: IssueType;
  message: string;
  status: "open";
}

export interface ProcurementListFieldEvidenceDraft {
  canonical_item_index: number;
  field_name: string;
  field_value: string;
  source_file_name: string;
  evidence_type: string;
  locator: Record<string, unknown>;
  confidence?: number;
  note: string;
}

export interface ProcurementListIgnoredObjectDraft {
  source_file_name: string;
  object_role: IgnoredRole;
  original_name: string;
  recommended_name: string;
  reason: string;
}

export interface MergeProcurementListObjectsResult {
  purchase_cards: ProcurementListPurchaseCardDraft[];
  canonical_items: ProcurementListCanonicalItemDraft[];
  confirmations: ProcurementListConfirmationDraft[];
  field_evidence: ProcurementListFieldEvidenceDraft[];
  ignored_objects: ProcurementListIgnoredObjectDraft[];
}

interface SourcedPurchaseObject {
  source_file_name: string;
  file_profile: ProcurementListExtractionFileProfile;
  object: ProcurementListPurchaseObject;
}

interface ObjectCluster {
  primaryObjects: SourcedPurchaseObject[];
  accessories: SourcedPurchaseObject[];
}

const STRING_FIELDS = [
  "specification",
  "material_requirement",
  "technical_requirement",
  "test_requirement",
  "technical_standard",
  "applicable_conditions",
  "main_dimensions",
  "tolerance",
  "performance_indicators",
  "accessory_materials",
  "supply_form",
  "acceptance_documents",
  "invoice_type",
  "delivery_address",
  "remarks",
] as const satisfies ReadonlyArray<keyof ProcurementListExtractionFields>;

const SEARCH_REQUIRED_FIELDS = [
  "recommended_name",
  "specification",
  "quantity",
  "unit",
  "delivery_address",
] as const;

function normalizeText(value: unknown): string {
  return String(value ?? "").trim();
}

function hasText(value: unknown): boolean {
  return normalizeText(value).length > 0;
}

function unique(values: Array<string | undefined | null>): string[] {
  const seen = new Set<string>();
  const result: string[] = [];
  for (const value of values) {
    const normalized = normalizeText(value);
    if (!normalized || seen.has(normalized)) {
      continue;
    }
    seen.add(normalized);
    result.push(normalized);
  }
  return result;
}

function canonicalNameText(object: ProcurementListPurchaseObject): string {
  return [object.recommended_name, object.original_name, object.fields.specification]
    .map(normalizeText)
    .filter(Boolean)
    .join(" ");
}

function compactText(value: string): string {
  return value.toLowerCase().replace(/\s+/g, "");
}

function normalizedMatchKey(object: ProcurementListPurchaseObject): string {
  const text = compactText(canonicalNameText(object))
    .replace(/\s+/g, "")
    .replace(/[覆盖性能指标图纸构造节点]/g, "");

  if (text.includes("塑料保护罩") || text.includes("保护罩")) {
    return "保护罩";
  }
  return text.slice(0, 24);
}

function objectsLikelyMatch(left: ProcurementListPurchaseObject, right: ProcurementListPurchaseObject): boolean {
  const leftKey = normalizedMatchKey(left);
  const rightKey = normalizedMatchKey(right);
  if (!leftKey || !rightKey) {
    return false;
  }
  if (leftKey === rightKey || leftKey.includes(rightKey) || rightKey.includes(leftKey)) {
    return true;
  }

  const leftText = canonicalNameText(left);
  const rightText = canonicalNameText(right);
  const leftName = compactText(left.recommended_name || left.original_name);
  const rightName = compactText(right.recommended_name || right.original_name);
  if (
    leftName.length >= 3
    && rightName.length >= 3
    && (leftName.includes(rightName) || rightName.includes(leftName))
  ) {
    return true;
  }

  return leftText.includes("保护罩") && rightText.includes("保护罩");
}

function isExcelObject(object: SourcedPurchaseObject): boolean {
  return object.file_profile.file_type === "excel";
}

function sourceFileName(payload: ProcurementListExtractionPayloadWithSource): string {
  return payload.source_file_name ?? payload.file_profile.source_file_name ?? "";
}

function flattenObjects(payloads: ProcurementListExtractionPayloadWithSource[]): {
  primaryObjects: SourcedPurchaseObject[];
  accessories: SourcedPurchaseObject[];
  ignoredObjects: ProcurementListIgnoredObjectDraft[];
} {
  const primaryObjects: SourcedPurchaseObject[] = [];
  const accessories: SourcedPurchaseObject[] = [];
  const ignoredObjects: ProcurementListIgnoredObjectDraft[] = [];

  for (const payload of payloads) {
    const fileName = sourceFileName(payload);
    for (const object of payload.purchase_objects) {
      const sourced = {
        source_file_name: fileName,
        file_profile: payload.file_profile,
        object,
      };
      if (object.object_role === "primary_item") {
        primaryObjects.push(sourced);
      } else if (object.object_role === "accessory") {
        accessories.push(sourced);
      } else {
        ignoredObjects.push({
          source_file_name: fileName,
          object_role: object.object_role,
          original_name: object.original_name,
          recommended_name: object.recommended_name,
          reason: "非采购主对象，仅保留为分类结果。",
        });
      }
    }
  }

  return { primaryObjects, accessories, ignoredObjects };
}

function clusterPrimaryObjects(primaryObjects: SourcedPurchaseObject[]): ObjectCluster[] {
  const clusters: ObjectCluster[] = [];

  for (const object of primaryObjects) {
    const existing = clusters.find((cluster) => cluster.primaryObjects.some((candidate) => (
      objectsLikelyMatch(candidate.object, object.object)
    )));
    if (existing) {
      existing.primaryObjects.push(object);
    } else {
      clusters.push({ primaryObjects: [object], accessories: [] });
    }
  }

  return clusters;
}

function attachAccessories(clusters: ObjectCluster[], accessories: SourcedPurchaseObject[]): void {
  for (const accessory of accessories) {
    const matchingCluster = clusters.find((cluster) => cluster.primaryObjects.some((primary) => (
      objectsLikelyMatch(primary.object, accessory.object)
    ))) ?? clusters[0];
    if (matchingCluster) {
      matchingCluster.accessories.push(accessory);
    }
  }
}

function chooseRecommendedName(objects: SourcedPurchaseObject[]): string {
  const sourceObjects = objects.some(isExcelObject) ? objects.filter(isExcelObject) : objects;
  const names = unique(sourceObjects.map(({ object }) => object.recommended_name || object.original_name));
  const withQualifier = names.find((name) => /螺栓|孔/.test(name) && name.length > 4);
  return withQualifier ?? names.sort((a, b) => b.length - a.length)[0] ?? "";
}

function collectFieldValues(cluster: ObjectCluster, field: keyof ProcurementListExtractionFields): string[] {
  const objects = [...cluster.primaryObjects, ...cluster.accessories];
  return unique(objects.map(({ object }) => normalizeText(object.fields[field])));
}

function joinField(cluster: ObjectCluster, field: keyof ProcurementListExtractionFields): string {
  return collectFieldValues(cluster, field).join("；");
}

function firstNumber(cluster: ObjectCluster, field: "quantity"): number | null {
  const objects = cluster.primaryObjects.some(isExcelObject)
    ? cluster.primaryObjects.filter(isExcelObject)
    : cluster.primaryObjects;
  for (const { object } of objects) {
    const value = object.fields[field];
    if (typeof value === "number" && Number.isFinite(value) && value > 0) {
      return value;
    }
  }
  return null;
}

function firstText(cluster: ObjectCluster, field: keyof ProcurementListExtractionFields): string {
  const objects = cluster.primaryObjects.some(isExcelObject)
    ? cluster.primaryObjects.filter(isExcelObject)
    : [...cluster.primaryObjects, ...cluster.accessories];
  return unique(objects.map(({ object }) => normalizeText(object.fields[field])))[0] ?? "";
}

function fieldSources(cluster: ObjectCluster): Record<string, string[]> {
  const sources: Record<string, string[]> = {};
  for (const sourced of [...cluster.primaryObjects, ...cluster.accessories]) {
    for (const field of STRING_FIELDS) {
      if (hasText(sourced.object.fields[field])) {
        sources[field] = unique([...(sources[field] ?? []), sourced.source_file_name]);
      }
    }
    if (sourced.object.fields.quantity > 0) {
      sources.quantity = unique([...(sources.quantity ?? []), sourced.source_file_name]);
    }
    if (sourced.object.fields.pending_confirmations.length > 0) {
      sources.pending_confirmations = unique([...(sources.pending_confirmations ?? []), sourced.source_file_name]);
    }
  }
  return sources;
}

function collectEvidence(cluster: ObjectCluster, canonicalItemIndex: number): ProcurementListFieldEvidenceDraft[] {
  const evidence: ProcurementListFieldEvidenceDraft[] = [];
  for (const sourced of [...cluster.primaryObjects, ...cluster.accessories]) {
    for (const item of sourced.object.evidence) {
      const fieldName = normalizeText(item.field);
      const rawValue = (sourced.object.fields as unknown as Record<string, unknown>)[fieldName];
      evidence.push({
        canonical_item_index: canonicalItemIndex,
        field_name: fieldName,
        field_value: normalizeText(rawValue) || normalizeText(item.quote),
        source_file_name: item.source_file_name || sourced.source_file_name,
        evidence_type: item.source_type,
        locator: {
          page_number: item.page_number,
          row_index: item.row_index,
          region_label: item.region_label,
        },
        confidence: item.confidence,
        note: item.quote,
      });
    }
  }
  return evidence;
}

function collectConflictConfirmations(
  cluster: ObjectCluster,
  canonicalItemIndex: number,
): ProcurementListConfirmationDraft[] {
  const confirmations: ProcurementListConfirmationDraft[] = [];
  const quantityValues = unique(cluster.primaryObjects
    .map(({ object }) => object.fields.quantity > 0 ? String(object.fields.quantity) : ""));
  const unitValues = collectFieldValues(cluster, "unit");

  if (quantityValues.length > 1) {
    confirmations.push({
      canonical_item_index: canonicalItemIndex,
      field_name: "quantity",
      issue_type: "conflict",
      message: `数量存在冲突：${quantityValues.join(" vs ")}`,
      status: "open",
    });
  }
  if (unitValues.length > 1) {
    confirmations.push({
      canonical_item_index: canonicalItemIndex,
      field_name: "unit",
      issue_type: "conflict",
      message: `单位存在冲突：${unitValues.join(" vs ")}`,
      status: "open",
    });
  }

  return confirmations;
}

function collectPendingConfirmations(
  cluster: ObjectCluster,
  canonicalItemIndex: number,
): ProcurementListConfirmationDraft[] {
  const confirmations: ProcurementListConfirmationDraft[] = [];
  const pending = unique([...cluster.primaryObjects, ...cluster.accessories]
    .flatMap(({ object }) => object.fields.pending_confirmations));
  for (const message of pending) {
    confirmations.push({
      canonical_item_index: canonicalItemIndex,
      field_name: message.includes("密封胶") ? "accessory_materials" : "pending_confirmations",
      issue_type: "uncertain",
      message,
      status: "open",
    });
  }
  return confirmations;
}

function missingFields(fields: Record<string, unknown>, recommendedName: string): string[] {
  const missing: string[] = [];
  for (const field of SEARCH_REQUIRED_FIELDS) {
    if (field === "recommended_name") {
      if (!hasText(recommendedName)) {
        missing.push(field);
      }
    } else if (field === "quantity") {
      if (typeof fields.quantity !== "number" || fields.quantity <= 0) {
        missing.push(field);
      }
    } else if (!hasText(fields[field])) {
      missing.push(field);
    }
  }
  return missing;
}

function buildHumanFixHint(missing: string[]): string {
  if (missing.length === 0) {
    return "关键采购字段已具备，可进入 1688 搜索。";
  }
  return `请补充 ${missing.join("、")} 后再进入 1688 搜索。`;
}

function averageConfidence(cluster: ObjectCluster): number {
  const values = [...cluster.primaryObjects, ...cluster.accessories]
    .map(({ object }) => object.confidence)
    .filter((value): value is number => typeof value === "number" && Number.isFinite(value));
  if (values.length === 0) {
    return 0.5;
  }
  return Number((values.reduce((sum, value) => sum + value, 0) / values.length).toFixed(2));
}

function matchingPdfDiagnostics(
  cluster: ObjectCluster,
  explicitDiagnostics: ProcurementListPdfDiagnosticInput[],
): ProcurementListPdfDiagnosticInput[] {
  const sourceNames = new Set(cluster.primaryObjects.map((object) => object.source_file_name));
  const fromProfiles = cluster.primaryObjects
    .filter(({ file_profile }) => file_profile.file_type.includes("pdf"))
    .map(({ source_file_name, file_profile }) => ({
      source_file_name,
      file_type: file_profile.file_type,
      drawing_no: file_profile.drawing_no,
      diagnostics: file_profile.diagnostics,
    }));
  const explicit = explicitDiagnostics.filter((diagnostic) => sourceNames.has(diagnostic.source_file_name));
  return [...fromProfiles, ...explicit];
}

function buildClusterDrafts(
  cluster: ObjectCluster,
  canonicalItemIndex: number,
  explicitDiagnostics: ProcurementListPdfDiagnosticInput[],
): {
  canonicalItem: ProcurementListCanonicalItemDraft;
  purchaseCard: ProcurementListPurchaseCardDraft;
  confirmations: ProcurementListConfirmationDraft[];
  evidence: ProcurementListFieldEvidenceDraft[];
} {
  const recommendedName = chooseRecommendedName(cluster.primaryObjects);
  const originalNames = unique(cluster.primaryObjects.map(({ object }) => object.original_name));
  const sourceFiles = unique([...cluster.primaryObjects, ...cluster.accessories].map((object) => object.source_file_name));
  const accessoryNames = unique(cluster.accessories.flatMap(({ object }) => [
    object.recommended_name,
    object.original_name,
    object.fields.accessory_materials,
  ]));
  const accessoryMaterials = unique([
    ...collectFieldValues(cluster, "accessory_materials"),
    ...accessoryNames,
  ]).join("；");
  const fields: Record<string, unknown> = {
    specification: joinField(cluster, "specification"),
    material_requirement: joinField(cluster, "material_requirement"),
    technical_requirement: joinField(cluster, "technical_requirement"),
    test_requirement: joinField(cluster, "test_requirement"),
    technical_standard: joinField(cluster, "technical_standard"),
    applicable_conditions: joinField(cluster, "applicable_conditions"),
    main_dimensions: joinField(cluster, "main_dimensions"),
    tolerance: joinField(cluster, "tolerance"),
    performance_indicators: joinField(cluster, "performance_indicators"),
    accessory_materials: accessoryMaterials,
    supply_form: joinField(cluster, "supply_form"),
    acceptance_documents: joinField(cluster, "acceptance_documents"),
    quantity: firstNumber(cluster, "quantity"),
    unit: firstText(cluster, "unit"),
    invoice_type: firstText(cluster, "invoice_type"),
    delivery_address: firstText(cluster, "delivery_address"),
    remarks: joinField(cluster, "remarks"),
    pdf_drawing_no: unique(cluster.primaryObjects.map(({ file_profile }) => file_profile.drawing_no)).join("；"),
  };
  const missing = missingFields(fields, recommendedName);
  const canSearch1688 = missing.length === 0;
  const fieldSourceMap = fieldSources(cluster);
  const pendingConfirmations = unique([...cluster.primaryObjects, ...cluster.accessories]
    .flatMap(({ object }) => object.fields.pending_confirmations));
  const confirmations = [
    ...collectConflictConfirmations(cluster, canonicalItemIndex),
    ...collectPendingConfirmations(cluster, canonicalItemIndex),
    ...missing.map<ProcurementListConfirmationDraft>((field) => ({
      canonical_item_index: canonicalItemIndex,
      field_name: field,
      issue_type: "missing",
      message: `缺少关键采购字段：${field}`,
      status: "open",
    })),
  ];
  const evidence = collectEvidence(cluster, canonicalItemIndex);
  const canonicalItem: ProcurementListCanonicalItemDraft = {
    status: "review_required",
    recommended_name: recommendedName,
    original_name: originalNames.join("；"),
    specification: normalizeText(fields.specification),
    quantity: fields.quantity as number | null,
    unit: normalizeText(fields.unit),
    fields,
    evidence_refs: evidence.map((item) => `${item.source_file_name}:${item.field_name}`),
    missing_fields: missing,
    human_fix_hint: buildHumanFixHint(missing),
    can_search_1688: canSearch1688,
    confidence: averageConfidence(cluster),
    manual_revision_status: "none",
  };
  const purchaseCard: ProcurementListPurchaseCardDraft = {
    recommended_name: recommendedName,
    original_names: originalNames,
    source_files: sourceFiles,
    fields,
    field_sources: fieldSourceMap,
    pending_confirmations: pendingConfirmations,
    pdf_diagnostics: matchingPdfDiagnostics(cluster, explicitDiagnostics),
  };

  return { canonicalItem, purchaseCard, confirmations, evidence };
}

export function mergeProcurementListObjects(
  input: MergeProcurementListObjectsInput,
): MergeProcurementListObjectsResult {
  const flattened = flattenObjects(input.extractionPayloads);
  const hasExcelPrimaryObjects = flattened.primaryObjects.some(isExcelObject);
  const clusters = clusterPrimaryObjects(flattened.primaryObjects)
    .filter((cluster) => !hasExcelPrimaryObjects || cluster.primaryObjects.some(isExcelObject));
  attachAccessories(clusters, flattened.accessories);

  const canonical_items: ProcurementListCanonicalItemDraft[] = [];
  const purchase_cards: ProcurementListPurchaseCardDraft[] = [];
  const confirmations: ProcurementListConfirmationDraft[] = [];
  const field_evidence: ProcurementListFieldEvidenceDraft[] = [];

  clusters.forEach((cluster, index) => {
    const drafts = buildClusterDrafts(cluster, index, input.pdfDiagnostics ?? []);
    canonical_items.push(drafts.canonicalItem);
    purchase_cards.push(drafts.purchaseCard);
    confirmations.push(...drafts.confirmations);
    field_evidence.push(...drafts.evidence);
  });

  return {
    purchase_cards,
    canonical_items,
    confirmations,
    field_evidence,
    ignored_objects: flattened.ignoredObjects,
  };
}
