import { spawnSync } from "node:child_process";
import fs from "node:fs";
import { readFile } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";

export const cloudWebDistPath = "web/hwlab-cloud-web/dist";
export const cloudWebDistBuildCommand = "cd web/hwlab-cloud-web && bun run build";
export const cloudWebDistFreshnessCommand = cloudWebDistBuildCommand;
export const cloudWebAppSourceFiles = Object.freeze(["src/main.ts", "src/App.vue"]);
export const cloudWebDistRuntimeFiles = Object.freeze(["index.html", "app.js", "assets/index.css", "assets/favicon.svg", "help.md"]);
export const cloudWebDistAliasFiles = Object.freeze(["gate/index.html", "diagnostics/gate/index.html", "help/index.html", "skills/index.html", "access/index.html"]);

type FreshnessStatus = "pass" | "blocked";

interface DistFileStatus {
  kind: "runtime" | "alias";
  path: string;
  expectedPath: string;
  distPath: string;
  status: "match" | "missing_expected" | "missing_dist" | "mismatch";
}

interface DistFreshness {
  status: FreshnessStatus;
  distPath: string;
  buildCommand: string;
  freshnessCommand: string;
  files: DistFileStatus[];
  mismatches: string[];
}

function absolutePath(rootDir: string, relativePath: string): string {
  return path.resolve(rootDir, relativePath);
}

function exists(filePath: string): boolean {
  return fs.existsSync(filePath);
}

export function readCloudWebAppSource(rootDir: string): string {
  return collectSourceFiles(path.join(rootDir, "src")).map((file) => fs.readFileSync(file, "utf8")).join("\n");
}

export async function buildCloudWebDist(rootDir: string): Promise<string> {
  const distRoot = absolutePath(rootDir, "dist");
  runViteBuild(rootDir, distRoot);
  finalizeGeneratedDist(rootDir, distRoot);
  return distRoot;
}

export async function inspectCloudWebDistFreshness(rootDir: string): Promise<DistFreshness> {
  const distRoot = absolutePath(rootDir, "dist");
  const expectedRoot = fs.mkdtempSync(path.join(os.tmpdir(), "hwlab-cloud-web-expected-"));
  const expectedDist = path.join(expectedRoot, "dist");
  const files: DistFileStatus[] = [];
  const mismatches: string[] = [];

  try {
    runViteBuild(rootDir, expectedDist);
    finalizeGeneratedDist(rootDir, expectedDist);

    for (const relativePath of cloudWebDistRuntimeFiles) {
      await compareFile({
        files,
        mismatches,
        kind: "runtime",
        relativePath,
        expectedPath: path.join(expectedDist, relativePath),
        distPath: path.join(distRoot, relativePath)
      });
    }

    for (const aliasPath of cloudWebDistAliasFiles) {
      await compareFile({
        files,
        mismatches,
        kind: "alias",
        relativePath: aliasPath,
        expectedPath: path.join(expectedDist, aliasPath),
        distPath: path.join(distRoot, aliasPath)
      });
    }
  } finally {
    fs.rmSync(expectedRoot, { recursive: true, force: true });
  }

  return {
    status: mismatches.length === 0 ? "pass" : "blocked",
    distPath: cloudWebDistPath,
    buildCommand: cloudWebDistBuildCommand,
    freshnessCommand: cloudWebDistFreshnessCommand,
    files,
    mismatches
  };
}

export function cloudWebDistFreshnessMessage(distFreshness: { mismatches: string[] }): string {
  const mismatches = distFreshness.mismatches.length > 0 ? distFreshness.mismatches.join(", ") : "unknown";
  return [`Cloud Web dist is stale or missing before image build: ${mismatches}.`, `Run ${cloudWebDistBuildCommand} before publishing ${cloudWebDistPath}.`].join(" ");
}

export async function assertCloudWebDistFresh(rootDir: string): Promise<DistFreshness> {
  const distFreshness = await inspectCloudWebDistFreshness(rootDir);
  if (distFreshness.status !== "pass") throw new Error(cloudWebDistFreshnessMessage(distFreshness));
  return distFreshness;
}

