/**
 * printDispatch — the ONE place a PDF is handed to a printer.
 *
 *   printFile(event, { queue, options, filePath, source })  → PrintResult
 *   printFiles(event, [...])                                 → PrintResult[] (shared batchId)
 *   stagePrintFile(pathName, fileName, buffer)               → absolute path under the spool dir
 *
 * Routing is decided PER PRINTER (never per org/tenant): the queue name is looked up in the
 * instance-local binding table (printSpoolDb.findBindingForQueue, keyed by CUST_Printer.CupsName).
 *   no binding → driver 'local'  : `lp -d <queue> …` on this host, exactly what the routes did
 *                                  before (our own prod keeps running unchanged);
 *   binding    → driver 'agent'  : the job is spooled for the tenant's LogShip Print Agent
 *                                  (server/api/print-agent/*), which runs the identical lp
 *                                  command inside its LAN. Response status 'queued' + jobId.
 *
 * Security: the former routes interpolated request values into a shell string. Here the
 * queue must match SAFE_QUEUE, the option string is tokenised and whitelisted, and the local
 * driver uses execFile (no shell at all).
 *
 * The result is never thrown: `ok:false` + `error` so every caller keeps its own response shape.
 */

import { execFile } from 'node:child_process'
import { existsSync, mkdirSync } from 'node:fs'
import { writeFile, stat } from 'node:fs/promises'
import path from 'node:path'
import getTokenHelper from './getTokenHelper'
import { enqueueJobs, findBindingForQueue, hasAnyBindings, isAgentOnline, newBatchId, type NewPrintJob } from './printSpoolDb'

export interface PrintJobSpec {
  /** CUPS queue name; '' = the host's CUPS default printer (local driver only) */
  queue: string
  /** CUPS option string, e.g. '-o fit-to-page -o media=A4' */
  options?: string
  /** absolute path of the staged PDF */
  filePath: string
  /** route name for the job log, e.g. 'print/labelprint' */
  source: string
  batchId?: string | null
}

export interface PrintResult {
  ok: boolean
  status: 'printed' | 'queued' | 'failed'
  driver: 'local' | 'agent'
  queue: string
  options: string
  stdout: string
  stderr: string
  error?: string
  /** CUPS "request id is <queue>-<n>" (local driver) */
  requestId?: string
  jobId?: number
  batchId?: string | null
  agentId?: number
  agentName?: string
  agentOnline?: boolean
}

export const SAFE_QUEUE = /^[A-Za-z0-9_.-]{1,64}$/
const SAFE_OPTION_VALUE = /^[A-Za-z0-9=_.,:+\-]{1,80}$/
const SAFE_FLAG = /^-(o|n|t|H|P|q)$/

const DEFAULT_STORAGE = '/root/storage'

/** Spool/staging root: runtimeConfig.printStorageDir (PRINT_STORAGE_DIR / NUXT_PRINT_STORAGE_DIR). */
export const printStorageDir = (): string => {
  try {
    const cfg: any = useRuntimeConfig()
    const dir = String(cfg?.printStorageDir || '').trim()
    if (dir) return dir
  } catch { /* outside a Nitro context */ }
  return process.env.PRINT_STORAGE_DIR || DEFAULT_STORAGE
}

export const sanitizePathName = (p: any, fallback = 'print') =>
  String(p ?? '').replace(/\\/g, '/').replace(/\.\.+/g, '_').replace(/[^A-Za-z0-9/_.-]/g, '_').replace(/^\/+/, '').replace(/\/+$/, '') || fallback

export const sanitizeFileName = (f: any, fallback = 'document.pdf') =>
  String(f ?? '').replace(/[^A-Za-z0-9_.-]/g, '_').replace(/^\.+/, '') || fallback

