import { XMLParser, XMLBuilder } from 'fast-xml-parser';
import { ApiAsnDto, ApiItemDto, ApiOrderDto } from '@wms/shared';
import { DomainEvent } from '@/common/events/event-bus.service';
import { ErpAdapter, NormalizedInbound, OutboundErpMessage, OutboundContext } from './erp-adapter';
import { ValidationError } from '@/common/errors/domain-error';

/**
 * SAP IDoc adapter (XML IDocs as exported by SAP PI/PO or the IDoc XML port).
 *
 * Inbound supported:
 *   MATMAS05  (material master)         -> ITEMS
 *   ORDERS05  (purchase order)          -> ASN   (expected receipt from a vendor PO)
 *   DESADV / DELVRY03 (inbound delivery) -> ASN
 *   ORDRSP / SALES ORDER (ORDERS05 with EDI_DC40.MESTYP=ORDRSP or SALESORDER) -> ORDER
 * Outbound produced:
 *   asn.closed    -> WMMBXY-style goods receipt (GRN) document
 *   order.shipped -> SHPCON-style goods issue (PGI) confirmation
 *
 * Segment names follow SAP standard IDoc types; field mapping can be adjusted
 * per connector through config.fieldMap without code changes.
 */
export class SapIdocAdapter implements ErpAdapter {
  readonly type = 'SAP_IDOC';
  private readonly parser = new XMLParser({ ignoreAttributes: false, parseTagValue: false, trimValues: true });
  private readonly builder = new XMLBuilder({ ignoreAttributes: false, format: true, suppressEmptyNode: true });

  parseInbound(raw: string, config: Record<string, unknown>): NormalizedInbound[] {
    let doc: Record<string, unknown>;
    try {
      doc = this.parser.parse(raw) as Record<string, unknown>;
    } catch (e) {
      throw new ValidationError(`Invalid IDoc XML: ${(e as Error).message}`);
    }
    const idocs = this.findIdocs(doc);
    if (!idocs.length) throw new ValidationError('No IDOC element found');
    const out: NormalizedInbound[] = [];
    for (const idoc of idocs) {
      const control = (idoc.EDI_DC40 ?? {}) as Record<string, string>;
      const mestyp = (control.MESTYP ?? '').toUpperCase();
      const idoctyp = (control.IDOCTYP ?? '').toUpperCase();
      const docnum = control.DOCNUM;
      const storerCode = (config.storerCode as string | undefined) ?? control.SNDPRN;
      const warehouseCode = (config.warehouseCode as string | undefined) ?? 'MAIN';
      if (mestyp === 'MATMAS' || idoctyp.startsWith('MATMAS')) out.push({ kind: 'ITEMS', storerCode, items: this.parseMatmas(idoc), externalId: docnum });
      else if (mestyp === 'DESADV' || idoctyp.startsWith('DELVRY')) out.push({ kind: 'ASN', asn: this.parseDesadv(idoc, storerCode, warehouseCode), externalId: docnum });
      else if (mestyp === 'ORDRSP' || mestyp === 'SALESORDER' || (config.ordersAsOutbound as boolean)) out.push({ kind: 'ORDER', order: this.parseOrdersAsSalesOrder(idoc, storerCode, warehouseCode), externalId: docnum });
      else if (mestyp === 'ORDERS' || idoctyp.startsWith('ORDERS')) out.push({ kind: 'ASN', asn: this.parseOrdersAsPo(idoc, storerCode, warehouseCode), externalId: docnum });
      else throw new ValidationError(`Unsupported IDoc message type ${mestyp || idoctyp}`);
    }
    return out;
  }

