/**
 * Shared server-side OCR core (tesseract.js, deu+eng).
 *
 * Extracted from server/api/mobile/ocr-scan.post.ts so both the mobile return
 * page and the incoming-invoice pipeline run the same code. Adds optional
 * WORD-LEVEL bounding boxes (needed by the per-vendor learning layer to remember
 * where each field sits on a scanned invoice).
 *
 * Reuses the same temp dir (.tmp-ocr/) and language cache (.tmp-ocr/lang) as the
 * original route so downloaded traineddata is shared, not re-fetched.
 */

import { createWorker } from 'tesseract.js'
import { writeFileSync, unlinkSync, existsSync, mkdirSync } from 'fs'
import { join } from 'path'

export interface OcrWord {
  text: string
  confidence: number
  bbox: { x0: number; y0: number; x1: number; y1: number }
}

export interface OcrResult {
  text: string
  confidence: number
  lines: string[]
  words: OcrWord[]
}

const DEFAULT_TIMEOUT_MS = 90_000
const TESS_LANG_REMOTE = 'https://tessdata.projectnaptha.com/4.0.0'
const TESS_CORE_VERSION = '7.0.0' // keep in sync with tesseract.js-core in package.json

/**
 * Resolve where tesseract.js loads its core .wasm from. The production `.output`
 * bundle can omit `tesseract.js-core/*.wasm`, so prefer the wasm in the real
 * node_modules (fully self-hosted), and fall back to the pinned CDN (same
 * approach the langPath above already uses) so OCR never dies on a packaging gap.
 */
const resolveCorePath = (): string => {
  const candidates = [
    join(process.cwd(), 'node_modules', 'tesseract.js-core'),
    join(process.cwd(), '.output', 'server', 'node_modules', 'tesseract.js-core')
  ]
  for (const dir of candidates) {
    if (existsSync(join(dir, 'tesseract-core.wasm'))) return dir
  }
  return `https://cdn.jsdelivr.net/npm/tesseract.js-core@${TESS_CORE_VERSION}`
}

const withTimeout = <T>(promise: Promise<T>, ms: number, label: string): Promise<T> =>
  new Promise<T>((resolve, reject) => {
    const timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms)
    promise.then(
      (v) => { clearTimeout(timer); resolve(v) },
      (e) => { clearTimeout(timer); reject(e) }
    )
  })

/** Flatten tesseract's block tree (when output.blocks=true) into a flat word list. */
const wordsFromData = (data: any): OcrWord[] => {
  const out: OcrWord[] = []
  const pushWord = (w: any) => {
    if (!w?.text) return
    const b = w.bbox || {}
    out.push({
      text: String(w.text),
      confidence: Number(w.confidence) || 0,
      bbox: { x0: b.x0 ?? 0, y0: b.y0 ?? 0, x1: b.x1 ?? 0, y1: b.y1 ?? 0 }
    })
  }
  // v6/v7 with { blocks: true }: data.blocks -> paragraphs -> lines -> words
  if (Array.isArray(data?.blocks)) {
    for (const block of data.blocks) {
      for (const para of block?.paragraphs || []) {
        for (const line of para?.lines || []) {
          for (const w of line?.words || []) pushWord(w)
        }
      }
    }
  }
  // Legacy fallback: flat data.words
  if (out.length === 0 && Array.isArray(data?.words)) {
    for (const w of data.words) pushWord(w)
  }
  return out
}

/**
 * Run OCR on an image (Buffer or file path). Returns text, lines, and — when
 * `withWords` is set — word-level boxes.
 */
export const runOcr = async (
  image: Buffer | string,
  opts: { lang?: string; timeoutMs?: number; withWords?: boolean } = {}
): Promise<OcrResult> => {
  const lang = opts.lang || 'deu+eng'
  const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS

  const tmpDir = join(process.cwd(), '.tmp-ocr')
  if (!existsSync(tmpDir)) mkdirSync(tmpDir, { recursive: true })
  const langCache = join(tmpDir, 'lang')
  if (!existsSync(langCache)) mkdirSync(langCache, { recursive: true })

  let tmpPath: string | null = null
  let imageInput: Buffer | string = image
  if (Buffer.isBuffer(image)) {
    const reqId = Date.now().toString(36) + Math.random().toString(36).slice(2, 6)
    tmpPath = join(tmpDir, `${reqId}.png`)
    writeFileSync(tmpPath, image)
    imageInput = tmpPath
  }

  const worker: any = await createWorker(lang, 1, {
    cachePath: langCache,
    langPath: TESS_LANG_REMOTE,
    corePath: resolveCorePath()
  } as any)

  try {
    const result: any = await withTimeout(
      worker.recognize(imageInput, {}, opts.withWords ? { blocks: true } : undefined),
      timeoutMs,
      'Tesseract.recognize'
    )
    const data = result?.data || {}
    const text: string = data.text || ''
    const lines = text
      .split('\n')
      .map((l: string) => l.trim())
      .filter((l: string) => l.length > 2)
    return {
      text,
      confidence: Number(data.confidence) || 0,
      lines,
      words: opts.withWords ? wordsFromData(data) : []
    }
  } finally {
    try { await worker.terminate() } catch {}
    if (tmpPath && existsSync(tmpPath)) { try { unlinkSync(tmpPath) } catch {} }
  }
}

export default runOcr
