/**
 * Mandatory WRNexusJS private release workflow.
 *
 *   bun run release:prepare   # verify, enforce migration coverage, stage packages
 *   # review, commit, and push the framework repository
 *   bun run release:private   # publish/resume, update docs, verify llms, commit + push docs
 *
 * Set WRNEXUS_DOCS_ROOT when the docs repository is not D:\\Company\\wrnexusjs.
 */

import { existsSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { spawnSync } from "node:child_process";
import { updateMigrationVersions } from "../packages/cli/src/update.ts";
import { validateAndHashStage } from "./lib/package-integrity.ts";

const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const docsRoot = resolve(process.env.WRNEXUS_DOCS_ROOT ?? "D:\\Company\\wrnexusjs");
const npm = process.platform === "win32" ? "npm.cmd" : "npm";
const mode = process.argv[2];

interface PackageInfo {
  dir: string;
  name: string;
  version: string;
  dependencies: Record<string, string>;
}

function run(
  command: string,
  args: string[],
  cwd = root,
  options: { capture?: boolean; allowFailure?: boolean } = {},
) {
  const result = spawnSync(command, args, {
    cwd,
    encoding: options.capture ? "utf8" : undefined,
    stdio: options.capture ? "pipe" : "inherit",
    env: process.env,
  });
  if (result.error) throw result.error;
  if (result.status !== 0 && !options.allowFailure) {
    const detail = options.capture ? `\n${result.stderr || result.stdout || ""}` : "";
    throw new Error(`${command} ${args.join(" ")} failed with ${result.status}.${detail}`);
  }
  return result;
}

function output(command: string, args: string[], cwd = root): string {
  return String(run(command, args, cwd, { capture: true }).stdout ?? "").trim();
}

function packages(): PackageInfo[] {
  return readdirSync(join(root, "packages"), { withFileTypes: true })
    .filter((entry) => entry.isDirectory())
    .map((entry) => {
      const file = join(root, "packages", entry.name, "package.json");
      if (!existsSync(file)) return null;
      const manifest = JSON.parse(readFileSync(file, "utf8"));
      if (!manifest.name?.startsWith("@wrnexus/")) return null;
      return {
        dir: entry.name,
        name: manifest.name as string,
        version: manifest.version as string,
        dependencies: (manifest.dependencies ?? {}) as Record<string, string>,
      };
    })
    .filter((value): value is PackageInfo => value !== null);
}

function alignedVersion(all: PackageInfo[]): string {
  const versions = new Set(all.map((pkg) => pkg.version));
  if (versions.size !== 1) {
    throw new Error(`Package versions are not aligned: ${[...versions].join(", ")}`);
  }
  const version = all[0]?.version;
  if (!version || !/^\d+\.\d+\.\d+$/.test(version)) {
    throw new Error(`Invalid release version: ${version ?? "missing"}`);
  }
  return version;
}

function sortedPackages(all: PackageInfo[]): PackageInfo[] {
  const byName = new Map(all.map((pkg) => [pkg.name, pkg]));
  const seen = new Set<string>();
  const result: PackageInfo[] = [];
  const visit = (pkg: PackageInfo) => {
    if (seen.has(pkg.name)) return;
    seen.add(pkg.name);
    for (const dependency of Object.keys(pkg.dependencies)) {
      const internal = byName.get(dependency);
      if (internal) visit(internal);
    }
    result.push(pkg);
  };
  all.forEach(visit);
  return result;
}

function requireMigration(version: string) {
  if (!updateMigrationVersions().includes(version)) {
    throw new Error(
      `Release ${version} has no update migration entry. Add an idempotent migration (a documented no-op is acceptable) to packages/cli/src/update.ts.`,
    );
  }
}

function validateStaging(all: PackageInfo[], version: string) {
  const integrityPath = join(root, ".publish", "PACKAGE-INTEGRITY.json");
  if (!existsSync(integrityPath)) {
    throw new Error("Missing .publish/PACKAGE-INTEGRITY.json; stage verification did not run.");
  }
  const integrity = JSON.parse(readFileSync(integrityPath, "utf8"));
  const recorded = new Map(
    (Array.isArray(integrity.packages) ? integrity.packages : []).map((entry: any) => [
      entry.name,
      entry,
    ]),
  );

  for (const pkg of all) {
    const stage = join(root, ".publish", pkg.dir);
    const manifestPath = join(stage, "package.json");
    const dist = join(stage, "dist", "index.js");
    if (!existsSync(manifestPath) || !existsSync(dist)) {
      throw new Error(`Missing staged artifact for ${pkg.name}; run release:prepare again.`);
    }
    const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
    if (manifest.version !== version || manifest.publishConfig?.access !== "restricted") {
      throw new Error(`${pkg.name} staging is not restricted ${version}.`);
    }

    const actual = validateAndHashStage(stage, manifest);
    const expected = recorded.get(pkg.name);
    if (!expected || JSON.stringify(expected) !== JSON.stringify(actual)) {
      throw new Error(`${pkg.name} staging integrity does not match PACKAGE-INTEGRITY.json.`);
    }
  }

  if (recorded.size !== all.length) {
    throw new Error(
      `Staging integrity lists ${recorded.size} packages but the workspace contains ${all.length}.`,
    );
  }
}

function requireCleanAndPushed(repo: string, label: string) {
  // `status --porcelain` can report false worktree changes on Windows when a
  // generated LF file replaces an autocrlf checkout with identical Git
  // content. Compare canonical diffs and untracked files instead.
  const worktreeChanged = run("git", ["diff", "--quiet"], repo, {
    capture: true,
    allowFailure: true,
  }).status;
  const indexChanged = run("git", ["diff", "--cached", "--quiet"], repo, {
    capture: true,
    allowFailure: true,
  }).status;
  const untracked = output("git", ["ls-files", "--others", "--exclude-standard"], repo);
  if (worktreeChanged || indexChanged || untracked) {
    throw new Error(`${label} repository has uncommitted changes.`);
  }
  run("git", ["fetch", "origin"], repo);
  const branch = output("git", ["branch", "--show-current"], repo);
  const head = output("git", ["rev-parse", "HEAD"], repo);
  const remote = output("git", ["rev-parse", `origin/${branch}`], repo);
  if (head !== remote) throw new Error(`${label} HEAD is not synchronized with origin/${branch}.`);
}

function registryHas(pkg: PackageInfo, version: string): boolean {
  const result = run(npm, ["view", `${pkg.name}@${version}`, "version", "--json"], root, {
    capture: true,
    allowFailure: true,
  });
  return result.status === 0 && String(result.stdout).includes(version);
}

function verifyPrivate(pkg: PackageInfo, version: string) {
  const status = output(npm, ["access", "get", "status", pkg.name]);
  if (!status.endsWith(": private")) throw new Error(`${pkg.name}@${version} is not private.`);
}

function syncDocsManifest(all: PackageInfo[], version: string) {
  const file = join(docsRoot, "package.json");
  const manifest = JSON.parse(readFileSync(file, "utf8"));
  manifest.dependencies ??= {};
  manifest.devDependencies ??= {};
  for (const pkg of all) {
    if (pkg.name === "@wrnexus/cli") {
      manifest.devDependencies[pkg.name] = `^${version}`;
      delete manifest.dependencies[pkg.name];
    } else {
      manifest.dependencies[pkg.name] = `^${version}`;
      delete manifest.devDependencies[pkg.name];
    }
  }
  manifest.wrnexus = { ...(manifest.wrnexus ?? {}), version };
  writeFileSync(file, JSON.stringify(manifest, null, 2) + "\n");
}

function verifyDocs(all: PackageInfo[], version: string) {
  const manifest = JSON.parse(readFileSync(join(docsRoot, "package.json"), "utf8"));
  if (manifest.wrnexus?.version !== version) throw new Error("Docs version marker is stale.");
  const llms = readFileSync(join(docsRoot, "public", "llms.txt"), "utf8");
  if (!llms.includes(`# WRNexusJS documentation ${version}`)) {
    throw new Error("Docs public/llms.txt has a stale or missing release header.");
  }
  for (const pkg of all) {
    if (!llms.includes(`## ${pkg.name}`)) {
      throw new Error(`Docs public/llms.txt is missing ${pkg.name}.`);
    }
  }
  const full = readFileSync(join(docsRoot, "public", "llms-full.txt"), "utf8");
  if (!full.includes(`WRNexusJS ${version}`) || !full.includes("/packages/helpers")) {
    throw new Error("Docs public/llms-full.txt is stale or incomplete.");
  }
}

function prepare(all: PackageInfo[], version: string) {
  requireMigration(version);
  console.log(`\nPreparing WRNexusJS ${version} (${all.length} packages)…\n`);

  run(process.execPath, ["run", "scripts/generate-ui-component-reference.mjs"], root);

  run(process.execPath, ["run", "format"]);
  run(process.execPath, ["run", "check:workspace"]);
  run(process.execPath, ["run", "validate:0.8"]);
  run(process.execPath, ["run", "security:framework"]);
  run(process.execPath, ["run", "sbom"]);
  run(process.execPath, ["run", "benchmark:framework"]);
  run(process.execPath, ["run", "check"]);
  run(process.execPath, ["run", "check:editor-compiler"]);
  run(process.execPath, ["run", "check:editor-language-server"]);
  run(process.execPath, ["run", "check:editor-extension"]);
  run(process.execPath, ["run", "scripts/publish-packages.ts"]);
  run(process.execPath, ["run", "test:staged-consumers"]);
  validateStaging(all, version);
  console.log(
    `\n✓ Release ${version} is prepared. Review changes, commit and push the framework repository, then run: bun run release:private\n`,
  );
}

function verifyUiComponentReference(): void {
  const generatedFiles = [
    "packages/ui/component-catalog.json",
    "packages/ui/component-reference.json",
    "packages/ui/COMPONENTS.md",
  ];

  const result = run(
    process.execPath,
    ["run", "scripts/generate-ui-component-reference.mjs"],
    root,
    {
      capture: true,
      allowFailure: true,
    },
  );

  if (result.status !== 0) {
    const detail = result.stderr || result.stdout || "";

    throw new Error(`Failed to generate the UI component reference.${detail ? `\n${detail}` : ""}`);
  }

  // The generator intentionally produces stable semantic JSON, while Prettier
  // controls repository formatting. Format only the generated files before the
  // Git comparison so short arrays and Windows line endings cannot create a
  // permanent generate/format release loop.
  run(process.execPath, ["x", "prettier", "--write", ...generatedFiles], root);

  // Do not use `git status --short` here. On Windows, the generator writes LF
  // while an autocrlf checkout may contain CRLF. `git status` can report those
  // files as modified even when their canonical Git content is identical.
  // `git diff --quiet` applies Git's normal text conversion and only fails for
  // a real generated-content difference.
  const changedStatus = run("git", ["diff", "--quiet", "--", ...generatedFiles], root, {
    capture: true,
    allowFailure: true,
  }).status;

  if (changedStatus === 0) {
    return;
  }

  const changed = output("git", ["diff", "--name-status", "--", ...generatedFiles], root);

  throw new Error(
    [
      "UI component reference is stale.",
      "",
      changed || generatedFiles.join("\n"),
      "",
      "Run:",
      "  bun run scripts/generate-ui-component-reference.mjs",
      "  bun run format",
      "  git add packages/ui/component-catalog.json packages/ui/component-reference.json packages/ui/COMPONENTS.md",
      '  git commit -m "docs(ui): refresh component reference"',
      "  git push origin main",
    ].join("\n"),
  );
}

function publish(all: PackageInfo[], version: string) {
  if (!existsSync(docsRoot)) throw new Error(`Docs repository not found: ${docsRoot}`);
  requireMigration(version);
  run(process.execPath, ["run", "check:workspace"]);
  run(process.execPath, ["run", "validate:0.8"]);
  run(process.execPath, ["run", "security:framework"]);
  run(process.execPath, ["run", "check:editor-compiler"]);
  run(process.execPath, ["run", "check:editor-language-server"]);
  run(process.execPath, ["run", "check:editor-extension"]);
  requireCleanAndPushed(root, "Framework");
  requireCleanAndPushed(docsRoot, "Docs");

  verifyUiComponentReference();

  run(process.execPath, ["run", "format"]);

  requireCleanAndPushed(root, "Framework after generated references");

  run(process.execPath, ["run", "scripts/publish-packages.ts"]);

  // Rebuild from the committed source and refuse publication if staging drifts.
  run(process.execPath, ["run", "scripts/publish-packages.ts"]);
  validateStaging(all, version);
  requireCleanAndPushed(root, "Framework after staging");
  output(npm, ["whoami"]);

  for (const pkg of sortedPackages(all)) {
    if (registryHas(pkg, version)) {
      console.log(`  = ${pkg.name}@${version} already published; verifying`);
    } else {
      console.log(`  + publishing ${pkg.name}@${version}`);
      run(npm, ["publish", join(root, ".publish", pkg.dir), "--access", "restricted"]);
    }
    verifyPrivate(pkg, version);
  }

  console.log(`\nUpdating docs repository at ${docsRoot}…\n`);
  run(
    process.execPath,
    [
      "x",
      "--package",
      `@wrnexus/cli@${version}`,
      "wrnexus",
      "update",
      `--version=${version}`,
      "--no-verify",
    ],
    docsRoot,
  );
  syncDocsManifest(all, version);
  run(process.execPath, ["install"], docsRoot);
  run(npm, ["install", "--package-lock-only", "--ignore-scripts"], docsRoot);
  run(process.execPath, ["run", "docs:generate"], docsRoot);
  run(process.execPath, ["run", "docs:generate"], docsRoot);
  // Generators also refresh ignored route/type artifacts. Format after
  // generation so the subsequent format check validates the final tree.
  run(process.execPath, ["run", "format"], docsRoot);
  run(process.execPath, ["run", "check"], docsRoot);
  run(process.execPath, ["run", "build"], docsRoot);
  verifyDocs(all, version);

  run("git", ["add", "--all"], docsRoot);
  if (output("git", ["status", "--porcelain"], docsRoot)) {
    run("git", ["commit", "-m", `docs: update portal for WRNexusJS ${version}`], docsRoot);
  }
  run("git", ["push", "origin", "HEAD"], docsRoot);
  requireCleanAndPushed(docsRoot, "Docs");
  console.log(`\n✓ WRNexusJS ${version} published privately and docs are updated and pushed.\n`);
}

const all = packages();
const version = alignedVersion(all);
if (mode === "prepare") prepare(all, version);
else if (mode === "publish") publish(all, version);
else {
  throw new Error("Usage: bun scripts/release.ts prepare | publish");
}
