#!/usr/bin/env tsx

/**
 * Migration Script: Fresh/Deno/Preact → TanStack Start/React/Cloudflare Workers
 *
 * Converts a Deco storefront from the old Fresh/Deno stack to the new TanStack Start stack.
 * Ships as part of @decocms/blocks-cli — run from a site's root directory.
 *
 * Usage (from your Fresh site directory):
 *   npx -p @decocms/blocks-cli deco-migrate [options]
 *
 * Options:
 *   --source <dir>    Source directory (default: current directory)
 *   --dry-run         Preview changes without writing files
 *   --verbose         Show detailed output
 *   --help            Show this help message
 *
 * Phases:
 *   1. Analyze  — Scan source site, categorize files, detect patterns
 *   2. Scaffold — Create target structure (configs, routes, infra files)
 *   3. Transform — Convert source files (imports, JSX, Fresh APIs, Deno-isms, Tailwind)
 *   4. Cleanup  — Delete old artifacts, move static → public
 *   5. Report   — Generate MIGRATION_REPORT.md with findings
 *   6. Verify   — Smoke test the migrated output
 */

import { execSync } from "node:child_process";
import * as fs from "node:fs";
import * as path from "node:path";
import { setKvNamespaceIdInJsonc } from "./lib/wrangler-config";
import { banner, green, red, stat, yellow } from "./migrate/colors";
import { loadConfig, validateConfig } from "./migrate/config";
import { analyze } from "./migrate/phase-analyze";
import { cleanup } from "./migrate/phase-cleanup";
import { cleanupAudit } from "./migrate/phase-cleanup-audit";
import { compile } from "./migrate/phase-compile";
import { report } from "./migrate/phase-report";
import { scaffold } from "./migrate/phase-scaffold";
import { transform } from "./migrate/phase-transform";
import { verify } from "./migrate/phase-verify";
import { detectSourceLayout, explainNonClassicLayout } from "./migrate/source-layout";
import { createContext, logPhase } from "./migrate/types";

function parseArgs(args: string[]): {
  source: string;
  dryRun: boolean;
  verbose: boolean;
  help: boolean;
  strict: boolean;
  withBuild: boolean;
  noCompile: boolean;
  noCleanupAudit: boolean;
} {
  let source = ".";
  let dryRun = false;
  let verbose = false;
  let help = false;
  let strict = false;
  let withBuild = false;
  let noCompile = false;
  let noCleanupAudit = false;

  for (let i = 0; i < args.length; i++) {
    switch (args[i]) {
      case "--source":
        source = args[++i];
        break;
      case "--dry-run":
        dryRun = true;
        break;
      case "--verbose":
        verbose = true;
        break;
      case "--strict":
        strict = true;
        break;
      case "--with-build":
        withBuild = true;
        break;
      case "--no-compile":
        noCompile = true;
        break;
      case "--no-cleanup-audit":
        noCleanupAudit = true;
        break;
      case "--help":
      case "-h":
        help = true;
        break;
    }
  }

  return {
    source,
    dryRun,
    verbose,
    help,
    strict,
    withBuild,
    noCompile,
    noCleanupAudit,
  };
}

function showHelp() {
  console.log(`
  @decocms/blocks-cli — Migration Script: Fresh/Deno → TanStack Start

  Usage:
    npx -p @decocms/blocks-cli deco-migrate [options]

  Options:
    --source <dir>        Source directory (default: .)
    --dry-run             Preview changes without writing files
    --verbose             Show detailed output for every file
    --strict              Fail (exit 2) when typecheck/build report errors
    --with-build          Also run \`vite build\` after typecheck (slower)
    --no-compile          Skip the post-bootstrap compile phase entirely
    --no-cleanup-audit    Skip the post-migration cleanup audit (run separately
                          via \`deco-post-cleanup\` if needed)
    --help, -h            Show this help message

  Environment (all optional — each step is skipped, never fatal, when unset):
    DECO_CONTROL_PLANE_TOKEN  Provision the site via the control-plane's
                              SITE_CREATE: repo, org ownership, the site's OWN
                              Cloudflare KV namespace, and the deploy CR.
    DECO_ORG                  Studio organization id that will own the site.
                              Required together with the token above.
    DECO_CONTROL_PLANE_URL    Override the control-plane base URL.
    SUPABASE_ACCESS_TOKEN     Flip the site's analytics provider.

  Examples:
    npx -p @decocms/blocks-cli deco-migrate --dry-run --verbose
    npx -p @decocms/blocks-cli deco-migrate --source ./my-site
    npx -p @decocms/blocks-cli deco-migrate --strict --with-build
    npx -p @decocms/blocks-cli deco-migrate
  `);
}

