import { Injectable, Logger } from '@nestjs/common';
import { ImportBatchType, Prisma, PurchaseOrderStatus } from '@prisma/client';
import { SupplierService } from '@modules/platform-master/services/supplier.service';
import { RobotSkuService } from '@modules/robot-manager/services/robot-sku.service';
import type {
  ExecuteResult,
  FieldMetadata,
  Importer,
  ParsedRow,
  ValidationIssue,
} from '../importer.interface';

/**
 * PurchaseOrderImporter — M1 PO 批量导入（PRD §2.1 第 1 行）
 *
 * 一个 PO 在 Excel 里 = N 行（每行一个 PO line），同 poNo 多行合并成同一 PO。
 * 不在 import 路径触发占位 RobotUnit 生成（PRD §3.2 决策）—— 复用现有 PO publish 路径，用户后续点 publish。
 *
 * 字段：
 *   - poNo (string, 必填, unique cross-rows)
 *   - supplierCode (FK -> platform_master.suppliers.code, 必填)
 *   - currencyCode (string, 必填)
 *   - orderedAt (date, 必填)
 *   - lineNo (number > 0, 必填)
 *   - skuCode (FK -> robot_manager.robot_skus.code, 必填)
 *   - quantity (number > 0, 必填)
 *   - unitPrice (number ≥ 0, 必填)
 *   - notes (string, 可选)
 *   - expectedAt (date, 可选)
 */
export interface POImporterInput {
  poNo: string;
  supplierCode: string;
  currencyCode: string;
  orderedAt: string;  // ISO date
  expectedAt?: string;
  notes?: string;
  lineNo: number;
  skuCode: string;
  quantity: number;
  unitPrice: number;
}

@Injectable()
export class PurchaseOrderImporter implements Importer<POImporterInput> {
  private readonly logger = new Logger(PurchaseOrderImporter.name);
  readonly type = ImportBatchType.PURCHASE_ORDER;

  readonly fieldMetadata: FieldMetadata[] = [
    { field: 'poNo', label: { zh: 'PO 编号', en: 'PO No' }, type: 'string', required: true, example: 'PO-2026-001', description: { zh: '采购单编号，同 PO 多行用同一 poNo', en: 'PO number; multiple lines share same poNo' } },
    { field: 'supplierCode', label: { zh: '供应商代码', en: 'Supplier Code' }, type: 'fk', required: true, fkRef: { table: 'suppliers', column: 'code' }, example: 'SUP-001', description: { zh: '填供应商 code（不是 name）', en: 'Fill supplier code (not name)' } },
    { field: 'currencyCode', label: { zh: '货币', en: 'Currency' }, type: 'enum', required: true, enumValues: ['CNY', 'USD', 'EUR', 'AED'], example: 'USD' },
    { field: 'orderedAt', label: { zh: '下单日期', en: 'Ordered At' }, type: 'date', required: true, example: '2026-05-18' },
    { field: 'expectedAt', label: { zh: '预计到货', en: 'Expected At' }, type: 'date', required: false, example: '2026-06-01' },
    { field: 'lineNo', label: { zh: '行号', en: 'Line No' }, type: 'number', required: true, example: '1' },
    { field: 'skuCode', label: { zh: 'SKU 代码', en: 'SKU Code' }, type: 'fk', required: true, fkRef: { table: 'robot_skus', column: 'code' }, example: 'SKU-AEGIS-STD' },
    { field: 'quantity', label: { zh: '数量', en: 'Quantity' }, type: 'number', required: true, example: '10' },
    { field: 'unitPrice', label: { zh: '单价', en: 'Unit Price' }, type: 'number', required: true, example: '12500' },
    { field: 'notes', label: { zh: '备注', en: 'Notes' }, type: 'string', required: false, example: 'Q3 batch' },
  ];

  constructor(
    private readonly supplierService: SupplierService,
    private readonly skuService: RobotSkuService,
  ) {}