  formatOutbound(event: DomainEvent, context: OutboundContext, config: Record<string, unknown>): OutboundErpMessage | null {
    const sender = (config.senderPartner as string) ?? 'WMS';
    const receiver = (config.receiverPartner as string) ?? 'SAP';
    const now = new Date();
    const control = { TABNAM: 'EDI_DC40', DIRECT: '2', SNDPRT: 'LS', SNDPRN: sender, RCVPRT: 'LS', RCVPRN: receiver, CREDAT: now.toISOString().slice(0, 10).replace(/-/g, ''), CRETIM: now.toISOString().slice(11, 19).replace(/:/g, '') };
    if (event.type === 'asn.closed') {
      const summary = (event.payload.summary as { lines?: { skuCode: string; good: string; damaged: string; expectedLot?: string; externalLineRef?: string }[] }) ?? {};
      const items = (summary.lines ?? []).map((l, i) => ({ E1BP2017_GM_ITEM_CREATE: { MATERIAL: l.skuCode, PLANT: context.warehouseCode, STGE_LOC: config.storageLocation ?? '0001', ENTRY_QNT: l.good, ENTRY_UOM: 'EA', PO_NUMBER: event.payload.externalRef, PO_ITEM: l.externalLineRef ?? String((i + 1) * 10), MVT_IND: 'B', MOVE_TYPE: '101', BATCH: l.expectedLot ?? '', DAMAGED_QNT: l.damaged } }));
      const payload = { IDOC: { '@_BEGIN': '1', EDI_DC40: { ...control, MESTYP: 'MBGMCR', IDOCTYP: 'MBGMCR03' }, E1MBGMCR: { E1BP2017_GM_HEAD_01: { PSTNG_DATE: control.CREDAT, DOC_DATE: control.CREDAT, REF_DOC_NO: event.payload.grnNumber, HEADER_TXT: `WMS GRN ${event.payload.asnNumber}` }, E1BP2017_GM_CODE: { GM_CODE: '01' }, ITEMS: items } } };
      return { messageType: 'SAP_GOODS_RECEIPT', externalId: event.payload.grnNumber as string, payload, rawPayload: this.builder.build({ MBGMCR03: payload }) };
    }
    if (event.type === 'order.shipped' || event.type === 'order.short_closed') {
      const doc = (context.document ?? {}) as { lines?: { skuCode: string; shippedQty: string; externalLineRef?: string; lotNumber?: string }[] };
      const items = (doc.lines ?? []).map((l, i) => ({ E1EDL24: { POSNR: l.externalLineRef ?? String((i + 1) * 10), MATNR: l.skuCode, LFIMG: l.shippedQty, VRKME: 'EA', CHARG: l.lotNumber ?? '' } }));
      const payload = { IDOC: { '@_BEGIN': '1', EDI_DC40: { ...control, MESTYP: 'SHPCON', IDOCTYP: 'DELVRY03' }, E1EDL20: { VBELN: event.payload.externalRef, TRAID: event.payload.dnNumber, PODAT: control.CREDAT, POTIM: control.CRETIM, E1EDL18: { QUALF: 'PGI' }, ITEMS: items } } };
      return { messageType: 'SAP_GOODS_ISSUE', externalId: event.payload.dnNumber as string, payload, rawPayload: this.builder.build({ DELVRY03: payload }) };
    }
    return null;
  }

  // ---------------- parsers ----------------
  private findIdocs(doc: Record<string, unknown>): Record<string, unknown>[] {
    const found: Record<string, unknown>[] = [];
    const walk = (node: unknown) => {
      if (!node || typeof node !== 'object') return;
      for (const [k, v] of Object.entries(node as Record<string, unknown>)) {
        if (k === 'IDOC') (Array.isArray(v) ? v : [v]).forEach((x) => found.push(x as Record<string, unknown>));
        else walk(v);
      }
    };
    walk(doc);
    return found;
  }

  private arr<T>(v: unknown): T[] {
    return v === undefined || v === null ? [] : Array.isArray(v) ? (v as T[]) : [v as T];
  }

