/**
 * Heuristic field extraction for German/EU supplier invoices (Tier 1 text +
 * Tier 2 OCR). Pure functions — no I/O. Returns a partial ZugferdInvoiceData plus
 * per-field confidence and (best-effort) bounding boxes.
 *
 * A vendor profile, when supplied, is applied FIRST (its learned anchors/boxes
 * win and carry high confidence); these generic anchors are the cold-start
 * fallback for unknown vendors.
 */

import type { ZugferdInvoiceData } from '../zugferd/buildInvoiceXml'
import type { TextItem } from './pdfTextLayer'

export interface FieldBox {
  x: number; y: number; w: number; h: number
  /** 1-based page + page-normalized (0..1, top-left) box for the UI overlay. */
  page?: number; nx?: number; ny?: number; nw?: number; nh?: number
}

export interface ExtractionResult {
  fields: Partial<ZugferdInvoiceData>
  confidence: Record<string, number>
  bbox: Record<string, FieldBox>
}

/* ------------------------------- parsing ------------------------------- */

/** Parse a German/Intl money token ("1.234,56" | "1,234.56" | "1234,56" | "1234.56"). */
export const parseAmount = (raw: string): number | null => {
  if (!raw) return null
  let s = String(raw).replace(/[^0-9.,-]/g, '')
  if (!s) return null
  const hasDot = s.includes('.')
  const hasComma = s.includes(',')
  if (hasDot && hasComma) {
    // last separator is the decimal one
    if (s.lastIndexOf(',') > s.lastIndexOf('.')) s = s.replace(/\./g, '').replace(',', '.')
    else s = s.replace(/,/g, '')
  } else if (hasComma) {
    // comma decimal (German) if 1-2 trailing digits, else thousands sep
    const m = s.match(/,(\d{1,2})$/)
    s = m ? s.replace(/\./g, '').replace(',', '.') : s.replace(/,/g, '')
  }
  const n = Number(s)
  return Number.isFinite(n) ? n : null
}

/** Parse a date string to ISO yyyy-mm-dd; supports dd.mm.yyyy, dd/mm/yyyy, yyyy-mm-dd, dd. Monat yyyy. */
export const parseDate = (raw: string): string => {
  if (!raw) return ''
  const s = String(raw).trim()
  let m = s.match(/(\d{4})-(\d{2})-(\d{2})/)
  if (m) return `${m[1]}-${m[2]}-${m[3]}`
  m = s.match(/(\d{1,2})[.\/-](\d{1,2})[.\/-](\d{2,4})/)
  if (m) {
    let [, d, mo, y] = m
    if (y.length === 2) y = (Number(y) > 70 ? '19' : '20') + y
    return `${y}-${mo.padStart(2, '0')}-${d.padStart(2, '0')}`
  }
  const months: Record<string, string> = {
    januar: '01', jan: '01', februar: '02', feb: '02', märz: '03', maerz: '03', mar: '03',
    april: '04', apr: '04', mai: '05', juni: '06', jun: '06', juli: '07', jul: '07',
    august: '08', aug: '08', september: '09', sep: '09', oktober: '10', okt: '10', oct: '10',
    november: '11', nov: '11', dezember: '12', dez: '12', dec: '12'
  }
  m = s.match(/(\d{1,2})\.?\s+([A-Za-zäöü]+)\s+(\d{4})/)
  if (m && months[m[2].toLowerCase()]) {
    return `${m[3]}-${months[m[2].toLowerCase()]}-${m[1].padStart(2, '0')}`
  }
  return ''
}

const AMOUNT_RE = '([0-9][0-9.,]*[0-9]|[0-9])'
const DATE_RE = '(\\d{4}-\\d{2}-\\d{2}|\\d{1,2}[.\\/-]\\d{1,2}[.\\/-]\\d{2,4}|\\d{1,2}\\.?\\s+[A-Za-zäöü]+\\s+\\d{4})'

