/**
 * Gated ZUGFeRD orchestrator — the single entry point used by every route that
 * transmits an invoice PDF (email, Lexoffice upload, Amazon upload).
 *
 *   const outBuffer = await maybeZugferdPdf(event, token, invoiceId, pdfBuffer)
 *
 * Behaviour:
 *  - Resolve the invoice's partner and its `IsZugPferdInvoice` flag.
 *  - Flag false / column missing / any error  → return `plainPdfBuffer` unchanged
 *    (today's behaviour is fully preserved — fail-soft, never blocks send/upload).
 *  - Flag true → gather invoice data (lines, tax, buyer + seller address/VAT),
 *    build the EN16931 CII XML and merge it into the iDempiere PDF.
 *
 * No PDF is built from scratch: `plainPdfBuffer` is iDempiere's own invoice PDF;
 * we only generate the XML and embed it.
 *
 * `gatherZugferdData` / `INVOICE_EXPAND` are exported so the diagnostic preview
 * route can build the XML for any invoice without the flag gate.
 */
import buildInvoiceXml, { type ZugferdInvoiceData, type ZugferdLine, type ZugferdTaxBreakdown } from './buildInvoiceXml'
import embedZugferdXml from './embedXml'

/**
 * Top-level expands only. iDempiere rejects a nested $expand inside a master
 * expand ("Expanding a master only support the $select query operator"), so the
 * address chain (bpartner_location → c_location → c_country) is resolved with a
 * follow-up c_location fetch instead (see resolveBpLocationAddress).
 */
export const INVOICE_EXPAND =
  'C_BPartner_ID,C_BPartner_Location_ID,C_Currency_ID,C_Order_ID,AD_Org_ID,C_PaymentTerm_ID'

const isZugFlag = (v: any): boolean => v === true || v === 'Y'

const num = (v: any): number => Number(v) || 0

const round2 = (n: number): number => Math.round((Number(n) || 0) * 100) / 100

const round4 = (n: number): number => Math.round((Number(n) || 0) * 10000) / 10000

/** Add `days` to a YYYY-MM-DD / ISO date string; returns YYYY-MM-DD ('' if unparseable). */
const addDays = (dateStr: string, days: number): string => {
  const m = String(dateStr || '').match(/(\d{4})-(\d{2})-(\d{2})/)
  if (!m) return ''
  const d = new Date(Date.UTC(Number(m[1]), Number(m[2]) - 1, Number(m[3])))
  d.setUTCDate(d.getUTCDate() + (Number(days) || 0))
  return d.toISOString().slice(0, 10)
}

const fkVal = (v: any): any => (v && typeof v === 'object' ? (v.id ?? v.identifier) : v)

type ZugAddress = { street: string; city: string; postcode: string; countryCode: string }
const EMPTY_ADDRESS: ZugAddress = { street: '', city: '', postcode: '', countryCode: 'DE' }

/** Fetch a c_location (+ its country) and map it to a ZUGFeRD postal address.
 *  `$expand=C_Country_ID` here is a top-level expand on the primary resource,
 *  which iDempiere allows (the rejected case is an expand nested in an expand). */
const fetchAddress = async (event: any, token: any, cLocationId: any): Promise<ZugAddress> => {
  if (!cLocationId) return { ...EMPTY_ADDRESS }
  try {
    const loc: any = await event.context.fetch(`models/c_location/${cLocationId}?$expand=C_Country_ID`, 'GET', token, null)
    if (!loc?.id) return { ...EMPTY_ADDRESS }
    const country = loc?.C_Country_ID || {}
    return {
      street: [loc?.Address1, loc?.Address2].filter(Boolean).join(' ') || '',
      city: loc?.City || '',
      postcode: loc?.Postal || '',
      countryCode: country?.CountryCode || 'DE',
    }
  } catch (e) {
    console.error('[ZUGFeRD] c_location lookup failed:', e)
    return { ...EMPTY_ADDRESS }
  }
}

/** Resolve a postal address from a c_bpartner_location reference. The ref may be
 *  an already-expanded record (carries C_Location_ID) or just an { id } stub, in
 *  which case the bpartner_location is fetched to find its C_Location_ID first. */
const resolveBpLocationAddress = async (event: any, token: any, bpLocRef: any): Promise<ZugAddress> => {
  let cLocId = fkVal(bpLocRef?.C_Location_ID)
  if (!cLocId) {
    const bpLocId = fkVal(bpLocRef)
    if (bpLocId) {
      try {
        const bpLoc: any = await event.context.fetch(`models/c_bpartner_location/${bpLocId}`, 'GET', token, null)
        cLocId = fkVal(bpLoc?.C_Location_ID)
      } catch (e) {
        console.error('[ZUGFeRD] c_bpartner_location lookup failed:', e)
      }
    }
  }
  return fetchAddress(event, token, cLocId)
}

