/**
 * Parse a downloaded Amazon Ads (Sponsored Ads) invoice PDF into an AdsInvoice.
 *
 * The console PDFs ("INVOICE-<no>.pdf") are Chromium-rendered with a clean text
 * layer, so no OCR is needed: the pdfjs text items (via the inbox's
 * extractTextLayer) are regrouped into layout lines by their y position and read
 * with label regexes. German and English templates are supported (the account
 * language decides which one the console renders).
 *
 * Header layout: three columns (Von | Zu | Informationen zur Rechnung), so the
 * header block is split by x into columns before reading issuer / payer.
 * Campaign table: the campaign NAME wraps over several lines around the row that
 * carries type · clicks · CPC · amount — name fragments are assigned to the
 * nearest row by y.
 */
import { extractTextLayer, type TextItem, type TextPage } from '../inbox/pdfTextLayer'
import type { AdsInvoice, AdsInvoiceLine } from './types'
import { round2 } from './types'

export interface Line { y: number; x: number; text: string; items: TextItem[] }

/** Group text items of a page into visual lines (top → bottom, left → right). */
export const layoutLines = (page: TextPage, tolerance = 3): Line[] => {
  const sorted = [...page.items].sort((a, b) => (b.y - a.y) || (a.x - b.x))
  const lines: Line[] = []
  for (const it of sorted) {
    const last = lines[lines.length - 1]
    if (last && Math.abs(last.y - it.y) <= tolerance) { last.items.push(it); continue }
    lines.push({ y: it.y, x: it.x, text: '', items: [it] })
  }
  for (const l of lines) {
    l.items.sort((a, b) => a.x - b.x)
    let text = ''
    let prevEnd: number | null = null
    for (const it of l.items) {
      const gap = prevEnd == null ? 0 : it.x - prevEnd
      if (text && gap > 1.5) text += gap > 12 ? '   ' : ' '
      text += it.text
      prevEnd = it.x + (it.w || 0)
    }
    l.text = text.replace(/\s+/g, ' ').trim()
    l.x = l.items[0]?.x ?? 0
  }
  return lines.filter(l => l.text)
}

const MONTHS: Record<string, string> = { jan: '01', feb: '02', mar: '03', mär: '03', maerz: '03', apr: '04', may: '05', mai: '05', jun: '06', jul: '07', aug: '08', sep: '09', oct: '10', okt: '10', nov: '11', dec: '12', dez: '12' }

/** dd-mm-yyyy · dd.mm.yyyy · yyyy-mm-dd · "31 Jan 2026" · "Jan 31, 2026" → YYYY-MM-DD */
export const parseAdsDate = (s: any): string | null => {
  if (!s) return null
  const str = String(s).trim()
  let m = str.match(/(\d{1,2})[-.\/](\d{1,2})[-.\/](\d{4})/)
  if (m) return `${m[3]}-${m[2].padStart(2, '0')}-${m[1].padStart(2, '0')}`
  m = str.match(/(\d{4})-(\d{2})-(\d{2})/)
  if (m) return `${m[1]}-${m[2]}-${m[3]}`
  m = str.match(/(\d{1,2})\.?\s+([A-Za-zäÄ]{3,})\.?\s+(\d{4})/)
  if (m) { const mo = MONTHS[m[2].slice(0, 3).toLowerCase()]; if (mo) return `${m[3]}-${mo}-${m[1].padStart(2, '0')}` }
  m = str.match(/([A-Za-z]{3,})\.?\s+(\d{1,2}),?\s+(\d{4})/)
  if (m) { const mo = MONTHS[m[1].slice(0, 3).toLowerCase()]; if (mo) return `${m[3]}-${mo}-${m[2].padStart(2, '0')}` }
  return null
}

