import { execFile, spawnSync } from "child_process";
import { existsSync, readFileSync, statSync, unwatchFile, watchFile } from "fs";
import { dirname, join, resolve } from "path";
import { closeWatcher, FS_WATCH_RETRY_DELAY_MS, isSafeFsWatchPathError, watchWithErrorHandler } from "../utils/fs-watch.js";
import { createGitEnvironment } from "../utils/git-env.js";
/**
 * Find git metadata paths by walking up from cwd.
 * Handles both regular git repos (.git is a directory) and worktrees (.git is a file).
 */
function findGitPaths(cwd) {
    let dir = cwd;
    while (true) {
        const gitPath = join(dir, ".git");
        if (existsSync(gitPath)) {
            try {
                const stat = statSync(gitPath);
                if (stat.isFile()) {
                    const content = readFileSync(gitPath, "utf8").trim();
                    if (content.startsWith("gitdir: ")) {
                        const gitDir = resolve(dir, content.slice(8).trim());
                        const headPath = join(gitDir, "HEAD");
                        if (!existsSync(headPath))
                            return null;
                        const commonDirPath = join(gitDir, "commondir");
                        const commonGitDir = existsSync(commonDirPath)
                            ? resolve(gitDir, readFileSync(commonDirPath, "utf8").trim())
                            : gitDir;
                        return { repoDir: dir, commonGitDir, headPath };
                    }
                }
                else if (stat.isDirectory()) {
                    const headPath = join(gitPath, "HEAD");
                    if (!existsSync(headPath))
                        return null;
                    return { repoDir: dir, commonGitDir: gitPath, headPath };
                }
            }
            catch {
                return null;
            }
        }
        const parent = dirname(dir);
        if (parent === dir)
            return null;
        dir = parent;
    }
}
/** Ask git for the current branch. Returns null on detached HEAD or if git is unavailable. */
function resolveBranchWithGitSync(repoDir) {
    const result = spawnSync("git", ["--no-optional-locks", "symbolic-ref", "--quiet", "--short", "HEAD"], {
        cwd: repoDir,
        encoding: "utf8",
        env: createGitEnvironment(),
        stdio: ["ignore", "pipe", "ignore"],
    });
    const branch = result.status === 0 ? result.stdout.trim() : "";
    return branch || null;
}
/** Ask git for the current branch asynchronously. Returns null on detached HEAD or if git is unavailable. */
function resolveBranchWithGitAsync(repoDir) {
    return new Promise((resolvePromise) => {
        execFile("git", ["--no-optional-locks", "symbolic-ref", "--quiet", "--short", "HEAD"], {
            cwd: repoDir,
            encoding: "utf8",
            env: createGitEnvironment(),
        }, (error, stdout) => {
            if (error) {
                resolvePromise(null);
                return;
            }
            const branch = stdout.trim();
            resolvePromise(branch || null);
        });
    });
}
function isWslEnvironment() {
    return process.platform === "linux" && !!(process.env.WSL_DISTRO_NAME || process.env.WSL_INTEROP);
}
function isWindowsMountedRepoPath(repoDir) {
    return /^\/mnt\/[a-z](?:\/|$)/i.test(repoDir);
}
function shouldPollGitHead(repoDir) {
    return isWslEnvironment() && isWindowsMountedRepoPath(repoDir);
}
/**
 * Provides git branch and extension statuses - data not otherwise accessible to extensions.
 * Token stats, model info available via ctx.sessionManager and ctx.model.
 */