async function main() {
  const opts = parseArgs(process.argv.slice(2));

  if (opts.help) {
    showHelp();
    process.exit(0);
  }

  const sourceDir = path.resolve(opts.source);

  banner("@decocms/blocks-cli — Migrate: Fresh/Deno → TanStack Start");
  stat("Source", sourceDir);
  stat("Mode", opts.dryRun ? yellow("DRY RUN") : green("EXECUTE"));
  stat("Verbose", opts.verbose ? "yes" : "no");

  // Load optional per-site config from `.deco-migrate.config.json`. Drives
  // section-conventions hardcoded lists today; future fields will tune
  // import rewrites, scaffolding, etc.
  const siteConfig = loadConfig(sourceDir);
  if (siteConfig) {
    validateConfig(siteConfig);
    stat("Config", green(".deco-migrate.config.json (loaded)"));
  }

  const ctx = createContext(sourceDir, {
    dryRun: opts.dryRun,
    verbose: opts.verbose,
    config: siteConfig,
  });

  // Phase 0: Source-layout detection. `classic` (root sections/) and `modern`
  // (src/sections/) are both supported — the analyzer scans src/ natively for
  // modern layouts. Only genuinely ambiguous (`mixed`) or unrecognizable
  // (`empty`) layouts abort with an actionable error before we touch files.
  const layout = detectSourceLayout(sourceDir);
  ctx.layout = layout;
  if (layout === "mixed" || layout === "empty") {
    console.error(red(`Error: ${layout} source layout`));
    console.error("");
    console.error(explainNonClassicLayout(layout, sourceDir));
    process.exit(2);
  }
  if (layout === "modern") {
    console.log(`  Source layout: modern (src/) — scanning src/ natively`);
  }

  try {
    // Phase 1: Analyze source
    analyze(ctx);

    // Phase 2: Scaffold target structure
    scaffold(ctx);

    // Phase 3: Transform source files
    transform(ctx);

    // Phase 4: Cleanup old artifacts
    cleanup(ctx);

    // Phase 5: Generate report
    report(ctx);

    // Phase 6: Verify (smoke test)
    const ok = verify(ctx);
    if (!ok) {
      process.exit(2);
    }

    // Phase 7: Bootstrap (install + generate)
    if (!ctx.dryRun) {
      bootstrap(ctx);
      await provisionSite(ctx.sourceDir);
      await provisionAnalytics(ctx.sourceDir);
    }

    // Phase 8: Compile (typecheck + optional build)
    // Skipped in dry-run, when --no-compile is passed, or when bootstrap
    // didn't install dependencies (handled inside `compile`).
    if (!opts.noCompile) {
      const compileResult = compile(ctx, {
        strict: opts.strict,
        withBuild: opts.withBuild,
      });
      if (compileResult.shouldFail) {
        process.exit(2);
      }
    }

    // Phase 9: Post-migration cleanup audit
    // Read-only scan that catches residual debt the migration script
    // can't (or won't) fix. Always informational unless --strict is on,
    // in which case warning-severity findings exit 2.
    if (!opts.noCleanupAudit) {
      const auditFailed = cleanupAudit(ctx, { strict: opts.strict });
      if (auditFailed) {
        process.exit(2);
      }
    }
  } catch (error) {
    console.error(`\n  ${red("Migration failed:")}`, error);
    process.exit(1);
  }
}

