/**
 * Inbound-invoice extraction orchestrator (Tier 0 → 1 → 2), fail-soft.
 *
 *   Tier 0  embedded e-invoice XML (ZUGFeRD/Factur-X/XRechnung) — perfect, no OCR
 *   Tier 1  text-layer PDF (pdfjs) → provider field extraction
 *   Tier 2  scanned/image PDF → rasterize → OCR → provider field extraction
 *
 * Self-contained: NO iDempiere/HTTP calls (only local SQLite for the vendor
 * profile), so it can run detached in the background after upload. Returns a
 * normalized partial ZugferdInvoiceData + per-field confidence/bbox + the tier
 * and engine that produced it.
 */

import { parseEinvoice } from './parseEinvoiceXml'
import { extractTextLayer, type TextItem } from './pdfTextLayer'
import { rasterizePdf } from './rasterizePdf'
import { runOcr } from '../ocr/tesseractOcr'
import { runExtraction } from './providers'
import { fingerprintFromData, applyProfile, matchVendorProfile } from './vendorProfile'
import { computeLayoutSignature } from './layoutFingerprint'
import type { InboxCtx } from './inboxDb'
import type { ZugferdInvoiceData } from '../zugferd/buildInvoiceXml'

export interface PipelineResult {
  sourceTier: number          // 0/1/2, or -1 when nothing could be read
  engine: string
  vendorFingerprint: string
  extracted: Partial<ZugferdInvoiceData>
  confidence: Record<string, number>
  bbox: Record<string, any>
}

const isXmlInput = (mimeType?: string, filename?: string): boolean =>
  /xml/i.test(mimeType || '') || /\.xml$/i.test(filename || '')

/** Every field present in a Tier-0 parse is authoritative → confidence 1.0. */
const tier0Confidence = (data: Partial<ZugferdInvoiceData>): Record<string, number> => {
  const c: Record<string, number> = {}
  for (const k of Object.keys(data)) c[k] = 1
  if (data.seller?.name) c['seller.name'] = 1
  if (data.seller?.vatId) c['seller.vatId'] = 1
  if (data.paymentIban) c['paymentIban'] = 1
  return c
}

// Reference fields arrive from the text layer / OCR with stray separators.
//  - IBAN / VAT id  → strip whitespace AND '.' ',' '-'  (bare alphanumerics, e.g. "DE 12 3456…" → "DE123456…")
//  - invoice number → strip whitespace / '.' / ',' but KEEP '-' (it's a real separator, e.g. "RE-2024-001")
const cleanId = (v: any): any => v == null ? v : String(v).replace(/[\s.,\-]+/g, '')
const cleanDocNo = (v: any): any => v == null ? v : String(v).replace(/[\s.,]+/g, '')

/** Light post-extraction normalization: defaults + derive missing totals + clean refs. */
const normalize = (f: Partial<ZugferdInvoiceData>): Partial<ZugferdInvoiceData> => {
  const out: any = { ...f }
  if (!out.currency) out.currency = 'EUR'
  if (!out.typeCode) out.typeCode = '380'
  if (out.documentNo) out.documentNo = cleanDocNo(out.documentNo)
  if (out.paymentIban) out.paymentIban = cleanId(out.paymentIban)
  if (out.seller?.vatId) out.seller = { ...out.seller, vatId: cleanId(out.seller.vatId) }
  if (out.buyer?.vatId) out.buyer = { ...out.buyer, vatId: cleanId(out.buyer.vatId) }
  const g = Number(out.grandTotal)
  const n = Number(out.lineTotal ?? out.taxBasisTotal)
  const t = Number(out.taxTotal)
  if (Number.isFinite(g) && Number.isFinite(n) && !Number.isFinite(t)) out.taxTotal = Math.round((g - n) * 100) / 100
  if (Number.isFinite(g) && Number.isFinite(t) && !Number.isFinite(n)) out.lineTotal = Math.round((g - t) * 100) / 100
  if (out.taxBasisTotal == null && out.lineTotal != null) out.taxBasisTotal = out.lineTotal
  if (out.duePayable == null && out.grandTotal != null) out.duePayable = out.grandTotal
  return out
}

const flattenTextItems = (pages: { items: TextItem[] }[]): TextItem[] =>
  pages.flatMap(p => p.items || [])

