import { existsSync } from "node:fs";
import { readFile, readdir, rename, stat, writeFile, mkdir, cp, } from "node:fs/promises";
import { dirname, join, relative } from "node:path";
import { parse as parseYaml } from "yaml";
import { getMigrationChecklistPath, MIGRATION_CHECKLIST_FILE, seedMigrationChecklist, } from "./migration-checklist.js";
/** Top-level directories we never move into the backup (they would bloat
 * it, or -- in the case of `scratch` itself -- recurse infinitely). */
const GUARDED_TOP_LEVEL = new Set([
    ".cache",
    ".git",
    ".superblocks",
    ".turbo",
    "build",
    "coverage",
    "dist",
    "node_modules",
    "scratch",
]);
/** Top-level entries that, if present in backup, map onto fullstack client/. */
const BACKUP_TO_CLIENT_MAPPINGS = [
    // Pages are overwhelmingly user-authored; always overwrite the template
    // stub (which is a blank <div>).
    {
        backupSubpath: "pages",
        clientSubpath: "pages",
        diffAgainstTemplate: false,
    },
    // Components may be stock shadcn files that match the fullstack template
    // byte-for-byte. Skip those to avoid a giant no-op diff.
    {
        backupSubpath: "components",
        clientSubpath: "components",
        diffAgainstTemplate: true,
    },
    // These are one-file directories in both templates; diff to avoid
    // overwriting template improvements when the user never touched them.
    { backupSubpath: "lib", clientSubpath: "lib", diffAgainstTemplate: true },
];
/** Single-file diff targets at the backup root. Each one: if the v2 app
 * shipped a version that differs from the fullstack template's baseline,
 * port it over; otherwise leave the template copy in place.
 *
 * `router.tsx` / `App.tsx` / `root.tsx` are critical here: the shadcn-demo-app
 * legacy template registers real user routes via `router.tsx`, and the
 * fullstack template's default router only wires up a blank `Page1` stub.
 * Without porting the user's router, the migrated `client/pages/*` files
 * exist on disk but are unrouted, so the live-edit tree looks like a
 * default/empty app and the editor falls through to the v2 UI.
 */