function bootstrap(ctx: { sourceDir: string }) {
  logPhase("Bootstrap (install + generate)");

  let failures = 0;
  const run = (cmd: string, label: string, critical = false) => {
    console.log(`  Running: ${label}...`);
    try {
      execSync(cmd, { cwd: ctx.sourceDir, stdio: "pipe" });
      console.log(`  ${green("✓")} ${label}`);
    } catch (e: any) {
      failures++;
      const icon = critical ? red("✗") : yellow("⚠");
      console.log(`  ${icon} ${label} failed: ${e.message?.split("\n")[0]}`);
      if (critical) {
        console.log(`\n  ${red("Bootstrap aborted.")} Fix the error above and run manually.\n`);
        return false;
      }
    }
    return true;
  };

  // bun is the fleet-wide canonical package manager for decocms storefronts.
  // We hardcode it here (instead of sniffing process.env.npm_execpath) so a
  // freshly-migrated site always commits a bun.lock and never accidentally
  // ships a package-lock.json that drifts vs bun.lock under CF Workers Builds.
  // See MIGRATION_TOOLING_PLAN.md and the package-json template for the
  // matching `packageManager` field that pins the version.
  const pm = "bun";
  if (!run(`${pm} install`, "Install dependencies", true)) return;
  run("bunx tsx node_modules/@decocms/blocks-cli/scripts/generate-blocks.ts", "Generate CMS blocks");
  // generate-invoke emits src/server/invoke.gen.ts with top-level
  // createServerFn declarations + the forwardResponseCookies bridge that
  // propagates VTEX Set-Cookie headers (orderFormId, segment, sc…) to the
  // browser. Without this file, the site falls back to the proxy
  // `~/runtime.ts` route which hits /deco/invoke and used to drop cookies,
  // making the cart appear empty at /checkout after addItemToCart. The
  // upstream invoke handler now also forwards cookies correctly, but
  // running the generator gives every freshly-migrated site the canonical
  // RPC path so VTEX hooks (useCart, useUser, useWishlist) work end-to-end.
  run(
    "bunx tsx node_modules/@decocms/blocks-cli/scripts/generate-invoke.ts",
    "Generate VTEX invoke server functions",
  );
  run("bunx tsr generate", "Generate TanStack routes");

  if (failures > 0) {
    console.log(
      `\n  ${yellow("Bootstrap completed with warnings.")} Check errors above before running dev.\n`,
    );
  } else {
    console.log(`\n  ${green("Ready!")} Run \`${pm} run dev\` to start the dev server.\n`);
  }
}

/** A site slug: lower-case, the shape SITE_CREATE itself enforces. */
const SITE_SLUG = /^[a-z0-9][a-z0-9._-]*$/;

// control-plane.decocms.com has no DNS record; with it as the default, provisioning
// failed on every run that did not override DECO_CONTROL_PLANE_URL, as a fetch error the
// phase prints as a warning and moves past. This is the host that serves /mcp.
const CONTROL_PLANE_URL = "https://control-plane.infra.deco.cx";

/** `owner/name` of the repo in `dir`, from its `origin` remote, or null. */
function repoFullName(dir: string): string | null {
  try {
    const url = execSync("git remote get-url origin", { cwd: dir, stdio: "pipe" })
      .toString()
      .trim();
    const m = url.match(/[:/]([^/:]+)\/([^/]+?)(?:\.git)?$/);
    return m ? `${m[1]}/${m[2]}` : null;
  } catch {
    return null;
  }
}

