/**
 * Vendor fingerprinting + the per-vendor learning loop.
 *
 * Fingerprint priority: VAT id → IBAN → normalized name. A profile remembers
 * defaults (currency / charge / tax / doctype / tax-included / linked c_bpartner)
 * and per-field hints (the box where a value sat + the last sample) so the next
 * invoice from the same vendor prefills with high confidence.
 *
 * Learning happens ONLY on a confirmed invoice (see confirm route) and ONLY from
 * the values the user approved — rejected/edited-away data never trains.
 */

import type { ZugferdInvoiceData } from '../zugferd/buildInvoiceXml'
import { parseAmount, parseDate, type ExtractionResult, type FieldBox } from './fieldExtractors'
import type { TextItem } from './pdfTextLayer'
import { layoutSimilarity } from './layoutFingerprint'
import { getVendorProfile, listVendorProfiles, upsertVendorProfile, type InboxCtx } from './inboxDb'

/**
 * Find the learned template for a document: exact layout-hash match first, then a
 * fuzzy Jaccard match over the stored token sets (so a vendor whose letterhead
 * text shifts slightly still resolves to the same template instead of missing).
 */
export const matchVendorProfile = async (ctx: InboxCtx, matchKey: string, tokens: string[] = [], minSim = 0.6) => {
  const exact = await getVendorProfile(ctx, matchKey)
  if (exact) return exact
  if (!tokens.length) return null
  try {
    let best: any = null, bestScore = 0
    for (const p of await listVendorProfiles(ctx)) {
      const pt: string[] = p?.field_hints?.__tokens || []
      if (!pt.length) continue
      const s = layoutSimilarity(tokens, pt)
      if (s > bestScore) { bestScore = s; best = p }
    }
    return bestScore >= minSim ? best : null
  } catch { return null }
}

/** Concatenate the text whose item-centers fall inside a normalized region box. */
const readRegion = (items: TextItem[] | undefined, box: FieldBox): string => {
  if (!items?.length || box?.nx == null) return ''
  const hits = items.filter(it => {
    if (it.nx == null) return false
    const cx = it.nx + (it.nw || 0) / 2, cy = (it.ny ?? 0) + (it.nh || 0) / 2
    return cx >= (box.nx as number) && cx <= (box.nx as number) + (box.nw || 0) &&
           cy >= (box.ny as number) && cy <= (box.ny as number) + (box.nh || 0)
  })
  hits.sort((a, b) => Math.abs((a.ny ?? 0) - (b.ny ?? 0)) > 0.012 ? (a.ny ?? 0) - (b.ny ?? 0) : (a.nx ?? 0) - (b.nx ?? 0))
  return hits.map(h => h.text).join(' ').replace(/\s+/g, ' ').trim()
}

/** Parse + set a learned field value (read from its region) with high confidence. */
const setLearned = (fields: any, confidence: any, bbox: any, key: string, val: string, box: FieldBox) => {
  if (!val) return
  const C = 0.9
  if (key === 'documentNo') { fields.documentNo = val; confidence.documentNo = C }
  else if (key === 'issueDate') { const d = parseDate(val); if (!d) return; fields.issueDate = d; confidence.issueDate = C }
  else if (key === 'dueDate') { const d = parseDate(val); if (!d) return; fields.dueDate = d; confidence.dueDate = C }
  else if (key === 'grandTotal') { const n = parseAmount(val); if (n == null) return; fields.grandTotal = n; confidence.grandTotal = C }
  else if (key === 'lineTotal') { const n = parseAmount(val); if (n == null) return; fields.lineTotal = n; fields.taxBasisTotal = n; confidence.lineTotal = C }
  else if (key === 'taxTotal') { const n = parseAmount(val); if (n == null) return; fields.taxTotal = n; confidence.taxTotal = C }
  else if (key === 'seller.vatId') { fields.seller = { ...(fields.seller || {}), vatId: val.replace(/\s+/g, '') }; confidence['seller.vatId'] = C }
  else if (key === 'seller.name') { fields.seller = { ...(fields.seller || {}), name: val }; confidence['seller.name'] = C }
  else if (key === 'paymentIban') { fields.paymentIban = val.replace(/\s+/g, ''); confidence.paymentIban = C }
  else return
  bbox[key] = box
}

const LEGAL_SUFFIXES = /\b(gmbh|ag|ug|kg|ohg|gbr|mbh|e\.?\s?k|co\.?\s?kg|ltd|inc|llc|s\.?a|b\.?v|e\.?\s?v|co|company|haftungsbeschränkt)\b/gi

export const normalizeVendorName = (name?: string): string =>
  String(name || '')
    .toLowerCase()
    .replace(/&/g, ' und ')
    .replace(LEGAL_SUFFIXES, ' ')
    .replace(/[^a-z0-9äöüß ]/g, ' ')
    .replace(/\s+/g, ' ')
    .trim()

const normVat = (v?: string) => String(v || '').toUpperCase().replace(/[^A-Z0-9]/g, '')
const normIban = (v?: string) => String(v || '').toUpperCase().replace(/[^A-Z0-9]/g, '')

