/**
 * Points every image's `sourceUrl` in a blog export file at its copy in
 * storage/uploads on the CMS, instead of the old WordPress address, and writes
 * a new file. Run it after `npm run export:link-images`, which fills in `url`
 * and `file`.
 *
 *   npm run export:storage-urls -- wordpress-import/blog-export.json
 *   npm run export:storage-urls -- in.json --out out.json --base https://cms.example.com
 *
 * `sourceUrl` becomes `<base>/uploads/<file.storageKey>` (it must be a full
 * URL; see src/features/blog-import/format.ts). Only images whose file is in
 * storage, with the size the file records, are changed; any other image is
 * listed and left as it was. The output is checked against the export schema.
 * Default output: the input name with `.storage` before `.json`.
 */
import { readFile, stat, writeFile } from "node:fs/promises";
import path from "node:path";
import { parseArgs } from "node:util";

import { blogExportSchema } from "@/features/blog-import/format";

const { values: args, positionals } = parseArgs({
  allowPositionals: true,
  options: {
    out: { type: "string" },
    base: { type: "string", default: "https://secondphonenumber.worksqr.com" },
    storage: { type: "string", default: "storage/uploads" },
  },
});

type ImageRef = {
  sourceUrl: string;
  url?: string;
  file?: { storageKey: string; size: number } | null;
};

/** Every image reference in the file, with where it is, e.g. `posts[3].featuredImage`. */
function findImages(
  value: unknown,
  where: string,
  found: { ref: ImageRef; where: string }[],
) {
  if (Array.isArray(value)) {
    value.forEach((item, i) => findImages(item, `${where}[${i}]`, found));
  } else if (value && typeof value === "object") {
    if ("sourceUrl" in value && "alt" in value)
      found.push({ ref: value as ImageRef, where });
    for (const [key, child] of Object.entries(value)) {
      findImages(child, where ? `${where}.${key}` : key, found);
    }
  }
  return found;
}

async function main() {
  const input = positionals[0];
  if (!input) {
    throw new Error(
      "Usage: npm run export:storage-urls -- <file.json> [--out out.json] [--base url]",
    );
  }
  const base = new URL(args.base).origin;
  const output = args.out ?? input.replace(/\.json$/i, "") + ".storage.json";
  const data = JSON.parse(
    await readFile(path.resolve(input), "utf8"),
  ) as unknown;

  const images = findImages(data, "", []);
  let changed = 0;
  const skipped: string[] = [];
  for (const { ref, where } of images) {
    const key = ref.file?.storageKey;
    if (!key) {
      skipped.push(
        `${where}: no file.storageKey (run export:link-images first)`,
      );
      continue;
    }
    const size = await stat(path.resolve(args.storage, key)).then(
      (info) => info.size,
      () => null,
    );
    if (size === null) {
      skipped.push(`${where}: ${key} is not in ${args.storage}`);
      continue;
    }
    if (size !== ref.file?.size) {
      skipped.push(
        `${where}: ${key} is ${size} bytes, the file records ${ref.file?.size}`,
      );
      continue;
    }
    ref.sourceUrl = `${base}/uploads/${key}`;
    changed++;
  }

  const checked = blogExportSchema.safeParse(data);
  if (!checked.success) {
    for (const issue of checked.error.issues.slice(0, 20)) {
      console.error(`  ${issue.path.join(".")}: ${issue.message}`);
    }
    throw new Error(
      "The result doesn't match the export format; nothing was written.",
    );
  }

  await writeFile(path.resolve(output), `${JSON.stringify(data, null, 2)}\n`);
  console.log(
    `${changed} of ${images.length} images now point at ${base}/uploads/. Wrote ${output}`,
  );
  for (const line of skipped) console.log(`  unchanged: ${line}`);
  if (skipped.length) process.exitCode = 1;
}

main().catch((error: unknown) => {
  console.error(error instanceof Error ? error.message : error);
  process.exitCode = 1;
});