/**
 * Provision the site on the control-plane (SITE_CREATE) and write the KV
 * namespace id it returns into the scaffolded `wrangler.jsonc`.
 *
 * SITE_CREATE is idempotent and create-if-absent at every step: it claims the
 * admin `sites` row, the org ownership, the site's OWN Cloudflare KV namespace
 * (`deco-kv-<site>` — never shared, since `index:live` is one key per namespace
 * and two sites sharing one would clobber each other's live pointer), the
 * production environment, and the Deco CR that makes the site actually deploy.
 * So a migrated site comes out of the box deployable and fast-deploy-ready
 * instead of needing a dashboard click per worker.
 *
 * Inert without credentials: prints the equivalent call and continues. A
 * migration must never fail because an operator ran it without a token — and
 * the id is not load-bearing anyway, since the builder re-forces the DECO_KV id
 * from `CF_KV_NAMESPACE_ID` on every build.
 */
async function provisionSite(sourceDir: string): Promise<void> {
  logPhase("Provision site (control-plane)");

  let siteName: string;
  try {
    siteName = JSON.parse(fs.readFileSync(path.join(sourceDir, "package.json"), "utf8")).name;
  } catch {
    console.log(`  ${yellow("⚠")} Could not read package.json — skipping site provision`);
    return;
  }

  const base = process.env.DECO_CONTROL_PLANE_URL || CONTROL_PLANE_URL;
  const token = process.env.DECO_CONTROL_PLANE_TOKEN;
  const org = process.env.DECO_ORG;
  const repo = repoFullName(sourceDir);

  const args: Record<string, string> = { name: siteName, org: org ?? "<DECO_ORG>" };
  // Shared/existing-repo mode: the repo already exists (we just migrated it),
  // so SITE_CREATE must reference it rather than generate one from a template.
  if (repo) args.repo = repo;

  if (!token || !org) {
    const which = [!token && "DECO_CONTROL_PLANE_TOKEN", !org && "DECO_ORG"]
      .filter(Boolean)
      .join(" + ");
    console.log(`  ${yellow("⚠")} ${which} not set — skipping site provision`);
    printSiteCreate(base, args);
    return;
  }

  try {
    const res = await fetch(`${base}/mcp`, {
      method: "POST",
      headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
      signal: AbortSignal.timeout(30_000),
      body: JSON.stringify({
        jsonrpc: "2.0",
        id: 1,
        method: "tools/call",
        params: { name: "SITE_CREATE", arguments: args },
      }),
    });
    if (!res.ok) {
      console.log(
        `  ${yellow("⚠")} SITE_CREATE failed (${res.status}): ${(await res.text()).slice(0, 160)}`,
      );
      printSiteCreate(base, args);
      return;
    }
    const body = (await res.json()) as {
      error?: { message?: string };
      result?: { structuredContent?: unknown; content?: Array<{ text?: string }> };
    };
    if (body.error) {
      console.log(`  ${yellow("⚠")} SITE_CREATE error: ${body.error.message ?? "unknown"}`);
      printSiteCreate(base, args);
      return;
    }
    const out = readToolResult(body.result);
    console.log(
      `  ${green("✓")} ${siteName} provisioned (org ${org}${out?.cluster ? `, cluster ${out.cluster}` : ""})`,
    );

    if (out?.kvNamespaceId) {
      applyKvNamespaceId(sourceDir, out.kvNamespaceId);
    } else {
      // SITE_CREATE omits it on a knative site, when CF isn't configured, or
      // when provisioning failed — the absence is the signal, not a default.
      console.log(
        `  ${yellow("⚠")} no kvNamespaceId returned — DECO_KV id left blank in wrangler.jsonc`,
      );
    }
  } catch (err: any) {
    console.log(`  ${yellow("⚠")} control-plane request failed: ${err.message}`);
    printSiteCreate(base, args);
  }
}

/** MCP tool output: `structuredContent` when present, else the JSON text block. */
function readToolResult(
  result: { structuredContent?: unknown; content?: Array<{ text?: string }> } | undefined,
): { kvNamespaceId?: string; cluster?: string } | null {
  if (!result) return null;
  if (result.structuredContent && typeof result.structuredContent === "object") {
    return result.structuredContent as { kvNamespaceId?: string; cluster?: string };
  }
  const text = result.content?.[0]?.text;
  if (!text) return null;
  try {
    return JSON.parse(text);
  } catch {
    return null;
  }
}

