import { watch } from "node:fs";
import { readFile, readdir } from "node:fs/promises";
import { join, resolve } from "node:path";
import { getCheckoutShortstat, getCheckoutStatus, getPullRequestStatus, hasOriginRemote, resolveGhPath, resolveAbsoluteGitDir, } from "../utils/checkout-git.js";
import { parseGitRevParsePath } from "../utils/git-rev-parse-path.js";
import { runGitCommand } from "../utils/run-git-command.js";
import { READ_ONLY_GIT_ENV } from "./checkout-git-utils.js";
import { normalizeWorkspaceId } from "./workspace-registry-model.js";
const WORKSPACE_GIT_WATCH_DEBOUNCE_MS = 500;
const BACKGROUND_GIT_FETCH_INTERVAL_MS = 180000;
const WORKING_TREE_WATCH_FALLBACK_REFRESH_MS = 5000;
export class WorkspaceGitServiceImpl {
    constructor(options) {
        this.workspaceTargets = new Map();
        this.repoTargets = new Map();
        this.workspaceTargetSetups = new Map();
        this.workingTreeWatchTargets = new Map();
        this.workingTreeWatchSetups = new Map();
        this.logger = options.logger.child({ module: "workspace-git-service" });
        this.paseoHome = options.paseoHome;
        this.deps = {
            watch: options.deps?.watch ?? watch,
            readdir: options.deps?.readdir ?? readdir,
            getCheckoutStatus: options.deps?.getCheckoutStatus ?? getCheckoutStatus,
            getCheckoutShortstat: options.deps?.getCheckoutShortstat ?? getCheckoutShortstat,
            getPullRequestStatus: options.deps?.getPullRequestStatus ?? getPullRequestStatus,
            resolveGhPath: options.deps?.resolveGhPath ?? resolveGhPath,
            resolveAbsoluteGitDir: options.deps?.resolveAbsoluteGitDir ?? resolveAbsoluteGitDir,
            hasOriginRemote: options.deps?.hasOriginRemote ?? hasOriginRemote,
            runGitFetch: options.deps?.runGitFetch ?? runGitFetch,
            runGitCommand: options.deps?.runGitCommand ?? runGitCommand,
            now: options.deps?.now ?? (() => new Date()),
        };
    }
    async subscribe(params, listener) {
        const cwd = normalizeWorkspaceId(params.cwd);
        const target = await this.ensureWorkspaceTarget(cwd);
        target.listeners.add(listener);
        return {
            initial: target.latestSnapshot ?? (await this.getSnapshot(cwd)),
            unsubscribe: () => {
                this.removeWorkspaceListener(cwd, listener);
            },
        };
    }
    async getSnapshot(cwd) {
        cwd = normalizeWorkspaceId(cwd);
        const target = this.workspaceTargets.get(cwd);
        if (target?.latestSnapshot) {
            return target.latestSnapshot;
        }
        const ensuredTarget = await this.ensureWorkspaceTarget(cwd);
        return ensuredTarget.latestSnapshot ?? (await this.refreshSnapshot(cwd));
    }
    peekSnapshot(cwd) {
        cwd = normalizeWorkspaceId(cwd);
        return this.workspaceTargets.get(cwd)?.latestSnapshot ?? null;
    }
    async refresh(cwd, _options) {
        cwd = normalizeWorkspaceId(cwd);
        const target = this.workspaceTargets.get(cwd);
        if (target) {
            await this.refreshWorkspaceTarget(target);
            return;
        }
        await this.ensureWorkspaceTarget(cwd);
    }
    async requestWorkingTreeWatch(cwd, onChange) {
        cwd = normalizeWorkspaceId(cwd);
        const target = await this.ensureWorkingTreeWatchTarget(cwd);
        target.listeners.add(onChange);
        return {
            repoRoot: target.repoRoot,
            unsubscribe: () => {
                this.removeWorkingTreeWatchListener(cwd, onChange);
            },
        };
    }
    scheduleRefreshForCwd(cwd) {
        cwd = normalizeWorkspaceId(cwd);
        const target = this.workspaceTargets.get(cwd);
        if (target) {
            this.scheduleWorkspaceRefresh(target);
        }
    }
    dispose() {
        for (const target of this.workspaceTargets.values()) {
            this.closeWorkspaceTarget(target);
        }
        this.workspaceTargets.clear();
        this.workspaceTargetSetups.clear();
        for (const target of this.repoTargets.values()) {
            this.closeRepoTarget(target);
        }
        this.repoTargets.clear();
        for (const target of this.workingTreeWatchTargets.values()) {
            this.closeWorkingTreeWatchTarget(target);
        }
        this.workingTreeWatchTargets.clear();
        this.workingTreeWatchSetups.clear();
    }
    async ensureWorkspaceTarget(cwd) {
        const existingTarget = this.workspaceTargets.get(cwd);
        if (existingTarget) {
            return existingTarget;
        }
        const existingSetup = this.workspaceTargetSetups.get(cwd);
        if (existingSetup) {
            return existingSetup;
        }
        const setup = this.createWorkspaceTarget(cwd).finally(() => {
            this.workspaceTargetSetups.delete(cwd);
        });
        this.workspaceTargetSetups.set(cwd, setup);
        return setup;
    }
    async ensureWorkingTreeWatchTarget(cwd) {
        const existingTarget = this.workingTreeWatchTargets.get(cwd);
        if (existingTarget) {
            return existingTarget;
        }
        const existingSetup = this.workingTreeWatchSetups.get(cwd);
        if (existingSetup) {
            return existingSetup;
        }
        const setup = this.createWorkingTreeWatchTarget(cwd).finally(() => {
            this.workingTreeWatchSetups.delete(cwd);
        });
        this.workingTreeWatchSetups.set(cwd, setup);
        return setup;
    }
    async createWorkspaceTarget(cwd) {
        const target = {
            cwd,
            listeners: new Set(),
            watchers: [],
            debounceTimer: null,
            refreshPromise: null,
            refreshQueued: false,
            latestSnapshot: null,
            latestFingerprint: null,
            repoGitRoot: null,
        };
        const initial = await this.refreshSnapshot(cwd);
        this.rememberSnapshot(target, initial);
        this.workspaceTargets.set(cwd, target);
        const gitDir = await this.deps.resolveAbsoluteGitDir(cwd);
        if (!gitDir) {
            return target;
        }
        const repoGitRoot = await this.resolveWorkspaceGitRefsRoot(gitDir);
        target.repoGitRoot = repoGitRoot;
        this.startWorkspaceWatchers(target, gitDir, repoGitRoot);
        await this.ensureRepoTarget(target);
        return target;
    }
    async createWorkingTreeWatchTarget(cwd) {
        const repoRoot = await this.resolveCheckoutWatchRoot(cwd);
        const target = {
            cwd,
            repoRoot,
            repoWatchPath: null,
            watchers: [],
            watchedPaths: new Set(),
            fallbackRefreshInterval: null,
            linuxTreeRefreshPromise: null,
            linuxTreeRefreshQueued: false,
            listeners: new Set(),
        };
        const repoWatchPath = repoRoot ?? cwd;
        target.repoWatchPath = repoWatchPath;
        const watchPaths = new Set([repoWatchPath]);
        const gitDir = await this.deps.resolveAbsoluteGitDir(cwd);
        if (gitDir) {
            watchPaths.add(gitDir);
        }
        let hasRecursiveRepoCoverage = false;
        const allowRecursiveRepoWatch = process.platform !== "linux";
        if (process.platform === "linux") {
            hasRecursiveRepoCoverage = await this.ensureLinuxRepoTreeWatchers(target, repoWatchPath);
        }
        for (const watchPath of watchPaths) {
            if (process.platform === "linux" && watchPath === repoWatchPath) {
                continue;
            }
            const shouldTryRecursive = watchPath === repoWatchPath && allowRecursiveRepoWatch;
            const watcherIsRecursive = this.addWorkingTreeWatcher(target, watchPath, shouldTryRecursive);
            if (watchPath === repoWatchPath && watcherIsRecursive) {
                hasRecursiveRepoCoverage = true;
            }
        }
        const missingRepoCoverage = repoRoot === null || !hasRecursiveRepoCoverage;
        if (target.watchers.length === 0 || missingRepoCoverage) {
            target.fallbackRefreshInterval = setInterval(() => {
                this.scheduleWorkspaceRefresh(cwd);
                for (const listener of target.listeners) {
                    listener();
                }
            }, WORKING_TREE_WATCH_FALLBACK_REFRESH_MS);
            this.logger.warn({
                cwd,
                intervalMs: WORKING_TREE_WATCH_FALLBACK_REFRESH_MS,
                reason: target.watchers.length === 0 ? "no_watchers" : "missing_recursive_repo_root_coverage",
            }, "Working tree watchers unavailable; using timed refresh fallback");
        }
        this.workingTreeWatchTargets.set(cwd, target);
        return target;
    }
    async resolveCheckoutWatchRoot(cwd) {
        try {
            const { stdout } = await this.deps.runGitCommand(["rev-parse", "--show-toplevel"], {
                cwd,
                env: READ_ONLY_GIT_ENV,
            });
            return parseGitRevParsePath(stdout);
        }
        catch {
            return null;
        }
    }
    async resolveWorkspaceGitRefsRoot(gitDir) {
        try {
            const commonDir = (await readFile(join(gitDir, "commondir"), "utf8")).trim();
            if (commonDir.length > 0) {
                return resolve(gitDir, commonDir);
            }
        }
        catch {
            return gitDir;
        }
        return gitDir;
    }
    startWorkspaceWatchers(target, gitDir, repoGitRoot) {
        for (const watchPath of new Set([join(gitDir, "HEAD"), join(repoGitRoot, "refs", "heads")])) {
            let watcher = null;
            try {
                watcher = this.deps.watch(watchPath, { recursive: false }, () => {
                    this.scheduleWorkspaceRefresh(target);
                });
            }
            catch (error) {
                this.logger.warn({ err: error, cwd: target.cwd, watchPath }, "Failed to start workspace git watcher");
            }
            if (!watcher) {
                continue;
            }
            watcher.on("error", (error) => {
                this.logger.warn({ err: error, cwd: target.cwd, watchPath }, "Workspace git watcher error");
            });
            target.watchers.push(watcher);
        }
    }
    async ensureRepoTarget(workspaceTarget) {
        const repoGitRoot = workspaceTarget.repoGitRoot;
        if (!repoGitRoot) {
            return;
        }
        const existingTarget = this.repoTargets.get(repoGitRoot);
        if (existingTarget) {
            existingTarget.workspaceKeys.add(workspaceTarget.cwd);
            return;
        }
        const hasOrigin = await this.deps.hasOriginRemote(workspaceTarget.cwd);
        if (!hasOrigin) {
            return;
        }
        const targetAfterProbe = this.repoTargets.get(repoGitRoot);
        if (targetAfterProbe) {
            targetAfterProbe.workspaceKeys.add(workspaceTarget.cwd);
            return;
        }
        const repoTarget = {
            repoGitRoot,
            cwd: workspaceTarget.cwd,
            workspaceKeys: new Set([workspaceTarget.cwd]),
            intervalId: setInterval(() => {
                void this.runRepoFetch(repoTarget);
            }, BACKGROUND_GIT_FETCH_INTERVAL_MS),
            fetchInFlight: false,
        };
        this.repoTargets.set(repoGitRoot, repoTarget);
        void this.runRepoFetch(repoTarget);
    }
    scheduleWorkspaceRefresh(targetOrCwd) {
        const target = typeof targetOrCwd === "string"
            ? this.workspaceTargets.get(normalizeWorkspaceId(targetOrCwd))
            : targetOrCwd;
        if (!target) {
            return;
        }
        if (target.debounceTimer) {
            clearTimeout(target.debounceTimer);
        }
        target.debounceTimer = setTimeout(() => {
            target.debounceTimer = null;
            void this.refreshWorkspaceTarget(target);
        }, WORKSPACE_GIT_WATCH_DEBOUNCE_MS);
    }
    addWorkingTreeWatcher(target, watchPath, shouldTryRecursive) {
        if (target.watchedPaths.has(watchPath)) {
            return false;
        }
        const { cwd } = target;
        const onChange = () => {
            if (process.platform === "linux" && target.repoWatchPath) {
                void this.refreshLinuxRepoTreeWatchers(target);
            }
            this.scheduleWorkspaceRefresh(cwd);
            for (const listener of target.listeners) {
                listener();
            }
        };
        const createWatcher = (recursive) => this.deps.watch(watchPath, { recursive }, () => {
            onChange();
        });
        let watcher = null;
        let watcherIsRecursive = false;
        try {
            if (shouldTryRecursive) {
                watcher = createWatcher(true);
                watcherIsRecursive = true;
            }
            else {
                watcher = createWatcher(false);
            }
        }
        catch (error) {
            if (shouldTryRecursive) {
                try {
                    watcher = createWatcher(false);
                    this.logger.warn({ err: error, watchPath, cwd }, "Working tree recursive watch unavailable; using non-recursive fallback");
                }
                catch (fallbackError) {
                    this.logger.warn({ err: fallbackError, watchPath, cwd }, "Failed to start working tree watcher");
                }
            }
            else {
                this.logger.warn({ err: error, watchPath, cwd }, "Failed to start working tree watcher");
            }
        }
        if (!watcher) {
            return false;
        }
        watcher.on("error", (error) => {
            this.logger.warn({ err: error, watchPath, cwd }, "Working tree watcher error");
        });
        target.watchers.push(watcher);
        target.watchedPaths.add(watchPath);
        return watcherIsRecursive;
    }
    async ensureLinuxRepoTreeWatchers(target, rootPath) {
        const directories = await this.listLinuxWatchDirectories(rootPath);
        let complete = true;
        for (const directory of directories) {
            const watcherWasRecursive = this.addWorkingTreeWatcher(target, directory, false);
            if (!watcherWasRecursive && !target.watchedPaths.has(directory)) {
                complete = false;
            }
        }
        return complete && target.watchedPaths.has(rootPath);
    }
    async refreshLinuxRepoTreeWatchers(target) {
        if (process.platform !== "linux" || !target.repoWatchPath) {
            return;
        }
        const rootPath = target.repoWatchPath;
        if (target.linuxTreeRefreshPromise) {
            target.linuxTreeRefreshQueued = true;
            return;
        }
        target.linuxTreeRefreshPromise = (async () => {
            do {
                target.linuxTreeRefreshQueued = false;
                try {
                    await this.ensureLinuxRepoTreeWatchers(target, rootPath);
                }
                catch (error) {
                    this.logger.warn({
                        err: error,
                        cwd: target.cwd,
                        rootPath,
                    }, "Failed to refresh Linux working tree watchers");
                }
            } while (target.linuxTreeRefreshQueued);
        })();
        try {
            await target.linuxTreeRefreshPromise;
        }
        finally {
            target.linuxTreeRefreshPromise = null;
        }
    }
    async listLinuxWatchDirectories(rootPath) {
        const directories = [];
        const pending = [rootPath];
        while (pending.length > 0) {
            const directory = pending.pop();
            if (!directory) {
                continue;
            }
            directories.push(directory);
            let entries;
            try {
                entries = await this.deps.readdir(directory, { withFileTypes: true });
            }
            catch {
                continue;
            }
            for (const entry of entries) {
                if (!entry.isDirectory() || entry.name === ".git") {
                    continue;
                }
                pending.push(join(directory, entry.name));
            }
        }
        return directories;
    }
    async refreshWorkspaceTarget(target) {
        if (target.refreshPromise) {
            target.refreshQueued = true;
            return;
        }
        target.refreshPromise = (async () => {
            do {
                target.refreshQueued = false;
                try {
                    const snapshot = await this.refreshSnapshot(target.cwd);
                    this.rememberSnapshot(target, snapshot, { notify: true });
                }
                catch (error) {
                    this.logger.warn({ err: error, cwd: target.cwd }, "Failed to refresh workspace git snapshot");
                }
            } while (target.refreshQueued);
        })();
        try {
            await target.refreshPromise;
        }
        finally {
            target.refreshPromise = null;
        }
    }
    async refreshSnapshot(cwd) {
        return loadWorkspaceGitRuntimeSnapshot(cwd, { paseoHome: this.paseoHome }, this.deps.now(), this.deps);
    }
    rememberSnapshot(target, snapshot, options) {
        target.latestSnapshot = snapshot;
        const fingerprint = JSON.stringify(snapshot);
        if (target.latestFingerprint === fingerprint) {
            return;
        }
        target.latestFingerprint = fingerprint;
        if (!options?.notify) {
            return;
        }
        for (const listener of target.listeners) {
            listener(snapshot);
        }
    }
    async runRepoFetch(target) {
        if (target.fetchInFlight) {
            return;
        }
        target.fetchInFlight = true;
        this.logger.debug({ repoGitRoot: target.repoGitRoot, cwd: target.cwd }, "Running background git fetch");
        try {
            await this.deps.runGitFetch(target.cwd);
        }
        catch (error) {
            this.logger.warn({ err: error, repoGitRoot: target.repoGitRoot, cwd: target.cwd }, "Background git fetch failed");
        }
        finally {
            target.fetchInFlight = false;
            await Promise.all(Array.from(target.workspaceKeys, async (workspaceKey) => {
                const workspaceTarget = this.workspaceTargets.get(workspaceKey);
                if (!workspaceTarget) {
                    return;
                }
                await this.refreshWorkspaceTarget(workspaceTarget);
            }));
        }
    }
    removeWorkspaceListener(cwd, listener) {
        const target = this.workspaceTargets.get(cwd);
        if (!target) {
            return;
        }
        target.listeners.delete(listener);
        if (target.listeners.size > 0) {
            return;
        }
        this.removeWorkspaceTarget(target);
    }
    removeWorkspaceTarget(target) {
        if (target.repoGitRoot) {
            const repoTarget = this.repoTargets.get(target.repoGitRoot);
            repoTarget?.workspaceKeys.delete(target.cwd);
            if (repoTarget && repoTarget.workspaceKeys.size === 0) {
                this.closeRepoTarget(repoTarget);
                this.repoTargets.delete(target.repoGitRoot);
            }
        }
        this.closeWorkspaceTarget(target);
        this.workspaceTargets.delete(target.cwd);
    }
    removeWorkingTreeWatchListener(cwd, listener) {
        const target = this.workingTreeWatchTargets.get(cwd);
        if (!target) {
            return;
        }
        target.listeners.delete(listener);
        if (target.listeners.size > 0) {
            return;
        }
        this.closeWorkingTreeWatchTarget(target);
        this.workingTreeWatchTargets.delete(cwd);
    }
    closeWorkspaceTarget(target) {
        if (target.debounceTimer) {
            clearTimeout(target.debounceTimer);
            target.debounceTimer = null;
        }
        for (const watcher of target.watchers) {
            watcher.close();
        }
        target.watchers = [];
        target.listeners.clear();
    }
    closeWorkingTreeWatchTarget(target) {
        if (target.fallbackRefreshInterval) {
            clearInterval(target.fallbackRefreshInterval);
            target.fallbackRefreshInterval = null;
        }
        for (const watcher of target.watchers) {
            watcher.close();
        }
        target.watchers = [];
        target.watchedPaths.clear();
        target.listeners.clear();
    }
    closeRepoTarget(target) {
        if (target.intervalId) {
            clearInterval(target.intervalId);
            target.intervalId = null;
        }
        target.workspaceKeys.clear();
    }
}
async function loadWorkspaceGitRuntimeSnapshot(cwd, context, now, deps) {
    const checkoutStatus = await deps.getCheckoutStatus(cwd, context);
    if (!checkoutStatus.isGit) {
        return buildNotGitSnapshot(cwd);
    }
    const [diffStat, github] = await Promise.all([
        deps.getCheckoutShortstat(cwd, context),
        loadGitHubSnapshot({
            cwd,
            remoteUrl: checkoutStatus.remoteUrl,
            now,
            deps,
        }),
    ]);
    return {
        cwd,
        git: {
            isGit: true,
            repoRoot: checkoutStatus.repoRoot,
            mainRepoRoot: checkoutStatus.isPaseoOwnedWorktree ? checkoutStatus.mainRepoRoot : null,
            currentBranch: checkoutStatus.currentBranch,
            remoteUrl: checkoutStatus.remoteUrl,
            isPaseoOwnedWorktree: checkoutStatus.isPaseoOwnedWorktree,
            isDirty: checkoutStatus.isDirty,
            aheadBehind: checkoutStatus.aheadBehind,
            aheadOfOrigin: checkoutStatus.aheadOfOrigin,
            behindOfOrigin: checkoutStatus.behindOfOrigin,
            diffStat,
        },
        github,
    };
}
async function loadGitHubSnapshot(options) {
    if (!hasGitHubRemoteUrl(options.remoteUrl)) {
        return {
            featuresEnabled: false,
            pullRequest: null,
            error: null,
            refreshedAt: null,
        };
    }
    try {
        await options.deps.resolveGhPath();
    }
    catch {
        return {
            featuresEnabled: false,
            pullRequest: null,
            error: null,
            refreshedAt: null,
        };
    }
    try {
        const result = await options.deps.getPullRequestStatus(options.cwd);
        return {
            featuresEnabled: true,
            pullRequest: result.status,
            error: null,
            refreshedAt: options.now.toISOString(),
        };
    }
    catch (error) {
        return {
            featuresEnabled: true,
            pullRequest: null,
            error: {
                message: error instanceof Error ? error.message : String(error),
            },
            refreshedAt: options.now.toISOString(),
        };
    }
}
function hasGitHubRemoteUrl(remoteUrl) {
    if (!remoteUrl) {
        return false;
    }
    return (remoteUrl.includes("github.com/") ||
        remoteUrl.startsWith("git@github.com:") ||
        remoteUrl.startsWith("ssh://git@github.com/"));
}
function buildNotGitSnapshot(cwd) {
    return {
        cwd,
        git: {
            isGit: false,
            repoRoot: null,
            mainRepoRoot: null,
            currentBranch: null,
            remoteUrl: null,
            isPaseoOwnedWorktree: false,
            isDirty: null,
            aheadBehind: null,
            aheadOfOrigin: null,
            behindOfOrigin: null,
            diffStat: null,
        },
        github: {
            featuresEnabled: false,
            pullRequest: null,
            error: null,
            refreshedAt: null,
        },
    };
}
async function runGitFetch(cwd) {
    await runGitCommand(["fetch", "origin", "--prune"], {
        cwd,
        env: {
            ...process.env,
            GIT_TERMINAL_PROMPT: "0",
        },
        timeout: 120000,
    });
}
//# sourceMappingURL=workspace-git-service.js.map