function runViteBuild(rootDir: string, outDir: string): void {
  ensureWebDeps(rootDir);
  const rootLocalVite = path.join(rootDir, "node_modules", ".bin", "vite");
  const packageLocalVite = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "node_modules", ".bin", "vite");
  const localVite = exists(rootLocalVite) ? rootLocalVite : exists(packageLocalVite) ? packageLocalVite : null;
  const command = localVite ?? "npx";
  const args = localVite
    ? ["build", "--outDir", outDir, "--emptyOutDir"]
    : ["--yes", "vite@5.4.10", "build", "--outDir", outDir, "--emptyOutDir"];
  const result = spawnSync(command, args, { cwd: rootDir, encoding: "utf8" });
  if (result.stdout) process.stdout.write(result.stdout);
  if (result.stderr) process.stderr.write(result.stderr);
  if (result.status !== 0) throw new Error(`Vite build failed with exit ${result.status ?? "unknown"}`);
}

function finalizeGeneratedDist(rootDir: string, distRoot: string): void {
  const helpSource = absolutePath(rootDir, "help.md");
  if (exists(helpSource)) fs.copyFileSync(helpSource, path.join(distRoot, "help.md"));
  for (const aliasPath of cloudWebDistAliasFiles) {
    const distPath = path.join(distRoot, aliasPath);
    fs.mkdirSync(path.dirname(distPath), { recursive: true });
    fs.copyFileSync(path.join(distRoot, "index.html"), distPath);
  }
}

async function compareFile(input: { files: DistFileStatus[]; mismatches: string[]; kind: "runtime" | "alias"; relativePath: string; expectedPath: string; distPath: string }): Promise<void> {
  const entry: DistFileStatus = {
    kind: input.kind,
    path: input.relativePath,
    expectedPath: input.expectedPath,
    distPath: input.distPath,
    status: "match"
  };

  if (!exists(input.expectedPath)) entry.status = "missing_expected";
  else if (!exists(input.distPath)) entry.status = "missing_dist";
  else {
    const [expectedText, distText] = await Promise.all([readFile(input.expectedPath, "utf8"), readFile(input.distPath, "utf8")]);
    if (expectedText !== distText) entry.status = "mismatch";
  }

  if (entry.status !== "match") input.mismatches.push(input.relativePath);
  input.files.push(entry);
}

function collectSourceFiles(dir: string): string[] {
  const out: string[] = [];
  for (const entry of fs.readdirSync(dir)) {
    const full = path.join(dir, entry);
    const stat = fs.statSync(full);
    if (stat.isDirectory()) out.push(...collectSourceFiles(full));
    else if (/\.(ts|vue|css)$/u.test(full)) out.push(full);
  }
  return out.sort();
}

function ensureWebDeps(rootDir: string): void {
  const lockPath = path.join(rootDir, "bun.lock");
  const nodeModulesPath = path.join(rootDir, "node_modules");
  if (!fs.existsSync(lockPath)) return;
  if (hasRequiredWebDeps(nodeModulesPath)) return;

  const repoRoot = path.resolve(rootDir, "..", "..");
  const worktreeDepsHelper = path.join(repoRoot, "scripts", "worktree-deps.mjs");
  if (fs.existsSync(worktreeDepsHelper)) {
    const helperResult = spawnSync("node", [worktreeDepsHelper, "--web", "--quiet"], { cwd: repoRoot, encoding: "utf8", stdio: ["ignore", "inherit", "inherit"] });
    if (helperResult.status !== 0) throw new Error(`worktree web dependency setup exited with code ${helperResult.status ?? "unknown"}`);
    if (hasRequiredWebDeps(nodeModulesPath)) return;
    throw new Error("worktree web dependency setup completed but required packages are still missing");
  }

  const result = spawnSync("bun", ["install", "--frozen-lockfile"], { cwd: rootDir, encoding: "utf8", stdio: ["ignore", "inherit", "inherit"] });
  if (result.status !== 0) throw new Error(`bun install exited with code ${result.status ?? "unknown"}`);
}

function hasRequiredWebDeps(nodeModulesPath: string): boolean {
  return ["vite", "vue", "@vitejs/plugin-vue", "vue-tsc", "vitest"].every((name) => fs.existsSync(path.join(nodeModulesPath, name))) &&
    [path.join(".bin", "vite"), path.join(".bin", "vue-tsc")].every((name) => fs.existsSync(path.join(nodeModulesPath, name)));
}

if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
  const args = process.argv.slice(2);
  const rootIndex = args.indexOf("--root");
  const rootDir = rootIndex >= 0 ? path.resolve(args[rootIndex + 1] ?? ".") : path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
  if (args.includes("--build")) await buildCloudWebDist(rootDir);
  const freshness = await inspectCloudWebDistFreshness(rootDir);
  process.stdout.write(`${JSON.stringify(freshness, null, args.includes("--pretty") ? 2 : 0)}\n`);
}