/** Write the site's own KV namespace id into the scaffolded wrangler.jsonc. */
function applyKvNamespaceId(sourceDir: string, id: string): void {
  const file = path.join(sourceDir, "wrangler.jsonc");
  try {
    const src = fs.readFileSync(file, "utf-8");
    const next = setKvNamespaceIdInJsonc(src, id);
    if (next === src) {
      console.log(`  ${yellow("⚠")} wrangler.jsonc has no DECO_KV binding — id not written`);
      return;
    }
    fs.writeFileSync(file, next);
    console.log(`  ${green("✓")} wrangler.jsonc DECO_KV id → ${id}`);
  } catch (err: any) {
    console.log(`  ${yellow("⚠")} could not write wrangler.jsonc: ${err.message}`);
  }
}

function printSiteCreate(base: string, args: Record<string, string>): void {
  console.log(`  Run manually once you have a control-plane token:`);
  console.log(
    `  curl -s ${base}/mcp -XPOST -H 'content-type: application/json' \\\n` +
      `    -H "authorization: Bearer $DECO_CONTROL_PLANE_TOKEN" \\\n` +
      `    -d '${JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/call", params: { name: "SITE_CREATE", arguments: args } })}'`,
  );
}

// Supabase project ref for decocms — the central platform DB that tracks all sites.
const DECOCMS_SUPABASE_REF = "ozksgdmyrqcxcwhnbepg";

async function provisionAnalytics(sourceDir: string): Promise<void> {
  logPhase("Provision analytics (decocms Supabase)");

  let siteName: string;
  try {
    const pkg = JSON.parse(fs.readFileSync(path.join(sourceDir, "package.json"), "utf8"));
    siteName = pkg.name;
  } catch {
    console.log(`  ${yellow("⚠")} Could not read package.json — skipping analytics provision`);
    return;
  }

  const token = process.env.SUPABASE_ACCESS_TOKEN;
  if (!token) {
    console.log(`  ${yellow("⚠")} SUPABASE_ACCESS_TOKEN not set — skipping analytics provision`);
    printAnalyticsSQL(siteName);
    return;
  }

  // The Supabase Management API takes raw SQL with no bind parameters, so the
  // only safe move is to refuse anything that isn't a plain site slug rather
  // than interpolate whatever `package.json` happens to contain.
  if (!SITE_SLUG.test(siteName)) {
    console.log(
      `  ${yellow("⚠")} "${siteName}" is not a plain site slug — skipping analytics provision`,
    );
    return;
  }

  const sql = `UPDATE public.sites SET metadata = metadata || '{"analytics": "onedollarstats"}'::jsonb WHERE name = '${siteName}'`;

  try {
    const res = await fetch(
      `https://api.supabase.com/v1/projects/${DECOCMS_SUPABASE_REF}/database/query`,
      {
        method: "POST",
        headers: {
          Authorization: `Bearer ${token}`,
          "Content-Type": "application/json",
        },
        body: JSON.stringify({ query: sql }),
      },
    );
    if (res.ok) {
      console.log(`  ${green("✓")} ${siteName} → analytics: onedollarstats`);
    } else {
      const body = await res.text();
      console.log(`  ${yellow("⚠")} Supabase update failed (${res.status}): ${body.slice(0, 120)}`);
      printAnalyticsSQL(siteName);
    }
  } catch (err: any) {
    console.log(`  ${yellow("⚠")} Supabase request failed: ${err.message}`);
    printAnalyticsSQL(siteName);
  }
}

function printAnalyticsSQL(siteName: string): void {
  console.log(`  Run manually on decocms Supabase (${DECOCMS_SUPABASE_REF}):`);
  console.log(
    `  UPDATE public.sites SET metadata = metadata || '{"analytics": "onedollarstats"}'::jsonb WHERE name = '${siteName}';`,
  );
}

main();