  parseRow(rawRow: Record<string, unknown>, rowNo: number): ParsedRow<POImporterInput> {
    const parseIssues: ValidationIssue[] = [];
    // 解析时按 label.zh 或 label.en 都尝试匹配（用户可能下中文模板填数据）
    const get = (field: FieldMetadata): unknown => {
      return rawRow[field.label.zh] ?? rawRow[field.label.en] ?? rawRow[field.field];
    };

    const obj: any = {};
    for (const f of this.fieldMetadata) {
      const raw = get(f);
      if (raw == null || raw === '') {
        if (f.required) {
          parseIssues.push({
            rowNo, field: f.field, code: 'IMPORT_REQUIRED_MISSING',
            params: { row: String(rowNo), field: f.label.zh }, severity: 'ERROR',
          });
        }
        obj[f.field] = undefined;
        continue;
      }
      // 类型转换
      try {
        if (f.type === 'number') {
          const n = typeof raw === 'number' ? raw : Number(raw);
          if (!Number.isFinite(n)) throw new Error(`not a number: ${raw}`);
          obj[f.field] = n;
        } else if (f.type === 'date') {
          const d = raw instanceof Date ? raw : new Date(String(raw));
          if (Number.isNaN(d.getTime())) throw new Error(`invalid date: ${raw}`);
          obj[f.field] = d.toISOString();
        } else if (f.type === 'enum') {
          const s = String(raw).trim();
          if (!f.enumValues!.includes(s)) {
            parseIssues.push({
              rowNo, field: f.field, code: 'IMPORT_ENUM_INVALID',
              params: { row: String(rowNo), field: f.label.zh, value: s, allowed: f.enumValues!.slice(0, 5).join(', ') },
              severity: 'ERROR',
            });
            obj[f.field] = undefined;
            continue;
          }
          obj[f.field] = s;
        } else {
          obj[f.field] = String(raw).trim();
        }
      } catch (e: any) {
        parseIssues.push({
          rowNo, field: f.field, code: 'IMPORT_TYPE_MISMATCH',
          params: { row: String(rowNo), field: f.label.zh, expected: f.type, error: e.message },
          severity: 'ERROR',
        });
        obj[f.field] = undefined;
      }
    }

    return {
      rowNo,
      raw: rawRow,
      typed: parseIssues.some((i) => i.severity === 'ERROR') ? null : (obj as POImporterInput),
      parseIssues,
    };
  }

  async validateReferences(rows: ParsedRow<POImporterInput>[]): Promise<ValidationIssue[]> {
    const issues: ValidationIssue[] = [];
    const supplierCodes = new Set<string>();
    const skuCodes = new Set<string>();
    for (const r of rows) {
      if (r.typed?.supplierCode) supplierCodes.add(r.typed.supplierCode);
      if (r.typed?.skuCode) skuCodes.add(r.typed.skuCode);
    }
    const [supplierMap, skuMap] = await Promise.all([
      this.supplierService.findByCodesIn([...supplierCodes]),
      this.skuService.findByCodesIn([...skuCodes]),
    ]);
    for (const r of rows) {
      if (!r.typed) continue;
      if (r.typed.supplierCode && !supplierMap.has(r.typed.supplierCode)) {
        issues.push({
          rowNo: r.rowNo, field: 'supplierCode', code: 'IMPORT_FK_NOT_FOUND',
          params: { row: String(r.rowNo), field: '供应商代码', value: r.typed.supplierCode, table: '供应商' },
          severity: 'ERROR',
        });
      }
      if (r.typed.skuCode && !skuMap.has(r.typed.skuCode)) {
        issues.push({
          rowNo: r.rowNo, field: 'skuCode', code: 'IMPORT_FK_NOT_FOUND',
          params: { row: String(r.rowNo), field: 'SKU 代码', value: r.typed.skuCode, table: 'SKU' },
          severity: 'ERROR',
        });
      }
    }
    return issues;
  }

