/**
 * Value-independent layout fingerprint — identifies a vendor's invoice TEMPLATE
 * regardless of the variable values (so a misread IBAN/amount can't break
 * recognition). Inspired by invoice2data's keyword approach (company name + tax
 * number + email/website are the strongest, most stable signals), extended with
 * PDF metadata (Producer/Creator) and page aspect. Page COUNT is deliberately
 * NOT used — the same vendor sends 1 or many pages.
 *
 * Returns a stable `hash` (exact-match key) + the `tokens` set (for Jaccard
 * similarity matching across minor variations).
 */
import crypto from 'node:crypto'
import { PDFDocument } from 'pdf-lib'

const STOP = new Set([
  'rechnung', 'invoice', 'gmbh', 'und', 'der', 'die', 'das', 'für', 'fuer', 'von', 'mit', 'bei',
  'the', 'and', 'for', 'inc', 'ltd', 'datum', 'seite', 'page', 'nummer', 'number', 'kunde', 'kunden'
])

export interface LayoutSignature { hash: string; tokens: string[] }

const sha1 = (s: string) => crypto.createHash('sha1').update(s).digest('hex').slice(0, 16)

/**
 * @param text       extracted plain text (page-1 letterhead matters most)
 * @param pdfBuffer  original PDF (for metadata + page geometry); optional for XML
 */
export const computeLayoutSignature = async (text: string, pdfBuffer?: Buffer | null): Promise<LayoutSignature> => {
  const tokens = new Set<string>()
  const t = text || ''

  // Strongest, most stable anchors: email domains, website, VAT id, IBAN.
  for (const m of t.matchAll(/[a-z0-9._%+-]+@([a-z0-9.-]+\.[a-z]{2,})/gi)) tokens.add('dom:' + m[1].toLowerCase())
  for (const m of t.matchAll(/\bwww\.([a-z0-9.-]+\.[a-z]{2,})/gi)) tokens.add('dom:' + m[1].toLowerCase())
  for (const m of t.matchAll(/\b([A-Z]{2}\d{8,12})\b/g)) tokens.add('vat:' + m[1])
  for (const m of t.matchAll(/\b([A-Z]{2}\d{2}(?:\s?[A-Z0-9]){11,30})\b/g)) tokens.add('iban:' + m[1].replace(/\s+/g, ''))

  // Letterhead words (first ~8 non-empty lines) — vendor name + address terms.
  const lines = t.split('\n').map(l => l.trim()).filter(Boolean).slice(0, 8)
  for (const ln of lines) {
    for (const w of ln.toLowerCase().split(/[^a-zäöüß0-9]+/)) {
      if (w.length >= 4 && !/^\d+$/.test(w) && !STOP.has(w)) tokens.add('w:' + w)
    }
  }

  // PDF metadata (vendor's invoicing software/template) + page aspect (NOT count).
  if (pdfBuffer) {
    try {
      const doc = await PDFDocument.load(pdfBuffer, { updateMetadata: false, throwOnInvalidObject: false })
      for (const v of [safe(() => doc.getProducer()), safe(() => doc.getCreator()), safe(() => doc.getAuthor()), safe(() => doc.getTitle())]) {
        if (v) for (const w of String(v).toLowerCase().split(/[^a-z0-9]+/)) if (w.length >= 4 && !STOP.has(w)) tokens.add('m:' + w)
      }
      const p0 = doc.getPages()[0]
      if (p0) { const { width, height } = p0.getSize(); if (width && height) tokens.add('ar:' + (width / height).toFixed(2)) }
    } catch {}
  }

  const arr = [...tokens].sort()
  // Hash a capped, most-distinctive subset for a stable exact key.
  const keyTokens = arr.filter(x => x.startsWith('dom:') || x.startsWith('vat:') || x.startsWith('iban:') || x.startsWith('m:') || x.startsWith('ar:'))
  const hashBasis = (keyTokens.length ? keyTokens : arr.slice(0, 20)).join('|')
  return { hash: arr.length ? sha1(hashBasis) : '', tokens: arr }
}

const safe = <T>(fn: () => T): T | null => { try { return fn() } catch { return null } }

/** Jaccard similarity over two token sets (0..1). */
export const layoutSimilarity = (a: string[] = [], b: string[] = []): number => {
  if (!a.length || !b.length) return 0
  const B = new Set(b)
  let inter = 0
  for (const x of new Set(a)) if (B.has(x)) inter++
  const uni = new Set([...a, ...b]).size
  return uni ? inter / uni : 0
}

export default computeLayoutSignature
