/**
 * Permit cost — what the issuing state charged for the permit (Nash, 2026-09-24).
 *
 * "This is not the permitting company's service charge. This is not a
 * credit-card/processing fee. This is not a routing fee. This is specifically
 * the permit fee paid to the issuing state/agency as shown on the permit
 * itself."
 *
 * Pure: reading a fee out of extracted permit text, deciding how much to
 * trust it, who may see it, and what a trip's fees add up to. The I/O lives
 * in src/lib/data/permit-cost.ts.
 */
import type { Permit, TripRole } from '@/types/db'

export type PermitCostStatus = 'found' | 'not_found' | 'needs_review' | 'manually_entered' | 'corrected'
export type PermitCostConfidence = 'high' | 'medium' | 'low'
export type PermitCostSource = 'extraction' | 'manual' | 'api'

/* ------------------------------------------------------------- wording */

/** §24: the words to use, and the ones to avoid because they imply charges we do not calculate. */
export const PERMIT_COST_LABEL = 'State Permit Fee'
export const TOTAL_COMPLETE_LABEL = 'Total Permit Fees'
export const TOTAL_INCOMPLETE_LABEL = 'Known Permit Fees'
export const PERMIT_COST_DISCLAIMER =
  'State permit fees shown on uploaded permits only. Does not include permitting-company service fees, payment-processing charges, routing fees, or other third-party charges.'
export const PERMIT_COST_NOT_FOUND = '—'
export const PERMIT_COST_NOT_FOUND_HINT = 'Permit fee was not found on the uploaded permit.'
export const PERMIT_COST_REVIEW_HINT =
  'Several amounts appear on this permit and none is clearly the state fee. Check the permit and enter the amount.'

/* ---------------------------------------------------------- visibility */

/**
 * §13/§14: pilots never see permit cost, however much trip access they have.
 * The API strips the fields too — this is not only a front-end rule.
 */
export function canSeePermitCost(role: TripRole | string | null | undefined): boolean {
  if (!role) return false
  return ['broker', 'dispatcher', 'driver', 'admin'].includes(role)
}

/** Who may correct a cost: the trip's broker or dispatcher, and internal staff. A driver sees but does not edit. */
export function canEditPermitCost(params: { role: TripRole | string | null | undefined; isInternal: boolean }): boolean {
  if (params.isInternal) return true
  return params.role === 'broker' || params.role === 'dispatcher'
}

export const PERMIT_COST_FIELDS = [
  'permit_cost_amount', 'permit_cost_currency', 'permit_cost_status', 'permit_cost_confidence', 'permit_cost_source',
  'permit_cost_extracted_text', 'permit_cost_extracted_amount', 'permit_cost_manual_override', 'permit_cost_verified',
  'permit_cost_updated_by', 'permit_cost_updated_by_user_id', 'permit_cost_last_updated_at', 'cost_counts_toward_total',
] as const

/** Remove every cost field from a permit before it leaves the server for a role that may not see it. */
export function stripPermitCost<T extends object>(permit: T): T {
  const out = { ...permit } as Record<string, unknown>
  for (const f of PERMIT_COST_FIELDS) delete out[f]
  return out as T
}

export function stripPermitCostFor<T extends object>(role: TripRole | string | null | undefined, permits: T[]): T[] {
  return canSeePermitCost(role) ? permits : permits.map(stripPermitCost)
}

/* ---------------------------------------------------------- extraction */

/**
 * Labels that introduce the state's charge. Ordered: the more specific the
 * label, the more we trust it.
 */
export const STRONG_FEE_LABELS = ['permit fee', 'permit cost', 'state fee', 'permit amount', 'fee paid', 'total fee', 'total fees', 'fees paid', 'permit fees']
export const WEAK_FEE_LABELS = ['amount paid', 'total amount', 'payment amount', 'amount due', 'total due', 'total', 'amount', 'fee', 'cost']

/**
 * Labels that are money on a permit but never the permit's own fee — §2:
 * "do not blindly assume every dollar amount on the document is the permit
 * price."
 */
export const EXCLUDED_LABELS = [
  'bond', 'surety', 'fine', 'penalty', 'insurance', 'liability', 'coverage', 'deductible', 'escort fee', 'escort',
  'service fee', 'service charge', 'processing fee', 'convenience fee', 'credit card', 'card fee', 'transaction fee',
  'routing fee', 'route fee', 'agent fee', 'broker fee', 'gvw', 'gross weight', 'weight', 'valuation', 'cargo value', 'deposit', 'refund', 'balance',
]

export interface FeeCandidate {
  amount: number
  label: string
  /** The text the amount was read from, kept for audit. */
  text: string
  strength: 'strong' | 'weak'
  excluded: boolean
}

const MONEY = /\$\s?([0-9][0-9,]*(?:\.[0-9]{1,2})?)|([0-9][0-9,]*\.[0-9]{2})\s?(?:USD|dollars)?/gi

function toAmount(raw: string): number | null {
  const n = Number(raw.replace(/,/g, ''))
  return Number.isFinite(n) && n >= 0 ? Math.round(n * 100) / 100 : null
}