/**
 * Seller postal address: scan the org partner's c_bpartner_locations (each →
 * c_location) and return the first one that actually has a city; falls back to
 * the first resolvable address. Tolerates an org partner with several locations
 * where only one carries the full registered address.
 */
const resolveSellerAddress = async (event: any, token: any, bpId: any): Promise<ZugAddress> => {
  if (!bpId) return { ...EMPTY_ADDRESS }
  try {
    const res: any = await event.context.fetch(
      `models/c_bpartner_location?$filter=C_BPartner_ID eq ${bpId}&$orderby=c_bpartner_location_id asc&$top=10`,
      'GET', token, null,
    )
    const recs: any[] = res?.records || []
    let firstAddr: ZugAddress | null = null
    for (const r of recs) {
      const addr = await fetchAddress(event, token, fkVal(r?.C_Location_ID))
      if (!firstAddr) firstAddr = addr
      if (addr.city) return addr
    }
    return firstAddr || { ...EMPTY_ADDRESS }
  } catch (e) {
    console.error('[ZUGFeRD] seller address resolution failed:', e)
    return { ...EMPTY_ADDRESS }
  }
}

/**
 * Resolve the SELLER = the c_bpartner linked to the organization
 * (ad_org.C_BPartner_ID). The seller is the org's partner — NOT the invoice's
 * C_BPartner_ID (that is the buyer/customer). Prefers the app's dedicated
 * org→bpartner endpoint (CLAUDE.md recipe); falls back to a direct fetch.
 * Returns the full c_bpartner record (incl. TaxID) or {}.
 */
const resolveOrgBpartner = async (event: any, token: any, orgId: any): Promise<any> => {
  // 1) Dedicated endpoint: GET /api/admin/organizations/{id}/bpartner
  try {
    const bp: any = await $fetch(`/api/admin/organizations/${orgId}/bpartner`, {
      headers: { cookie: getHeader(event, 'cookie') || '' },
    })
    if (bp?.id) return bp
  } catch (e) {
    console.error('[ZUGFeRD] org→bpartner endpoint failed, falling back to direct fetch:', e)
  }
  // 2) Direct fallback: ad_org → C_BPartner_ID → c_bpartner
  try {
    const orgRes: any = await event.context.fetch(`models/ad_org/${orgId}`, 'GET', token, null)
    const bpId = fkVal(orgRes?.C_BPartner_ID)
    if (bpId) {
      const bp: any = await event.context.fetch(`models/c_bpartner/${bpId}`, 'GET', token, null)
      if (bp?.id) return bp
    } else {
      console.error(`[ZUGFeRD] Org ${orgId} has no linked C_BPartner_ID.`)
    }
  } catch (e) {
    console.error('[ZUGFeRD] org→bpartner direct fallback failed:', e)
  }
  return {}
}

/** True when the (already expanded) invoice's partner opted into ZUGFeRD. */
export const isZugferdEnabled = (inv: any): boolean => {
  const partner = inv?.C_BPartner_ID || {}
  return isZugFlag(partner?.IsZugPferdInvoice ?? partner?.isZugPferdInvoice)
}

/**
 * Human-readable warnings about master data that makes the e-invoice invalid or
 * incomplete (things the user must fix in iDempiere, not in code). The most
 * important is a missing seller VAT id — without it BR-S-02 / BR-CO-26 fail.
 */
export const zugferdDataWarnings = (data: ZugferdInvoiceData): string[] => {
  // Only genuine blockers that make the e-invoice INVALID (hard EN16931 errors).
  // Soft notices (missing Handelsregisternummer, incomplete address) are intentionally
  // omitted — they're warnings on some validators but Lexware accepts the invoice.
  const w: string[] = []
  if (!data?.seller?.vatId) {
    w.push("Umsatzsteuer-ID des Verkäufers fehlt – bitte C_BPartner.TaxID am Organisations-Geschäftspartner (ad_org.C_BPartner_ID) pflegen. Ohne sie ist die E-Rechnung ungültig (BR-S-02 / BR-CO-26).")
  }
  if (!data?.seller?.name) w.push('Name des Verkäufers (Organisations-Geschäftspartner) fehlt.')
  if (!data?.buyer?.name) w.push('Name des Rechnungsempfängers fehlt.')
  if (!(data?.lines?.length)) w.push('Die Rechnung enthält keine Positionen.')
  return w
}

/**
 * Map an invoice (already fetched with INVOICE_EXPAND) + its lines/tax/seller
 * into the normalized model the XML builder consumes. Does NOT check the flag.
 */
