import fs from "node:fs";
import path from "node:path";
const MIGRATION_MARKER = ".migrated-to-per-project";
const PRE_MIGRATION_SUFFIX = ".pre-migration";
const RECOVERY_MARKER = ".pre-migration-recovered";
/**
 * One-time migration from centralized state (`<server-cwd>/.bobbit/state/`)
 * to per-project state (`<project-root>/.bobbit/state/`).
 *
 * Distributes goals, sessions, tasks, teams, gates, and staff to the
 * correct project's state directory based on `projectId` tags.
 *
 * MIGRATION-ONLY default-project behavior
 * ----------------------------------------
 * Legacy records predate multi-project and therefore carry no `projectId`.
 * To avoid dropping them on the floor, this migration anchors such records
 * to a single "migration target" project: `projectRegistry.getByPath(serverCwd)`
 * if one is registered, else `projects[0]`. The variable below is named
 * `defaultProject` for historical reasons — it is NOT a runtime default
 * project. Bobbit has no runtime default project concept any more:
 *
 *   - `ProjectRegistry` does not expose `ensureDefaultProject()`.
 *   - `ProjectContextManager` does not expose `getDefault()` /
 *     `getDefaultOrNull()` / `getDefaultProjectId()` /
 *     `getDefaultProjectIdOrNull()`.
 *   - `POST /api/goals`, `POST /api/sessions`, and `POST /api/staff` require
 *     an explicit `projectId` or a `cwd` that matches a registered
 *     project's `rootPath`; otherwise they return 400.
 *
 * The `projects[0]` anchor below runs at most once per install (guarded by
 * the `.migrated-to-per-project` marker file). If you are tempted to reuse
 * this fallback anywhere outside this function, don't — resolve a project
 * explicitly via `resolveProjectForRequest` instead. See
 * [docs/internals.md — Multi-project architecture / State migration] for
 * the full rationale.
 *
 * Renamed (not deleted) central files get a `.pre-migration` suffix.
 * A marker file prevents re-running.
 *
 * Uses synchronous fs operations — runs once at startup.
 */
