import { execFileSync } from 'node:child_process';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { debug, warn } from '../infrastructure/logger.js';
import { getNative, isNativeAvailable } from '../infrastructure/native.js';
import { DbError, toErrorMessage } from '../shared/errors.js';
import { getDatabase } from './better-sqlite3.js';
import { Repository } from './repository/base.js';
import { NativeRepository } from './repository/native-repository.js';
import { SqliteRepository } from './repository/sqlite-repository.js';
/** Lazy-loaded package version (read once from package.json). */
let _packageVersion;
function getPackageVersion() {
    if (_packageVersion !== undefined)
        return _packageVersion;
    try {
        const connDir = path.dirname(fileURLToPath(import.meta.url));
        const pkgPath = path.join(connDir, '..', '..', 'package.json');
        _packageVersion = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'))
            .version;
    }
    catch (e) {
        debug(`Failed to read package version: ${toErrorMessage(e)}`);
        _packageVersion = '';
    }
    return _packageVersion;
}
/** Warn once per process when DB version mismatches the running codegraph version. */
let _versionWarned = false;
/** Check and warn (once) if the running codegraph version differs from the DB build version. */
function warnOnVersionMismatch(getBuildVersion) {
    if (_versionWarned)
        return;
    _versionWarned = true;
    try {
        const buildVersion = getBuildVersion();
        const currentVersion = getPackageVersion();
        if (buildVersion && currentVersion && buildVersion !== currentVersion) {
            warn(`DB was built with codegraph v${buildVersion}, running v${currentVersion}. Consider: codegraph build --no-incremental`);
        }
    }
    catch (e) {
        debug(`Version mismatch check skipped (build_meta may not exist): ${toErrorMessage(e)}`);
    }
}
let _cachedRepoRoot; // undefined = not computed, null = not a git repo
let _cachedRepoRootCwd; // cwd at the time the cache was populated
/**
 * Return the git worktree/repo root for the given directory (or cwd).
 * Uses `git rev-parse --show-toplevel` which returns the correct root
 * for both regular repos and git worktrees.
 * Results are cached per-process when called without arguments.
 * The cache is keyed on cwd so it invalidates if the working directory changes
 * (e.g. MCP server serving multiple sessions).
 */
export function findRepoRoot(fromDir) {
    const dir = fromDir || process.cwd();
    if (!fromDir && _cachedRepoRoot !== undefined && _cachedRepoRootCwd === dir) {
        return _cachedRepoRoot;
    }
    let root = null;
    try {
        const raw = execFileSync('git', ['rev-parse', '--show-toplevel'], {
            cwd: dir,
            encoding: 'utf-8',
            stdio: ['pipe', 'pipe', 'pipe'],
        }).trim();
        // Use realpathSync to resolve symlinks (macOS /var → /private/var) and
        // 8.3 short names (Windows RUNNER~1 → runneradmin) so the ceiling path
        // matches the realpathSync'd dir in findDbPath.
        try {
            root = fs.realpathSync(raw);
        }
        catch (e) {
            debug(`realpathSync failed for git root "${raw}", using resolve: ${toErrorMessage(e)}`);
            root = path.resolve(raw);
        }
    }
    catch (e) {
        debug(`git rev-parse failed for "${dir}": ${toErrorMessage(e)}`);
        root = null;
    }
    if (!fromDir) {
        _cachedRepoRoot = root;
        _cachedRepoRootCwd = dir;
    }
    return root;
}
/** Reset the cached repo root (for testing). */
export function _resetRepoRootCache() {
    _cachedRepoRoot = undefined;
    _cachedRepoRootCwd = undefined;
}
function isProcessAlive(pid) {
    try {
        process.kill(pid, 0);
        return true;
    }
    catch (e) {
        debug(`PID ${pid} not alive: ${e.code || toErrorMessage(e)}`);
        return false;
    }
}
export function acquireAdvisoryLock(dbPath) {
    const lockPath = `${dbPath}.lock`;
    try {
        if (fs.existsSync(lockPath)) {
            const content = fs.readFileSync(lockPath, 'utf-8').trim();
            const pid = Number(content);
            if (pid && pid !== process.pid && isProcessAlive(pid)) {
                warn(`Another process (PID ${pid}) may be using this database. Proceeding with caution.`);
            }
        }
    }
    catch (e) {
        debug(`Advisory lock read failed: ${toErrorMessage(e)}`);
    }
    try {
        fs.writeFileSync(lockPath, String(process.pid), 'utf-8');
    }
    catch (e) {
        debug(`Advisory lock write failed: ${toErrorMessage(e)}`);
    }
}
export function releaseAdvisoryLock(lockPath) {
    try {
        const content = fs.readFileSync(lockPath, 'utf-8').trim();
        if (Number(content) === process.pid) {
            fs.unlinkSync(lockPath);
        }
    }
    catch (e) {
        debug(`Advisory lock release failed for ${lockPath}: ${toErrorMessage(e)}`);
    }
}
/**
 * Check if two paths refer to the same directory.
 * Handles Windows 8.3 short names (RUNNER~1 vs runneradmin) and macOS
 * symlinks (/tmp vs /private/tmp) where string comparison fails.
 */