export class FooterDataProvider {
    static { this.WATCH_DEBOUNCE_MS = 500; }
    constructor(cwd) {
        this.extensionStatuses = new Map();
        this.cachedBranch = undefined;
        this.gitPaths = undefined;
        this.headWatcher = null;
        this.headWatchFilePath = null;
        this.headWatchFileListener = null;
        this.reftableWatcher = null;
        this.reftableTablesListWatcher = null;
        this.reftableTablesListPath = null;
        this.reftableTablesListFingerprint = null;
        this.reftableTablesListWatchFileListener = null;
        this.branchChangeCallbacks = new Set();
        this.availableProviderCount = 0;
        this.refreshTimer = null;
        this.gitWatcherRetryTimer = null;
        this.refreshInFlight = false;
        this.refreshPending = false;
        this.disposed = false;
        this.gitWatchersStarted = false;
        this.cwd = cwd;
    }
    /** Start post-frame git branch watching. Branch discovery remains lazy for first render. */
    startGitWatcher() {
        if (this.disposed || this.gitWatchersStarted)
            return;
        this.gitWatchersStarted = true;
        this.ensureGitPaths();
        this.setupGitWatcher();
    }
    /** Current git branch, null if not in repo, "detached" if detached HEAD */
    getGitBranch() {
        if (this.cachedBranch === undefined) {
            this.ensureGitPaths();
            this.cachedBranch = this.resolveGitBranchSync();
        }
        return this.cachedBranch;
    }
    /** Extension status texts set via ctx.ui.setStatus() */
    getExtensionStatuses() {
        return this.extensionStatuses;
    }
    /** Subscribe to git branch changes. Returns unsubscribe function. */
    onBranchChange(callback) {
        this.branchChangeCallbacks.add(callback);
        return () => this.branchChangeCallbacks.delete(callback);
    }
    /** Internal: set extension status */
    setExtensionStatus(key, text) {
        if (text === undefined) {
            this.extensionStatuses.delete(key);
        }
        else {
            this.extensionStatuses.set(key, text);
        }
    }
    /** Internal: clear extension statuses */
    clearExtensionStatuses() {
        this.extensionStatuses.clear();
    }
    /** Number of unique providers with available models (for footer display) */
    getAvailableProviderCount() {
        return this.availableProviderCount;
    }
    /** Internal: update available provider count */
    setAvailableProviderCount(count) {
        this.availableProviderCount = count;
    }
    setCwd(cwd) {
        if (this.cwd === cwd) {
            return;
        }
        const restartGitWatcher = this.gitWatchersStarted;
        this.cwd = cwd;
        if (this.refreshTimer) {
            clearTimeout(this.refreshTimer);
            this.refreshTimer = null;
        }
        this.clearGitWatchers();
        this.cachedBranch = undefined;
        this.gitPaths = undefined;
        if (restartGitWatcher) {
            this.gitWatchersStarted = false;
            this.startGitWatcher();
        }
        this.notifyBranchChange();
    }
    /** Internal: cleanup */
    dispose() {
        this.disposed = true;
        if (this.refreshTimer) {
            clearTimeout(this.refreshTimer);
            this.refreshTimer = null;
        }
        this.clearGitWatchers();
        this.gitWatchersStarted = false;
        this.branchChangeCallbacks.clear();
    }
    ensureGitPaths() {
        if (this.gitPaths !== undefined)
            return;
        this.gitPaths = findGitPaths(this.cwd);
    }
    notifyBranchChange() {
        for (const cb of this.branchChangeCallbacks)
            cb();
    }
    scheduleRefresh() {
        if (this.disposed || this.refreshTimer)
            return;
        if (this.refreshInFlight) {
            this.refreshPending = true;
            return;
        }
        this.refreshTimer = setTimeout(() => {
            this.refreshTimer = null;
            void this.refreshGitBranchAsync();
        }, FooterDataProvider.WATCH_DEBOUNCE_MS);
    }
    async refreshGitBranchAsync() {
        if (this.disposed)
            return;
        if (this.refreshInFlight) {
            this.refreshPending = true;
            return;
        }
        this.refreshInFlight = true;
        try {
            const nextBranch = await this.resolveGitBranchAsync();
            if (this.disposed)
                return;
            if (this.cachedBranch !== undefined && this.cachedBranch !== nextBranch) {
                this.cachedBranch = nextBranch;
                this.notifyBranchChange();
                return;
            }
            this.cachedBranch = nextBranch;
        }
        finally {
            this.refreshInFlight = false;
            if (this.refreshPending && !this.disposed) {
                this.refreshPending = false;
                this.scheduleRefresh();
            }
        }
    }
    resolveGitBranchSync() {
        try {
            if (!this.gitPaths)
                return null;
            const content = readFileSync(this.gitPaths.headPath, "utf8").trim();
            if (content.startsWith("ref: refs/heads/")) {
                const branch = content.slice(16);
                return branch === ".invalid" ? (resolveBranchWithGitSync(this.gitPaths.repoDir) ?? "detached") : branch;
            }
            return "detached";
        }
        catch {
            return null;
        }
    }
    async resolveGitBranchAsync() {
        try {
            if (!this.gitPaths)
                return null;
            const content = readFileSync(this.gitPaths.headPath, "utf8").trim();
            if (content.startsWith("ref: refs/heads/")) {
                const branch = content.slice(16);
                return branch === ".invalid"
                    ? ((await resolveBranchWithGitAsync(this.gitPaths.repoDir)) ?? "detached")
                    : branch;
            }
            return "detached";
        }
        catch {
            return null;
        }
    }
    clearGitWatchers() {
        closeWatcher(this.headWatcher);
        this.headWatcher = null;
        if (this.headWatchFilePath && this.headWatchFileListener) {
            unwatchFile(this.headWatchFilePath, this.headWatchFileListener);
            this.headWatchFilePath = null;
            this.headWatchFileListener = null;
        }
        closeWatcher(this.reftableWatcher);
        this.reftableWatcher = null;
        closeWatcher(this.reftableTablesListWatcher);
        this.reftableTablesListWatcher = null;
        if (this.reftableTablesListPath && this.reftableTablesListWatchFileListener) {
            unwatchFile(this.reftableTablesListPath, this.reftableTablesListWatchFileListener);
        }
        this.reftableTablesListPath = null;
        this.reftableTablesListFingerprint = null;
        this.reftableTablesListWatchFileListener = null;
        if (this.gitWatcherRetryTimer) {
            clearTimeout(this.gitWatcherRetryTimer);
            this.gitWatcherRetryTimer = null;
        }
    }
    installHeadPollingFallback() {
        if (!this.gitPaths || this.headWatchFilePath || this.headWatchFileListener) {
            return;
        }
        this.headWatchFilePath = this.gitPaths.headPath;
        this.headWatchFileListener = (current, previous) => {
            if (current.mtimeMs !== previous.mtimeMs || current.ctimeMs !== previous.ctimeMs || current.size !== previous.size) {
                this.scheduleRefresh();
            }
        };
        watchFile(this.headWatchFilePath, { interval: 1000 }, this.headWatchFileListener);
    }
    installReftableTablesListPolling(tablesListPath) {
        if (this.reftableTablesListWatchFileListener) {
            return;
        }
        this.reftableTablesListWatchFileListener = (current, previous) => {
            if (current.mtimeMs !== previous.mtimeMs ||
                current.ctimeMs !== previous.ctimeMs ||
                current.size !== previous.size) {
                this.scheduleReftableRefresh();
            }
        };
        watchFile(tablesListPath, { interval: 250 }, this.reftableTablesListWatchFileListener);
    }
    scheduleGitWatcherRetry() {
        if (this.disposed || !this.gitWatchersStarted || this.gitWatcherRetryTimer) {
            return;
        }
        this.gitWatcherRetryTimer = setTimeout(() => {
            this.gitWatcherRetryTimer = null;
            this.ensureGitPaths();
            this.setupGitWatcher();
        }, FS_WATCH_RETRY_DELAY_MS);
    }
    handleGitWatcherError() {
        this.clearGitWatchers();
        this.scheduleGitWatcherRetry();
    }
    readReftableTablesListFingerprint() {
        if (!this.reftableTablesListPath || !existsSync(this.reftableTablesListPath)) {
            return null;
        }
        try {
            const stat = statSync(this.reftableTablesListPath);
            const content = readFileSync(this.reftableTablesListPath, "utf8");
            return `${stat.size}:${stat.mtimeMs}:${stat.ctimeMs}:${content}`;
        }
        catch {
            return null;
        }
    }
    scheduleReftableRefresh() {
        const fingerprint = this.readReftableTablesListFingerprint();
        if (fingerprint !== null && fingerprint === this.reftableTablesListFingerprint) {
            return;
        }
        this.reftableTablesListFingerprint = fingerprint;
        this.scheduleRefresh();
    }
    handleReftableDirectoryEvent(filename) {
        if (filename === "tables.list") {
            this.scheduleReftableRefresh();
            return;
        }
        this.scheduleRefresh();
    }
    setupGitWatcher() {
        this.clearGitWatchers();
        this.ensureGitPaths();
        if (!this.gitPaths)
            return;
        const pollGitHead = shouldPollGitHead(this.gitPaths.repoDir);
        // Watch the directory containing HEAD, not HEAD itself.
        // Git uses atomic writes (write temp, rename over HEAD), which changes the inode.
        // fs.watch on a file stops working after the inode changes.
        this.headWatcher = watchWithErrorHandler(dirname(this.gitPaths.headPath), (_eventType, filename) => {
            if (!filename || filename === "HEAD") {
                this.scheduleRefresh();
            }
        }, (error) => {
            if (isSafeFsWatchPathError(error)) {
                this.installHeadPollingFallback();
                return;
            }
            this.handleGitWatcherError();
        });
        if (pollGitHead) {
            this.installHeadPollingFallback();
        }
        if (!this.headWatcher && !this.headWatchFileListener) {
            return;
        }
        // In reftable repos, branch switches update files in the reftable directory
        // instead of HEAD. Watch it separately so the footer picks up those changes.
        const reftableDir = join(this.gitPaths.commonGitDir, "reftable");
        if (existsSync(reftableDir)) {
            this.reftableTablesListPath = join(reftableDir, "tables.list");
            this.reftableTablesListFingerprint = this.readReftableTablesListFingerprint();
            this.reftableWatcher = watchWithErrorHandler(reftableDir, (_eventType, filename) => {
                this.handleReftableDirectoryEvent(filename);
            }, (error) => {
                if (isSafeFsWatchPathError(error)) {
                    return;
                }
                this.handleGitWatcherError();
            });
            const tablesListPath = this.reftableTablesListPath;
            if (tablesListPath && existsSync(tablesListPath)) {
                this.reftableTablesListWatcher = watchWithErrorHandler(tablesListPath, () => {
                    this.scheduleReftableRefresh();
                }, (error) => {
                    if (isSafeFsWatchPathError(error)) {
                        this.installReftableTablesListPolling(tablesListPath);
                        return;
                    }
                    this.handleGitWatcherError();
                });
                this.installReftableTablesListPolling(tablesListPath);
            }
        }
    }
}
//# sourceMappingURL=footer-data-provider.js.map