/** Find a labeled value: <label> ... <captureRegex> on the same or next ~40 chars. */
const matchLabeled = (text: string, labels: string[], captureRe: string): string | null => {
  for (const label of labels) {
    const re = new RegExp(`${label}\\s*[:#\\-]?\\s*${captureRe}`, 'i')
    const m = text.match(re)
    if (m && m[1]) return m[1].trim()
  }
  return null
}

/* ------------------------------ locators ------------------------------ */

const findBox = (value: string, items?: TextItem[]): FieldBox | undefined => {
  if (!items || !value) return undefined
  const needle = value.trim().slice(0, 24)
  const hit = items.find(it => it.text && it.text.includes(needle))
  if (!hit) return undefined
  return { x: hit.x, y: hit.y, w: hit.w, h: hit.h, page: hit.page, nx: hit.nx, ny: hit.ny, nw: hit.nw, nh: hit.nh }
}

const CURRENCY_MAP: Array<[RegExp, string]> = [
  [/€|\bEUR\b|\bEURO?\b/i, 'EUR'],
  [/\$|\bUSD\b/i, 'USD'],
  [/£|\bGBP\b/i, 'GBP'],
  [/\bCHF\b|\bSFr\b/i, 'CHF']
]

/* ------------------------------ extract ------------------------------- */