function isSameDirectory(a, b) {
    if (path.resolve(a) === path.resolve(b))
        return true;
    try {
        const sa = fs.statSync(a);
        const sb = fs.statSync(b);
        return sa.dev === sb.dev && sa.ino === sb.ino;
    }
    catch (e) {
        debug(`isSameDirectory stat failed: ${toErrorMessage(e)}`);
        return false;
    }
}
export function openDb(dbPath) {
    // Flush any deferred DB close from a previous build (avoids WAL contention)
    flushDeferredClose();
    const dir = path.dirname(dbPath);
    if (!fs.existsSync(dir))
        fs.mkdirSync(dir, { recursive: true });
    acquireAdvisoryLock(dbPath);
    const Database = getDatabase();
    const db = new Database(dbPath);
    db.pragma('journal_mode = WAL');
    db.pragma('busy_timeout = 5000');
    db.__lockPath = `${dbPath}.lock`;
    return db;
}
export function closeDb(db) {
    db.close();
    if (db.__lockPath)
        releaseAdvisoryLock(db.__lockPath);
}
/** Pending deferred-close DB handles (not yet closed). */
const _deferredDbs = [];
/**
 * Synchronously close any DB handles queued by `closeDbDeferred()`.
 * Call before deleting DB files or in test teardown to avoid EBUSY on Windows.
 */
export function flushDeferredClose() {
    while (_deferredDbs.length > 0) {
        const db = _deferredDbs.pop();
        try {
            db.close();
        }
        catch (e) {
            debug(`Deferred DB close failed (handle may already be closed): ${toErrorMessage(e)}`);
        }
    }
}
/**
 * Schedule DB close on the next event loop tick. Useful for incremental
 * builds where the WAL checkpoint in db.close() is expensive (~250ms on
 * Windows) and doesn't need to block the caller.
 *
 * The advisory lock is released immediately so subsequent opens succeed.
 * The actual handle close (+ WAL checkpoint) happens asynchronously.
 * Call `flushDeferredClose()` before deleting the DB file.
 */
