/**
 * Tax rate of an incoming (AP) invoice as read from the DOCUMENT — shared by the
 * inbox list preview and the review screen.
 *
 * Only an e-invoice XML breakdown (tier 0) or a high-confidence provider result is
 * taken at face value. For text/OCR documents the AMOUNTS decide (gross vs net), because
 * the heuristic "first n % in the text" / a misread tax amount is unreliable — a stored
 * `taxes: [{ rate: 19 }]` next to gross == net must NOT claim "19 % detected".
 *
 * `certain` = the rate is backed by an authoritative breakdown or by amounts that agree
 * with each other. The review only WARNS about a differing tax pick when certain.
 */
export interface InboxTaxRate {
  rate: number | null
  certain: boolean
  /** EN16931 tax category of the dominant breakdown entry (S, AE, K, E, Z, O …), when known */
  category: string
  source: 'document' | 'amounts' | 'tax-amount' | 'text' | 'none'
}

const num = (v: any): number => (v === '' || v == null ? NaN : Number(v))
const round1 = (r: number) => Math.round(r * 10) / 10
const MAX_PLAUSIBLE_RATE = 30

export const deriveInboxTaxRate = (input: {
  extracted?: any
  confidence?: any
  sourceTier?: number | null
  /** editable overrides from the review form (fall back to the extracted values) */
  net?: any
  tax?: any
  gross?: any
}): InboxTaxRate => {
  const ex = input?.extracted || {}
  const conf = input?.confidence || {}
  const breakdown = (Array.isArray(ex.taxes) ? ex.taxes : []).filter((x: any) => Number.isFinite(Number(x?.rate)))
  const dominant = breakdown.length
    ? [...breakdown].sort((a: any, b: any) => (Math.abs(Number(b?.basis)) || 0) - (Math.abs(Number(a?.basis)) || 0))[0]
    : null
  const category = String(dominant?.category || '')

  // 1. authoritative breakdown (e-invoice XML / high-confidence provider)
  if (dominant && (Number(input?.sourceTier) === 0 || Number(conf.taxes) >= 0.9)) {
    return { rate: Number(dominant.rate), certain: true, category, source: 'document' }
  }

  const net = Math.abs(num(input?.net ?? ex.lineTotal ?? ex.taxBasisTotal))
  const gross = Math.abs(num(input?.gross ?? ex.grandTotal))
  const tax = Math.abs(num(input?.tax ?? ex.taxTotal))
  const taxPlausible = net > 0 && tax > 0 && tax / net <= MAX_PLAUSIBLE_RATE / 100

  // 2. amounts: gross vs net
  if (net > 0 && gross > 0) {
    if (Math.abs(gross - net) < 0.01) {
      // gross == net → no VAT on the document … unless a plausible tax amount says the
      // net was misread as gross; then the rate is only a guess.
      if (taxPlausible && gross - tax > 0) return { rate: round1((tax / (gross - tax)) * 100), certain: false, category, source: 'tax-amount' }
      return { rate: 0, certain: true, category, source: 'amounts' }
    }
    if (gross > net) {
      const r = round1(((gross / net) - 1) * 100)
      if (r <= MAX_PLAUSIBLE_RATE) return { rate: r, certain: true, category, source: 'amounts' }
    }
  }

  // 3. tax amount vs net
  if (taxPlausible) return { rate: round1((tax / net) * 100), certain: !(gross > 0), category, source: 'tax-amount' }

  // 4. rate read from the text / lines — a hint only
  if (dominant && Number(dominant.rate) <= MAX_PLAUSIBLE_RATE && Number(conf.taxes) >= 0.5) {
    return { rate: Number(dominant.rate), certain: false, category, source: 'text' }
  }
  const lineRate = (Array.isArray(ex.lines) ? ex.lines : []).map((l: any) => Number(l?.taxRate)).find((r: number) => Number.isFinite(r) && r > 0)
  if (lineRate != null) return { rate: lineRate, certain: false, category, source: 'text' }

  return { rate: null, certain: false, category, source: 'none' }
}

/** Tax amount the review should start with: the extracted one unless it contradicts gross − net. */
export const saneInboxTaxTotal = (ex: any): number | '' => {
  // amounts that can be reconciled (net + tax = gross) are authoritative
  const fixed = reconcileInboxAmounts(ex)
  if (fixed.consistent && fixed.tax != null) return fixed.tax
  const net = num(ex?.lineTotal ?? ex?.taxBasisTotal)
  const gross = num(ex?.grandTotal)
  const tax = num(ex?.taxTotal)
  if (!Number.isFinite(tax)) return Number.isFinite(net) && Number.isFinite(gross) ? Math.round((gross - net) * 100) / 100 : ''
  if (Number.isFinite(net) && Number.isFinite(gross) && Math.abs(net + tax - gross) > 0.05) {
    const implausible = Math.abs(tax) >= Math.abs(net) || (net !== 0 && Math.abs(tax / net) > MAX_PLAUSIBLE_RATE / 100)
    if (implausible) return Math.round((gross - net) * 100) / 100
  }
  return tax
}

export const formatInboxTaxRate = (rate: number | null | undefined): string =>
  rate == null ? '' : String(Math.round(rate * 10) / 10).replace('.', ',')

/* ------------------------------------------------------------------------------------------
 * Amount reconciliation + review confidence
 * ---------------------------------------------------------------------------------------- */

