import { execFile } from "node:child_process";
import { watch } from "node:fs";
import { readFile } from "node:fs/promises";
import { join, resolve } from "node:path";
import { promisify } from "node:util";
import { getCheckoutShortstat, getCheckoutStatus, getPullRequestStatus, hasOriginRemote, resolveGhPath, resolveAbsoluteGitDir, } from "../utils/checkout-git.js";
import { normalizeWorkspaceId } from "./workspace-registry-model.js";
const execFileAsync = promisify(execFile);
const WORKSPACE_GIT_WATCH_DEBOUNCE_MS = 500;
const BACKGROUND_GIT_FETCH_INTERVAL_MS = 180000;
export class WorkspaceGitServiceImpl {
    constructor(options) {
        this.workspaceTargets = new Map();
        this.repoTargets = new Map();
        this.workspaceTargetSetups = new Map();
        this.logger = options.logger.child({ module: "workspace-git-service" });
        this.seaworkHome = options.seaworkHome;
        this.deps = {
            watch,
            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,
            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;
        }
        return 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);
    }
    dispose() {
        for (const target of this.workspaceTargets.values()) {
            this.closeWorkspaceTarget(target);
        }
        this.workspaceTargets.clear();
        for (const target of this.repoTargets.values()) {
            this.closeRepoTarget(target);
        }
        this.repoTargets.clear();
        this.workspaceTargetSetups.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 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 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(target) {
        if (target.debounceTimer) {
            clearTimeout(target.debounceTimer);
        }
        target.debounceTimer = setTimeout(() => {
            target.debounceTimer = null;
            void this.refreshWorkspaceTarget(target);
        }, WORKSPACE_GIT_WATCH_DEBOUNCE_MS);
    }
    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, { seaworkHome: this.seaworkHome }, 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);
    }
    closeWorkspaceTarget(target) {
        if (target.debounceTimer) {
            clearTimeout(target.debounceTimer);
            target.debounceTimer = null;
        }
        for (const watcher of target.watchers) {
            watcher.close();
        }
        target.watchers = [];
        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.isSeaworkOwnedWorktree ? checkoutStatus.mainRepoRoot : null,
            currentBranch: checkoutStatus.currentBranch,
            remoteUrl: checkoutStatus.remoteUrl,
            isSeaworkOwnedWorktree: checkoutStatus.isSeaworkOwnedWorktree,
            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,
            isSeaworkOwnedWorktree: 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 execFileAsync("git", ["fetch", "origin", "--prune"], {
        cwd,
        env: {
            ...process.env,
            GIT_TERMINAL_PROMPT: "0",
        },
    });
}
//# sourceMappingURL=workspace-git-service.js.map