export function migrateToPerProjectState(centralStateDir, projectRegistry, serverCwd) {
    const markerPath = path.join(centralStateDir, MIGRATION_MARKER);
    // 1. Already migrated?
    if (fs.existsSync(markerPath))
        return;
    const projects = projectRegistry.list();
    if (projects.length === 0) {
        // No projects registered — nothing to migrate to
        return;
    }
    // Find the default project: prefer the one at serverCwd, else first registered
    const defaultProject = projectRegistry.getByPath(serverCwd) ?? projects[0];
    console.log(`[migration] Starting per-project state migration. Default project: "${defaultProject.name}" (${defaultProject.id})`);
    // Helper: resolve project for a given projectId
    const resolveProject = (projectId) => {
        if (projectId) {
            const p = projectRegistry.get(projectId);
            if (p)
                return p;
        }
        return defaultProject;
    };
    // Helper: ensure <project>/.bobbit/state/ exists
    const ensureProjectStateDir = (project) => {
        const dir = path.join(project.rootPath, ".bobbit", "state");
        fs.mkdirSync(dir, { recursive: true });
        return dir;
    };
    // Helper: read a JSON array file, return empty array if missing/corrupt
    function readJsonArray(filePath) {
        try {
            if (!fs.existsSync(filePath))
                return [];
            const data = JSON.parse(fs.readFileSync(filePath, "utf-8"));
            return Array.isArray(data) ? data : [];
        }
        catch {
            console.log(`[migration] Warning: could not parse ${filePath}, skipping`);
            return [];
        }
    }
    // Helper: merge items into an existing JSON array file by ID field, write back
    function mergeAndWrite(targetFile, newItems, idField) {
        if (newItems.length === 0)
            return;
        const dir = path.dirname(targetFile);
        fs.mkdirSync(dir, { recursive: true });
        const existing = readJsonArray(targetFile);
        const existingIds = new Set(existing.map((item) => String(item[idField])));
        let added = 0;
        for (const item of newItems) {
            if (!existingIds.has(String(item[idField]))) {
                existing.push(item);
                added++;
            }
        }
        fs.writeFileSync(targetFile, JSON.stringify(existing, null, 2), "utf-8");
        if (added > 0) {
            console.log(`[migration] Wrote ${added} new items to ${targetFile}`);
        }
    }
    // Helper: safely rename a file with .pre-migration suffix
    function renameForBackup(filePath) {
        try {
            if (fs.existsSync(filePath)) {
                const backupPath = filePath + PRE_MIGRATION_SUFFIX;
                // Don't overwrite an existing backup
                if (!fs.existsSync(backupPath)) {
                    fs.renameSync(filePath, backupPath);
                    console.log(`[migration] Renamed ${path.basename(filePath)} → ${path.basename(backupPath)}`);
                }
            }
        }
        catch (err) {
            console.log(`[migration] Warning: could not rename ${filePath}: ${err}`);
        }
    }
    // ── 2. Read central goals.json and build goal→project map ──
    const centralGoalsFile = path.join(centralStateDir, "goals.json");
    const centralGoals = readJsonArray(centralGoalsFile);
    const goalProjectMap = new Map();
    const goalsByProject = new Map();
    for (const goal of centralGoals) {
        const project = resolveProject(goal.projectId);
        goalProjectMap.set(goal.id, project);
        let bucket = goalsByProject.get(project.id);
        if (!bucket) {
            bucket = [];
            goalsByProject.set(project.id, bucket);
        }
        bucket.push(goal);
    }
    // Write goals to per-project state dirs
    for (const [projectId, goals] of goalsByProject) {
        const project = projectRegistry.get(projectId);
        const stateDir = ensureProjectStateDir(project);
        mergeAndWrite(path.join(stateDir, "goals.json"), goals, "id");
    }
    console.log(`[migration] Distributed ${centralGoals.length} goals across ${goalsByProject.size} project(s)`);
    // ── 3. Sessions ──
    const centralSessionsFile = path.join(centralStateDir, "sessions.json");
    const centralSessions = readJsonArray(centralSessionsFile);
    const sessionsByProject = new Map();
    for (const session of centralSessions) {
        const project = resolveProject(session.projectId);
        let bucket = sessionsByProject.get(project.id);
        if (!bucket) {
            bucket = [];
            sessionsByProject.set(project.id, bucket);
        }
        bucket.push(session);
    }
    for (const [projectId, sessions] of sessionsByProject) {
        const project = projectRegistry.get(projectId);
        const stateDir = ensureProjectStateDir(project);
        mergeAndWrite(path.join(stateDir, "sessions.json"), sessions, "id");
    }
    console.log(`[migration] Distributed ${centralSessions.length} sessions across ${sessionsByProject.size} project(s)`);
    // ── 4. Tasks — resolve project via goalId → goal's project ──
    const centralTasksFile = path.join(centralStateDir, "tasks.json");
    const centralTasks = readJsonArray(centralTasksFile);
    const tasksByProject = new Map();
    for (const task of centralTasks) {
        const project = goalProjectMap.get(task.goalId) ?? defaultProject;
        let bucket = tasksByProject.get(project.id);
        if (!bucket) {
            bucket = [];
            tasksByProject.set(project.id, bucket);
        }
        bucket.push(task);
    }
    for (const [projectId, tasks] of tasksByProject) {
        const project = projectRegistry.get(projectId);
        const stateDir = ensureProjectStateDir(project);
        mergeAndWrite(path.join(stateDir, "tasks.json"), tasks, "id");
    }
    console.log(`[migration] Distributed ${centralTasks.length} tasks`);
    // ── 5. Teams — resolve project via goalId ──
    // TeamStore uses "team-state.json" (with legacy "gateway-swarms.json" fallback)
    let centralTeamsFile = path.join(centralStateDir, "team-state.json");
    if (!fs.existsSync(centralTeamsFile)) {
        centralTeamsFile = path.join(centralStateDir, "gateway-swarms.json");
    }
    const centralTeams = readJsonArray(centralTeamsFile);
    const teamsByProject = new Map();
    for (const team of centralTeams) {
        const project = goalProjectMap.get(team.goalId) ?? defaultProject;
        let bucket = teamsByProject.get(project.id);
        if (!bucket) {
            bucket = [];
            teamsByProject.set(project.id, bucket);
        }
        bucket.push(team);
    }
    for (const [projectId, teams] of teamsByProject) {
        const project = projectRegistry.get(projectId);
        const stateDir = ensureProjectStateDir(project);
        mergeAndWrite(path.join(stateDir, "team-state.json"), teams, "goalId");
    }
    console.log(`[migration] Distributed ${centralTeams.length} teams`);
    // ── 6. Gates — single gates.json file, distribute by goalId ──
    const centralGatesFile = path.join(centralStateDir, "gates.json");
    const centralGates = readJsonArray(centralGatesFile);
    const gatesByProject = new Map();
    for (const gate of centralGates) {
        const project = goalProjectMap.get(gate.goalId) ?? defaultProject;
        let bucket = gatesByProject.get(project.id);
        if (!bucket) {
            bucket = [];
            gatesByProject.set(project.id, bucket);
        }
        bucket.push(gate);
    }
    for (const [projectId, gates] of gatesByProject) {
        const project = projectRegistry.get(projectId);
        const stateDir = ensureProjectStateDir(project);
        // Gates use composite key (goalId::gateId), so merge with a custom approach
        const targetFile = path.join(stateDir, "gates.json");
        fs.mkdirSync(path.dirname(targetFile), { recursive: true });
        const existing = readJsonArray(targetFile);
        const existingKeys = new Set(existing.map((g) => `${g.goalId}::${g.gateId}`));
        let added = 0;
        for (const gate of gates) {
            const key = `${gate.goalId}::${gate.gateId}`;
            if (!existingKeys.has(key)) {
                existing.push(gate);
                added++;
            }
        }
        fs.writeFileSync(targetFile, JSON.stringify(existing, null, 2), "utf-8");
        if (added > 0) {
            console.log(`[migration] Wrote ${added} gate states to ${targetFile}`);
        }
    }
    console.log(`[migration] Distributed ${centralGates.length} gate states`);
    // ── 7. Staff — no projectId, all go to default project ──
    const centralStaffFile = path.join(centralStateDir, "staff.json");
    const centralStaff = readJsonArray(centralStaffFile);
    if (centralStaff.length > 0) {
        const stateDir = ensureProjectStateDir(defaultProject);
        mergeAndWrite(path.join(stateDir, "staff.json"), centralStaff, "id");
        console.log(`[migration] Moved ${centralStaff.length} staff agents to default project`);
    }
    // ── 8. Rename central files for backup ──
    // Skip renaming when the central state dir IS the default project's state dir
    // (same physical path). Renaming would delete the per-project file we just wrote.
    const defaultProjectStateDir = path.resolve(path.join(defaultProject.rootPath, ".bobbit", "state"));
    const centralResolved = path.resolve(centralStateDir);
    // Use case-insensitive comparison on Windows (drive letter casing can vary)
    const pathsEqual = process.platform === "win32"
        ? centralResolved.toLowerCase() === defaultProjectStateDir.toLowerCase()
        : centralResolved === defaultProjectStateDir;
    if (!pathsEqual) {
        renameForBackup(centralGoalsFile);
        renameForBackup(centralSessionsFile);
        renameForBackup(centralTasksFile);
        renameForBackup(centralStaffFile);
        renameForBackup(centralGatesFile);
        // Team store files
        renameForBackup(path.join(centralStateDir, "team-state.json"));
        renameForBackup(path.join(centralStateDir, "gateway-swarms.json"));
        // Search index — will be rebuilt per-project on first access
        renameForBackup(path.join(centralStateDir, "search.db"));
    }
    else {
        console.log("[migration] Central state dir is the default project dir — skipping backup rename");
    }
    // ── 9. Write migration marker ──
    try {
        fs.writeFileSync(markerPath, new Date().toISOString(), "utf-8");
        console.log("[migration] Per-project state migration complete. Marker written.");
    }
    catch (err) {
        console.error("[migration] Failed to write migration marker:", err);
    }
}
/**
 * Recovery pass: if `.pre-migration` backup files exist in the state dir,
 * merge any entries missing from the current state files back in.
 *
 * This fixes data loss caused by the original migration code which
 * unconditionally renamed central files even when central dir == default
 * project dir — effectively deleting the sessions it just wrote.
 *
 * Runs once (writes a `.pre-migration-recovered` marker). Safe to re-run.
 */
