import { mkdirSync } from "node:fs"
import path from "node:path"
import { AgentConfig, ScanJob, ScanResult } from "../types.js"
import { commandExists, runCommand } from "../commands.js"
import { hasOpenCvPostprocessRuntime, postprocessScan } from "./postprocess.js"
import { log } from "../logger.js"

const mimeTypes = {
    pdf: "application/pdf",
    png: "image/png",
    tiff: "image/tiff",
}

const stringSetting = (settings: Record<string, unknown> | undefined, key: string) => {
    const value = settings?.[key]
    return typeof value === "string" && value.trim() ? value.trim() : undefined
}

const numberSetting = (settings: Record<string, unknown> | undefined, key: string) => {
    const value = settings?.[key]
    if (typeof value === "number" && Number.isFinite(value)) return value
    if (typeof value === "string" && value.trim()) {
        const parsed = Number(value)
        if (Number.isFinite(parsed)) return parsed
    }

    return undefined
}

const booleanSetting = (settings: Record<string, unknown> | undefined, key: string, fallback: boolean) => {
    const value = settings?.[key]
    if (typeof value === "boolean") return value
    if (typeof value === "string") return ["1", "true", "yes", "ja", "on"].includes(value.trim().toLowerCase())
    return fallback
}

const profileSetting = (
    settings: Record<string, unknown> | undefined,
    fallback: AgentConfig["postprocessProfile"]
): AgentConfig["postprocessProfile"] => {
    const value = settings?.postprocessProfile
    if (value === "document" || value === "receipt" || value === "raw") return value
    return fallback
}

const ensureFilenameExtension = (filename: string, format: AgentConfig["scanFormat"]) => {
    const ext = path.extname(filename)
    if (!ext) return `${filename}.${format}`

    const expectedExt = `.${format}`
    if (ext.toLowerCase() === expectedExt) return filename
    return `${filename.slice(0, -ext.length)}${expectedExt}`
}

const fallbackRawResult = (scanOutputPath: string, jobId: string): ScanResult => ({
    path: scanOutputPath,
    filename: `${jobId}.raw.png`,
    mimeType: "image/png",
})

export const hasSane = () => commandExists("scanimage")

export const listScanners = async () => {
    if (!await hasSane()) return []

    const result = await runCommand("scanimage", ["-L"], { timeoutMs: 10_000 })
    if (result.code !== 0) return []

    return result.stdout
        .split(/\r?\n/)
        .map((line) => line.trim())
        .filter((line) => line.startsWith("device `"))
        .map((line) => line.match(/device `([^']+)'/)?.[1])
        .filter((device): device is string => Boolean(device))
}

export const runScan = async (config: AgentConfig, job: ScanJob): Promise<ScanResult> => {
    if (!await hasSane()) {
        throw new Error("scanimage ist nicht installiert oder nicht im PATH")
    }

    mkdirSync(config.workDir, { recursive: true })

    const settings = job.settings || {}
    const format = stringSetting(settings, "format") as AgentConfig["scanFormat"] | undefined || config.scanFormat
    const resolution = numberSetting(settings, "resolution") || config.scanResolution
    const mode = stringSetting(settings, "mode") || config.scanMode
    const source = stringSetting(settings, "source") || config.scanSource
    const scannerName = job.scannerName || config.scannerName
    const filename = ensureFilenameExtension(job.requestedFilename || `${job.id}.${format}`, format)
    const outputPath = path.join(config.workDir, filename)
    const shouldPostprocess = booleanSetting(settings, "postprocess", config.scanPostprocess)
    const postprocessProfile = profileSetting(settings, config.postprocessProfile)
    const scanFormat = shouldPostprocess ? "png" : format
    const scanOutputPath = shouldPostprocess
        ? path.join(config.workDir, `${job.id}.raw.png`)
        : outputPath

    const args = [
        "--format",
        scanFormat,
        "--resolution",
        String(resolution),
        "--mode",
        mode,
        "--output-file",
        scanOutputPath,
    ]

    if (source) args.push("--source", source)
    if (scannerName) args.push("--device-name", scannerName)

    const result = await runCommand("scanimage", args, { timeoutMs: 5 * 60 * 1000 })

    if (result.code !== 0) {
        throw new Error(result.stderr || `scanimage wurde mit Code ${result.code} beendet`)
    }

    if (shouldPostprocess) {
        if (!await hasOpenCvPostprocessRuntime(config)) {
            const message = "OpenCV-Nachbearbeitung ist aktiviert, aber python3 mit cv2, Pillow und numpy ist nicht verfügbar"
            if (config.postprocessStrict) throw new Error(message)

            log.warn(`${message}. Rohscan wird ohne Korrektur hochgeladen.`, {
                jobId: job.id,
                python: config.postprocessPython,
            })
            return fallbackRawResult(scanOutputPath, job.id)
        }

        try {
            return await postprocessScan(config, scanOutputPath, filename, format, postprocessProfile)
        } catch (error) {
            if (config.postprocessStrict) throw error

            log.warn("OpenCV-Nachbearbeitung fehlgeschlagen. Rohscan wird ohne Korrektur hochgeladen.", {
                jobId: job.id,
                error: error instanceof Error ? error.message : String(error),
            })
            return fallbackRawResult(scanOutputPath, job.id)
        }
    }

    return {
        path: outputPath,
        filename,
        mimeType: mimeTypes[format] || "application/octet-stream",
    }
}