export const gatherZugferdData = async (event: any, token: any, inv: any): Promise<ZugferdInvoiceData> => {
  const invoiceId = inv?.id
  const partner = inv?.C_BPartner_ID || {}

  const linesRes: any = await event.context.fetch(
    `models/c_invoiceline?$filter=C_Invoice_ID eq ${invoiceId}&$expand=M_Product_ID,C_Tax_ID&$orderby=Line asc&$top=1000`,
    'GET', token, null,
  )

  // iDempiere stores tax-inclusive ("Brutto") line amounts as gross; EN16931 wants
  // every line/total NET. When tax is included, convert each line back to net.
  const taxIncluded = inv?.IsTaxIncluded === true || inv?.IsTaxIncluded === 'Y'

  // Seller = the c_bpartner linked to the INVOICE's organization
  // (AD_Org_ID on the invoice → ad_org.C_BPartner_ID → c_bpartner). Its TaxID is the
  // seller's Umsatzsteuer-ID (BT-31, required by BR-S-02). Fail-soft per fetch.
  const orgId = fkVal(inv?.AD_Org_ID)
  let sellerBp: any = {}
  let sellerVatId = ''
  let sellerLegalRegId = ''
  let paymentIban = ''
  if (orgId) {
    // Seller c_bpartner = ad_org.C_BPartner_ID (via the dedicated endpoint).
    sellerBp = await resolveOrgBpartner(event, token, orgId)
    // iDempiere OData casing varies by model — accept TaxID / taxID / taxId.
    sellerVatId = String(sellerBp?.TaxID ?? sellerBp?.taxID ?? sellerBp?.taxId ?? '').trim()
    // BT-30 Handelsregisternummer — optional custom column `hrg_number` on the org
    // partner; emitted only when present (no notice when absent).
    sellerLegalRegId = String(
      sellerBp?.hrg_number ?? sellerBp?.Hrg_Number ?? sellerBp?.HRG_Number ?? sellerBp?.hrgNumber ?? '',
    ).trim()
    // Fallback: organization-level tax id (AD_OrgInfo.TaxID) when the bpartner has none.
    if (!sellerVatId) {
      try {
        const orgInfoRes: any = await event.context.fetch(
          `models/ad_orginfo?$filter=AD_Org_ID eq ${orgId}&$top=1`, 'GET', token, null,
        )
        const oi: any = orgInfoRes?.records?.[0] || {}
        sellerVatId = String(oi?.TaxID ?? oi?.taxID ?? oi?.taxId ?? '').trim()
      } catch { /* org-level VAT is a best-effort fallback */ }
    }
    try {
      const bankRes: any = await event.context.fetch(
        `models/c_bankaccount?$filter=IsDefault eq true and AD_Org_ID eq ${orgId}&$top=1`,
        'GET', token, null,
      )
      paymentIban = bankRes?.records?.[0]?.IBAN || ''
    } catch { /* IBAN is optional */ }
  }
  console.log(`[ZUGFeRD] seller resolved: org=${orgId} bp="${sellerBp?.Name || ''}" vatId="${sellerVatId}" hrb="${sellerLegalRegId}"`)

  // --- lines (net-consistent; BT-146 price kept >= 0, sign carried on the qty) ---
  const lineRecords: any[] = linesRes?.records || []
  const lines: ZugferdLine[] = lineRecords.map((r: any, i: number) => {
    const rate = num(r?.C_Tax_ID?.Rate)
    const rawAmt = num(r?.LineNetAmt)
    // Convert gross→net for tax-inclusive invoices so BT-131 is a true net amount.
    const netLine = taxIncluded ? rawAmt / (1 + rate / 100) : rawAmt
    const absQty = Math.abs(num(r?.QtyInvoiced ?? r?.QtyEntered ?? 1)) || 1
    // BT-146 (item net price) must NOT be negative — keep it >= 0, sign the quantity.
    const unitPrice = round4(Math.abs(netLine) / absQty)
    const billedQty = netLine < 0 ? -absQty : absQty
    const lineTotal = round2(unitPrice * billedQty)
    return {
      lineId: num(r?.Line) || i + 1,
      name: r?.M_Product_ID?.Name || r?.M_Product_ID?.identifier || r?.Description || '-',
      qty: billedQty,
      unitCode: 'C62',
      netPrice: unitPrice,
      lineTotal,
      taxRate: rate,
      taxCategory: rate > 0 ? 'S' : 'Z',
    }
  })

  // --- VAT breakdown derived from the EMITTED line amounts, so BR-S-08 / BR-CO-13 /
  //     BR-CO-10 reconcile to the cent (mixing iDempiere's gross+net was the bug). ---
  const byRate = new Map<number, ZugferdTaxBreakdown>()
  for (const l of lines) {
    const b = byRate.get(l.taxRate) || { rate: l.taxRate, basis: 0, amount: 0, category: l.taxCategory }
    b.basis = round2(b.basis + l.lineTotal)
    byRate.set(l.taxRate, b)
  }
  const taxes: ZugferdTaxBreakdown[] = Array.from(byRate.values()).map(b => ({
    ...b,
    amount: round2((b.basis * b.rate) / 100),
  }))

  // --- totals (computed from our own net figures → internally consistent) ---
  const lineTotal = round2(lines.reduce((s, l) => s + l.lineTotal, 0))
  const taxBasisTotal = round2(taxes.reduce((s, t) => s + t.basis, 0))
  const taxTotal = round2(taxes.reduce((s, t) => s + t.amount, 0))
  const grandTotal = round2(taxBasisTotal + taxTotal)

  const docBaseType = String(fkVal(inv?.DocBaseType) || '')
  const isCredit = docBaseType === 'ARC' || docBaseType === 'APC'

  // Payment terms / due date (BR-CO-25) and delivery date (BR-FX-EN-04).
  const issueDate = inv?.DateInvoiced || inv?.DateAcct || ''
  const netDays = num(inv?.C_PaymentTerm_ID?.NetDays)
  const dueDate = inv?.C_PaymentTerm_ID ? addDays(issueDate, netDays) : ''
  const paymentTermName = inv?.C_PaymentTerm_ID?.Name || inv?.C_PaymentTerm_ID?.identifier || ''
  const paymentTerms = paymentTermName
    || (dueDate ? `Zahlbar bis ${dueDate}` : 'Zahlbar sofort nach Erhalt')

  // BT-29 seller identifier — any string; guarantees BR-CO-26 even with no VAT ID.
  const sellerId = sellerBp?.Value || sellerBp?.TaxID || (sellerBp?.id ? `BP-${sellerBp.id}` : '')

  // Resolve postal addresses via follow-up c_location fetches (no nested expand).
  const [sellerAddress, buyerAddress] = await Promise.all([
    resolveSellerAddress(event, token, sellerBp?.id),
    resolveBpLocationAddress(event, token, inv?.C_BPartner_Location_ID),
  ])

  return {
    documentNo: inv?.DocumentNo || String(invoiceId),
    issueDate,
    dueDate: dueDate || undefined,
    paymentTerms,
    deliveryDate: issueDate,
    typeCode: isCredit ? '381' : '380',
    currency: inv?.C_Currency_ID?.identifier || 'EUR',
    orderRef: inv?.POReference || inv?.C_Order_ID?.DocumentNo || '',
    seller: {
      name: sellerBp?.Name || '',
      id: sellerId || undefined,
      legalRegId: sellerLegalRegId || undefined,
      vatId: sellerVatId,
      ...sellerAddress,
    },
    buyer: {
      name: partner?.Name || partner?.identifier || '',
      vatId: String(partner?.TaxID ?? partner?.taxID ?? partner?.taxId ?? '').trim(),
      ...buyerAddress,
    },
    lines,
    taxes,
    lineTotal,
    taxBasisTotal,
    taxTotal,
    grandTotal,
    duePayable: grandTotal,
    paymentIban: paymentIban || undefined,
  }
}

