import { mkdirSync, writeFileSync, readFileSync, readdirSync, unlinkSync, statSync } from 'node:fs';
import { join } from 'node:path';
import { logger } from '../../utils/logger.js';
import { summarizeError } from '../../utils/error-display.js';
// ─── Constants ─────────────────────────────────────────────────────────────
export const DEFAULT_MAX_CHARS = 50_000;
const OVERFLOW_DIR = '.goodvibes/.overflow';
const DEFAULT_RETENTION = {
    maxAgeMs: 60 * 60 * 1000, // 1 hour
    maxCount: Infinity,
    maxSizeBytes: Infinity,
};
// ─── File Backend ────────────────────────────────────────────────────────────
/**
 * FileBackend — spills overflow content to `.goodvibes/.overflow/` on disk.
 */
export class FileBackend {
    type = 'file';
    overflowDir;
    constructor(baseDir) {
        this.overflowDir = join(baseDir, OVERFLOW_DIR);
    }
    ensureDir() {
        try {
            mkdirSync(this.overflowDir, { recursive: true });
            return true;
        }
        catch {
            return false;
        }
    }
    _safePath(id) {
        if (id.includes('/') || id.includes('\\') || id.includes('\0') || id.includes('..'))
            return null;
        const resolved = join(this.overflowDir, id);
        // Prevent path traversal — id must not escape overflowDir
        return resolved.startsWith(this.overflowDir + '/') ? resolved : null;
    }
    write(filename, content) {
        try {
            if (!this.ensureDir())
                return null;
            const target = this._safePath(filename);
            if (!target)
                return null;
            writeFileSync(target, content, 'utf-8');
            return {
                id: filename,
                filename,
                content,
                sizeBytes: new TextEncoder().encode(content).length,
                createdAt: Date.now(),
                backendType: 'file',
            };
        }
        catch {
            return null;
        }
    }
    read(id) {
        const target = this._safePath(id);
        if (!target)
            return null;
        try {
            return readFileSync(target, 'utf-8');
        }
        catch {
            return null;
        }
    }
    cleanup(policy) {
        const cfg = { ...DEFAULT_RETENTION, ...policy };
        const now = Date.now();
        let files;
        try {
            files = readdirSync(this.overflowDir);
        }
        catch {
            return;
        }
        const entries = [];
        for (const file of files) {
            const p = join(this.overflowDir, file);
            try {
                const s = statSync(p);
                entries.push({ file, path: p, mtimeMs: s.mtimeMs, size: s.size });
            }
            catch { }
        }
        entries.sort((a, b) => a.mtimeMs - b.mtimeMs);
        const toDelete = new Set();
        for (const e of entries) {
            if (now - e.mtimeMs >= cfg.maxAgeMs)
                toDelete.add(e.path);
        }
        const rem = entries.filter((e) => !toDelete.has(e.path));
        if (cfg.maxCount !== Infinity && rem.length > cfg.maxCount) {
            const excess = rem.length - cfg.maxCount;
            for (let i = 0; i < excess; i++)
                toDelete.add(rem[i].path);
        }
        const ac = entries.filter((e) => !toDelete.has(e.path));
        if (cfg.maxSizeBytes !== Infinity) {
            let total = ac.reduce((s, e) => s + e.size, 0);
            for (const e of ac) {
                if (total <= cfg.maxSizeBytes)
                    break;
                toDelete.add(e.path);
                total -= e.size;
            }
        }
        for (const p of toDelete) {
            try {
                unlinkSync(p);
            }
            catch { }
        }
    }
    /** Lists all overflow entries. Reads file content eagerly — acceptable for small overflow directories. */
    list() {
        let files;
        try {
            files = readdirSync(this.overflowDir);
        }
        catch {
            return [];
        }
        const result = [];
        for (const file of files) {
            const p = join(this.overflowDir, file);
            try {
                const s = statSync(p);
                let content = '';
                try {
                    content = readFileSync(p, 'utf-8');
                }
                catch { }
                result.push({ id: file, filename: file, content, sizeBytes: s.size, createdAt: s.mtimeMs, backendType: 'file' });
            }
            catch { }
        }
        return result;
    }
}
// ─── Ledger Backend ──────────────────────────────────────────────────────────
/**
 * LedgerBackend — stores overflow entries in-process (Map).
 * Ephemeral: entries are lost on process exit.
 */