  async validateBusinessRules(rows: ParsedRow<POImporterInput>[]): Promise<ValidationIssue[]> {
    const issues: ValidationIssue[] = [];
    // (PO + lineNo) 必须 unique cross-rows
    const seen = new Map<string, number>();
    for (const r of rows) {
      if (!r.typed) continue;
      if (r.typed.quantity <= 0) {
        issues.push({
          rowNo: r.rowNo, field: 'quantity', code: 'IMPORT_BUSINESS_RULE_VIOLATION',
          params: { row: String(r.rowNo), rule: 'quantity 必须 > 0' }, severity: 'ERROR',
        });
      }
      if (r.typed.unitPrice < 0) {
        issues.push({
          rowNo: r.rowNo, field: 'unitPrice', code: 'IMPORT_BUSINESS_RULE_VIOLATION',
          params: { row: String(r.rowNo), rule: 'unitPrice 不能为负' }, severity: 'ERROR',
        });
      }
      const key = `${r.typed.poNo}#${r.typed.lineNo}`;
      if (seen.has(key)) {
        issues.push({
          rowNo: r.rowNo, field: 'lineNo', code: 'IMPORT_DUPLICATE_KEY',
          params: { row: String(r.rowNo), field: 'PO + lineNo', value: key, prevRow: String(seen.get(key)) },
          severity: 'ERROR',
        });
      } else {
        seen.set(key, r.rowNo);
      }
    }
    return issues;
  }

  /**
   * 执行写库 — 同 poNo 多行合成同一 PurchaseOrder + N PurchaseOrderLine
   * 不触发占位 RobotUnit（用户后续手动 PO publish）
   */
  async execute(
    rows: POImporterInput[],
    tx: Prisma.TransactionClient,
    userId: string,
    organizationId: string,
  ): Promise<ExecuteResult> {
    // 按 poNo 分组
    const groups = new Map<string, POImporterInput[]>();
    for (const r of rows) {
      const arr = groups.get(r.poNo) ?? [];
      arr.push(r);
      groups.set(r.poNo, arr);
    }

    // 先 batch query supplier code → id 和 sku code → id
    const supplierCodes = [...new Set(rows.map((r) => r.supplierCode))];
    const skuCodes = [...new Set(rows.map((r) => r.skuCode))];
    const [supplierMap, skuMap] = await Promise.all([
      this.supplierService.findByCodesIn(supplierCodes),
      this.skuService.findByCodesIn(skuCodes),
    ]);

    const rowToPoId = new Map<number, string>(); // rowNo (lineNo within group, but unique rows) → poId
    const inputToRowNoMap = new Map<POImporterInput, number>();
    rows.forEach((r, idx) => inputToRowNoMap.set(r, idx + 1));

    for (const [poNo, lines] of groups.entries()) {
      const first = lines[0];
      const supplier = supplierMap.get(first.supplierCode)!;
      // 计算 totalAmount
      let total = 0;
      for (const l of lines) total += l.quantity * l.unitPrice;

      const po = await tx.purchaseOrder.create({
        data: {
          poNo,
          supplierId: supplier.id,
          currencyCode: first.currencyCode,
          totalAmount: total,
          status: PurchaseOrderStatus.DRAFT, // import 不 publish，留 DRAFT 让用户后续点 publish
          orderedAt: new Date(first.orderedAt),
          expectedAt: first.expectedAt ? new Date(first.expectedAt) : undefined,
          notes: first.notes,
          organizationId,
          createdById: userId,
          lines: {
            create: lines.map((l) => ({
              lineNo: l.lineNo,
              skuId: skuMap.get(l.skuCode)!.id,
              quantity: l.quantity,
              unitPrice: l.unitPrice,
              totalPrice: l.quantity * l.unitPrice,
              currencyCode: l.currencyCode,
              expectedAt: l.expectedAt ? new Date(l.expectedAt) : undefined,
            })),
          },
        },
      });
      // 每行映射回 PO id
      for (const l of lines) {
        const rowNo = inputToRowNoMap.get(l)!;
        rowToPoId.set(rowNo, po.id);
      }
    }

    return {
      rowResults: rows.map((r) => {
        const rowNo = inputToRowNoMap.get(r)!;
        return { rowNo, entityIds: [rowToPoId.get(rowNo)!] };
      }),
      sideEffects: { recordCount: groups.size, notes: `created ${groups.size} PO with ${rows.length} lines` },
    };
  }
}