const BACKUP_SINGLE_FILE_DIFFS = [
    { backupPath: "index.css", clientPath: "client/index.css" },
    { backupPath: "router.tsx", clientPath: "client/router.tsx" },
    { backupPath: "App.tsx", clientPath: "client/App.tsx" },
    { backupPath: "root.tsx", clientPath: "client/root.tsx" },
];
export const SCRATCH_DIR_NAME = "scratch";
export const BACKUP_SUBDIR_NAME = "v2-backup";
export const MIGRATION_STATE_FILE = "migration-state.json";
export { MIGRATION_CHECKLIST_FILE };
export const MIGRATION_STATE_VERSION = 1;
export async function migrateToFullstack(args) {
    const { appRootDirPath, templateSourceDirPath, logger } = args;
    logger.info("[migrate] Starting v2->v3 restructure", {
        appRootDirPath,
        templateSourceDirPath,
    });
    await assertDirectoryExists(appRootDirPath, "app root");
    await assertDirectoryExists(templateSourceDirPath, "template source");
    const scratchDirPath = join(appRootDirPath, SCRATCH_DIR_NAME);
    const backupDirPath = join(scratchDirPath, BACKUP_SUBDIR_NAME);
    await assertBackupSlotIsEmpty(backupDirPath);
    await moveAllTopLevelIntoBackup({ appRootDirPath, backupDirPath, logger });
    await copyTemplateIntoAppRoot({
        appRootDirPath,
        templateSourceDirPath,
        logger,
    });
    let restructuredFiles = 0;
    restructuredFiles += await remapBackupDirsIntoClient({
        backupDirPath,
        appRootDirPath,
        templateSourceDirPath,
        logger,
    });
    restructuredFiles += await remapBackupSingleFiles({
        backupDirPath,
        appRootDirPath,
        templateSourceDirPath,
        logger,
    });
    const apiStubResult = await stubV2Apis({
        backupDirPath,
        appRootDirPath,
        logger,
    });
    restructuredFiles += apiStubResult.apiStubs.length;
    const state = {
        version: MIGRATION_STATE_VERSION,
        startedAt: new Date().toISOString(),
        targetTemplateName: "app-fullstack",
        sourceTemplateName: process.env.SUPERBLOCKS_APP_TEMPLATE_NAME ?? "shadcn-demo-app",
        backupDirPath: relative(appRootDirPath, backupDirPath),
        apiChecklist: apiStubResult.checklist,
    };
    const stateFilePath = join(scratchDirPath, MIGRATION_STATE_FILE);
    await writeFile(stateFilePath, JSON.stringify(state, null, 2) + "\n", "utf-8");
    logger.info("[migrate] Wrote migration-state.json", { stateFilePath });
    const migrationChecklist = await seedMigrationChecklist({
        appRootDirPath,
        apiChecklist: apiStubResult.checklist.map((entry) => ({
            name: entry.name,
            stubPath: entry.stubPath,
        })),
    });
    const migrationChecklistFilePath = getMigrationChecklistPath(appRootDirPath);
    logger.info("[migrate] Wrote migration-checklist.json", {
        migrationChecklistFilePath,
        itemCount: migrationChecklist.items.length,
    });
    // Update the local template marker read by the csb-mock-server so the
    // restarted dev-server process launches with `app-fullstack` env vars
    // (entryPoint `client/root.tsx`, etc.). Production SABS gets the new
    // template from the server-side row at pod start; the mock has no such
    // signal and would otherwise cache the pre-migration template forever.
    // Harmless in non-mock environments — the file is mock-specific.
    const sbTemplatePath = join(appRootDirPath, ".sb_template");
    await writeFile(sbTemplatePath, "app-fullstack", "utf-8");
    logger.info("[migrate] Wrote .sb_template", { sbTemplatePath });
    return {
        scratchDirPath,
        backupDirPath,
        restructuredFiles,
        apiStubs: apiStubResult.apiStubs,
        stateFilePath,
        migrationChecklistFilePath,
    };
}
// --------------- Step 1: move current tree into scratch/v2-backup ---------------
async function assertBackupSlotIsEmpty(backupDirPath) {
    if (existsSync(backupDirPath)) {
        // If the backup already exists we would silently overwrite the user's
        // existing rollback point, which is the worst possible failure mode.
        throw new Error(`Refusing to overwrite existing backup at ${backupDirPath}. A prior migration is either in progress or left behind state; resolve it manually before retrying.`);
    }
}
async function moveAllTopLevelIntoBackup(opts) {
    const { appRootDirPath, backupDirPath, logger } = opts;
    await mkdir(backupDirPath, { recursive: true });
    const entries = await readdir(appRootDirPath, { withFileTypes: true });
    for (const entry of entries) {
        if (GUARDED_TOP_LEVEL.has(entry.name))
            continue;
        // The scratch dir was just created one level up from the backup; skip it
        // even if the GUARDED list changes later -- moving scratch/ into itself
        // would recurse forever.
        if (entry.name === SCRATCH_DIR_NAME)
            continue;
        const from = join(appRootDirPath, entry.name);
        const to = join(backupDirPath, entry.name);
        await rename(from, to);
        logger.debug("[migrate] Moved to backup", { entry: entry.name });
    }
}
// --------------- Step 2: overlay fullstack template onto app root ---------------
async function copyTemplateIntoAppRoot(opts) {
    const { appRootDirPath, templateSourceDirPath, logger } = opts;
    const entries = await readdir(templateSourceDirPath, { withFileTypes: true });
    for (const entry of entries) {
        // Match the same guarded set so we never copy in build output / installs
        // from the bundled template (the copy script already filters these, but
        // defense in depth guards against the monorepo fallback path).
        if (GUARDED_TOP_LEVEL.has(entry.name))
            continue;
        const from = join(templateSourceDirPath, entry.name);
        const to = join(appRootDirPath, entry.name);
        await cp(from, to, { recursive: true });
    }
    logger.debug("[migrate] Template overlaid onto app root");
}
// --------------- Step 3: remap backup content onto the new layout ---------------
async function remapBackupDirsIntoClient(opts) {
    const { backupDirPath, appRootDirPath, templateSourceDirPath, logger } = opts;
    let copied = 0;
    for (const mapping of BACKUP_TO_CLIENT_MAPPINGS) {
        const src = join(backupDirPath, mapping.backupSubpath);
        if (!existsSync(src))
            continue;
        const dst = join(appRootDirPath, "client", mapping.clientSubpath);
        const templateEquivalent = join(templateSourceDirPath, "client", mapping.clientSubpath);
        copied += await copyTreeWithDiff({
            src,
            dst,
            templateBaseline: mapping.diffAgainstTemplate ? templateEquivalent : null,
            // pages/<Page>/apis/ is where v2 YAML APIs live; they are handled by
            // the API stub pass separately, so keep them out of the client/ tree.
            skipSubpath: mapping.backupSubpath === "pages" ? "apis" : null,
            logger,
        });
    }
    return copied;
}
async function remapBackupSingleFiles(opts) {
    const { backupDirPath, appRootDirPath, templateSourceDirPath, logger } = opts;
    let copied = 0;
    for (const mapping of BACKUP_SINGLE_FILE_DIFFS) {
        const backupFile = join(backupDirPath, mapping.backupPath);
        if (!existsSync(backupFile))
            continue;
        const destFile = join(appRootDirPath, mapping.clientPath);
        const templateFile = join(templateSourceDirPath, mapping.clientPath);
        if (existsSync(templateFile) &&
            (await filesEqual(backupFile, templateFile))) {
            logger.debug("[migrate] Skipping unmodified single file", {
                file: mapping.backupPath,
            });
            continue;
        }
        await mkdir(dirname(destFile), { recursive: true });
        await cp(backupFile, destFile);
        copied += 1;
    }
    return copied;
}
async function copyTreeWithDiff(opts) {
    const { src, dst, templateBaseline, skipSubpath, logger } = opts;
    let copied = 0;
    const stack = [src];
    while (stack.length > 0) {
        const current = stack.pop();
        const entries = await readdir(current, { withFileTypes: true });
        for (const entry of entries) {
            const absEntry = join(current, entry.name);
            const relFromSrc = relative(src, absEntry);
            // Drop top-level `apis/` under pages/<Page>/ (handled by stubV2Apis).
            if (skipSubpath && relFromSrc.split("/").includes(skipSubpath)) {
                continue;
            }
            if (entry.isDirectory()) {
                stack.push(absEntry);
                continue;
            }
            if (!entry.isFile())
                continue;
            if (templateBaseline) {
                const baselineFile = join(templateBaseline, relFromSrc);
                if (existsSync(baselineFile) &&
                    (await filesEqual(absEntry, baselineFile))) {
                    logger.debug("[migrate] Skipping unmodified file", {
                        path: relFromSrc,
                    });
                    continue;
                }
            }
            const destFile = join(dst, relFromSrc);
            await mkdir(dirname(destFile), { recursive: true });
            await cp(absEntry, destFile);
            copied += 1;
        }
    }
    return copied;
}
// --------------- Step 4: stub v2 APIs --------------------------------------------
async function stubV2Apis(opts) {
    const { backupDirPath, appRootDirPath, logger } = opts;
    const yamls = await findApiYamls(backupDirPath);
    const apiStubs = [];
    const checklist = [];
    const seenNames = new Set();
    for (const yamlPath of yamls) {
        const rawName = await extractApiName(yamlPath).catch(() => null);
        const fallback = dirname(yamlPath).split("/").pop() ?? "Api";
        const baseName = toValidApiName(rawName ?? fallback);
        const uniqueName = ensureUniqueName(baseName, seenNames);
        seenNames.add(uniqueName);
        const stubRelPath = join("server", "apis", uniqueName, "api.ts");
        const stubAbsPath = join(appRootDirPath, stubRelPath);
        await mkdir(dirname(stubAbsPath), { recursive: true });
        await writeFile(stubAbsPath, renderApiStub(uniqueName, yamlPath, backupDirPath), "utf-8");
        apiStubs.push(stubRelPath);
        checklist.push({
            name: uniqueName,
            yamlPathInBackup: relative(backupDirPath, yamlPath),
            stubPath: stubRelPath,
            status: "stub",
        });
    }
    await writeApiRegistry({ appRootDirPath, stubs: checklist, logger });
    logger.info("[migrate] Stubbed v2 APIs", { count: apiStubs.length });
    return { apiStubs, checklist };
}
async function findApiYamls(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) {
            const abs = join(dir, entry.name);
            if (entry.isDirectory()) {
                stack.push(abs);
                continue;
            }
            if (!entry.isFile())
                continue;
            if (entry.name !== "api.yaml" && entry.name !== "api.yml")
                continue;
            // Only treat files inside an `apis/` directory as v2 API definitions
            // so we do not pick up unrelated YAML that happens to be named api.yaml.
            const parts = relative(rootDir, abs).split("/");
            if (!parts.includes("apis"))
                continue;
            results.push(abs);
        }
    }
    return results.sort();
}
async function extractApiName(yamlPath) {
    const raw = await readFile(yamlPath, "utf-8");
    const parsed = parseYaml(raw);
    if (parsed &&
        typeof parsed === "object" &&
        "metadata" in parsed &&
        typeof parsed.metadata === "object" &&
        parsed.metadata &&
        typeof parsed.metadata.name ===
            "string") {
        return parsed.metadata.name;
    }
    return null;
}
/** Produce a JS-identifier-safe PascalCase-ish name. Not perfect, but
 * deterministic, and mirrors the server's v2 API slug requirements enough
 * for the stubs to compile. */
