import { access, readFile, readdir, rm, writeFile } from "node:fs/promises";
import { join } from "node:path";
import bodyParser from "body-parser";
import { parse as parseYaml } from "yaml";
import { getErrorMeta } from "../util/logger.js";
import { getFullstackTemplateDir } from "./get-fullstack-template-dir.js";
import { MIGRATION_CHECKLIST_FILE, markMigrationChecklistCompleted, readMigrationChecklist, retryFailedSeedApiItems, } from "./migration-checklist.js";
import { classifyIntegrationMutations } from "./migration-classifier.js";
import { migrateToFullstack } from "./restructure.js";
import { detectUnsupportedIntegrations } from "./unsupported-integrations.js";
async function pathExists(path) {
    try {
        await access(path);
        return true;
    }
    catch {
        return false;
    }
}
const TARGET_TEMPLATE_NAME = "app-fullstack";
const MIGRATION_STATE_FILE = "migration-state.json";
const SCRATCH_DIR_NAME = "scratch";
let inMemoryStatus = { state: "idle" };
/**
 * Recursively finds all api.yaml/api.yml files inside `apis/` directories
 * under `rootDir`. Used to scan the live app tree before migration starts
 * (before a v2-backup exists).
 */
async function findLiveApiYamls(rootDir) {
    const results = [];
    const stack = [rootDir];
    while (stack.length > 0) {
        const dir = stack.pop();
        let entries;
        try {
            entries = await readdir(dir, { withFileTypes: true });
        }
        catch {
            continue;
        }
        for (const entry of entries) {
            if (entry.name === SCRATCH_DIR_NAME || entry.name === "node_modules") {
                continue;
            }
            const abs = join(dir, entry.name);
            if (entry.isDirectory()) {
                stack.push(abs);
            }
            else if (entry.isFile() &&
                (entry.name === "api.yaml" || entry.name === "api.yml")) {
                // Only treat files inside an `apis/` directory as v2 API definitions.
                const rel = abs.substring(rootDir.length + 1);
                if (rel.split("/").includes("apis")) {
                    results.push(abs);
                }
            }
        }
    }
    return results.sort();
}
/**
 * Extracts integration entries from a set of api.yaml paths by parsing each
 * file and classifying its steps.
 *
 * v2 YAML structure: `blocks[].step.integration` (not a top-level `steps`
 * array). Each `block.step` is the object passed to the classifier.
 */
async function extractIntegrationsFromYamls(yamlPaths) {
    const integrationSteps = new Map();
    for (const yamlPath of yamlPaths) {
        try {
            const raw = await readFile(yamlPath, "utf-8");
            const parsed = parseYaml(raw);
            for (const block of parsed?.blocks ?? []) {
                const step = block.step;
                if (!step || typeof step !== "object")
                    continue;
                const integrationId = step["integration"];
                if (typeof integrationId !== "string" || integrationId.length === 0) {
                    continue;
                }
                const existing = integrationSteps.get(integrationId) ?? [];
                existing.push(step);
                integrationSteps.set(integrationId, existing);
            }
        }
        catch {
            // Skip unreadable YAMLs
        }
    }
    return Array.from(integrationSteps.entries()).map(([id, steps]) => ({
        id,
        displayName: id,
        defaultPolicy: "ask",
        mutationKind: classifyIntegrationMutations(steps),
    }));
}
async function collectMigrationIntegrations(appRootDir) {
    const backupDir = join(appRootDir, SCRATCH_DIR_NAME, "v2-backup");
    const yamlPaths = await findLiveApiYamls(backupDir);
    return extractIntegrationsFromYamls(yamlPaths);
}
/**
 * Scans the live (pre-migration) app tree for v2 API YAML files and
 * classifies the integrations they reference. Called when there is no
 * v2-backup yet (i.e., the migration has not started) so the write-policy
 * modal can be shown before the user clicks Migrate.
 */