/** Writes the PDF under the storage root and returns its absolute path. */
export const stagePrintFile = async (pathName: any, fileName: any, content: Buffer): Promise<string> => {
  const dir = path.join(printStorageDir(), sanitizePathName(pathName))
  if (!existsSync(dir)) mkdirSync(dir, { recursive: true })
  const full = path.join(dir, sanitizeFileName(fileName))
  await writeFile(full, content)
  return full
}

/**
 * Tokenises a CUPS option string and rejects anything that is not `-o key[=value]`,
 * `-n <copies>`, `-t <title>` etc. Throws on an unsafe token.
 */
export const parsePrintOptions = (options: any): string[] => {
  const tokens = String(options ?? '').trim().split(/\s+/).filter(Boolean)
  const out: string[] = []
  for (let i = 0; i < tokens.length; i++) {
    const t = tokens[i] as string
    if (SAFE_FLAG.test(t)) {
      const v = tokens[i + 1] as string | undefined
      if (!v || !SAFE_OPTION_VALUE.test(v)) throw new Error(`invalid print option value after ${t}`)
      if (t === '-n' && !/^\d{1,3}$/.test(v)) throw new Error('invalid copy count')
      out.push(t, v)
      i++
      continue
    }
    // "-ofit-to-page" style (flag glued to its value)
    if (/^-o[A-Za-z]/.test(t) && SAFE_OPTION_VALUE.test(t.slice(2))) { out.push('-o', t.slice(2)); continue }
    throw new Error(`invalid print option token: ${t}`)
  }
  return out
}

const sessionIds = async (event: any): Promise<{ userId: number | null; roleId: number | null; orgId: number }> => {
  const orgId = Number(getCookie(event, 'logship_organization_id') || 0) || 0
  let userId: number | null = Number(getCookie(event, 'logship_user_id') || 0) || null
  let roleId: number | null = Number(getCookie(event, 'logship_role_id') || 0) || null
  if (!userId || !roleId) {
    try {
      const token = await getTokenHelper(event)
      const payload = JSON.parse(Buffer.from(String(token).split('.')[1], 'base64').toString())
      userId = userId || Number(payload?.AD_User_ID) || null
      roleId = roleId || Number(payload?.AD_Role_ID) || null
    } catch { /* anonymous (service) caller */ }
  }
  return { userId, roleId, orgId }
}

const runLp = (queue: string, optionTokens: string[], filePath: string): Promise<{ stdout: string; stderr: string }> =>
  new Promise((resolve, reject) => {
    const args = [...(queue ? ['-d', queue] : []), ...optionTokens, filePath]
    execFile('lp', args, { timeout: 60_000, maxBuffer: 1024 * 1024, encoding: 'utf8' }, (err: any, stdout: string, stderr: string) => {
      if (err) {
        err.stdout = String(stdout || '')
        err.stderr = String(stderr || '')
        return reject(err)
      }
      resolve({ stdout: String(stdout || ''), stderr: String(stderr || '') })
    })
  })

const parseRequestId = (stdout: string) => /request id is (\S+)/i.exec(stdout || '')?.[1]

const failed = (spec: PrintJobSpec, driver: 'local' | 'agent', error: string, extra: Partial<PrintResult> = {}): PrintResult => ({
  ok: false, status: 'failed', driver, queue: spec.queue, options: String(spec.options ?? '').trim(), stdout: '', stderr: '', error, ...extra
})

/**
 * Prints several files as one batch. Local-driver items run sequentially (CUPS keeps the
 * order), agent-driver items are inserted in ONE transaction with a shared batchId.
 */