  private parseMatmas(idoc: Record<string, unknown>): ApiItemDto[] {
    return this.arr<Record<string, unknown>>(idoc.E1MARAM).map((m) => {
      const makt = this.arr<Record<string, string>>(m.E1MAKTM).find((t) => (t.SPRAS_ISO ?? t.SPRAS ?? 'EN').toUpperCase().startsWith('E')) ?? this.arr<Record<string, string>>(m.E1MAKTM)[0] ?? {};
      const marm = this.arr<Record<string, string>>(m.E1MARMM);
      const caseUom = marm.find((u) => ['CS', 'CAR', 'KAR', 'CT'].includes((u.MEINH ?? '').toUpperCase()));
      const packs: ApiItemDto['packs'] = [];
      if (caseUom && Number(caseUom.UMREZ) > 0) packs.push({ level: 'CASE', uomCode: 'CS', qtyInBase: Number(caseUom.UMREZ) / Number(caseUom.UMREN || 1), barcode: caseUom.EAN11 || undefined });
      const palUom = marm.find((u) => ['PAL', 'PL'].includes((u.MEINH ?? '').toUpperCase()));
      if (palUom && Number(palUom.UMREZ) > 0) packs.push({ level: 'PALLET', uomCode: 'PL', qtyInBase: Number(palUom.UMREZ) / Number(palUom.UMREN || 1) });
      const m2 = m as Record<string, string>;
      return {
        code: m2.MATNR?.replace(/^0+/, '') ?? '',
        description: makt.MAKTX ?? m2.MATNR ?? '',
        category: m2.MATKL,
        brand: undefined,
        storageCondition: (m2.TEMPB ?? '').startsWith('F') ? 'FROZEN' : (m2.TEMPB ?? '').startsWith('C') ? 'CHILLED' : 'AMBIENT',
        rotationRule: m2.MHDHB && Number(m2.MHDHB) > 0 ? 'FEFO' : 'FIFO',
        lotControlled: (m2.XCHPF ?? '') === 'X',
        expiryControlled: !!m2.MHDHB && Number(m2.MHDHB) > 0,
        serialControlled: !!m2.SERNP,
        shelfLifeDays: m2.MHDHB ? Number(m2.MHDHB) : undefined,
        baseUom: 'EA',
        weightKg: m2.NTGEW ? Number(m2.NTGEW) : undefined,
        cbm: m2.VOLUM ? Number(m2.VOLUM) / (m2.VOLEH === 'CCM' ? 1_000_000 : m2.VOLEH === 'DM3' || m2.VOLEH === 'L' ? 1000 : 1) : undefined,
        hsCode: m2.STAWN,
        barcodes: m2.EAN11 ? [{ barcode: m2.EAN11, packLevel: 'EACH' }] : [],
        packs,
        attributes: { sapMaterial: m2.MATNR, materialType: m2.MTART },
      };
    });
  }

  private parseOrdersAsPo(idoc: Record<string, unknown>, storerCode: string | undefined, warehouseCode: string): ApiAsnDto {
    const head = (idoc.E1EDK01 ?? {}) as Record<string, string>;
    const refs = this.arr<Record<string, string>>(idoc.E1EDK02);
    const poNo = refs.find((r) => r.QUALF === '001')?.BELNR ?? head.BELNR ?? '';
    const dates = this.arr<Record<string, string>>(idoc.E1EDK03);
    const deliveryDate = dates.find((d) => d.IDDAT === '002' || d.IDDAT === '012')?.DATUM;
    const partner = this.arr<Record<string, string>>(idoc.E1EDKA1).find((p) => p.PARVW === 'LF');
    const lines = this.arr<Record<string, unknown>>(idoc.E1EDP01).map((p) => {
      const p2 = p as Record<string, string>;
      const obj = this.arr<Record<string, string>>(p.E1EDP19).find((o) => o.QUALF === '001' || o.QUALF === '002');
      return { lineRef: p2.POSEX, itemCode: (obj?.IDTNR ?? '').replace(/^0+/, ''), qty: Number(p2.MENGE), packLevel: 'EACH' as const, lot: undefined, expiryDate: undefined, unitValue: p2.NETPR ? Number(p2.NETPR) : undefined };
    });
    return { warehouseCode, storerCode, reference: poNo, type: 'PURCHASE', supplierCode: partner?.PARTN, expectedAt: deliveryDate ? `${deliveryDate.slice(0, 4)}-${deliveryDate.slice(4, 6)}-${deliveryDate.slice(6, 8)}T08:00:00+03:00` : undefined, priority: 5, documents: [{ type: 'PO', number: poNo }], lines };
  }