export const extractFields = (text: string, items?: TextItem[]): ExtractionResult => {
  const fields: Partial<ZugferdInvoiceData> = {}
  const confidence: Record<string, number> = {}
  const bbox: Record<string, FieldBox> = {}
  const set = (key: string, value: any, conf: number, boxValue?: string) => {
    ;(fields as any)[key] = value
    confidence[key] = conf
    const b = findBox(boxValue ?? String(value), items)
    if (b) bbox[key] = b
  }

  const flat = text.replace(/\r/g, '')

  // Document number
  const docNo = matchLabeled(flat,
    ['Rechnungs?-?\\s?(?:nr|nummer|no)\\.?', 'Invoice\\s*(?:no|number|#)', 'Beleg-?\\s?(?:nr|nummer)', 'Rechnung'],
    '([A-Za-z0-9][A-Za-z0-9\\-_/.]{2,30})')
  if (docNo) set('documentNo', docNo, 0.8)

  // Invoice date
  const dateRaw = matchLabeled(flat, ['Rechnungsdatum', 'Belegdatum', 'Invoice\\s*date', 'Datum', 'Date'], DATE_RE)
  if (dateRaw) { const iso = parseDate(dateRaw); if (iso) set('issueDate', iso, 0.8, dateRaw) }

  // Due date
  const dueRaw = matchLabeled(flat, ['Fälligkeitsdatum', 'fällig\\s*(?:am|bis)?', 'Due\\s*date', 'Zahlbar\\s*bis'], DATE_RE)
  if (dueRaw) { const iso = parseDate(dueRaw); if (iso) set('dueDate', iso, 0.7, dueRaw) }

  // Currency
  for (const [re, code] of CURRENCY_MAP) { if (re.test(flat)) { set('currency', code, 0.6); break } }
  if (!fields.currency) set('currency', 'EUR', 0.3)

  // Grand total (gross / amount due)
  const grossRaw = matchLabeled(flat,
    ['Gesamtbetrag', 'Rechnungsbetrag', 'Zu\\s*zahlen(?:der\\s*Betrag)?', 'Gesamtsumme', 'Bruttobetrag', 'Brutto',
     'Total\\s*(?:amount)?', 'Amount\\s*due', 'Grand\\s*total'],
    AMOUNT_RE)
  if (grossRaw != null) { const a = parseAmount(grossRaw); if (a != null) set('grandTotal', a, 0.7, grossRaw) }

  // Net / line total
  const netRaw = matchLabeled(flat,
    ['Nettobetrag', 'Netto', 'Zwischensumme', 'Gesamt\\s*netto', 'Net\\s*(?:amount|total)?', 'Subtotal'],
    AMOUNT_RE)
  if (netRaw != null) { const a = parseAmount(netRaw); if (a != null) { set('lineTotal', a, 0.6, netRaw); set('taxBasisTotal', a, 0.5) } }

  // Tax (rate + amount)
  const taxAmtRaw = matchLabeled(flat,
    ['(?:zzgl\\.?\\s*)?(?:MwSt|USt|Umsatzsteuer|Mehrwertsteuer)\\.?\\s*(?:\\d{1,2}[.,]?\\d?\\s*%)?', 'VAT', 'Tax', 'Steuer'],
    AMOUNT_RE)
  const rateMatch = flat.match(/\b(\d{1,2}(?:[.,]\d)?)\s*%/)
  const rate = rateMatch ? parseAmount(rateMatch[1]) : null
  const taxAmt = taxAmtRaw != null ? parseAmount(taxAmtRaw) : null
  if (taxAmt != null) set('taxTotal', taxAmt, 0.55, taxAmtRaw || undefined)
  if (rate != null || taxAmt != null) {
    fields.taxes = [{
      rate: rate ?? 19,
      amount: taxAmt ?? 0,
      basis: (fields.taxBasisTotal as number) ?? 0,
      category: (rate ?? 19) > 0 ? 'S' : 'Z'
    }]
    confidence['taxes'] = rate != null ? 0.55 : 0.3
  }

  // VAT id (seller)
  const vat = flat.match(/\b([A-Z]{2}\d{8,12})\b/) || flat.match(/USt-?IdNr\.?\s*[:]?\s*([A-Z]{2}\d{8,12})/i)
  // IBAN
  const ibanM = flat.match(/\b([A-Z]{2}\d{2}(?:\s?[A-Z0-9]){11,30})\b/)
  const iban = ibanM ? ibanM[1].replace(/\s+/g, '') : null

  // Seller name — topmost line carrying a legal-form suffix, else first non-trivial line.
  const lines = flat.split('\n').map(l => l.trim()).filter(l => l.length > 2)
  const suffixRe = /\b(GmbH(?:\s*&\s*Co\.?\s*KG)?|AG|UG(?:\s*\(haftungsbeschränkt\))?|KG|OHG|GbR|e\.?K\.?|mbH|Ltd\.?|Inc\.?|S\.?A\.?|B\.?V\.?|e\.?V\.?)\b/i
  let sellerName = lines.find(l => suffixRe.test(l) && l.length <= 60) || lines[0] || ''
  sellerName = sellerName.replace(/\s{2,}/g, ' ').trim()

  const seller: any = {}
  if (sellerName) { seller.name = sellerName; confidence['seller.name'] = suffixRe.test(sellerName) ? 0.6 : 0.35; const b = findBox(sellerName, items); if (b) bbox['seller.name'] = b }
  if (vat) { seller.vatId = vat[1]; confidence['seller.vatId'] = 0.85; const b = findBox(vat[1], items); if (b) bbox['seller.vatId'] = b }
  if (Object.keys(seller).length) fields.seller = seller
  if (iban) { fields.paymentIban = iban; confidence['paymentIban'] = 0.85; const b = findBox(ibanM![1], items); if (b) bbox['paymentIban'] = b }

  // Tax-included heuristic: explicit "inkl. MwSt" / "incl. VAT", or gross≈net+tax.
  if (/inkl\.?\s*(?:MwSt|USt)|incl\.?\s*VAT|Bruttopreise/i.test(flat)) {
    fields.isTaxIncluded = true as any; confidence['isTaxIncluded'] = 0.6
  } else if (/zzgl\.?\s*(?:MwSt|USt)|excl\.?\s*VAT|Nettopreise/i.test(flat)) {
    fields.isTaxIncluded = false as any; confidence['isTaxIncluded'] = 0.6
  }

  return { fields, confidence, bbox }
}

export default extractFields