/** "1.234,56" · "1,234.56" · "19,27" · "19.27" → number */
export const parseAdsAmount = (s: any): number | null => {
  if (s == null) return null
  let str = String(s).replace(/[^\d.,-]/g, '')
  if (!str) return null
  const lastComma = str.lastIndexOf(','), lastDot = str.lastIndexOf('.')
  if (lastComma > -1 && lastDot > -1) {
    if (lastComma > lastDot) str = str.replace(/\./g, '').replace(',', '.')
    else str = str.replace(/,/g, '')
  } else if (lastComma > -1) {
    // "19,27" (de decimal) vs "1,234" (en thousands) — a comma followed by exactly 3 digits at the end is ambiguous; treat as decimal only when 1-2 digits follow
    const after = str.length - lastComma - 1
    str = after === 3 && !/,\d{3}$/.test(str) ? str.replace(/,/g, '') : (after === 3 ? str.replace(/,/g, '') : str.replace(',', '.'))
  }
  const n = Number(str)
  return Number.isFinite(n) ? n : null
}

const AMOUNT_RE = '(?:[A-Z]{3}\\s*|€\\s*)?(-?\\d{1,3}(?:[.,]\\d{3})*(?:[.,]\\d{2})|-?\\d+(?:[.,]\\d{2}))(?:\\s*(?:[A-Z]{3}|€))?'
const PROGRAM_RE = '(SPONSORED\\s+(?:PRODUCTS?|BRANDS?|DISPLAY(?:\\s+FOR\\s+FIRE\\s+TV)?|TV)|AMAZON\\s+LIVE|CREATOR\\s+CONNECTIONS|AMAZON\\s+DSP)'

const findAfter = (lines: Line[], labelRe: RegExp): string | null => {
  for (const l of lines) {
    const m = l.text.match(labelRe)
    if (m) { const rest = l.text.slice((m.index || 0) + m[0].length).trim(); if (rest) return rest }
  }
  return null
}

const amountAfter = (lines: Line[], labelRe: RegExp): number | null => {
  for (const l of lines) {
    const m = l.text.match(labelRe)
    if (!m) continue
    const rest = l.text.slice((m.index || 0) + m[0].length)
    const am = rest.match(new RegExp(AMOUNT_RE))
    if (am) return parseAdsAmount(am[1])
  }
  return null
}

