import { existsSync } from "node:fs";
import { normalizeWorkspaceId } from "./workspace-registry-model.js";
const DEFAULT_RECONCILE_INTERVAL_MS = 60000;
function deriveWorkspaceKindFromMetadata(metadata) {
    if (metadata.projectKind !== "git")
        return "directory";
    if (metadata.isWorktree)
        return "worktree";
    return "local_checkout";
}
function chooseCanonicalProject(projects) {
    return [...projects].sort((left, right) => {
        const leftRemote = left.projectId.startsWith("remote:");
        const rightRemote = right.projectId.startsWith("remote:");
        if (leftRemote !== rightRemote) {
            return leftRemote ? -1 : 1;
        }
        const createdAt = Date.parse(left.createdAt) - Date.parse(right.createdAt);
        if (createdAt !== 0) {
            return createdAt;
        }
        return left.projectId.localeCompare(right.projectId);
    })[0];
}
export class WorkspaceReconciliationService {
    constructor(options) {
        this.timer = null;
        this.running = false;
        this.projectRegistry = options.projectRegistry;
        this.workspaceRegistry = options.workspaceRegistry;
        this.logger = options.logger.child({ module: "workspace-reconciliation" });
        this.intervalMs = options.intervalMs ?? DEFAULT_RECONCILE_INTERVAL_MS;
        this.onChanges = options.onChanges ?? null;
        this.workspaceGitService = options.workspaceGitService ?? null;
    }
    start() {
        if (this.timer)
            return;
        this.logger.info({ intervalMs: this.intervalMs }, "Starting workspace reconciliation service");
        this.timer = setInterval(() => void this.runSafe(), this.intervalMs);
        // Run once immediately on start
        void this.runSafe();
    }
    stop() {
        if (this.timer) {
            clearInterval(this.timer);
            this.timer = null;
        }
    }
    async runOnce() {
        return this.reconcile();
    }
    async runSafe() {
        if (this.running)
            return;
        this.running = true;
        try {
            const result = await this.reconcile();
            if (result.changesApplied.length > 0) {
                this.logger.info({ changeCount: result.changesApplied.length, durationMs: result.durationMs }, "Reconciliation pass completed with changes");
            }
        }
        catch (error) {
            this.logger.error({ err: error }, "Reconciliation pass failed");
        }
        finally {
            this.running = false;
        }
    }
    async reconcile() {
        const start = Date.now();
        const changes = [];
        const allProjects = await this.projectRegistry.list();
        const allWorkspaces = await this.workspaceRegistry.list();
        const activeProjects = allProjects.filter((p) => !p.archivedAt);
        const activeWorkspaces = allWorkspaces.filter((w) => !w.archivedAt);
        const workspacesByProject = new Map();
        for (const workspace of activeWorkspaces) {
            const list = workspacesByProject.get(workspace.projectId) ?? [];
            list.push(workspace);
            workspacesByProject.set(workspace.projectId, list);
        }
        // 1. Archive workspaces whose directories no longer exist
        const missingWorkspaces = activeWorkspaces.filter((workspace) => !existsSync(workspace.cwd));
        await Promise.all(missingWorkspaces.map(async (workspace) => {
            const timestamp = new Date().toISOString();
            await this.workspaceRegistry.archive(workspace.workspaceId, timestamp);
            changes.push({
                kind: "workspace_archived",
                workspaceId: workspace.workspaceId,
                directory: workspace.cwd,
                reason: "directory_missing",
            });
            // Update the in-memory list for the project orphan check below
            const siblings = workspacesByProject.get(workspace.projectId);
            if (siblings) {
                const updated = siblings.filter((w) => w.workspaceId !== workspace.workspaceId);
                workspacesByProject.set(workspace.projectId, updated);
            }
        }));
        // 2. Merge duplicate active project records that point at the same repo root.
        await this.mergeDuplicateProjectsByRoot(activeProjects, workspacesByProject, changes);
        // 3. Archive orphaned projects (all workspaces archived/removed)
        const orphanedProjects = activeProjects.filter((project) => {
            const siblings = workspacesByProject.get(project.projectId) ?? [];
            return siblings.length === 0;
        });
        await Promise.all(orphanedProjects.map(async (project) => {
            const timestamp = new Date().toISOString();
            await this.projectRegistry.archive(project.projectId, timestamp);
            changes.push({
                kind: "project_archived",
                projectId: project.projectId,
                directory: project.rootPath,
                reason: "no_active_workspaces",
            });
        }));
        // 4. Reconcile git metadata for active projects whose directories still exist
        const projectsToReconcile = activeProjects.filter((project) => {
            if (project.archivedAt)
                return false;
            const siblings = workspacesByProject.get(project.projectId) ?? [];
            if (siblings.length === 0)
                return false;
            if (!existsSync(project.rootPath))
                return false;
            return true;
        });
        await Promise.all(projectsToReconcile.map((project) => this.reconcileProject(project, workspacesByProject.get(project.projectId) ?? [], changes)));
        if (changes.length > 0 && this.onChanges) {
            this.onChanges(changes);
        }
        return { changesApplied: changes, durationMs: Date.now() - start };
    }
    async mergeDuplicateProjectsByRoot(activeProjects, workspacesByProject, changes) {
        const projectsByRoot = new Map();
        for (const project of activeProjects) {
            if (project.kind !== "git") {
                continue;
            }
            const rootKey = normalizeWorkspaceId(project.rootPath);
            const group = projectsByRoot.get(rootKey) ?? [];
            group.push(project);
            projectsByRoot.set(rootKey, group);
        }
        for (const duplicates of projectsByRoot.values()) {
            if (duplicates.length < 2) {
                continue;
            }
            const canonical = chooseCanonicalProject(duplicates);
            const duplicateProjects = duplicates.filter((project) => project.projectId !== canonical.projectId);
            await this.mergeDuplicateProjectCustomName(canonical, duplicateProjects, changes);
            await Promise.all(duplicateProjects.flatMap((project) => (workspacesByProject.get(project.projectId) ?? []).map(async (workspace) => {
                const timestamp = new Date().toISOString();
                const updatedWorkspace = {
                    ...workspace,
                    projectId: canonical.projectId,
                    updatedAt: timestamp,
                };
                await this.workspaceRegistry.upsert(updatedWorkspace);
                changes.push({
                    kind: "workspace_updated",
                    workspaceId: workspace.workspaceId,
                    directory: workspace.cwd,
                    fields: {
                        projectId: canonical.projectId,
                    },
                });
                const canonicalSiblings = workspacesByProject.get(canonical.projectId) ?? [];
                canonicalSiblings.push(updatedWorkspace);
                workspacesByProject.set(canonical.projectId, canonicalSiblings);
            })));
            for (const project of duplicateProjects) {
                workspacesByProject.set(project.projectId, []);
            }
        }
    }
    async mergeDuplicateProjectCustomName(canonical, duplicateProjects, changes) {
        if (canonical.customName) {
            return;
        }
        const customName = duplicateProjects.find((project) => project.customName)?.customName ?? null;
        if (!customName) {
            return;
        }
        const timestamp = new Date().toISOString();
        await this.projectRegistry.upsert({
            ...canonical,
            customName,
            updatedAt: timestamp,
        });
        canonical.customName = customName;
        changes.push({
            kind: "project_updated",
            projectId: canonical.projectId,
            directory: canonical.rootPath,
            fields: { customName },
        });
    }
    async reconcileProject(project, siblings, changes) {
        const directoryName = project.rootPath.split(/[\\/]/).findLast(Boolean) ?? project.rootPath;
        const currentGit = await this.readWorkspaceGitMetadata(project.rootPath, directoryName);
        const projectUpdates = {};
        const mappedKind = currentGit.projectKind === "git" ? "git" : "non_git";
        if (project.kind !== mappedKind) {
            projectUpdates.kind = mappedKind;
            projectUpdates.displayName = currentGit.projectDisplayName;
        }
        if (project.kind === "git" &&
            currentGit.projectKind === "git" &&
            project.displayName !== currentGit.projectDisplayName) {
            projectUpdates.displayName = currentGit.projectDisplayName;
        }
        if (Object.keys(projectUpdates).length > 0) {
            const timestamp = new Date().toISOString();
            await this.projectRegistry.upsert({
                ...project,
                ...projectUpdates,
                updatedAt: timestamp,
            });
            changes.push({
                kind: "project_updated",
                projectId: project.projectId,
                directory: project.rootPath,
                fields: projectUpdates,
            });
        }
        const existingSiblings = siblings.filter((workspace) => existsSync(workspace.cwd));
        await Promise.all(existingSiblings.map(async (workspace) => {
            const wsDirName = workspace.cwd.split(/[\\/]/).findLast(Boolean) ?? workspace.cwd;
            const wsGit = await this.readWorkspaceGitMetadata(workspace.cwd, wsDirName);
            const expectedKind = deriveWorkspaceKindFromMetadata(wsGit);
            const workspaceUpdates = {};
            if (wsGit.projectKind === "git" && workspace.displayName !== wsGit.workspaceDisplayName) {
                workspaceUpdates.displayName = wsGit.workspaceDisplayName;
            }
            if (workspace.kind !== expectedKind) {
                workspaceUpdates.kind = expectedKind;
            }
            if (Object.keys(workspaceUpdates).length === 0) {
                return;
            }
            const timestamp = new Date().toISOString();
            await this.workspaceRegistry.upsert({
                ...workspace,
                ...workspaceUpdates,
                updatedAt: timestamp,
            });
            changes.push({
                kind: "workspace_updated",
                workspaceId: workspace.workspaceId,
                directory: workspace.cwd,
                fields: workspaceUpdates,
            });
        }));
    }
    async readWorkspaceGitMetadata(cwd, directoryName) {
        if (!this.workspaceGitService) {
            return {
                projectKind: "directory",
                projectDisplayName: directoryName,
                workspaceDisplayName: directoryName,
                gitRemote: null,
                isWorktree: false,
                projectSlug: "untitled",
                repoRoot: null,
                currentBranch: null,
                remoteUrl: null,
            };
        }
        return this.workspaceGitService.getWorkspaceGitMetadata(cwd, { directoryName });
    }
}
//# sourceMappingURL=workspace-reconciliation-service.js.map