export function closeDbDeferred(db) {
    // Release the advisory lock immediately so the next open can proceed
    if (db.__lockPath) {
        releaseAdvisoryLock(db.__lockPath);
        db.__lockPath = undefined;
    }
    _deferredDbs.push(db);
    // Defer the expensive WAL checkpoint to after the caller returns
    setImmediate(() => {
        const idx = _deferredDbs.indexOf(db);
        if (idx !== -1) {
            _deferredDbs.splice(idx, 1);
            try {
                db.close();
            }
            catch (e) {
                debug(`Deferred DB close failed (may already be closed by flush): ${toErrorMessage(e)}`);
            }
        }
    });
}
/** Close both handles: NativeDatabase first (fast), then better-sqlite3 (releases lock). */
export function closeDbPair(pair) {
    if (pair.nativeDb) {
        try {
            pair.nativeDb.close();
        }
        catch (e) {
            debug(`closeDbPair: native close failed: ${toErrorMessage(e)}`);
        }
    }
    closeDb(pair.db);
}
/** Close NativeDatabase immediately, defer better-sqlite3 WAL checkpoint. */
export function closeDbPairDeferred(pair) {
    if (pair.nativeDb) {
        try {
            pair.nativeDb.close();
        }
        catch (e) {
            debug(`closeDbPairDeferred: native close failed: ${toErrorMessage(e)}`);
        }
    }
    closeDbDeferred(pair.db);
}
export function findDbPath(customPath) {
    if (customPath)
        return path.resolve(customPath);
    const rawCeiling = findRepoRoot();
    // Normalize ceiling with realpathSync to resolve 8.3 short names (Windows
    // RUNNER~1 → runneradmin) and symlinks (macOS /var → /private/var).
    // findRepoRoot already applies realpathSync internally, but the git output
    // may still contain short names on some Windows CI environments.
    let ceiling;
    if (rawCeiling) {
        try {
            ceiling = fs.realpathSync(rawCeiling);
        }
        catch (e) {
            debug(`realpathSync failed for ceiling "${rawCeiling}": ${toErrorMessage(e)}`);
            ceiling = rawCeiling;
        }
    }
    else {
        ceiling = null;
    }
    // Resolve symlinks (e.g. macOS /var → /private/var) so dir matches ceiling from git
    let dir;
    try {
        dir = fs.realpathSync(process.cwd());
    }
    catch (e) {
        debug(`realpathSync failed for cwd: ${toErrorMessage(e)}`);
        dir = process.cwd();
    }
    while (true) {
        const candidate = path.join(dir, '.codegraph', 'graph.db');
        if (fs.existsSync(candidate))
            return candidate;
        if (ceiling && isSameDirectory(dir, ceiling)) {
            debug(`findDbPath: stopped at git ceiling ${ceiling}`);
            break;
        }
        // Outside a git repo, cwd is the first (and only) directory we'll check.
        // Walking past it risks attaching to a stale .codegraph/ in an unrelated
        // parent — e.g. /private/tmp/.codegraph/ leaking into every /tmp/foo/ run,
        // or $HOME/.codegraph/ leaking into every scratch dir under $HOME.
        if (!ceiling) {
            debug(`findDbPath: no git ceiling, stopping at ${dir}`);
            break;
        }
        const parent = path.dirname(dir);
        if (parent === dir)
            break;
        dir = parent;
    }
    const base = ceiling || process.cwd();
    return path.join(base, '.codegraph', 'graph.db');
}
/** Open a database in readonly mode, with a user-friendly error if the DB doesn't exist. */
export function openReadonlyOrFail(customPath) {
    const dbPath = findDbPath(customPath);
    if (!fs.existsSync(dbPath)) {
        throw new DbError(`No codegraph database found at ${dbPath}.\nRun "codegraph build" first to analyze your codebase.`, { file: dbPath });
    }
    const Database = getDatabase();
    const db = new Database(dbPath, { readonly: true });
    db.pragma('busy_timeout = 5000');
    warnOnVersionMismatch(() => {
        const row = db
            .prepare('SELECT value FROM build_meta WHERE key = ?')
            .get('codegraph_version');
        return row?.value;
    });
    return db;
}
/** Open a NativeRepository via rusqlite, throwing DbError if the DB file is missing. */
function openRepoNative(customDbPath) {
    const dbPath = findDbPath(customDbPath);
    if (!fs.existsSync(dbPath)) {
        throw new DbError(`No codegraph database found at ${dbPath}.\nRun "codegraph build" first to analyze your codebase.`, { file: dbPath });
    }
    const native = getNative();
    const ndb = native.NativeDatabase.openReadonly(dbPath);
    try {
        warnOnVersionMismatch(() => ndb.getBuildMeta('codegraph_version'));
        const repo = new NativeRepository(ndb, dbPath);
        return {
            repo,
            close() {
                repo.closeFallback();
                ndb.close();
            },
        };
    }
    catch (innerErr) {
        ndb.close();
        throw innerErr;
    }
}
/**
 * Open a Repository from either an injected instance or a DB path.
 *
 * When `opts.repo` is a Repository instance, returns it directly (no DB opened).
 * When the native engine is available, opens a NativeDatabase (rusqlite) and
 * wraps it in NativeRepository. Otherwise falls back to better-sqlite3 via
 * SqliteRepository.
 */
