import { existsSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from 'fs';
import { join, dirname, sep } from 'path';
import { logger } from '../utils/logger.js';
import { summarizeError } from '../utils/error-display.js';
/**
 * ProjectIndex — In-memory project file index with token counts.
 *
 * Singleton per process. Loaded at startup from disk in the background.
 * Mutations are debounced: flush happens 5 seconds after last write.
 *
 * Disk format: v4 tree (see DiskFormat above).
 */
export class ProjectIndex {
    indexPath;
    projectRoot;
    baseDir;
    files = new Map(); // path -> tokens
    flushTimer = null;
    createdAt = new Date().toISOString();
    loaded = false;
    constructor(baseDir) {
        this.projectRoot = baseDir;
        this.baseDir = this.projectRoot;
        this.indexPath = join(this.projectRoot, '.goodvibes', 'project-index.json');
    }
    // ---------------------------------------------------------------------------
    // Lifecycle
    // ---------------------------------------------------------------------------
    /**
     * Load the index from disk.
     * Called at startup, safe to call multiple times.
     */
    async load() {
        if (!existsSync(this.indexPath)) {
            this.loaded = true;
            return;
        }
        try {
            const raw = readFileSync(this.indexPath, 'utf-8');
            const disk = JSON.parse(raw);
            if (disk.version === 4 && disk.tree) {
                this.createdAt = disk.created_at ?? this.createdAt;
                this.files = flattenTree(disk.tree);
            }
            this.loaded = true;
            logger.debug('ProjectIndex: loaded', { files: this.files.size });
        }
        catch (err) {
            throw new Error(`ProjectIndex load failed for ${this.indexPath}: ${summarizeError(err)}`);
        }
    }
    /**
     * Flush to disk immediately, bypassing debounce.
     */
    async forceFlush() {
        if (this.flushTimer !== null) {
            clearTimeout(this.flushTimer);
            this.flushTimer = null;
        }
        await this.flush();
    }
    // ---------------------------------------------------------------------------
    // Query
    // ---------------------------------------------------------------------------
    getFiles() {
        return Array.from(this.files.entries()).map(([path, tokens]) => ({ path, tokens }));
    }
    getFile(path) {
        const normalPath = this.normalizePath(path);
        const tokens = this.files.get(normalPath);
        if (tokens === undefined)
            return null;
        return { path: normalPath, tokens };
    }
    getFilesByPrefix(prefix) {
        const normalPrefix = this.normalizePath(prefix);
        const result = [];
        for (const [path, tokens] of this.files) {
            if (path.startsWith(normalPrefix)) {
                result.push({ path, tokens });
            }
        }
        return result;
    }
    /** Return count of files by extension (e.g. { ts: 42, json: 5 }). */
    getTypeCounts() {
        const counts = {};
        for (const path of this.files.keys()) {
            const dot = path.lastIndexOf('.');
            const ext = dot >= 0 ? path.slice(dot + 1) : 'other';
            counts[ext] = (counts[ext] ?? 0) + 1;
        }
        return counts;
    }
    getTotalTokens() {
        let total = 0;
        for (const tokens of this.files.values())
            total += tokens;
        return total;
    }
    // ---------------------------------------------------------------------------
    // Mutation
    // ---------------------------------------------------------------------------
    /**
     * Insert or update a file entry.
     * If tokens is not provided, attempts to estimate from disk.
     */
    upsertFile(path, tokens) {
        const normalPath = this.normalizePath(path);
        const t = tokens ?? this.files.get(normalPath) ?? 0;
        this.files.set(normalPath, t);
        this.scheduleFlush();
    }
    /**
     * Touch an existing file entry (update token count from disk if possible).
     * No-op if file not in index.
     */
    touchFile(path) {
        const normalPath = this.normalizePath(path);
        if (!this.files.has(normalPath))
            return;
        // Update token estimate from file size if readable
        try {
            const size = statSync(path).size;
            const tokens = Math.ceil(size / 4);
            this.files.set(normalPath, tokens);
            this.scheduleFlush();
        }
        catch (err) {
            // Stat failed; leave existing token estimate.
            logger.warn('[ProjectIndex] statSync failed for file update', { path, error: String(err) });
        }
    }
    removeFile(path) {
        const normalPath = this.normalizePath(path);
        if (this.files.delete(normalPath)) {
            this.scheduleFlush();
        }
    }
    // ---------------------------------------------------------------------------
    // Private helpers
    // ---------------------------------------------------------------------------
    normalizePath(inputPath) {
        let p = inputPath.split(sep).join('/');
        // Strip project root prefix for absolute paths
        const root = this.baseDir.split(sep).join('/');
        if (p.startsWith(root + '/')) {
            p = p.slice(root.length + 1);
        }
        p = p.replace(/^\.\//, '');
        return p;
    }
    async dispose() {
        if (this.flushTimer !== null) {
            clearTimeout(this.flushTimer);
            this.flushTimer = null;
        }
        await this.forceFlush();
    }
    /**
     * Re-root the project index to a new base directory.
     *
     * Flushes any pending writes to the current location, then resets all in-memory
     * state and re-points the index path to the new directory. A fresh load is
     * performed to pick up any existing index at the new location.
     *
     * @param newBaseDir - Absolute path to the new project root.
     */
    async reroot(newBaseDir) {
        // Step 1: flush pending writes to the current location.
        if (this.flushTimer !== null) {
            clearTimeout(this.flushTimer);
            this.flushTimer = null;
        }
        await this.forceFlush();
        // Step 2: reset state and re-point to new directory
        this.projectRoot = newBaseDir;
        this.baseDir = newBaseDir;
        this.indexPath = join(newBaseDir, '.goodvibes', 'project-index.json');
        this.files = new Map();
        this.loaded = false;
        this.createdAt = new Date().toISOString();
        // Step 3: load existing index at new location (no-op if not present)
        await this.load();
    }
    scheduleFlush() {
        if (this.flushTimer !== null)
            clearTimeout(this.flushTimer);
        this.flushTimer = setTimeout(() => {
            this.flushTimer = null;
            this.flush().catch(err => {
                logger.error('ProjectIndex: scheduled flush failed', { error: summarizeError(err) });
            });
        }, 5000);
        this.flushTimer.unref?.();
    }
    async flush() {
        mkdirSync(dirname(this.indexPath), { recursive: true });
        const tree = buildTree(this.files);
        const totalFiles = this.files.size;
        const dirs = new Set();
        for (const path of this.files.keys()) {
            const parts = path.split('/');
            for (let i = 1; i < parts.length; i++) {
                dirs.add(parts.slice(0, i).join('/'));
            }
        }
        const disk = {
            _format: 'tree: { "directory/": { "file.ext": token_count } }',
            version: 4,
            created_at: this.createdAt,
            updated_at: new Date().toISOString(),
            project_root: this.projectRoot,
            stats: {
                total_files: totalFiles,
                total_dirs: dirs.size,
                index_duration_ms: 0,
            },
            tree,
        };
        const tmpPath = `${this.indexPath}.tmp`;
        writeFileSync(tmpPath, JSON.stringify(disk) + '\n', 'utf-8');
        renameSync(tmpPath, this.indexPath);
    }
}
// ---------------------------------------------------------------------------
// Tree serialization helpers
// ---------------------------------------------------------------------------
/**
 * Flatten a v4 tree into a Map<path, tokens>.
 * Tree: { "src/": { "main.ts": 450 } } -> Map { "src/main.ts" => 450 }
 */
function flattenTree(tree, prefix = '') {
    const result = new Map();
    for (const [key, value] of Object.entries(tree)) {
        if (typeof value === 'number') {
            // File entry
            result.set(prefix + key, value);
        }
        else {
            // Directory: key ends with '/'
            const dirName = key.endsWith('/') ? key : key + '/';
            const nested = flattenTree(value, prefix + dirName);
            for (const [p, t] of nested)
                result.set(p, t);
        }
    }
    return result;
}
/**
 * Build a v4 tree from a flat Map<path, tokens>.
 * "src/main.ts" -> { "src/": { "main.ts": 450 } }
 */
function buildTree(files) {
    const root = {};
    for (const [path, tokens] of files) {
        const parts = path.split('/');
        let node = root;
        for (let i = 0; i < parts.length - 1; i++) {
            const dirKey = parts[i] + '/';
            if (!(dirKey in node)) {
                node[dirKey] = {};
            }
            node = node[dirKey];
        }
        const fileName = parts[parts.length - 1];
        if (fileName !== undefined) {
            node[fileName] = tokens;
        }
    }
    return root;
}
