/**
 * Tier 0 — embedded e-invoice XML (ZUGFeRD / Factur-X / XRechnung).
 *
 * The inverse of server/utils/zugferd/buildInvoiceXml.ts: detect & parse the
 * machine-readable invoice XML that an increasing share of German B2B suppliers
 * embed (Factur-X) or send standalone (XRechnung). When present this yields
 * perfect structured data with NO OCR — so it is always tried first.
 *
 * Handles CII (Cross-Industry-Invoice, the ZUGFeRD/Factur-X format) fully and
 * UBL (Invoice) best-effort. Fail-soft: returns null on anything unexpected so
 * the pipeline falls through to the text/scan tiers.
 */

import { PDFDocument, PDFName, PDFDict, PDFRawStream, PDFArray } from 'pdf-lib'
import { XMLParser } from 'fast-xml-parser'
import zlib from 'zlib'
import type { ZugferdInvoiceData, ZugferdLine, ZugferdTaxBreakdown } from '../zugferd/buildInvoiceXml'

const XML_INVOICE_HINTS = ['CrossIndustryInvoice', ':Invoice', '<Invoice', 'ubl:Invoice']

const looksLikeInvoiceXml = (s: string): boolean =>
  !!s && XML_INVOICE_HINTS.some(h => s.includes(h))

/** Inflate a stream if it is FlateDecode-encoded; otherwise return as-is. */
const maybeInflate = (raw: Uint8Array, filter: string | null): Buffer => {
  const buf = Buffer.from(raw)
  if (filter && /Flate/i.test(filter)) {
    try { return zlib.inflateSync(buf) } catch {}
    try { return zlib.inflateRawSync(buf) } catch {}
  }
  return buf
}

/**
 * Walk the PDF catalog's Names → EmbeddedFiles name tree and return the first
 * embedded file whose bytes look like an invoice XML.
 */
const extractViaPdfLib = async (pdfBuffer: Buffer): Promise<string | null> => {
  const pdfDoc = await PDFDocument.load(pdfBuffer, { throwOnInvalidObject: false, updateMetadata: false })
  const catalog: any = pdfDoc.catalog
  const names = catalog.lookup(PDFName.of('Names'), PDFDict)
  if (!names) return null
  const embedded = names.lookup(PDFName.of('EmbeddedFiles'), PDFDict)
  if (!embedded) return null

  // Collect filespec dicts from a (possibly nested) name tree.
  const filespecs: PDFDict[] = []
  const visit = (node: PDFDict) => {
    const namesArr = node.lookup(PDFName.of('Names'), PDFArray)
    if (namesArr) {
      for (let i = 1; i < namesArr.size(); i += 2) {
        const spec = namesArr.lookup(i, PDFDict)
        if (spec) filespecs.push(spec)
      }
    }
    const kids = node.lookup(PDFName.of('Kids'), PDFArray)
    if (kids) {
      for (let i = 0; i < kids.size(); i++) {
        const kid = kids.lookup(i, PDFDict)
        if (kid) visit(kid)
      }
    }
  }
  visit(embedded)

  for (const spec of filespecs) {
    const ef = spec.lookup(PDFName.of('EF'), PDFDict)
    if (!ef) continue
    const stream = (ef.lookup(PDFName.of('F'), PDFRawStream) || ef.lookup(PDFName.of('UF'), PDFRawStream)) as PDFRawStream | undefined
    if (!stream) continue
    const filterObj = stream.dict.lookup(PDFName.of('Filter'))
    const filter = filterObj ? filterObj.toString() : null
    const bytes = maybeInflate(stream.contents, filter)
    const text = bytes.toString('utf8')
    if (looksLikeInvoiceXml(text)) return text
  }
  return null
}

/** Brute-force fallback: find an uncompressed XML invoice region in the raw bytes. */
const extractViaRawScan = (pdfBuffer: Buffer): string | null => {
  const s = pdfBuffer.toString('latin1')
  const closers = ['</rsm:CrossIndustryInvoice>', '</CrossIndustryInvoice>', '</ubl:Invoice>', '</Invoice>']
  for (const closer of closers) {
    const end = s.indexOf(closer)
    if (end === -1) continue
    const startTag = s.lastIndexOf('<?xml', end)
    const openAlt = s.lastIndexOf('<rsm:CrossIndustryInvoice', end)
    const start = startTag !== -1 ? startTag : openAlt
    if (start !== -1 && start < end) {
      const xml = s.slice(start, end + closer.length)
      if (looksLikeInvoiceXml(xml)) return Buffer.from(xml, 'latin1').toString('utf8')
    }
  }
  return null
}