  private parseDesadv(idoc: Record<string, unknown>, storerCode: string | undefined, warehouseCode: string): ApiAsnDto {
    const head = (idoc.E1EDL20 ?? {}) as Record<string, unknown>;
    const h = head as Record<string, string>;
    const lines = this.arr<Record<string, unknown>>(head.E1EDL24).map((p) => {
      const p2 = p as Record<string, string>;
      const batch = this.arr<Record<string, string>>(p.E1EDL19).find((b) => b.QUALF === 'BAT');
      return { lineRef: p2.POSNR, itemCode: (p2.MATNR ?? '').replace(/^0+/, ''), qty: Number(p2.LFIMG), packLevel: 'EACH' as const, lot: p2.CHARG || batch?.PARAM || undefined, expiryDate: p2.VFDAT ? `${p2.VFDAT.slice(0, 4)}-${p2.VFDAT.slice(4, 6)}-${p2.VFDAT.slice(6, 8)}` : undefined };
    });
    return { warehouseCode, storerCode, reference: h.VBELN ?? '', type: 'PURCHASE', containerNo: h.TRAID, carrierName: h.TDLNR, expectedAt: h.LFDAT ? `${h.LFDAT.slice(0, 4)}-${h.LFDAT.slice(4, 6)}-${h.LFDAT.slice(6, 8)}T08:00:00+03:00` : undefined, priority: 5, documents: [{ type: 'DELIVERY', number: h.VBELN ?? '' }], lines };
  }

  private parseOrdersAsSalesOrder(idoc: Record<string, unknown>, storerCode: string | undefined, warehouseCode: string): ApiOrderDto {
    const head = (idoc.E1EDK01 ?? {}) as Record<string, string>;
    const refs = this.arr<Record<string, string>>(idoc.E1EDK02);
    const soNo = refs.find((r) => r.QUALF === '002' || r.QUALF === '001')?.BELNR ?? head.BELNR ?? '';
    const shipTo = this.arr<Record<string, string>>(idoc.E1EDKA1).find((p) => p.PARVW === 'WE') ?? this.arr<Record<string, string>>(idoc.E1EDKA1).find((p) => p.PARVW === 'AG');
    const dates = this.arr<Record<string, string>>(idoc.E1EDK03);
    const reqDate = dates.find((d) => d.IDDAT === '002')?.DATUM;
    const lines = this.arr<Record<string, unknown>>(idoc.E1EDP01).map((p) => {
      const p2 = p as Record<string, string>;
      const obj = this.arr<Record<string, string>>(p.E1EDP19).find((o) => o.QUALF === '002' || o.QUALF === '001');
      return { lineRef: p2.POSEX, itemCode: (obj?.IDTNR ?? '').replace(/^0+/, ''), qty: Number(p2.MENGE), packLevel: 'EACH' as const, lot: p2.CHARG || undefined, serials: [], minExpiryDate: undefined, unitPrice: p2.NETPR ? Number(p2.NETPR) : undefined };
    });
    return {
      warehouseCode, storerCode, reference: soNo, type: 'SALES', priority: 5, customerCode: shipTo?.PARTN,
      shipTo: { name: shipTo?.NAME1, contact: shipTo?.NAME2, phone: shipTo?.TELF1, address: { line1: shipTo?.STRAS, city: shipTo?.ORT01, postalCode: shipTo?.PSTLZ, country: shipTo?.LAND1 }, deliveryLocationCode: shipTo?.PARTN },
      requestedShipDate: reqDate ? `${reqDate.slice(0, 4)}-${reqDate.slice(4, 6)}-${reqDate.slice(6, 8)}` : undefined, lines,
    };
  }
}
