/**
 * Splits a blog export file into smaller batch files that import on their
 * own, so a big import can go through the admin panel's Import JSON screen
 * a few articles at a time.
 *
 *   npm run export:split -- wordpress-import/blog-export.storage.json
 *   npm run export:split -- in.json --size 5 --out wordpress-import/batches \
 *     --source-urls-from wordpress-import/blog-export.json
 *
 * Each batch holds `--size` articles (default 5), newest first, plus only the
 * categories and authors those articles use, so every batch is complete.
 * `--source-urls-from` takes each image's `sourceUrl` from another export of
 * the same blog, matched by `file.storageKey` (use it to put back the
 * WordPress addresses, which still serve the files, where a file points
 * `sourceUrl` at a server that doesn't). Every batch is checked against the
 * export schema. Writes blog-batch-01.json … and README.md in the out folder.
 */
import { mkdir, readFile, writeFile } from "node:fs/promises";
import path from "node:path";
import { parseArgs } from "node:util";

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

const { values: args, positionals } = parseArgs({
  allowPositionals: true,
  options: {
    size: { type: "string", default: "5" },
    out: { type: "string" },
    "source-urls-from": { type: "string" },
  },
});

type Ref = { sourceUrl: string; file?: { storageKey?: string } | null };

/** Calls `visit` on every image reference ({ sourceUrl, alt, … }) in a value. */
function eachImage(value: unknown, visit: (ref: Ref) => void) {
  if (Array.isArray(value)) {
    for (const item of value) eachImage(item, visit);
  } else if (value && typeof value === "object") {
    if ("sourceUrl" in value && "alt" in value) visit(value as Ref);
    for (const child of Object.values(value)) eachImage(child, visit);
  }
}

const readJson = async (file: string) =>
  JSON.parse(
    (await readFile(path.resolve(file), "utf8")).replace(/^﻿/, ""),
  ) as BlogExportInput;

async function main() {
  const input = positionals[0];
  const size = Number(args.size);
  if (!input || !Number.isInteger(size) || size < 1) {
    throw new Error(
      "Usage: npm run export:split -- <file.json> [--size 5] [--out dir] [--source-urls-from original.json]",
    );
  }
  const outDir = path.resolve(
    args.out ?? path.join(path.dirname(input), "batches"),
  );
  const data = structuredClone(await readJson(input));
  const checked = blogExportSchema.safeParse(data);
  if (!checked.success)
    throw new Error(`${input} doesn't match the export format.`);

  let restored = 0;
  if (args["source-urls-from"]) {
    const byKey = new Map<string, string>();
    eachImage(await readJson(args["source-urls-from"]), (ref) => {
      const key = ref.file?.storageKey;
      if (key) byKey.set(key, ref.sourceUrl);
    });
    eachImage(data, (ref) => {
      const original = ref.file?.storageKey && byKey.get(ref.file.storageKey);
      if (original && original !== ref.sourceUrl) {
        ref.sourceUrl = original;
        restored++;
      }
    });
  }

  // Newest first, so the first batches fill the blog's first page.
  const posts = [...data.posts].sort(
    (a, b) =>
      Date.parse(b.publishedAt ?? "") - Date.parse(a.publishedAt ?? "") ||
      a.slug.localeCompare(b.slug),
  );
  const count = Math.ceil(posts.length / size);
  const width = String(count).length < 2 ? 2 : String(count).length;
  await mkdir(outDir, { recursive: true });

  const rows: string[] = [];
  let totalPosts = 0;
  let totalFaqs = 0;
  for (let i = 0; i < count; i++) {
    const chunk = posts.slice(i * size, (i + 1) * size);
    const categories = new Set(chunk.map((post) => post.category));
    const authors = new Set(chunk.map((post) => post.author));
    const batch: BlogExportInput = {
      format: data.format,
      version: data.version,
      source: data.source,
      exportedAt: data.exportedAt,
      categories: (data.categories ?? []).filter((c) => categories.has(c.slug)),
      authors: (data.authors ?? []).filter((a) => authors.has(a.slug)),
      posts: chunk,
      redirects: [],
    };
    const result = blogExportSchema.safeParse(batch);
    if (!result.success) {
      throw new Error(
        `Batch ${i + 1} doesn't match the export format: ${result.error.issues[0]?.message}`,
      );
    }

    let images = 0;
    eachImage(batch, () => images++);
    const faqs = chunk.reduce((n, post) => n + (post.faqs?.length ?? 0), 0);
    totalPosts += chunk.length;
    totalFaqs += faqs;

    const name = `blog-batch-${String(i + 1).padStart(width, "0")}.json`;
    await writeFile(
      path.join(outDir, name),
      `${JSON.stringify(batch, null, 2)}\n`,
    );
    const day = (iso?: string | null) => (iso ?? "").slice(0, 10);
    rows.push(
      `| [${name}](${name}) | ${day(chunk.at(-1)?.publishedAt)} → ${day(chunk[0]?.publishedAt)} | ${chunk.length} | ${faqs} | ${images} | ${[...categories].join(", ")} | ${[...authors].join(", ")} |`,
      ...chunk.map((post) => `|  | \`${post.slug}\` | | | | | |`),
    );
    console.log(
      `${name}: ${chunk.length} articles, ${faqs} FAQs, ${images} image references`,
    );
  }

  const readme = `# Blog import batches

Made from \`${path.basename(input)}\` by \`npm run export:split\`${args["source-urls-from"] ? `, with each image's \`sourceUrl\` taken from \`${path.basename(args["source-urls-from"])}\` (${restored} changed)` : ""}. ${totalPosts} articles and ${totalFaqs} FAQs in ${count} files of up to ${size} articles, newest first. Each file holds only the categories and authors its articles use, so it imports on its own.

## Importing

Import the files in order, one at a time, in the admin panel (**Blogs › Import JSON**):

1. Choose the file and pick **Skip them** for articles already in the CMS (pick **Replace them** only to overwrite).
2. **Check file.** Expect every article as "Will create" (or "Skip"), nothing under "Can't be imported", and in the Images panel every image either already on the CMS, in the media library, or to upload (none unavailable).
3. **Import.** It uploads the missing images to the media library first, then imports.
4. Spot-check one article on the website, then move to the next file.

From the command line instead (\`admin-panel/\`, with \`LARAVEL_ADMIN_TOKEN_DEV\` or \`_PROD\` set):

\`\`\`bash
npm run laravel -- import wordpress-import/batches/blog-batch-01.json --write
npm run laravel -- verify wordpress-import/batches/blog-batch-01.cms.json
\`\`\`

## Files

| File | Published | Articles | FAQs | Images | Categories | Authors |
| --- | --- | --- | --- | --- | --- | --- |
${rows.join("\n")}
`;
  await writeFile(path.join(outDir, "README.md"), readme);
  console.log(
    `Wrote ${count} batches (${totalPosts} articles, ${totalFaqs} FAQs) to ${path.relative(process.cwd(), outDir)}/` +
      (args["source-urls-from"]
        ? `; ${restored} sourceUrl values restored`
        : ""),
  );
  if (totalPosts !== data.posts.length)
    throw new Error("Some articles weren't written.");
}

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