/** Compute the vendor fingerprint from extracted data. Returns '' when nothing usable. */
export const fingerprintFromData = (data: Partial<ZugferdInvoiceData>): string => {
  const vat = normVat(data?.seller?.vatId)
  if (vat) return `vat:${vat}`
  const iban = normIban(data?.paymentIban)
  if (iban) return `iban:${iban}`
  const name = normalizeVendorName(data?.seller?.name)
  if (name) return `name:${name}`
  return ''
}

/**
 * Apply a stored profile to a fresh extraction: fill defaults the heuristics
 * couldn't get, and boost confidence for fields whose value lands near the
 * remembered box. Mutates a copy and returns it.
 */
export const applyProfile = (extraction: ExtractionResult, profile: any, items?: TextItem[]): ExtractionResult => {
  if (!profile) return extraction
  const fields = { ...extraction.fields }
  const confidence = { ...extraction.confidence }
  const bbox = { ...(extraction.bbox || {}) }
  const hints = profile.field_hints || {}

  // Defaults: only used downstream (charge/tax/doctype) — surfaced via _profile.
  ;(fields as any)._profile = {
    cBpartnerId: profile.c_bpartner_id || null,
    vendorName: profile.vendor_name || null,
    currencyId: profile.currency_id || null,
    defaultChargeId: profile.default_charge_id || null,
    defaultTaxId: profile.default_tax_id || null,
    defaultDoctypeId: profile.default_doctype_id || null,
    isTaxIncluded: !!profile.is_tax_included,
    samplesCount: profile.samples_count || 0
  }

  if (profile.is_tax_included != null && fields.isTaxIncluded === undefined) {
    ;(fields as any).isTaxIncluded = !!profile.is_tax_included
    confidence['isTaxIncluded'] = Math.max(confidence['isTaxIncluded'] || 0, 0.7)
  }

  for (const key of Object.keys(hints)) {
    const hint = hints[key]
    if (!hint?.bbox) continue
    // 1) READ the value straight from the learned region (the big payoff — a
    //    field the user once corrected is auto-filled from the right spot).
    if (items?.length && hint.bbox.nx != null) {
      const val = readRegion(items, hint.bbox)
      if (val) setLearned(fields, confidence, bbox, key, val, hint.bbox)
    }
    // 2) Otherwise, at least boost confidence when the value sits near the box.
    const box: FieldBox | undefined = extraction.bbox?.[key]
    if (box && nearBox(hint.bbox, box)) confidence[key] = Math.min(1, (confidence[key] || 0.5) + 0.25)
  }

  return { fields, confidence, bbox }
}

// Compare in page-normalized coords (0..1) when available — works across renders
// and matches the editable overlay's coordinates; fall back to raw points.
const nearBox = (a: FieldBox, b: FieldBox): boolean => {
  if (a?.nx != null && b?.nx != null) {
    return Math.abs((a.nx || 0) - (b.nx || 0)) <= 0.04 && Math.abs((a.ny || 0) - (b.ny || 0)) <= 0.04
  }
  return Math.abs((a?.x || 0) - (b?.x || 0)) <= 40 && Math.abs((a?.y || 0) - (b?.y || 0)) <= 40
}

/**
 * Learn from a confirmed invoice. `approved` carries the final (user-approved)
 * field values + the box map from extraction + the resolved iDempiere defaults.
 * Upserts the profile (samples_count++).
 */
export const learnFromConfirmation = async (ctx: InboxCtx, params: {
  fingerprint: string
  data: Partial<ZugferdInvoiceData>
  bbox?: Record<string, FieldBox>
  layoutTokens?: string[]
  cBpartnerId?: number | null
  currencyId?: number | null
  chargeId?: number | null
  taxId?: number | null
  doctypeId?: number | null
  isTaxIncluded?: boolean
}) => {
  if (!params.fingerprint) return
  const existing = await getVendorProfile(ctx, params.fingerprint)
  const hints: any = existing?.field_hints || {}

  // Store box + last sample for every mappable field (position learning).
  const learnKeys = ['documentNo', 'issueDate', 'dueDate', 'grandTotal', 'taxTotal', 'lineTotal',
    'seller.name', 'seller.vatId', 'paymentIban', 'email', 'address.street', 'address.postal', 'address.city']
  for (const key of learnKeys) {
    const box = params.bbox?.[key]
    const sample = key.includes('.') ? (params.data as any)?.seller?.[key.split('.')[1]] : (params.data as any)?.[key]
    if (box || sample != null) {
      hints[key] = { ...(hints[key] || {}), bbox: box || hints[key]?.bbox, sample: sample ?? hints[key]?.sample }
    }
  }
  // Layout token set for value-independent (Jaccard) matching of future invoices.
  if (params.layoutTokens?.length) hints.__tokens = params.layoutTokens

  await upsertVendorProfile(ctx, params.fingerprint, {
    cBpartnerId: params.cBpartnerId ?? null,
    vendorName: params.data?.seller?.name ?? null,
    vatId: params.data?.seller?.vatId ?? null,
    iban: params.data?.paymentIban ?? null,
    currencyId: params.currencyId ?? null,
    defaultChargeId: params.chargeId ?? null,
    defaultTaxId: params.taxId ?? null,
    defaultDoctypeId: params.doctypeId ?? null,
    isTaxIncluded: params.isTaxIncluded,
    fieldHints: hints
  })
}

export default fingerprintFromData