export class LedgerBackend {
    type = 'ledger';
    entries = new Map();
    write(filename, content) {
        const entry = {
            id: filename, filename, content,
            sizeBytes: new TextEncoder().encode(content).length,
            createdAt: Date.now(),
            backendType: 'ledger',
        };
        this.entries.set(filename, entry);
        return entry;
    }
    read(id) { return this.entries.get(id)?.content ?? null; }
    cleanup(policy) {
        const cfg = { ...DEFAULT_RETENTION, ...policy };
        const now = Date.now();
        const sorted = Array.from(this.entries.values()).sort((a, b) => a.createdAt - b.createdAt);
        const toDelete = new Set();
        for (const e of sorted) {
            if (now - e.createdAt >= cfg.maxAgeMs)
                toDelete.add(e.id);
        }
        const rem = sorted.filter((e) => !toDelete.has(e.id));
        if (cfg.maxCount !== Infinity && rem.length > cfg.maxCount) {
            const excess = rem.length - cfg.maxCount;
            for (let i = 0; i < excess; i++)
                toDelete.add(rem[i].id);
        }
        const ac = sorted.filter((e) => !toDelete.has(e.id));
        if (cfg.maxSizeBytes !== Infinity) {
            let total = ac.reduce((s, e) => s + e.sizeBytes, 0);
            for (const e of ac) {
                if (total <= cfg.maxSizeBytes)
                    break;
                toDelete.add(e.id);
                total -= e.sizeBytes;
            }
        }
        for (const id of toDelete)
            this.entries.delete(id);
    }
    list() { return Array.from(this.entries.values()); }
}
// ─── Diagnostics Backend ─────────────────────────────────────────────────────
/**
 * DiagnosticsBackend — records overflow events as structured log entries.
 * Does NOT store content; `read()` always returns null.
 */
export class DiagnosticsBackend {
    type = 'diagnostics';
    log = [];
    write(filename, content) {
        const sizeBytes = new TextEncoder().encode(content).length;
        const createdAt = Date.now();
        this.log.push({ id: filename, filename, sizeBytes, createdAt });
        logger.info(`[overflow:diagnostics] spilled ${sizeBytes} bytes → ${filename}`);
        return { id: filename, filename, content: '', sizeBytes, createdAt, backendType: 'diagnostics' };
    }
    read(_id) { return null; }
    cleanup(policy) {
        const cfg = { ...DEFAULT_RETENTION, ...policy };
        const now = Date.now();
        const sorted = [...this.log].sort((a, b) => a.createdAt - b.createdAt);
        const toDelete = new Set();
        for (const e of sorted) {
            if (now - e.createdAt >= cfg.maxAgeMs)
                toDelete.add(e.id);
        }
        const rem = sorted.filter((e) => !toDelete.has(e.id));
        if (cfg.maxCount !== Infinity && rem.length > cfg.maxCount) {
            const excess = rem.length - cfg.maxCount;
            for (let i = 0; i < excess; i++)
                toDelete.add(rem[i].id);
        }
        const ac = sorted.filter((e) => !toDelete.has(e.id));
        if (cfg.maxSizeBytes !== Infinity) {
            let total = ac.reduce((s, e) => s + e.sizeBytes, 0);
            for (const e of ac) {
                if (total <= cfg.maxSizeBytes)
                    break;
                toDelete.add(e.id);
                total -= e.sizeBytes;
            }
        }
        for (let i = this.log.length - 1; i >= 0; i--) {
            if (toDelete.has(this.log[i].id))
                this.log.splice(i, 1);
        }
    }
    list() {
        return this.log.map((e) => ({ ...e, content: '', backendType: 'diagnostics' }));
    }
}
// ─── Backend Factory ─────────────────────────────────────────────────────────
/**
 * Create a spill backend by type. Defaults to `'file'`.
 */