export const extractEmbeddedInvoiceXml = async (pdfBuffer: Buffer): Promise<string | null> => {
  try {
    const viaLib = await extractViaPdfLib(pdfBuffer)
    if (viaLib) return viaLib
  } catch {}
  try {
    return extractViaRawScan(pdfBuffer)
  } catch { return null }
}

/* ---------------- CII / UBL → ZugferdInvoiceData ---------------- */

const parser = new XMLParser({
  ignoreAttributes: false,
  attributeNamePrefix: '@_',
  removeNSPrefix: true,
  parseTagValue: false,
  trimValues: true
})

const arr = (x: any): any[] => (x == null ? [] : Array.isArray(x) ? x : [x])
const txt = (x: any): string => {
  if (x == null) return ''
  if (typeof x === 'object') return String(x['#text'] ?? '')
  return String(x)
}
const num = (x: any): number => {
  const n = Number(String(txt(x)).replace(',', '.'))
  return Number.isFinite(n) ? n : 0
}
/** CII format-102 date YYYYMMDD → YYYY-MM-DD. */
const ymdToIso = (x: any): string => {
  const s = String(txt(x)).replace(/[^0-9]/g, '')
  if (s.length >= 8) return `${s.slice(0, 4)}-${s.slice(4, 6)}-${s.slice(6, 8)}`
  return ''
}

const ciiParty = (p: any) => {
  if (!p) return { name: '' }
  const addr = p.PostalTradeAddress || {}
  const taxReg = arr(p.SpecifiedTaxRegistration).map((r: any) => txt(r?.ID)).find(Boolean)
  return {
    name: txt(p.Name),
    vatId: taxReg || '',
    street: txt(addr.LineOne),
    city: txt(addr.CityName),
    postcode: txt(addr.PostcodeCode),
    countryCode: txt(addr.CountryID) || 'DE'
  }
}

const parseCii = (root: any): ZugferdInvoiceData | null => {
  const doc = root.ExchangedDocument
  const tx = root.SupplyChainTradeTransaction
  if (!doc || !tx) return null
  const agreement = tx.ApplicableHeaderTradeAgreement || {}
  const settlement = tx.ApplicableHeaderTradeSettlement || {}
  const delivery = tx.ApplicableHeaderTradeDelivery || {}
  const summation = settlement.SpecifiedTradeSettlementHeaderMonetarySummation || {}

  const lines: ZugferdLine[] = arr(tx.IncludedSupplyChainTradeLineItem).map((li: any, idx: number) => {
    const prod = li.SpecifiedTradeProduct || {}
    const agr = li.SpecifiedLineTradeAgreement || {}
    const del = li.SpecifiedLineTradeDelivery || {}
    const set = li.SpecifiedLineTradeSettlement || {}
    const tax = set.ApplicableTradeTax || {}
    const qtyNode = del.BilledQuantity
    return {
      lineId: Number(txt(li.AssociatedDocumentLineDocument?.LineID)) || idx + 1,
      name: txt(prod.Name) || '-',
      qty: num(qtyNode),
      unitCode: (qtyNode && qtyNode['@_unitCode']) || 'C62',
      netPrice: num(agr.NetPriceProductTradePrice?.ChargeAmount),
      lineTotal: num(set.SpecifiedTradeSettlementLineMonetarySummation?.LineTotalAmount),
      taxRate: num(tax.RateApplicablePercent),
      taxCategory: txt(tax.CategoryCode) || 'S'
    }
  })

  const taxes: ZugferdTaxBreakdown[] = arr(settlement.ApplicableTradeTax).map((t: any) => ({
    rate: num(t.RateApplicablePercent),
    basis: num(t.BasisAmount),
    amount: num(t.CalculatedAmount),
    category: txt(t.CategoryCode) || 'S'
  }))

  const typeCode = txt(doc.TypeCode) === '381' ? '381' : '380'

  return {
    documentNo: txt(doc.ID),
    issueDate: ymdToIso(doc.IssueDateTime?.DateTimeString),
    dueDate: ymdToIso(settlement.SpecifiedTradePaymentTerms?.DueDateDateTime?.DateTimeString) || undefined,
    paymentTerms: txt(settlement.SpecifiedTradePaymentTerms?.Description) || undefined,
    deliveryDate: ymdToIso(delivery.ActualDeliverySupplyChainEvent?.OccurrenceDateTime?.DateTimeString) || undefined,
    typeCode,
    currency: txt(settlement.InvoiceCurrencyCode) || 'EUR',
    orderRef: txt(agreement.BuyerOrderReferencedDocument?.IssuerAssignedID) || undefined,
    seller: ciiParty(agreement.SellerTradeParty),
    buyer: ciiParty(agreement.BuyerTradeParty),
    lines,
    taxes,
    lineTotal: num(summation.LineTotalAmount),
    taxBasisTotal: num(summation.TaxBasisTotalAmount),
    taxTotal: num(summation.TaxTotalAmount),
    grandTotal: num(summation.GrandTotalAmount),
    duePayable: num(summation.DuePayableAmount),
    paymentIban: txt(settlement.SpecifiedTradeSettlementPaymentMeans?.PayeePartyCreditorFinancialAccount?.IBANID) || undefined
  }
}