/**
 * The words that name an amount: what sits before it ON ITS OWN LINE. Scoping
 * to the line matters — "Credit card fee $3.50 / Permit Fee: $92.00" must not
 * let the card fee poison the permit fee on the next line.
 */
function labelBefore(text: string, index: number): string {
  const lineStart = text.lastIndexOf('\n', index - 1) + 1
  const from = Math.max(lineStart, index - 80)
  return text.slice(from, index).toLowerCase().replace(/\s+/g, ' ').trim()
}

/**
 * Every money amount in the text, with the label that introduces it. A number
 * only counts as money when it carries a currency marker or sits under a
 * money label — "Height 14.08" is not a fee.
 */
export function feeCandidates(text: string): FeeCandidate[] {
  if (!text) return []
  const out: FeeCandidate[] = []
  const re = new RegExp(MONEY.source, 'gi')
  let m: RegExpExecArray | null
  while ((m = re.exec(text)) !== null) {
    const amount = toAmount(m[1] ?? m[2] ?? '')
    if (amount === null) continue
    const matched = m[0].trim()
    const hasCurrencyMark = /[$]|usd|dollar/i.test(matched)
    const label = labelBefore(text, m.index)
    const excluded = EXCLUDED_LABELS.some((w) => label.includes(w))
    const strong = STRONG_FEE_LABELS.some((w) => label.includes(w))
    const weak = WEAK_FEE_LABELS.some((w) => label.includes(w))
    // No currency marker and no money label: a measurement, not a fee.
    if (!hasCurrencyMark && !strong && !weak) continue
    if (excluded && !strong) continue // named as something that is never the permit's own fee
    if (!strong && !weak && !hasCurrencyMark) continue
    out.push({ amount, label: label.slice(-40), text: matched, strength: strong ? 'strong' : 'weak', excluded: excluded && !strong })
  }
  return out
}

export interface PermitCostReading {
  amount: number | null
  currency: string
  status: PermitCostStatus
  confidence: PermitCostConfidence | null
  extractedText: string | null
  candidates: FeeCandidate[]
}

/**
 * Read the state fee out of a permit's text (§2, §3). A clear, uniquely
 * labelled fee is stored with high confidence. Several plausible amounts and
 * no clear winner is `needs_review` — "do not guess".
 */
export function readPermitCost(text: string | null | undefined, currency = 'USD'): PermitCostReading {
  const candidates = feeCandidates(text ?? '')
  const usable = candidates.filter((c) => !c.excluded)
  const none: PermitCostReading = { amount: null, currency, status: 'not_found', confidence: null, extractedText: null, candidates }
  if (usable.length === 0) return none

  const strong = usable.filter((c) => c.strength === 'strong')
  if (strong.length > 0) {
    const amounts = [...new Set(strong.map((c) => c.amount))]
    if (amounts.length === 1) {
      return { amount: amounts[0], currency, status: 'found', confidence: 'high', extractedText: strong[0].text, candidates }
    }
    // Several different strongly-labelled amounts: a "total fee" that covers
    // the others is the permit's fee; otherwise a human decides.
    const totals = strong.filter((c) => /total/.test(c.label))
    if (totals.length === 1) return { amount: totals[0].amount, currency, status: 'found', confidence: 'medium', extractedText: totals[0].text, candidates }
    return { amount: null, currency, status: 'needs_review', confidence: 'low', extractedText: null, candidates }
  }

  const amounts = [...new Set(usable.map((c) => c.amount))]
  if (amounts.length === 1) return { amount: amounts[0], currency, status: 'found', confidence: 'medium', extractedText: usable[0].text, candidates }
  return { amount: null, currency, status: 'needs_review', confidence: 'low', extractedText: null, candidates }
}

/** An explicit fee from a trusted source (the extractor's own field, or the Synchron API). */
export function readSuppliedCost(value: unknown, currency = 'USD'): PermitCostReading | null {
  const n = typeof value === 'number' ? value : typeof value === 'string' ? toAmount(value.replace(/[$\s]/g, '')) : null
  if (n === null || !Number.isFinite(n) || n < 0) return null
  return { amount: n, currency, status: 'found', confidence: 'high', extractedText: typeof value === 'string' ? value : null, candidates: [] }
}

/* ------------------------------------------------------------ display */

export type PermitCostLike = Partial<Pick<Permit,
  'permit_cost_amount' | 'permit_cost_currency' | 'permit_cost_status' | 'permit_cost_confidence' | 'permit_cost_manual_override' | 'cost_counts_toward_total'>>

export function formatMoney(amount: number, currency = 'USD'): string {
  return new Intl.NumberFormat('en-US', { style: 'currency', currency: currency || 'USD' }).format(amount)
}

/** The amount to show: a manual correction always wins over the extracted one (§4). */
export function effectiveCost(p: PermitCostLike): number | null {
  const manual = p.permit_cost_manual_override
  if (manual !== null && manual !== undefined) return Number(manual)
  const amount = p.permit_cost_amount
  return amount === null || amount === undefined ? null : Number(amount)
}

