/**
 * Local Model Manager
 *
 * Manages local GGUF models and the llama-server process.
 * Handles: download, list, start, stop, status.
 */
import { execSync, spawn } from "node:child_process";
import { existsSync, mkdirSync, readdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
import { DEFAULT_LOCAL_CONFIG } from "./types.js";
const PID_FILE = "llama-server.pid";
const MODELS_MANIFEST = "models.json";
export class LocalModelManager {
    constructor(config) {
        this.serverProcess = null;
        this.modelsDir = config?.modelsDir || join(homedir(), ".avis", "models");
        this.config = {
            ...DEFAULT_LOCAL_CONFIG,
            ...config,
            modelsDir: this.modelsDir,
        };
        mkdirSync(this.modelsDir, { recursive: true });
    }
    // ========================================================================
    // Model Management
    // ========================================================================
    /** Get all downloaded models */
    getModels() {
        const manifestPath = join(this.modelsDir, MODELS_MANIFEST);
        if (!existsSync(manifestPath))
            return [];
        try {
            return JSON.parse(readFileSync(manifestPath, "utf-8"));
        }
        catch {
            return [];
        }
    }
    /** Save models manifest */
    saveModels(models) {
        writeFileSync(join(this.modelsDir, MODELS_MANIFEST), JSON.stringify(models, null, 2));
    }
    /** Register a downloaded model */
    registerModel(model) {
        const models = this.getModels();
        const existing = models.findIndex((m) => m.id === model.id);
        if (existing >= 0) {
            models[existing] = model;
        }
        else {
            models.push(model);
        }
        this.saveModels(models);
    }
    /** Remove a model (delete files + unregister) */
    removeModel(modelId) {
        const models = this.getModels();
        const model = models.find((m) => m.id === modelId);
        if (!model)
            return false;
        // Stop server if this model is loaded
        const state = this.getServerState();
        if (state.running && state.modelId === modelId) {
            this.stopServer();
        }
        // Delete files
        if (existsSync(model.modelPath)) {
            unlinkSync(model.modelPath);
        }
        if (model.mmProjPath && existsSync(model.mmProjPath)) {
            unlinkSync(model.mmProjPath);
        }
        // Unregister
        this.saveModels(models.filter((m) => m.id !== modelId));
        return true;
    }
    /** Find a model by ID */
    findModel(modelId) {
        return this.getModels().find((m) => m.id === modelId);
    }
    /** List GGUF files in models directory (for discovery) */
    listGgufFiles() {
        if (!existsSync(this.modelsDir))
            return [];
        return readdirSync(this.modelsDir)
            .filter((f) => f.endsWith(".gguf") && !f.includes("mmproj"))
            .map((f) => join(this.modelsDir, f));
    }
    // ========================================================================
    // Server Management
    // ========================================================================
    /** Check if llama-server binary is available */
    isServerInstalled() {
        try {
            execSync("which llama-server", { stdio: "pipe" });
            return true;
        }
        catch {
            return false;
        }
    }
    /** Get current server state */
    getServerState() {
        const pidPath = join(this.modelsDir, PID_FILE);
        if (!existsSync(pidPath)) {
            return { running: false, pid: null, port: this.config.port, modelId: null, startedAt: null };
        }
        try {
            const data = JSON.parse(readFileSync(pidPath, "utf-8"));
            // Check if process is actually running
            try {
                process.kill(data.pid, 0); // signal 0 = check if alive
                return {
                    running: true,
                    pid: data.pid,
                    port: data.port || this.config.port,
                    modelId: data.modelId || null,
                    startedAt: data.startedAt || null,
                };
            }
            catch {
                // Process dead, clean up stale PID file
                unlinkSync(pidPath);
                return { running: false, pid: null, port: this.config.port, modelId: null, startedAt: null };
            }
        }
        catch {
            return { running: false, pid: null, port: this.config.port, modelId: null, startedAt: null };
        }
    }
    /** Start llama-server with a specific model */
    async startServer(modelId) {
        if (!this.isServerInstalled()) {
            return { success: false, error: "llama-server not found. Install with: brew install llama.cpp" };
        }
        const state = this.getServerState();
        if (state.running) {
            if (state.modelId === modelId) {
                return { success: true }; // Already running with this model
            }
            // Different model -- stop first
            this.stopServer();
        }
        const model = this.findModel(modelId);
        if (!model) {
            return { success: false, error: `Model "${modelId}" not found. Use /local list to see available models.` };
        }
        if (!existsSync(model.modelPath)) {
            return { success: false, error: `Model file not found: ${model.modelPath}` };
        }
        const args = [
            "--model",
            model.modelPath,
            "--port",
            String(this.config.port),
            "--ctx-size",
            String(model.contextSize || this.config.contextSize),
            "--n-gpu-layers",
            String(this.config.nGpuLayers),
            "--log-disable",
        ];
        if (model.mmProjPath && existsSync(model.mmProjPath)) {
            args.push("--mmproj", model.mmProjPath);
        }
        try {
            const child = spawn("llama-server", args, {
                detached: true,
                stdio: "ignore",
            });
            child.unref();
            if (!child.pid) {
                return { success: false, error: "Failed to start llama-server (no PID)" };
            }
            // Save PID file
            writeFileSync(join(this.modelsDir, PID_FILE), JSON.stringify({
                pid: child.pid,
                port: this.config.port,
                modelId,
                startedAt: Date.now(),
            }));
            this.serverProcess = child;
            // Wait for server to be ready
            const ready = await this.waitForReady(15_000);
            if (!ready) {
                this.stopServer();
                return { success: false, error: "llama-server started but didn't become ready within 15s" };
            }
            return { success: true };
        }
        catch (err) {
            return { success: false, error: `Failed to start: ${err instanceof Error ? err.message : err}` };
        }
    }
    /** Stop the running server */
    stopServer() {
        const state = this.getServerState();
        if (!state.running || !state.pid)
            return false;
        try {
            process.kill(state.pid, "SIGTERM");
        }
        catch {
            // Already dead
        }
        const pidPath = join(this.modelsDir, PID_FILE);
        if (existsSync(pidPath)) {
            unlinkSync(pidPath);
        }
        this.serverProcess = null;
        return true;
    }
    /** Wait for server to be ready */
    async waitForReady(timeoutMs) {
        const start = Date.now();
        while (Date.now() - start < timeoutMs) {
            try {
                const resp = await fetch(`http://localhost:${this.config.port}/health`);
                const data = (await resp.json());
                if (data.status === "ok")
                    return true;
            }
            catch {
                // Not ready yet
            }
            await new Promise((r) => setTimeout(r, 500));
        }
        return false;
    }
    /** Check server health */
    async isServerHealthy() {
        const state = this.getServerState();
        if (!state.running)
            return false;
        try {
            const resp = await fetch(`http://localhost:${state.port}/health`);
            const data = (await resp.json());
            return data.status === "ok";
        }
        catch {
            return false;
        }
    }
    /** Get formatted status string */
    async getStatusString() {
        const state = this.getServerState();
        const models = this.getModels();
        const lines = [];
        lines.push("Local Models:");
        if (models.length === 0) {
            lines.push("  (none downloaded)");
        }
        else {
            for (const m of models) {
                const sizeGB = (m.sizeBytes / 1024 ** 3).toFixed(1);
                const loaded = state.running && state.modelId === m.id ? " [LOADED]" : "";
                const vision = m.supportsVision ? " +vision" : "";
                lines.push(`  ${m.id} (${m.quant}, ${sizeGB}GB${vision})${loaded}`);
            }
        }
        lines.push("");
        if (state.running) {
            const healthy = await this.isServerHealthy();
            const uptime = state.startedAt ? Math.round((Date.now() - state.startedAt) / 1000 / 60) : "?";
            lines.push(`Server: running on port ${state.port} (PID ${state.pid}, ${uptime}min uptime)`);
            lines.push(`Health: ${healthy ? "healthy" : "unhealthy"}`);
            lines.push(`Model: ${state.modelId || "unknown"}`);
        }
        else {
            lines.push("Server: stopped");
        }
        return lines.join("\n");
    }
    /** Get the models directory path */
    getModelsDir() {
        return this.modelsDir;
    }
}
//# sourceMappingURL=local-manager.js.map