export function openRepo(customDbPath, opts = {}) {
    if (opts.repo != null) {
        if (!(opts.repo instanceof Repository)) {
            throw new TypeError(`openRepo: opts.repo must be a Repository instance, got ${Object.prototype.toString.call(opts.repo)}`);
        }
        return { repo: opts.repo, close() { } };
    }
    // Respect explicit engine selection: opts.engine > CODEGRAPH_ENGINE env > auto.
    // This ensures --engine wasm and benchmark workers bypass the native path.
    const engine = opts.engine || process.env.CODEGRAPH_ENGINE || 'auto';
    // Try native rusqlite path first (Phase 6.14)
    if (engine !== 'wasm' && isNativeAvailable()) {
        try {
            return openRepoNative(customDbPath);
        }
        catch (e) {
            // Re-throw user-visible errors (e.g. DB not found) — only silently
            // fall back for native-engine failures (e.g. incompatible native binary).
            if (e instanceof DbError)
                throw e;
            // Re-throw locking/busy errors — falling back to better-sqlite3 would
            // hit the same contention (and potentially hang without busy_timeout).
            const msg = toErrorMessage(e);
            if (/\b(busy|locked|SQLITE_BUSY|SQLITE_LOCKED)\b/i.test(msg)) {
                throw new DbError(`Database is busy (another process may be writing): ${msg}`, {});
            }
            debug(`openRepo: native path failed, falling back to better-sqlite3: ${msg}`);
        }
    }
    const db = openReadonlyOrFail(customDbPath);
    return {
        repo: new SqliteRepository(db),
        close() {
            db.close();
        },
    };
}
/**
 * Open a readonly DB with an optional NativeDatabase alongside it.
 *
 * Returns the better-sqlite3 handle (for backwards compat) plus an optional
 * NativeDatabase for modules that can use batched Rust query methods.
 * Callers should use nativeDb when available and fall back to db.prepare().
 */
export function openReadonlyWithNative(customPath) {
    const db = openReadonlyOrFail(customPath);
    // Respect explicit engine selection, consistent with openRepo().
    const engine = process.env.CODEGRAPH_ENGINE || 'auto';
    let nativeDb;
    if (engine !== 'wasm' && isNativeAvailable()) {
        try {
            const dbPath = findDbPath(customPath);
            const native = getNative();
            nativeDb = native.NativeDatabase.openReadonly(dbPath);
        }
        catch (e) {
            const msg = toErrorMessage(e);
            if (/\b(busy|locked|SQLITE_BUSY|SQLITE_LOCKED)\b/i.test(msg)) {
                debug(`openReadonlyWithNative: native path busy, skipping native DB: ${msg}`);
            }
            else {
                debug(`openReadonlyWithNative: native path failed: ${msg}`);
            }
        }
    }
    return {
        db,
        nativeDb,
        close() {
            db.close();
            if (nativeDb) {
                try {
                    nativeDb.close();
                }
                catch (e) {
                    debug(`openReadonlyWithNative: native close failed: ${toErrorMessage(e)}`);
                }
            }
        },
    };
}
//# sourceMappingURL=connection.js.map