const money2 = (n: number) => Math.round(n * 100) / 100
const finite = (v: any): number | null => { const n = num(v); return Number.isFinite(n) ? n : null }
const plausibleRate = (net: number, tax: number) => {
  if (!(net > 0) || tax < 0) return false
  const r = (tax / net) * 100
  return r <= MAX_PLAUSIBLE_RATE && Math.abs(r - Math.round(r * 2) / 2) <= 0.2   // x % or x.5 %
}

export interface ReconciledAmounts {
  net: number | null; tax: number | null; gross: number | null
  consistent: boolean
  /** which of net / tax / gross were replaced to make the three agree */
  changed: Array<'net' | 'tax' | 'gross'>
}

/**
 * Net + tax must equal gross. A value read from a LEARNED position breaks that whenever the
 * vendor's totals block floats vertically with the number of line items (easybell: the "Netto"
 * box of a 3-line invoice sits on the VAT line of a 2-line invoice). Try the alternative
 * readings (label-based heuristic values, the tax breakdown) and, last, derive the odd one out.
 * `alt` = values of the label-based extraction before the learned regions were applied.
 */
export const reconcileInboxAmounts = (ex: any, alt: any = {}): ReconciledAmounts => {
  const cur = { net: finite(ex?.lineTotal ?? ex?.taxBasisTotal), tax: finite(ex?.taxTotal), gross: finite(ex?.grandTotal) }
  const fits = (n: number | null, t: number | null, g: number | null) => n != null && t != null && g != null && n > 0 && Math.abs(n + t - g) <= 0.02
  if (fits(cur.net, cur.tax, cur.gross)) return { ...cur, consistent: true, changed: [] }

  const tb = Array.isArray(ex?.taxes) ? ex.taxes[0] : null
  const atb = Array.isArray(alt?.taxes) ? alt.taxes[0] : null
  const uniq = (xs: any[]) => [...new Set(xs.map(finite).filter((x): x is number => x != null))]
  const nets = uniq([cur.net, alt?.lineTotal, alt?.taxBasisTotal, tb?.basis, atb?.basis])
  const taxes = uniq([cur.tax, alt?.taxTotal, tb?.amount, atb?.amount])
  const grosses = uniq([cur.gross, alt?.grandTotal])
  const diff = (n: number, t: number, g: number) => (['net', 'tax', 'gross'] as const).filter((k, i) => [n, t, g][i] !== [cur.net, cur.tax, cur.gross][i])

  // fewest replacements first
  let best: ReconciledAmounts | null = null
  for (const g of grosses) for (const n of nets) for (const t of taxes) {
    if (!fits(n, t, g) || !plausibleRate(n, t)) continue
    const changed = diff(n, t, g)
    if (!best || changed.length < best.changed.length) best = { net: n, tax: t, gross: g, consistent: true, changed: [...changed] }
  }
  if (best) return best

  // derive the odd one out from the two others when that yields a sane VAT rate
  const { net, tax, gross } = cur
  if (gross != null && tax != null && gross > tax && plausibleRate(gross - tax, tax)) return { net: money2(gross - tax), tax, gross, consistent: true, changed: ['net'] }
  if (gross != null && net != null && gross >= net && plausibleRate(net, gross - net)) return { net, tax: money2(gross - net), gross, consistent: true, changed: ['tax'] }
  if (net != null && tax != null && gross == null && plausibleRate(net, tax)) return { net, tax, gross: money2(net + tax), consistent: true, changed: ['gross'] }
  return { ...cur, consistent: false, changed: [] }
}

/**
 * Review confidence in % — what the inbox badge shows. Only the fields a reviewer actually
 * checks count (weighted); internal helper keys with fixed heuristic confidences (currency 0.6,
 * taxBasisTotal 0.5, taxes, isTaxIncluded 0.7) used to drag a perfectly read document to ~85 %.
 * A missing key field counts as 0; amounts that add up are trusted, amounts that don't are not.
 */
export const inboxConfidenceScore = (extracted: any, confidence: any): number | null => {
  const conf = confidence || {}
  if (!extracted || !Object.keys(conf).length) return null
  const ex = extracted
  const c = (key: string, present: boolean) => present ? Math.max(0, Math.min(1, Number(conf[key] ?? 0.5))) : 0
  const amounts = reconcileInboxAmounts(ex)
  const amt = (key: string, present: boolean, role: 'net' | 'tax' | 'gross') => {
    if (!present) return 0
    if (!amounts.consistent) return Math.min(c(key, true), 0.4)
    return amounts.changed.includes(role) ? 0.8 : Math.max(c(key, true), 0.97)
  }
  const vendor = Math.max(c('seller.name', !!ex.seller?.name), c('seller.vatId', !!ex.seller?.vatId), c('paymentIban', !!ex.paymentIban))
  const parts: Array<[number, number]> = [
    [2, c('documentNo', !!ex.documentNo)],
    [1.5, c('issueDate', !!ex.issueDate)],
    [2, vendor],
    [2, amt('grandTotal', ex.grandTotal != null, 'gross')],
    [1, amt('lineTotal', (ex.lineTotal ?? ex.taxBasisTotal) != null, 'net')],
    [1, amt('taxTotal', ex.taxTotal != null || amounts.consistent, 'tax')]
  ]
  const total = parts.reduce((s, [w]) => s + w, 0)
  return Math.round((parts.reduce((s, [w, v]) => s + w * v, 0) / total) * 100)
}