export function recoverPreMigrationData(stateDir) {
    const recoveryMarker = path.join(stateDir, RECOVERY_MARKER);
    if (fs.existsSync(recoveryMarker))
        return;
    const filesToRecover = [
        { name: "sessions.json", idField: "id" },
        { name: "goals.json", idField: "id" },
        { name: "tasks.json", idField: "id" },
        { name: "staff.json", idField: "id" },
        { name: "team-state.json", idField: "goalId" },
    ];
    let totalRecovered = 0;
    for (const { name, idField } of filesToRecover) {
        const backupFile = path.join(stateDir, name + PRE_MIGRATION_SUFFIX);
        const currentFile = path.join(stateDir, name);
        if (!fs.existsSync(backupFile))
            continue;
        try {
            const backup = JSON.parse(fs.readFileSync(backupFile, "utf-8"));
            if (!Array.isArray(backup) || backup.length === 0)
                continue;
            const current = fs.existsSync(currentFile)
                ? JSON.parse(fs.readFileSync(currentFile, "utf-8"))
                : [];
            if (!Array.isArray(current))
                continue;
            const existingIds = new Set(current.map(item => String(item[idField])));
            let added = 0;
            for (const item of backup) {
                const id = String(item[idField]);
                if (id && !existingIds.has(id)) {
                    current.push(item);
                    existingIds.add(id);
                    added++;
                }
            }
            if (added > 0) {
                fs.writeFileSync(currentFile, JSON.stringify(current, null, 2), "utf-8");
                console.log(`[migration-recovery] Recovered ${added} entries into ${name}`);
                totalRecovered += added;
            }
        }
        catch (err) {
            console.warn(`[migration-recovery] Failed to recover ${name}: ${err}`);
        }
    }
    // Gates use composite key
    try {
        const gatesBackup = path.join(stateDir, "gates.json" + PRE_MIGRATION_SUFFIX);
        const gatesCurrent = path.join(stateDir, "gates.json");
        if (fs.existsSync(gatesBackup)) {
            const backup = JSON.parse(fs.readFileSync(gatesBackup, "utf-8"));
            const current = fs.existsSync(gatesCurrent)
                ? JSON.parse(fs.readFileSync(gatesCurrent, "utf-8"))
                : [];
            if (Array.isArray(backup) && Array.isArray(current)) {
                const existingKeys = new Set(current.map(g => `${g.goalId}::${g.gateId}`));
                let added = 0;
                for (const gate of backup) {
                    const key = `${gate.goalId}::${gate.gateId}`;
                    if (!existingKeys.has(key)) {
                        current.push(gate);
                        existingKeys.add(key);
                        added++;
                    }
                }
                if (added > 0) {
                    fs.writeFileSync(gatesCurrent, JSON.stringify(current, null, 2), "utf-8");
                    console.log(`[migration-recovery] Recovered ${added} entries into gates.json`);
                    totalRecovered += added;
                }
            }
        }
    }
    catch (err) {
        console.warn(`[migration-recovery] Failed to recover gates.json: ${err}`);
    }
    if (totalRecovered > 0) {
        console.log(`[migration-recovery] Total recovered: ${totalRecovered} entries`);
    }
    try {
        fs.writeFileSync(recoveryMarker, new Date().toISOString(), "utf-8");
    }
    catch { /* best effort */ }
}
//# sourceMappingURL=state-migration.js.map