/**
 * @param event             h3 event (provides event.context.fetch to iDempiere)
 * @param token             current bearer token
 * @param invoiceId         c_invoice id
 * @param plainPdfBuffer    iDempiere's invoice PDF (decoded from base64)
 * @returns                 hybrid ZUGFeRD PDF when the partner is flagged, else the input buffer
 */
export const maybeZugferdPdf = async (
  event: any,
  token: any,
  invoiceId: number | string,
  plainPdfBuffer: Buffer,
): Promise<Buffer> => {
  try {
    // One fetch resolves the gate (partner flag) + most of the buyer data.
    const inv: any = await event.context.fetch(
      `models/c_invoice/${invoiceId}?$expand=${INVOICE_EXPAND}`,
      'GET', token, null,
    )

    if (!isZugferdEnabled(inv)) {
      return plainPdfBuffer
    }

    const data = await gatherZugferdData(event, token, inv)
    const xml = buildInvoiceXml(data)
    const hybrid = await embedZugferdXml(plainPdfBuffer, xml)
    console.log(`[ZUGFeRD] Embedded factur-x.xml into invoice ${data.documentNo} (${hybrid.length} bytes)`)
    return hybrid
  } catch (err) {
    console.error(`[ZUGFeRD] Generation failed for invoice ${invoiceId}, sending plain PDF:`, err)
    return plainPdfBuffer
  }
}

export default maybeZugferdPdf