export const printFiles = async (event: any, specs: PrintJobSpec[]): Promise<PrintResult[]> => {
  const results: PrintResult[] = new Array(specs.length)
  const batchId = specs.length > 1 ? newBatchId() : null
  const agentItems: { idx: number; job: NewPrintJob; spec: PrintJobSpec; agent: any }[] = []
  const anyBindings = hasAnyBindings()
  const ids = anyBindings ? await sessionIds(event) : { userId: null, roleId: null, orgId: 0 }

  for (let idx = 0; idx < specs.length; idx++) {
    const spec = specs[idx] as PrintJobSpec
    const queue = String(spec.queue ?? '').trim()
    if (queue && !SAFE_QUEUE.test(queue)) { results[idx] = failed(spec, 'local', `invalid printer queue name: ${queue}`); continue }
    let optionTokens: string[]
    try { optionTokens = parsePrintOptions(spec.options) } catch (e: any) { results[idx] = failed(spec, 'local', e?.message || 'invalid print options'); continue }
    if (!spec.filePath || !existsSync(spec.filePath)) { results[idx] = failed(spec, 'local', 'print file missing: ' + spec.filePath); continue }

    const hit = (anyBindings && queue) ? findBindingForQueue(queue, ids.orgId) : null
    if (hit) {
      let size = 0
      try { size = (await stat(spec.filePath)).size } catch { /* keep 0 */ }
      agentItems.push({
        idx, spec, agent: hit.agent,
        job: {
          agentId: hit.agent.id, queue, options: optionTokens.join(' '), filePath: spec.filePath,
          fileName: path.basename(spec.filePath), fileSize: size, source: spec.source || 'print',
          batchId: spec.batchId ?? batchId, createdByUser: ids.userId, createdByRole: ids.roleId
        }
      })
      continue
    }

    // local driver — today's behaviour
    try {
      const { stdout, stderr } = await runLp(queue, optionTokens, spec.filePath)
      results[idx] = {
        ok: true, status: 'printed', driver: 'local', queue, options: optionTokens.join(' '),
        stdout, stderr, requestId: parseRequestId(stdout), batchId: spec.batchId ?? batchId
      }
    } catch (err: any) {
      results[idx] = failed(spec, 'local', err?.stderr || err?.message || 'lp failed', { stdout: err?.stdout || '', stderr: err?.stderr || '' })
    }
  }

  if (agentItems.length) {
    try {
      const jobIds = enqueueJobs(agentItems.map(a => a.job))
      agentItems.forEach((a, i) => {
        const jobId = jobIds[i] as number
        results[a.idx] = {
          ok: true, status: 'queued', driver: 'agent', queue: a.job.queue, options: a.job.options,
          stdout: `queued as print job ${jobId} for agent ${a.agent.name}`, stderr: '',
          jobId, batchId: a.job.batchId ?? null, agentId: a.agent.id, agentName: a.agent.name, agentOnline: isAgentOnline(a.agent)
        }
      })
      console.log(`[printDispatch] queued ${jobIds.length} job(s) → agent(s) ${[...new Set(agentItems.map(a => a.agent.name))].join(', ')}${batchId ? ' batch ' + batchId : ''}`)
    } catch (err: any) {
      agentItems.forEach(a => { results[a.idx] = failed(a.spec, 'agent', 'spool insert failed: ' + (err?.message || err)) })
    }
  }

  return results
}

export const printFile = async (event: any, spec: PrintJobSpec): Promise<PrintResult> => (await printFiles(event, [spec]))[0]

/**
 * Merges a PrintResult into the legacy response object of the old routes
 * (`message: 'success'|'failed'`, stdout, stderr) and adds the new fields.
 */
export const applyPrintResult = (data: any, r: PrintResult) => {
  data.message = r.ok ? 'success' : 'failed'
  data.stdout = r.stdout
  data.stderr = r.stderr
  data.printDriver = r.driver
  data.printStatus = r.status
  data.printer = data.printer ?? r.queue
  if (r.options) data.printOptions = r.options
  if (r.jobId) data.jobId = r.jobId
  if (r.batchId) data.batchId = r.batchId
  if (r.agentName) data.agentName = r.agentName
  if (r.driver === 'agent') data.agentOnline = r.agentOnline
  if (r.error) data.error = r.error
  return data
}