/** Minimal UBL (Invoice) mapping — enough to prefill header + totals. */
const parseUbl = (root: any): ZugferdInvoiceData | null => {
  const inv = root.Invoice || root
  if (!inv || !inv.ID) return null
  const seller = inv.AccountingSupplierParty?.Party || {}
  const buyer = inv.AccountingCustomerParty?.Party || {}
  const monetary = inv.LegalMonetaryTotal || {}
  const ublParty = (p: any) => ({
    name: txt(p?.PartyName?.Name) || txt(p?.PartyLegalEntity?.RegistrationName),
    vatId: txt(p?.PartyTaxScheme?.CompanyID),
    street: txt(p?.PostalAddress?.StreetName),
    city: txt(p?.PostalAddress?.CityName),
    postcode: txt(p?.PostalAddress?.PostalZone),
    countryCode: txt(p?.PostalAddress?.Country?.IdentificationCode) || 'DE'
  })
  return {
    documentNo: txt(inv.ID),
    issueDate: txt(inv.IssueDate),
    typeCode: txt(inv.InvoiceTypeCode) === '381' ? '381' : '380',
    currency: txt(inv.DocumentCurrencyCode) || 'EUR',
    seller: ublParty(seller),
    buyer: ublParty(buyer),
    lines: [],
    taxes: arr(inv.TaxTotal?.TaxSubtotal).map((t: any) => ({
      rate: num(t?.TaxCategory?.Percent),
      basis: num(t?.TaxableAmount),
      amount: num(t?.TaxAmount),
      category: txt(t?.TaxCategory?.ID) || 'S'
    })),
    lineTotal: num(monetary.LineExtensionAmount),
    taxBasisTotal: num(monetary.TaxExclusiveAmount),
    taxTotal: num(inv.TaxTotal?.TaxAmount),
    grandTotal: num(monetary.TaxInclusiveAmount),
    duePayable: num(monetary.PayableAmount)
  }
}

export const parseInvoiceXml = (xml: string): ZugferdInvoiceData | null => {
  try {
    const root = parser.parse(xml)
    if (root.CrossIndustryInvoice) return parseCii(root.CrossIndustryInvoice)
    if (root.Invoice) return parseUbl(root)
    return null
  } catch {
    return null
  }
}

/** Orchestrates Tier 0 from a PDF buffer and/or a standalone XML string. */
export const parseEinvoice = async (input: { pdf?: Buffer; xml?: string }): Promise<ZugferdInvoiceData | null> => {
  let xml = input.xml || null
  if (!xml && input.pdf) xml = await extractEmbeddedInvoiceXml(input.pdf)
  if (!xml) return null
  return parseInvoiceXml(xml)
}

export default parseEinvoice
