import dotenv from "dotenv";
import { Storage } from "@google-cloud/storage";
import {
  EDM_TEMPLATE_ROOT,
  EDM_TEMPLATE_SUBROOT,
} from "@/lib/storage/edm-storage";

dotenv.config();

/**
 * Move template objects into the v2 layout.
 *
 *   before   {EDM_TEMPLATE_ROOT}/{templateId}/...
 *   after    {EDM_TEMPLATE_ROOT}/templates/{templateId}/...
 *
 * Copy first, delete only when asked. A copy is server-side (no egress, bytes
 * provably identical) and leaves the old object readable, which matters because
 * the read path falls back to the old key: at no point during this is a template
 * unreadable, in either order of deploy and run.
 *
 *   tsx src/cmd/migrate-edm-storage-layout.ts                  # dry run
 *   tsx src/cmd/migrate-edm-storage-layout.ts --apply          # copy only
 *   tsx src/cmd/migrate-edm-storage-layout.ts --apply --delete-old
 *
 * Run --apply on its own first, confirm sends still work, and only then come
 * back with --delete-old. Deleting is the one step that cannot be undone.
 */

const APPLY = process.argv.includes("--apply");
const DELETE_OLD = process.argv.includes("--delete-old");
const limitArg = process.argv.indexOf("--limit");
const LIMIT = limitArg > -1 ? parseInt(process.argv[limitArg + 1] ?? "0", 10) : 0;

const main = async () => {
  const rawCredentials = process.env.GOOGLE_SERVICE_ACCOUNT_CREDENTIALS;
  const bucketName = process.env.GOOGLE_CLOUD_STORAGE_BUCKET_NAME;

  if (!rawCredentials || !bucketName) {
    console.error(
      "GOOGLE_SERVICE_ACCOUNT_CREDENTIALS and GOOGLE_CLOUD_STORAGE_BUCKET_NAME must be set.",
    );
    process.exit(1);
  }

  const client = new Storage({
    credentials: JSON.parse(rawCredentials),
    projectId: process.env.GOOGLE_CLOUD_PROJECT_ID,
  });
  const bucket = client.bucket(bucketName);

  const [files] = await bucket.getFiles({ prefix: `${EDM_TEMPLATE_ROOT}/` });

  const newPrefix = `${EDM_TEMPLATE_ROOT}/${EDM_TEMPLATE_SUBROOT}/`;
  // Anything already under the namespace is done. Filtering by prefix rather
  // than by a marker means re-running is safe and idempotent.
  const stale = files.filter((f) => !f.name.startsWith(newPrefix));

  console.log(`bucket           ${bucketName}`);
  console.log(`template root    ${EDM_TEMPLATE_ROOT}/`);
  console.log(`objects found    ${files.length}`);
  console.log(`to move          ${stale.length}`);
  console.log(`mode             ${APPLY ? (DELETE_OLD ? "apply + delete old" : "apply (copy only)") : "dry run"}`);
  console.log("");

  if (stale.length === 0) {
    console.log("Nothing to do — every object is already in the v2 layout.");
    return;
  }

  const targets = LIMIT > 0 ? stale.slice(0, LIMIT) : stale;
  if (LIMIT > 0 && stale.length > LIMIT) {
    console.log(`Limited to the first ${LIMIT} of ${stale.length}.\n`);
  }

  let copied = 0;
  let skipped = 0;
  let deleted = 0;
  const failures: string[] = [];

  for (const file of targets) {
    // "{root}/k-abc/active/template.json" -> "k-abc/active/template.json"
    const relative = file.name.slice(EDM_TEMPLATE_ROOT.length + 1);
    const destination = `${newPrefix}${relative}`;

    if (!APPLY) {
      console.log(`would copy  ${file.name}\n         -> ${destination}`);
      continue;
    }

    try {
      const destFile = bucket.file(destination);
      const [exists] = await destFile.exists();
      if (exists) {
        // A previous partial run got here. Not an error, and not worth
        // re-copying: the source is immutable.
        skipped += 1;
      } else {
        await file.copy(destFile);
        copied += 1;
      }

      if (DELETE_OLD) {
        await file.delete();
        deleted += 1;
      }
    } catch (err) {
      failures.push(`${file.name}: ${(err as Error).message}`);
    }
  }

  if (APPLY) {
    console.log(`copied  ${copied}`);
    console.log(`skipped ${skipped} (already present at destination)`);
    if (DELETE_OLD) console.log(`deleted ${deleted} old objects`);
  }

  if (failures.length > 0) {
    console.log(`\n${failures.length} failed:`);
    for (const f of failures) console.log(`  ${f}`);
    // Non-zero exit so a wrapper script does not treat a partial move as done.
    process.exit(1);
  }

  if (APPLY && !DELETE_OLD) {
    console.log(
      "\nOld objects left in place. Confirm a real send still renders, then re-run with --delete-old.",
    );
  }
};

void main().catch((err) => {
  console.error(err);
  process.exit(1);
});
