// CUPS media (label size) helpers.
//
// The label printers are CUPS queues on the same host as this app (PM2). Each
// queue's PPD carries a default PageSize (e.g. `Custom.3.5x1.7cm` for the
// 35×17 mm product labels on labelprinter-2, `w288h432` = 4×6 in on the
// shipping printers). `getQueueMediaMm()` reads that default via
// `lpoptions -p <queue> -l` so PDFs can be rendered at the real label size
// instead of relying on `-o fit-to-page` to shrink an oversized page.
//
// Fail-soft by design: on a dev Mac without CUPS queues, or when CUPS is
// unreachable, every call resolves to `null` (cached) and callers fall back to
// their legacy page formats.

import { promisify } from 'node:util'
import child_process from 'node:child_process'
import { readFile } from 'node:fs/promises'

export interface MediaMm { w: number; h: number }

const PT_TO_MM = 25.4 / 72
const SAFE_QUEUE = /^[A-Za-z0-9_.-]+$/
const CACHE_TTL = 10 * 60 * 1000

const NAMED_SIZES: Record<string, MediaMm> = {
  a3: { w: 297, h: 420 },
  a4: { w: 210, h: 297 },
  a5: { w: 148, h: 210 },
  a6: { w: 105, h: 148 },
  a7: { w: 74, h: 105 },
  a8: { w: 52, h: 74 },
  letter: { w: 215.9, h: 279.4 },
  legal: { w: 215.9, h: 355.6 }
}

const round1 = (n: number) => Math.round(n * 10) / 10

/**
 * Parses a CUPS/PPD PageSize token into millimetres.
 * Supported: `Custom.3.5x1.7cm`, `Custom.35x17mm`, `Custom.4x6in`,
 * `Custom.99x48` (points), `w288h432` (points), named sizes (A4…A8, Letter).
 * Returns null for anything else.
 */
export const parsePageSizeToken = (token: string): MediaMm | null => {
  const t = String(token || '').trim()
  if (!t) return null

  let m = /^Custom\.(\d+(?:\.\d+)?)x(\d+(?:\.\d+)?)(mm|cm|in|pt|m)?$/i.exec(t)
  if (m) {
    const a = Number(m[1]); const b = Number(m[2])
    const unit = (m[3] || 'pt').toLowerCase()
    const f = unit === 'mm' ? 1 : unit === 'cm' ? 10 : unit === 'in' ? 25.4 : unit === 'm' ? 1000 : PT_TO_MM
    const w = round1(a * f); const h = round1(b * f)
    return (w > 0 && h > 0) ? { w, h } : null
  }

  m = /^w(\d+(?:\.\d+)?)h(\d+(?:\.\d+)?)$/i.exec(t)
  if (m) {
    const w = round1(Number(m[1]) * PT_TO_MM); const h = round1(Number(m[2]) * PT_TO_MM)
    return (w > 0 && h > 0) ? { w, h } : null
  }

  const named = NAMED_SIZES[t.toLowerCase()]
  return named ? { ...named } : null
}

/** Extracts the starred (current) PageSize token from `lpoptions -l` output. */
export const parseLpoptionsPageSize = (output: string): MediaMm | null => {
  const lines = String(output || '').split(/\r?\n/)
  const line = lines.find(l => /^PageSize\//i.test(l)) || lines.find(l => /^media\//i.test(l))
  if (!line) return null
  const starred = /\*(\S+)/.exec(line.split(':').slice(1).join(':'))
  return starred ? parsePageSizeToken(starred[1]) : null
}

/** `PageSize=`/`media=` from a queue's `lpoptions -p <queue>` key=value output. */
export const parseLpoptionsQueueMedia = (output: string): MediaMm | null => {
  const m = /(?:^|\s)(?:media|PageSize)=(\S+)/.exec(String(output || ''))
  return m ? parsePageSizeToken(m[1]) : null
}

/** `*DefaultPageSize:` from a PPD file. */
export const parsePpdDefaultPageSize = (ppd: string): MediaMm | null => {
  const m = /^\*DefaultPageSize:\s*(\S+)/m.exec(String(ppd || ''))
  return m ? parsePageSizeToken(m[1]) : null
}

const cache = new Map<string, { value: MediaMm | null; ts: number }>()
const warned = new Set<string>()
const PPD_DIR = process.env.CUPS_PPD_DIR || '/etc/cups/ppd'

/**
 * Configured media of a CUPS queue in mm, cached 10 min per queue:
 *   1. queue-level option (`lpoptions -p <queue>` → `media=`/`PageSize=`)
 *   2. the queue's PPD `*DefaultPageSize:` (/etc/cups/ppd/<queue>.ppd —
 *      `lpoptions -l` only shows the generic `*Custom.WIDTHxHEIGHT` for custom
 *      label sizes, the real dims live in the PPD)
 *   3. starred token of `lpoptions -p <queue> -l`
 * Null when the queue is unknown, CUPS is unavailable or nothing parses.
 */
export const getQueueMediaMm = async (queue: string): Promise<MediaMm | null> => {
  const q = String(queue || '').trim()
  if (!q || !SAFE_QUEUE.test(q)) return null
  const hit = cache.get(q)
  if (hit && Date.now() - hit.ts < CACHE_TTL) return hit.value

  let value: MediaMm | null = null
  const exec = promisify(child_process.exec)
  const warn = (msg: string) => {
    if (warned.has(q)) return
    warned.add(q)
    console.warn(`[cupsMedia] ${q}: ${msg}`)
  }

  try {
    const { stdout } = await exec(`lpoptions -p ${q}`, { timeout: 3000 })
    value = parseLpoptionsQueueMedia(stdout)
  } catch (err: any) {
    warn(`lpoptions failed: ${err?.message || err}`)
  }

  if (!value) {
    try {
      const ppd = await readFile(`${PPD_DIR}/${q}.ppd`, 'utf8')
      value = parsePpdDefaultPageSize(ppd)
    } catch (err: any) {
      warn(`PPD not readable: ${err?.message || err}`)
    }
  }

  if (!value) {
    try {
      const { stdout } = await exec(`lpoptions -p ${q} -l`, { timeout: 3000 })
      value = parseLpoptionsPageSize(stdout)
    } catch { /* already warned above */ }
  }

  cache.set(q, { value, ts: Date.now() })
  return value
}

export const clearCupsMediaCache = () => { cache.clear() }

/** `-o media=Custom.<w>x<h>mm` — explicit page size for `lp`. */
export const mediaOption = (m: MediaMm): string => `-o media=Custom.${round1(m.w)}x${round1(m.h)}mm`