export function createSpillBackend(type = 'file', baseDir) {
    switch (type) {
        case 'file':
            if (!baseDir)
                throw new Error('File spill backend requires an explicit baseDir');
            return new FileBackend(baseDir);
        case 'ledger': return new LedgerBackend();
        case 'diagnostics': return new DiagnosticsBackend();
        default: {
            logger.info(`[overflow] Unknown spill backend "${String(type)}", falling back to file`);
            if (!baseDir)
                throw new Error('File spill backend requires an explicit baseDir');
            return new FileBackend(baseDir);
        }
    }
}
// ─── OverflowHandler ────────────────────────────────────────────────────────
/**
 * Handles large tool output by delegating overflow content to a pluggable
 * backend and returning a truncated version with a typed reference URI.
 *
 * Overflow references encode the backend type:
 *   `file:.goodvibes/.overflow/{filename}`
 *   `ledger:{filename}`
 *   `diagnostics:{filename}`
 *
 * Never throws — on write failure, returns truncated content without ref.
 */
export class OverflowHandler {
    backend;
    retention;
    constructor(config = {}) {
        if (!config.backend && (config.spillBackend ?? 'file') === 'file' && !config.baseDir) {
            throw new Error('OverflowHandler requires an explicit baseDir when using the file spill backend');
        }
        this.backend = config.backend ?? createSpillBackend(config.spillBackend ?? 'file', config.baseDir);
        this.retention = { ...DEFAULT_RETENTION, ...config.retention };
    }
    sanitizeLabel(label) {
        return label
            .toLowerCase()
            .replace(/[^a-z0-9-]/g, '-')
            .replace(/-+/g, '-')
            .replace(/^-|-$/g, '')
            .slice(0, 40);
    }
    buildRef(filename) {
        switch (this.backend.type) {
            case 'file': return `file:${OVERFLOW_DIR}/${filename}`;
            case 'ledger': return `ledger:${filename}`;
            case 'diagnostics': return `diagnostics:${filename}`;
            default: return `${this.backend.type}:${filename}`;
        }
    }
    /**
     * Handle potentially large content.
     * Returns unchanged content if within limit.
     * On overflow: delegates to active backend and returns typed ref.
     */
    handle(content, options) {
        const maxChars = options?.maxChars ?? DEFAULT_MAX_CHARS;
        if (content.length <= maxChars) {
            return { content };
        }
        const label = this.sanitizeLabel(options?.label ?? 'output');
        const filename = `${Date.now()}-${label}.txt`;
        let entry;
        try {
            entry = this.backend.write(filename, content);
        }
        catch (err) {
            logger.info(`[overflow] Backend write error: ${summarizeError(err)}`);
            entry = null;
        }
        if (!entry) {
            return {
                content: content.slice(0, maxChars) + `\n[... truncated at ${maxChars} chars]`,
            };
        }
        const ref = this.buildRef(filename);
        const notice = this.backend.type === 'file'
            ? `Full output: ${ref.replace(/^file:/, '')}`
            : `Spilled to ${this.backend.type} backend (ref: ${ref})`;
        return {
            content: content.slice(0, maxChars) + `\n[... truncated. ${notice}]`,
            overflowRef: ref,
            spillBackend: this.backend.type,
        };
    }
    /**
     * Prune entries that violate the retention policy.
     */
    cleanup(policy) {
        this.backend.cleanup({ ...this.retention, ...policy });
    }
    /** List current overflow entries from the active backend. */
    list() {
        return this.backend.list();
    }
    /** Return the active backend type. */
    get backendType() {
        return this.backend.type;
    }
}
// ─── Operator Cleanup Command ────────────────────────────────────────────────
/**
 * overflowCleanup — operator-facing cleanup command.
 *
 * Prunes overflow entries from the provided overflow handler.
 * Suitable for scripted operator invocations (e.g. CLI, cron).
 */
export function overflowCleanup(handler, policy) {
    const beforeCount = handler.list().length;
    handler.cleanup(policy);
    return { beforeCount };
}