export const parseAmazonAdsInvoicePdf = async (pdf: Buffer, fileName?: string): Promise<AdsInvoice | null> => {
  const layer = await extractTextLayer(pdf)
  if (!layer.hasText || !layer.pages.length) return null

  const pageLines = layer.pages.map(p => layoutLines(p))
  const all: Line[] = pageLines.flat()
  const first = pageLines[0]
  const pageW = layer.pages[0].width || 595

  /* ---- invoice number ---- */
  let invoiceNo: string | null = null
  for (const l of all) {
    const m = l.text.match(/(?:Nummer der Rechnung|Rechnungsnummer|Invoice (?:Number|No\.?|#)|Número de factura)\s*:?\s*([A-Z0-9-]{6,})/i)
    if (m) { invoiceNo = m[1]; break }
  }
  if (!invoiceNo) {
    const m = (fileName || '').match(/([0-9]{5,}[A-Z0-9]{4,})/)
    if (m) invoiceNo = m[1]
  }
  if (!invoiceNo) {
    // label and value on separate lines (right-aligned header block): take the next token-only line
    const idx = all.findIndex(l => /Nummer der Rechnung|Invoice Number/i.test(l.text))
    if (idx > -1) { const cand = all.slice(idx + 1, idx + 3).map(l => l.text.trim()).find(t => /^[A-Z0-9-]{6,}$/.test(t)); if (cand) invoiceNo = cand }
  }
  if (!invoiceNo) return null

  /* ---- dates / currency ---- */
  const invoiceDate = parseAdsDate(findAfter(all, /(?:Datum der Rechnung|Rechnungsdatum|Invoice Date|Fecha de (?:la )?factura)\s*:?/i))
  const periodRaw = findAfter(all, /(?:Rechnungszeitraum|Abrechnungszeitraum|Billing Period|Invoice Period|Periodo de facturación)\s*:?/i) || ''
  const periodDates = periodRaw.match(/\d{1,2}[-.\/]\d{1,2}[-.\/]\d{4}|\d{4}-\d{2}-\d{2}|\d{1,2}\s+[A-Za-zäÄ]{3,}\.?\s+\d{4}/g) || []
  const periodFrom = parseAdsDate(periodDates[0]) || null
  const periodTo = parseAdsDate(periodDates[1]) || periodFrom
  const currency = (findAfter(all, /(?:Währung der Rechnung|Rechnungswährung|Invoice Currency|Moneda de la factura)\s*:?/i) || '').match(/[A-Z]{3}/)?.[0]
    || (all.map(l => l.text).join(' ').match(/\b(EUR|USD|GBP|PLN|SEK)\b/)?.[1]) || 'EUR'

  /* ---- totals ---- */
  const net = amountAfter(all, /(?:Zwischensumme|Subtotal|Sub-?total|Kampagnengebühren insgesamt|Total campaign charges)\s*:?/i)
  let tax: number | null = null
  let taxRate: number | null = null
  for (const l of all) {
    const m = l.text.match(/\b(?:VAT|MwSt\.?|USt\.?|IVA|Tax)\s*\((\d{1,2}(?:[.,]\d+)?)\s*%\)[^\d-]*?(?:-\s*[A-Z ]+)?\s*(?:[A-Z]{3}\s*|€\s*)?(-?\d[\d.,]*)/i)
    if (m) { taxRate = Number(m[1].replace(',', '.')); tax = parseAdsAmount(m[2]); break }
  }
  if (tax == null) tax = amountAfter(all, /(?:Steuerzwischensumme|Steuerzwische|Tax Subtotal|Total tax)\s*:?/i)
  const gross = amountAfter(all, /(?:Fälliger Gesamtbetrag|Fälliger Rechnungsbetrag|Gesamtbetrag|Total Amount Due|Amount Due|Total due|Importe total)\s*:?/i)

  /* ---- payment method ---- */
  const fullText = all.map(l => l.text).join('\n')
  let paymentMethod: string | null = null
  if (/Zahlbar durch den Verkäufer|Seller Payable|Automatischer Abzug|Deduct(?:ed)? from (?:your )?(?:payment|disbursement)|Verkäuferkonto abgezogen/i.test(fullText)) paymentMethod = 'DEDUCT_FROM_PAYMENT'
  else if (/Kreditkarte|Credit Card/i.test(fullText)) paymentMethod = 'CREDIT_CARD'
  else if (/Lastschrift|Direct Debit/i.test(fullText)) paymentMethod = 'DIRECT_DEBIT'
  else if (/Überweisung|Wire|Funds Transfer/i.test(fullText)) paymentMethod = 'ELECTRONIC_FUNDS_TRANSFER'

  /* ---- header columns: issuer (Von/From) · payer (Zu/To) ---- */
  const summaryIdx = first.findIndex(l => /Zusammenfassung|Invoice Summary|Summary/i.test(l.text))
  const headerLines = first.slice(0, summaryIdx > 0 ? summaryIdx : Math.min(first.length, 14))
  // measured on the console PDF: Von at x≈51, Zu at x≈187, Informationen at x≈323 (page 596 pt)
  const colOf = (x: number) => x < pageW * 0.29 ? 0 : x < pageW * 0.53 ? 1 : 2
  const cols: string[][] = [[], [], []]
  for (const l of headerLines) {
    const byCol: Record<number, string[]> = {}
    for (const it of l.items) { const c = colOf(it.x); (byCol[c] ||= []).push(it.text) }
    for (const c of [0, 1, 2]) if (byCol[c]) cols[c].push(byCol[c].join(' ').replace(/\s+/g, ' ').trim())
  }
  const readParty = (rows: string[]) => {
    const clean = rows.filter(r => r && !/^(Von|From|Zu|To|An|De|Para)$/i.test(r))
    const vat = clean.map(r => r.match(/\b([A-Z]{2}\s?[0-9A-Z]{8,12})\b/) ).find(Boolean)?.[1]?.replace(/\s/g, '') || undefined
    const name = clean.find(r => !/Steuernummer|Tax|VAT|USt|Nummer der Rechnung|Invoice/i.test(r)) || undefined
    const addr = clean.filter(r => r !== name && !/Steuernummer|Tax|VAT|USt/i.test(r))
    const cityLine = addr.find(r => /\b[A-Z]{2}\s+\d{4,5}\b/.test(r)) || ''
    const cm = cityLine.match(/^(.*?)\s*\b([A-Z]{2})\s+(\d{4,5})\b/)
    return { name, vatId: vat, address1: addr.filter(r => r !== cityLine).join(' ').trim() || undefined,
      city: cm?.[1]?.trim() || undefined, countryCode: cm?.[2] || undefined, postal: cm?.[3] || undefined }
  }
  const issuer = readParty(cols[0])
  const payer = readParty(cols[1])
  const countryCode = issuer.countryCode || (/GERMANY|Deutschland/i.test(fullText) ? 'DE' : null)

  /* ---- campaign lines ---- */
  const lines: AdsInvoiceLine[] = []
  const rowRe = new RegExp(`^(.*?)\\s*${PROGRAM_RE}\\s+(\\d[\\d.,]*)\\s+${AMOUNT_RE}\\s+${AMOUNT_RE}\\s*$`, 'i')
  for (const page of pageLines) {
    const headIdx = page.findIndex(l => /(Kampagne|Campaign)\b.*(Kampagnentyp|Campaign Type|Type)/i.test(l.text))
    if (headIdx < 0) continue
    const header = page[headIdx]
    const typeX = header.items.find(it => /Kampagnentyp|Type/i.test(it.text))?.x ?? pageW * 0.3
    const endIdx = page.findIndex((l, i) => i > headIdx && /(Kampagnengebühren insgesamt|Total campaign charges|Häufig gestellte|Frequently asked)/i.test(l.text) && !new RegExp(PROGRAM_RE, 'i').test(l.text))
    const body = page.slice(headIdx + 1, endIdx > 0 ? endIdx : page.length)
    const rows: { y: number; line: AdsInvoiceLine; nameParts: { y: number; text: string }[] }[] = []
    const nameFrags: { y: number; text: string }[] = []
    for (const l of body) {
      const m = l.text.match(rowRe)
      if (m) {
        const nameOnRow = l.items.filter(it => it.x < typeX - 2).map(it => it.text).join(' ').trim()
        rows.push({ y: l.y, nameParts: nameOnRow ? [{ y: l.y, text: nameOnRow }] : [], line: {
          campaignName: '', program: m[2].replace(/\s+/g, ' ').toUpperCase(), costEventType: 'CLICKS',
          costEventCount: Number(String(m[3]).replace(/[.,]/g, '')) || 0,
          costPerUnit: parseAdsAmount(m[4]), amount: parseAdsAmount(m[5]) ?? 0 } })
      } else {
        const frag = l.items.filter(it => it.x < typeX - 2).map(it => it.text).join(' ').trim()
        if (frag && !/insgesamt|total/i.test(frag)) nameFrags.push({ y: l.y, text: frag })
      }
    }
    for (const f of nameFrags) {
      let best: any = null, bestD = Infinity
      for (const r of rows) { const d = Math.abs(r.y - f.y); if (d < bestD) { bestD = d; best = r } }
      if (best && bestD < 40) best.nameParts.push(f)
    }
    for (const r of rows) {
      r.line.campaignName = r.nameParts.sort((a, b) => b.y - a.y).map(p => p.text).join(' ').replace(/\s+/g, ' ').trim() || 'Kampagne'
      lines.push(r.line)
    }
  }

  const lineSum = round2(lines.reduce((s, l) => s + (l.amount || 0), 0))
  const netFinal = net ?? (lines.length ? lineSum : null)
  const grossFinal = gross ?? (netFinal != null && tax != null ? round2(netFinal + tax) : null)
  if (netFinal == null || grossFinal == null) return null
  const taxFinal = tax ?? round2(grossFinal - netFinal)

  return {
    invoiceNo,
    invoiceDate: invoiceDate || periodTo || new Date().toISOString().slice(0, 10),
    periodFrom, periodTo,
    currency,
    net: round2(netFinal), tax: round2(taxFinal), gross: round2(grossFinal),
    taxRate: taxRate ?? (netFinal > 0 ? Math.round((taxFinal / netFinal) * 1000) / 10 : null),
    paymentMethod,
    status: 'PAID_IN_FULL',
    countryCode,
    issuer, payer,
    lines,
    source: 'pdf',
    documentAvailable: true,
    fileName: fileName || `INVOICE-${invoiceNo}.pdf`
  }
}

export default parseAmazonAdsInvoicePdf