/** §7: never "$0.00" for a missing fee — that would read as "this permit was free". */
export function displayPermitCost(p: PermitCostLike): { text: string; hint: string | null; needsReview: boolean } {
  if (p.permit_cost_status === 'needs_review') return { text: 'Needs review', hint: PERMIT_COST_REVIEW_HINT, needsReview: true }
  const amount = effectiveCost(p)
  if (amount === null) return { text: PERMIT_COST_NOT_FOUND, hint: PERMIT_COST_NOT_FOUND_HINT, needsReview: false }
  return { text: formatMoney(amount, p.permit_cost_currency ?? 'USD'), hint: null, needsReview: false }
}

/* -------------------------------------------------------------- total */

export interface TripPermitFees {
  /** Sum of the fees we are confident about. */
  total: number
  currency: string
  /** Permits whose fee counted toward the total. */
  counted: number
  /** Permits with no reliable fee. */
  missing: number
  /** Revisions and duplicates that were deliberately not counted (§11). */
  notCounted: number
  complete: boolean
  label: string
  /** Shown under an incomplete total. */
  warning: string | null
  lines: Array<{ permitId: string; state: string; text: string; counted: boolean }>
}

type TotalPermit = PermitCostLike & Pick<Permit, 'id' | 'state_code'> & { permit_number?: string | null; supersedes_permit_id?: string | null; created_at?: string }

/**
 * §11: a trip may hold an original, a revision, a replacement and a duplicate
 * upload of the same permit. Only one of each real permit contributes a fee.
 * A permit that another permit supersedes drops out, and so does a later
 * upload carrying the same state and permit number.
 */
export function countableCostPermits<T extends TotalPermit>(permits: T[]): { countable: T[]; superseded: T[] } {
  const supersededIds = new Set(permits.map((p) => p.supersedes_permit_id).filter(Boolean) as string[])
  const seen = new Map<string, T>()
  const superseded: T[] = []
  for (const p of permits) {
    if (supersededIds.has(p.id) || p.cost_counts_toward_total === false) { superseded.push(p); continue }
    const number = (p.permit_number ?? '').trim().toUpperCase()
    // Without a permit number a permit cannot be matched to another, so it stands alone.
    const key = number ? `${(p.state_code ?? '').toUpperCase()}::${number}` : `id::${p.id}`
    const existing = seen.get(key)
    if (!existing) { seen.set(key, p); continue }
    // Keep the newest copy of the same permit; the other is a duplicate upload.
    const newer = (p.created_at ?? '') >= (existing.created_at ?? '') ? p : existing
    const older = newer === p ? existing : p
    seen.set(key, newer)
    superseded.push(older)
  }
  return { countable: [...seen.values()], superseded }
}

/** §8, §10: the trip's fees, and an honest label when some are unknown. */
export function tripPermitFees<T extends TotalPermit>(permits: T[]): TripPermitFees {
  const { countable, superseded } = countableCostPermits(permits)
  const currency = countable.find((p) => p.permit_cost_currency)?.permit_cost_currency ?? 'USD'
  let total = 0
  let counted = 0
  let missing = 0
  const lines: TripPermitFees['lines'] = []
  for (const p of countable) {
    const shown = displayPermitCost(p)
    const amount = p.permit_cost_status === 'needs_review' ? null : effectiveCost(p)
    if (amount === null) missing++
    else { total += amount; counted++ }
    lines.push({ permitId: p.id, state: p.state_code ?? '', text: shown.text, counted: amount !== null })
  }
  const complete = missing === 0 && counted > 0
  return {
    total: Math.round(total * 100) / 100,
    currency,
    counted,
    missing,
    notCounted: superseded.length,
    complete,
    label: complete ? TOTAL_COMPLETE_LABEL : TOTAL_INCOMPLETE_LABEL,
    warning: missing > 0
      ? `Permit fee was not detected for ${missing} permit${missing === 1 ? '' : 's'}. The actual trip permit total may be higher.`
      : null,
    lines,
  }
}

/** One sentence for the AI, for a role allowed to hear it (§15). */
export function permitFeesForAi(fees: TripPermitFees): string {
  if (fees.counted === 0) return 'No state permit fee has been detected on the permits for this trip yet.'
  const head = `${fees.label}: ${formatMoney(fees.total, fees.currency)} across ${fees.counted} permit${fees.counted === 1 ? '' : 's'}.`
  return `${head}${fees.warning ? ` ${fees.warning}` : ''} ${PERMIT_COST_DISCLAIMER}`
}

/**
 * §14/§15 belt and braces: a pilot must never receive a permit fee, even if
 * the AI backend ignores `permit_cost_visible`. Any currency amount in an
 * answer to a pilot is replaced rather than passed through.
 */
export function redactCostsForPilot(answer: string): string {
  return answer.replace(/\$\s?[0-9][0-9,]*(?:\.[0-9]{1,2})?/g, '[amount hidden]')
}

/** True when an answer still carries money — used to log a backend that ignored the flag. */
export function mentionsMoney(answer: string): boolean {
  return /\$\s?[0-9]/.test(answer)
}