export const runPipeline = async (
  pdfBuffer: Buffer,
  opts: { ctx?: InboxCtx; mimeType?: string; filename?: string } = {}
): Promise<PipelineResult> => {
  // ---- Tier 0: embedded / standalone e-invoice XML ----
  try {
    const xmlInput = isXmlInput(opts.mimeType, opts.filename) ? pdfBuffer.toString('utf8') : undefined
    const data = await parseEinvoice({ pdf: xmlInput ? undefined : pdfBuffer, xml: xmlInput })
    if (data && (data.documentNo || data.grandTotal != null)) {
      const extracted = normalize(data)
      return {
        sourceTier: 0,
        engine: 'einvoice-xml',
        vendorFingerprint: fingerprintFromData(extracted),
        extracted,
        confidence: tier0Confidence(extracted),
        bbox: {}
      }
    }
  } catch (e: any) {
    console.warn('[Inbox] Tier 0 (xml) failed:', e?.message || e)
  }

  // ---- Tier 1: text-layer PDF ----
  let text = ''
  let items: TextItem[] = []
  let tier = -1
  try {
    const tl = await extractTextLayer(pdfBuffer)
    if (tl.hasText) {
      text = tl.text
      items = flattenTextItems(tl.pages)
      tier = 1
    }
  } catch (e: any) {
    console.warn('[Inbox] Tier 1 (text layer) failed:', e?.message || e)
  }

  // ---- Tier 2: rasterize + OCR ----
  if (tier === -1) {
    try {
      const raster = await rasterizePdf(pdfBuffer)
      if (raster.pages.length) {
        const sharp = (await import('sharp')).default
        const texts: string[] = []
        const ocrItems: TextItem[] = []
        let pageNo = 0
        for (const png of raster.pages) {
          pageNo++
          // page pixel dims → normalize OCR word boxes (top-left origin, 0..1) so
          // the overlay + learned-region auto-fill work on scans just like text PDFs.
          let W = 0, H = 0
          try { const m = await sharp(png).metadata(); W = m.width || 0; H = m.height || 0 } catch {}
          const ocr = await runOcr(png, { withWords: true })
          texts.push(ocr.text)
          for (const w of ocr.words) {
            const x = w.bbox.x0, y = w.bbox.y0, ww = w.bbox.x1 - w.bbox.x0, hh = w.bbox.y1 - w.bbox.y0
            ocrItems.push({
              text: w.text, x, y, w: ww, h: hh,
              page: pageNo,
              ...(W && H ? { nx: x / W, ny: y / H, nw: ww / W, nh: hh / H } : {})
            })
          }
        }
        text = texts.join('\n')
        items = ocrItems
        tier = 2
      }
    } catch (e: any) {
      console.warn('[Inbox] Tier 2 (ocr) failed:', e?.message || e)
    }
  }

  // Nothing readable — return an empty shell (the doc is still attached & reviewable).
  if (tier === -1) {
    return { sourceTier: -1, engine: 'none', vendorFingerprint: '', extracted: normalize({}), confidence: {}, bbox: {} }
  }

  // ---- Field extraction via provider (self-hosted default, optional LLM) ----
  let result = await runExtraction({ text, items }, { ctx: opts.ctx })

  // Value-independent LAYOUT fingerprint identifies the vendor's template even if
  // a value (IBAN/amount) was misread → match key. Value fingerprint is fallback.
  let layout = { hash: '', tokens: [] as string[] }
  try { layout = await computeLayoutSignature(text, pdfBuffer) } catch {}
  const matchKey = layout.hash ? ('layout:' + layout.hash) : fingerprintFromData(result.fields)

  // Load the learned template (exact hash → fuzzy Jaccard fallback) → read each
  // learned field FROM its region + defaults.
  if (matchKey && opts.ctx) {
    try {
      const profile = await matchVendorProfile(opts.ctx, matchKey, layout.tokens)
      if (profile) result = { ...applyProfile(result, profile, items), engine: result.engine }
    } catch {}
  }

  const extracted: any = normalize(result.fields)
  extracted.__layout = { hash: layout.hash, tokens: layout.tokens }   // persisted for matching/learning
  return {
    sourceTier: tier,
    engine: result.engine,
    vendorFingerprint: matchKey,
    extracted,
    confidence: result.confidence,
    bbox: result.bbox
  }
}

export default runPipeline