function toValidApiName(name) {
    const cleaned = name.replace(/[^A-Za-z0-9_]/g, "_");
    if (cleaned.length === 0)
        return "Api";
    if (/^[0-9]/.test(cleaned))
        return "_" + cleaned;
    return cleaned;
}
function ensureUniqueName(name, seen) {
    if (!seen.has(name))
        return name;
    let i = 2;
    while (seen.has(`${name}_${i}`))
        i += 1;
    return `${name}_${i}`;
}
function renderApiStub(name, yamlPath, backupDirPath) {
    const originalRel = relative(backupDirPath, yamlPath);
    // scratch/v2-backup/ lives under the app root; spell the full project-
    // relative path in the TODO so users can jump straight to the source.
    const scratchRel = join(SCRATCH_DIR_NAME, BACKUP_SUBDIR_NAME, originalRel);
    return `import { api, z } from "@superblocksteam/sdk-api";

/**
 * Auto-generated v2->v3 migration stub.
 *
 * Original v2 definition lives at:
 *   ${scratchRel}
 *
 * The v2 YAML has been preserved in scratch/v2-backup/ so you can port the
 * block logic (and any custom integrations) into the run() below. Input/
 * output schemas are intentionally empty until you wire them up -- leaving
 * them unchanged will fail at runtime, which is the desired forcing
 * function.
 */
export default api({
  name: "${name}",
  input: z.object({}),
  output: z.object({}).optional(),
  async run() {
    return "TODO";
  },
});
`;
}
async function writeApiRegistry(opts) {
    const { appRootDirPath, stubs, logger } = opts;
    const registryPath = join(appRootDirPath, "server", "apis", "index.ts");
    // Sort by API name so the registry is stable regardless of the order in
    // which we walked the backup directory. Users eyeballing this file
    // expect alphabetical order, and diffs on subsequent manual edits are
    // easier to read.
    const orderedStubs = [...stubs].sort((a, b) => a.name.localeCompare(b.name));
    const imports = orderedStubs
        .map((s) => `import ${s.name} from "./${s.name}/api.js";`)
        .join("\n");
    const entries = orderedStubs.map((s) => `  ${s.name},`).join("\n");
    const contents = `/**
 * API Registry - Central export for all APIs.
 *
 * Regenerated by the v2->v3 migration. Each entry here corresponds to a
 * pre-migration YAML API preserved under scratch/v2-backup/. Feel free to
 * add / remove entries by hand as you port the logic; the restructure
 * script is idempotent against manual edits (it will not run again unless
 * the user re-initiates migration).
 */

${imports}

const apis = {
${entries}
} as const;

export default apis;

/** Type for useApi inference - exported for client type-only imports */
export type ApiRegistry = typeof apis;
`;
    await mkdir(dirname(registryPath), { recursive: true });
    await writeFile(registryPath, contents, "utf-8");
    logger.debug("[migrate] Rewrote server/apis/index.ts", {
        registryPath,
        count: stubs.length,
    });
}
// --------------- Helpers ---------------------------------------------------------
async function assertDirectoryExists(path, label) {
    try {
        const s = await stat(path);
        if (!s.isDirectory()) {
            throw new Error(`${label} path exists but is not a directory: ${path}`);
        }
    }
    catch (err) {
        if (err.code === "ENOENT") {
            throw new Error(`${label} directory does not exist: ${path}`);
        }
        throw err;
    }
}
async function filesEqual(a, b) {
    const [aBuf, bBuf] = await Promise.all([readFile(a), readFile(b)]);
    if (aBuf.length !== bBuf.length)
        return false;
    return aBuf.equals(bBuf);
}
//# sourceMappingURL=restructure.js.map