async function collectPreMigrationIntegrations(appRootDir) {
    const yamlPaths = await findLiveApiYamls(appRootDir);
    return extractIntegrationsFromYamls(yamlPaths);
}
/**
 * Resolve the phase a browser should see.
 *
 * If the in-memory status is actively driving the three-step flow
 * (`backup` | `restructure` | `flip`) it wins — those phases are only ever
 * live during the synchronous handler, so in-memory is the freshest truth.
 *
 * Otherwise we derive from disk: migration-state.json presence + the
 * checklist. Failed checklist items with `origin === "seed_api"` drive the
 * `done_with_unresolved` terminal phase. Disk is the single source of truth
 * and the only thing that survives a dev-server restart mid-translation.
 */
async function resolveStatus(appRootDir, opts = {}) {
    const ignoreInMemory = opts.ignoreInMemory ?? false;
    const stateFilePath = join(appRootDir, SCRATCH_DIR_NAME, MIGRATION_STATE_FILE);
    const [hasV2Backup, hasMigrationStateFile] = await Promise.all([
        pathExists(join(appRootDir, SCRATCH_DIR_NAME, "v2-backup")),
        pathExists(stateFilePath),
    ]);
    const [checklist, integrations] = await Promise.all([
        readMigrationChecklist(appRootDir),
        hasV2Backup
            ? collectMigrationIntegrations(appRootDir)
            : collectPreMigrationIntegrations(appRootDir),
    ]);
    const checklistPayload = checklist
        ? {
            overallStatus: checklist.overallStatus,
            items: checklist.items,
            lastUpdated: checklist.lastUpdated,
        }
        : undefined;
    if (!ignoreInMemory &&
        (inMemoryStatus.state === "backup" ||
            inMemoryStatus.state === "restructure" ||
            inMemoryStatus.state === "flip" ||
            (inMemoryStatus.state === "failed" && !hasMigrationStateFile))) {
        return {
            ...inMemoryStatus,
            hasV2Backup,
            checklist: checklistPayload,
            ...(integrations !== undefined && { integrations }),
        };
    }
    if (!hasMigrationStateFile) {
        const unsupportedStatus = await detectUnsupportedIntegrations(appRootDir);
        return {
            state: "idle",
            hasV2Backup,
            ...unsupportedStatus,
            checklist: checklistPayload,
            ...(integrations !== undefined && { integrations }),
        };
    }
    const seedApiItems = checklist?.items.filter((item) => item.origin === "seed_api") ?? [];
    const pendingCount = seedApiItems.filter((item) => item.status === "pending" || item.status === "in_progress").length;
    const failedCount = seedApiItems.filter((item) => item.status === "failed").length;
    const persistedState = await readPersistedMigrationState(stateFilePath);
    const startedAt = inMemoryStatus.startedAt ?? persistedState.startedAt;
    const backupCommitId = inMemoryStatus.backupCommitId ?? persistedState.backupCommitId;
    const migrationCommitId = inMemoryStatus.migrationCommitId ?? persistedState.migrationCommitId;
    const integrationWritePolicy = persistedState.integrationWritePolicy;
    if (checklist?.overallStatus === "completed") {
        return {
            state: "done",
            startedAt,
            backupCommitId,
            migrationCommitId,
            hasV2Backup,
            checklist: checklistPayload,
            ...(integrations !== undefined && { integrations }),
            ...(integrationWritePolicy !== undefined && { integrationWritePolicy }),
        };
    }
    if (pendingCount > 0) {
        return {
            state: "translating",
            startedAt,
            backupCommitId,
            migrationCommitId,
            hasV2Backup,
            checklist: checklistPayload,
            ...(integrations !== undefined && { integrations }),
            ...(integrationWritePolicy !== undefined && { integrationWritePolicy }),
        };
    }
    return {
        state: failedCount > 0 ? "done_with_unresolved" : "done",
        startedAt,
        backupCommitId,
        migrationCommitId,
        hasV2Backup,
        checklist: checklistPayload,
        ...(integrations !== undefined && { integrations }),
        ...(integrationWritePolicy !== undefined && { integrationWritePolicy }),
    };
}
async function readPersistedMigrationState(stateFilePath) {
    try {
        const raw = await readFile(stateFilePath, "utf-8");
        const parsed = JSON.parse(raw);
        return {
            startedAt: typeof parsed.startedAt === "string" ? parsed.startedAt : undefined,
            sourceTemplateName: typeof parsed.sourceTemplateName === "string"
                ? parsed.sourceTemplateName
                : undefined,
            backupDirPath: typeof parsed.backupDirPath === "string"
                ? parsed.backupDirPath
                : undefined,
            backupCommitId: typeof parsed.backupCommitId === "string"
                ? parsed.backupCommitId
                : undefined,
            migrationCommitId: typeof parsed.migrationCommitId === "string"
                ? parsed.migrationCommitId
                : undefined,
            integrationWritePolicy: parsed.integrationWritePolicy !== null &&
                typeof parsed.integrationWritePolicy === "object" &&
                !Array.isArray(parsed.integrationWritePolicy)
                ? parsed.integrationWritePolicy
                : undefined,
        };
    }
    catch {
        return {};
    }
}
async function persistMigrationStateMetadata(stateFilePath, metadata) {
    let parsed;
    try {
        const raw = await readFile(stateFilePath, "utf-8");
        parsed = JSON.parse(raw);
    }
    catch {
        return;
    }
    if (metadata.backupCommitId) {
        parsed.backupCommitId = metadata.backupCommitId;
    }
    if (metadata.migrationCommitId) {
        parsed.migrationCommitId = metadata.migrationCommitId;
    }
    await writeFile(stateFilePath, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
}
function setupCorsAndValidateMethod({ req, res, allowedMethod, allowHeaders, }) {
    if (req.method !== allowedMethod && req.method !== "OPTIONS") {
        res.statusCode = 405;
        res.end("Method not allowed");
        return false;
    }
    res.setHeader("Access-Control-Allow-Origin", "*");
    res.setHeader("Access-Control-Allow-Methods", `${allowedMethod}, OPTIONS`);
    if (allowHeaders) {
        res.setHeader("Access-Control-Allow-Headers", allowHeaders);
    }
    if (req.method === "OPTIONS") {
        res.statusCode = 204;
        res.end();
        return false;
    }
    return true;
}
export function registerMigrationRoutes(server, options) {
    const { appRootDir, getIsInitialized, syncService, logger } = options;
    // --- GET /sb-migration-status: poll for migration progress --------------
    server.middlewares.use("/sb-migration-status", (req, res, next) => {
        if (!getIsInitialized())
            return next();
        if (!setupCorsAndValidateMethod({ req, res, allowedMethod: "GET" })) {
            return;
        }
        res.setHeader("Content-Type", "application/json");
        void resolveStatus(appRootDir)
            .then((status) => {
            res.statusCode = 200;
            res.end(JSON.stringify(status));
        })
            .catch((error) => {
            logger.error("[migrate-route] /sb-migration-status failed", getErrorMeta(error));
            res.statusCode = 500;
            res.end(JSON.stringify({
                error: error instanceof Error ? error.message : String(error),
            }));
        });
    });
    server.middlewares.use("/sb-migrate-to-fullstack", bodyParser.json({ strict: true }));
    // --- POST /sb-migration-reset-state -------------------------------------
    // Clears migration-status artifacts from disk without mutating app files.
    // Abort reset uses this to prevent the next dev-server boot from
    // re-triggering internal migration turns off stale `scratch/*` state.
    server.middlewares.use("/sb-migration-reset-state", bodyParser.json({ strict: true }));
    server.middlewares.use("/sb-migration-reset-state", (req, res, next) => {
        void (async () => {
            if (!getIsInitialized()) {
                return next();
            }
            if (!setupCorsAndValidateMethod({
                req,
                res,
                allowedMethod: "POST",
                allowHeaders: "Content-Type",
            })) {
                return;
            }
            const scratchDirPath = join(appRootDir, SCRATCH_DIR_NAME);
            const stateFilePath = join(scratchDirPath, MIGRATION_STATE_FILE);
            const checklistPath = join(scratchDirPath, MIGRATION_CHECKLIST_FILE);
            await Promise.all([
                rm(stateFilePath, { force: true }),
                rm(checklistPath, { force: true }),
            ]);
            inMemoryStatus = { state: "idle" };
            const status = await resolveStatus(appRootDir, { ignoreInMemory: true });
            logger.info("[migrate-route] migration state reset requested", {
                state: status.state,
                hasV2Backup: status.hasV2Backup,
            });
            res.statusCode = 200;
            res.setHeader("Content-Type", "application/json");
            res.end(JSON.stringify({
                ok: true,
                status,
            }));
        })().catch((error) => {
            logger.error("[migrate-route] /sb-migration-reset-state failed", getErrorMeta(error));
            res.statusCode = 500;
            res.end(JSON.stringify({
                error: error instanceof Error ? error.message : String(error),
            }));
        });
    });
    server.middlewares.use("/sb-migrate-to-fullstack", (req, res, next) => {
        void (async () => {
            if (!getIsInitialized()) {
                return next();
            }
            if (!setupCorsAndValidateMethod({
                req,
                res,
                allowedMethod: "POST",
                allowHeaders: "Content-Type",
            })) {
                return;
            }
            logger.info("[migrate-route] /sb-migrate-to-fullstack requested");
            res.setHeader("Content-Type", "application/json");
            if (!syncService) {
                res.statusCode = 503;
                res.end(JSON.stringify({
                    error: "Migration is unavailable: the dev server was started without a SyncService (no Superblocks credentials?).",
                }));
                return;
            }
            if (await isAlreadyOnFullstack(appRootDir)) {
                res.statusCode = 409;
                res.end(JSON.stringify({
                    error: `App appears to already be on template '${TARGET_TEMPLATE_NAME}' (client/ and server/apis/index.ts exist). Migration is a one-way transition; refusing to run again.`,
                }));
                return;
            }
            if (inMemoryStatus.state !== "idle" &&
                inMemoryStatus.state !== "done" &&
                inMemoryStatus.state !== "done_with_unresolved" &&
                inMemoryStatus.state !== "failed") {
                res.statusCode = 409;
                res.end(JSON.stringify({
                    error: `A migration is already in progress (state: ${inMemoryStatus.state}). Please wait for it to complete.`,
                }));
                return;
            }
            res.statusCode = 202;
            res.end(JSON.stringify({
                accepted: true,
                message: "Migration started. The editor will reconnect automatically when the new runtime is ready.",
            }));
            const stateFilePath = join(appRootDir, SCRATCH_DIR_NAME, MIGRATION_STATE_FILE);
            let backupCommitId;
            let restructured = false;
            let autoSyncPaused = false;
            let watcherPaused = false;
            inMemoryStatus = {
                state: "backup",
                startedAt: new Date().toISOString(),
            };
            try {
                logger.info("[migrate-route] Step 1: pre-migration backup commit");
                const backup = await syncService.backupCurrentDirectory();
                backupCommitId = backup.commitId;
                if (!backupCommitId) {
                    const msg = "Pre-migration backup did not produce a commitId; aborting.";
                    logger.error("[migrate-route] " + msg);
                    inMemoryStatus = { state: "failed", error: msg };
                    return;
                }
                logger.info("[migrate-route] Backup commit created", {
                    backupCommitId,
                });
                inMemoryStatus = {
                    state: "restructure",
                    backupCommitId,
                    startedAt: inMemoryStatus.startedAt,
                };
                syncService.pauseAutoSync();
                autoSyncPaused = true;
                logger.info("[migrate-route] Pausing Vite file watcher");
                server.watcher.unwatch(appRootDir);
                watcherPaused = true;
                logger.info("[migrate-route] Step 2: restructuring local tree");
                await migrateToFullstack({
                    appRootDirPath: appRootDir,
                    templateSourceDirPath: getFullstackTemplateDir(),
                    logger,
                });
                restructured = true;
                await persistMigrationStateMetadata(stateFilePath, {
                    backupCommitId,
                });
                logger.info("[migrate-route] Local restructure complete");
                inMemoryStatus = {
                    state: "flip",
                    backupCommitId,
                    startedAt: inMemoryStatus.startedAt,
                };
                logger.info("[migrate-route] Step 3: atomic server-side template flip");
                const flip = await syncService.flipTemplate({
                    targetTemplateName: TARGET_TEMPLATE_NAME,
                });
                await persistMigrationStateMetadata(stateFilePath, {
                    backupCommitId,
                    migrationCommitId: flip.commitId,
                });
                logger.info("[migrate-route] Template flip complete", {
                    migrationCommitId: flip.commitId,
                });
                // After the flip, the SABS pod restarts and this process is
                // replaced. If by chance we survive, leave in-memory status at
                // `done` — the GET endpoint will derive `translating` /
                // `done_with_unresolved` from disk once Clark starts work.
                inMemoryStatus = {
                    state: "done",
                    backupCommitId,
                    migrationCommitId: flip.commitId,
                    startedAt: inMemoryStatus.startedAt,
                };
            }
            catch (error) {
                const errorMsg = error instanceof Error ? error.message : String(error);
                logger.error("[migrate-route] Migration failed", getErrorMeta(error));
                logger.info("[migrate-route] Migration context", {
                    stage: restructured
                        ? "flip"
                        : backupCommitId
                            ? "restructure"
                            : "backup",
                    backupCommitId,
                    restructured,
                });
                inMemoryStatus = {
                    state: "failed",
                    error: `Migration failed at ${restructured ? "flip" : backupCommitId ? "restructure" : "backup"} stage: ${errorMsg}`,
                    backupCommitId,
                    startedAt: inMemoryStatus.startedAt,
                };
            }
            finally {
                if (autoSyncPaused) {
                    try {
                        syncService.resumeAutoSync();
                    }
                    catch (error) {
                        logger.warn("[migrate-route] Failed to resume autosync after migration attempt", {
                            error: error instanceof Error ? error.message : String(error),
                        });
                    }
                }
                if (watcherPaused) {
                    try {
                        server.watcher.add(appRootDir);
                    }
                    catch (error) {
                        logger.warn("[migrate-route] Failed to rewatch app root after migration attempt", {
                            error: error instanceof Error ? error.message : String(error),
                        });
                    }
                }
            }
        })();
    });
    // --- POST /sb-migration-retry-unresolved --------------------------------
    server.middlewares.use("/sb-migration-retry-unresolved", bodyParser.json({ strict: true }));
    server.middlewares.use("/sb-migration-retry-unresolved", (req, res, next) => {
        void (async () => {
            if (!getIsInitialized())
                return next();
            if (!setupCorsAndValidateMethod({
                req,
                res,
                allowedMethod: "POST",
                allowHeaders: "Content-Type",
            })) {
                return;
            }
            res.setHeader("Content-Type", "application/json");
            try {
                const result = await retryFailedSeedApiItems(appRootDir);
                logger.info("[migrate-route] retry-unresolved flipped items", {
                    retriedCount: result.retriedCount,
                });
                const status = await resolveStatus(appRootDir, {
                    ignoreInMemory: true,
                });
                res.statusCode = 200;
                res.end(JSON.stringify({
                    ok: true,
                    retriedCount: result.retriedCount,
                    status,
                }));
            }
            catch (error) {
                logger.error("[migrate-route] /sb-migration-retry-unresolved failed", getErrorMeta(error));
                res.statusCode = 500;
                res.end(JSON.stringify({
                    error: error instanceof Error ? error.message : String(error),
                }));
            }
        })();
    });
    server.middlewares.use("/sb-migration-complete", bodyParser.json({ strict: true }));
    server.middlewares.use("/sb-migration-complete", (req, res, next) => {
        void (async () => {
            if (!getIsInitialized()) {
                return next();
            }
            if (!setupCorsAndValidateMethod({
                req,
                res,
                allowedMethod: "POST",
                allowHeaders: "Content-Type",
            })) {
                return;
            }
            if (!syncService) {
                res.statusCode = 503;
                res.end(JSON.stringify({
                    error: "Migration completion is unavailable: the dev server was started without a SyncService.",
                }));
                return;
            }
            const status = await resolveStatus(appRootDir, { ignoreInMemory: true });
            logger.info("[migrate-route] completion precheck status", {
                state: status.state,
                hasV2Backup: status.hasV2Backup,
            });
            if (status.state !== "done") {
                res.statusCode = 409;
                res.end(JSON.stringify({
                    error: "Migration can only be completed after all APIs are translated successfully.",
                    state: status.state,
                }));
                return;
            }
            const checklist = await readMigrationChecklist(appRootDir);
            if (!checklist) {
                res.statusCode = 404;
                res.end(JSON.stringify({
                    error: "Migration checklist not found.",
                }));
                return;
            }
            const completedChecklist = checklist.overallStatus === "completed"
                ? checklist
                : await markMigrationChecklistCompleted({
                    appRootDirPath: appRootDir,
                });
            if (checklist.overallStatus !== "completed") {
                logger.info("[migrate-route] marked checklist as completed");
            }
            await rm(join(appRootDir, SCRATCH_DIR_NAME, "v2-backup"), {
                recursive: true,
                force: true,
            });
            await rm(join(appRootDir, SCRATCH_DIR_NAME), {
                recursive: true,
                force: true,
            });
            logger.info("[migrate-route] removed scratch directory");
            const completionCommit = await syncService.commitLocalDraftChanges("migrate:complete");
            if (!completionCommit?.commitId) {
                throw new Error("Migration completion checkpoint was not created; aborting completion flow.");
            }
            logger.info("[migrate-route] Completion checkpoint created", {
                completionCommitId: completionCommit.commitId,
            });
            inMemoryStatus = {
                state: "done",
                startedAt: status.startedAt,
                backupCommitId: status.backupCommitId,
                migrationCommitId: status.migrationCommitId,
            };
            const updatedStatus = {
                state: "done",
                startedAt: status.startedAt,
                backupCommitId: status.backupCommitId,
                migrationCommitId: status.migrationCommitId,
                hasV2Backup: false,
                checklist: {
                    overallStatus: "completed",
                    items: completedChecklist.items,
                    lastUpdated: completedChecklist.lastUpdated,
                },
            };
            res.statusCode = 200;
            res.setHeader("Content-Type", "application/json");
            res.end(JSON.stringify({
                ok: true,
                completionCommitId: completionCommit.commitId,
                status: updatedStatus,
            }));
        })().catch((error) => {
            logger.error("[migrate-route] /sb-migration-complete failed", getErrorMeta(error));
            res.statusCode = 500;
            res.end(JSON.stringify({
                error: error instanceof Error ? error.message : String(error),
            }));
        });
    });
    // --- POST /sb-migration-set-write-policy -----------------------------------
    server.middlewares.use("/sb-migration-set-write-policy", bodyParser.json({ strict: true }));
    server.middlewares.use("/sb-migration-set-write-policy", (req, res, next) => {
        void (async () => {
            if (!getIsInitialized())
                return next();
            if (!setupCorsAndValidateMethod({
                req,
                res,
                allowedMethod: "POST",
                allowHeaders: "Content-Type",
            }))
                return;
            const body = req.body;
            if (!body?.policy ||
                typeof body.policy !== "object" ||
                Array.isArray(body.policy)) {
                res.statusCode = 400;
                res.setHeader("Content-Type", "application/json");
                res.end(JSON.stringify({ error: "policy object required" }));
                return;
            }
            const stateFilePath = join(appRootDir, SCRATCH_DIR_NAME, MIGRATION_STATE_FILE);
            let parsed;
            try {
                parsed = JSON.parse(await readFile(stateFilePath, "utf-8"));
            }
            catch {
                parsed = {};
            }
            parsed.integrationWritePolicy = body.policy;
            await writeFile(stateFilePath, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
            const status = await resolveStatus(appRootDir);
            res.statusCode = 200;
            res.setHeader("Content-Type", "application/json");
            res.end(JSON.stringify({ ok: true, status }));
        })().catch((error) => {
            logger.error("[migrate-route] /sb-migration-set-write-policy failed", getErrorMeta(error));
            res.statusCode = 500;
            res.end(JSON.stringify({
                error: error instanceof Error ? error.message : String(error),
            }));
        });
    });
}
async function isAlreadyOnFullstack(appRootDir) {
    const [hasClient, hasServerApisIndex] = await Promise.all([
        pathExists(join(appRootDir, "client")),
        pathExists(join(appRootDir, "server", "apis", "index.ts")),
    ]);
    return hasClient && hasServerApisIndex;
}
//# sourceMappingURL=migration-routes.js.map