#!/usr/bin/env node
import assert from "node:assert/strict";
import { mkdir, readFile, writeFile } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";

import { readDeployConfig, writeDeployConfig } from "./src/deploy-config.mjs";

const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const defaultDeployPath = "deploy/deploy.yaml";
const defaultHost = "74.48.78.17";

const args = parseArgs(process.argv.slice(2));
if (args.help) {
  process.stdout.write(`${usage()}\n`);
  process.exit(0);
}

const deploy = await readDeployConfig(repoRoot, args.deployConfig);
const result = await configureLane(deploy, args);

if (args.write) {
  await writeDeployConfig(repoRoot, args.deployConfig, deploy);
  for (const file of result.copiedFiles) {
    await mkdir(path.dirname(path.join(repoRoot, file.to)), { recursive: true });
    await writeFile(path.join(repoRoot, file.to), file.content, "utf8");
  }
}

process.stdout.write(`${JSON.stringify({
  ok: true,
  command: "hwlab-lane-expand",
  action: args.write ? "configured" : "planned",
  deployConfig: args.deployConfig,
  lane: result.lane,
  fromLane: result.fromLane,
  wrote: args.write,
  copiedFiles: result.copiedFiles.map((file) => ({ from: file.from, to: file.to })),
  config: laneSummary(result.config),
  next: {
    render: `node scripts/run-bun.mjs scripts/gitops-render.mjs --lane ${result.lane} --source-branch ${result.config.sourceBranch} --gitops-branch ${result.config.gitopsBranch} --catalog-path ${result.config.artifactCatalog} --image-tag-mode ${result.config.imageTagMode ?? "full"}`,
    controlPlane: `bun scripts/cli.ts hwlab node control-plane apply --lane ${result.lane} --confirm`,
    trigger: `bun scripts/cli.ts hwlab node control-plane trigger-current --lane ${result.lane} --confirm`
  }
}, null, 2)}\n`);

function parseArgs(argv) {
  const parsed = {
    action: "configure",
    lane: null,
    fromLane: "v02",
    deployConfig: defaultDeployPath,
    host: defaultHost,
    write: false,
    force: false,
    copyLaneFiles: true,
    help: false,
    webPort: null,
    apiPort: null,
    publicUrl: null
  };
  for (let index = 0; index < argv.length; index += 1) {
    const arg = argv[index];
    if (arg === "configure" || arg === "plan") parsed.action = arg;
    else if (arg === "--lane") parsed.lane = readOption(argv, ++index, arg);
    else if (arg === "--from") parsed.fromLane = readOption(argv, ++index, arg);
    else if (arg === "--deploy-config") parsed.deployConfig = readOption(argv, ++index, arg);
    else if (arg === "--host") parsed.host = readOption(argv, ++index, arg);
    else if (arg === "--web-port") parsed.webPort = parsePort(readOption(argv, ++index, arg), arg);
    else if (arg === "--api-port") parsed.apiPort = parsePort(readOption(argv, ++index, arg), arg);
    else if (arg === "--public-url") parsed.publicUrl = readOption(argv, ++index, arg).replace(/\/+$/u, "");
    else if (arg === "--write") parsed.write = true;
    else if (arg === "--force") parsed.force = true;
    else if (arg === "--no-copy-lane-files") parsed.copyLaneFiles = false;
    else if (arg === "--help" || arg === "-h") parsed.help = true;
    else throw new Error(`unknown argument ${arg}`);
  }
  if (!parsed.help) {
    assertLane(parsed.lane, "--lane");
    assertLane(parsed.fromLane, "--from");
    assert.notEqual(parsed.lane, parsed.fromLane, "--lane must differ from --from");
  }
  return parsed;
}

function readOption(argv, index, name) {
  const value = argv[index];
  if (!value || value.startsWith("--")) throw new Error(`${name} requires a value`);
  return value;
}

function parsePort(value, name) {
  const parsed = Number.parseInt(String(value), 10);
  if (!Number.isInteger(parsed) || parsed < 1 || parsed > 65535) throw new Error(`${name} must be a TCP port`);
  return parsed;
}

function usage() {
  return [
    "usage: node scripts/hwlab-lane-expand.mjs configure --lane v03 [--from v02] [--write]",
    "",
    "Create or preview a new HWLAB additive runtime lane by editing deploy/deploy.yaml only.",
    "The command derives branch, namespace, GitOps branch, artifact catalog, runtime path, FRP ports,",
    "and lane-specific SecretRef names from the target lane id so v04/v05/v06 do not require code edits.",
    "",
    "options:",
    "  --lane LANE             target lane id such as v03, v04, v05",
    "  --from LANE             source lane template; default: v02",
    "  --deploy-config PATH    default: deploy/deploy.yaml",
    "  --host HOST             public host for default legacy endpoints; default: 74.48.78.17",
    "  --web-port PORT         override derived web FRP port",
    "  --api-port PORT         override derived API FRP port",
    "  --public-url URL        override derived HTTPS public endpoint",
    "  --write                 write deploy config and copied lane prompt files",
    "  --force                 replace an existing target lane",
    "  --no-copy-lane-files    do not copy lane-scoped source files referenced by config"
  ].join("\n");
}

async function configureLane(deploy, options) {
  deploy.lanes ??= {};
  const source = deploy.lanes[options.fromLane];
  assert.ok(source && typeof source === "object", `deploy.lanes.${options.fromLane} is required`);
  if (deploy.lanes[options.lane] && !options.force) {
    throw new Error(`deploy.lanes.${options.lane} already exists; pass --force to replace it`);
  }

  const sourceInfo = laneInfo(options.fromLane, { host: options.host });
  const targetInfo = laneInfo(options.lane, {
    host: options.host,
    webPort: options.webPort,
    apiPort: options.apiPort,
    publicUrl: options.publicUrl
  });
  const replacements = laneReplacements(source, sourceInfo, targetInfo);
  const target = deepTransform(clone(source), replacements);

  target.name = targetInfo.versionName;
  target.node = target.node ?? source.node;
  assert.ok(target.node, "target lane node is required; set deploy.lanes.<lane>.node or source lane node");
  target.sourceBranch = targetInfo.sourceBranch;
  target.gitopsBranch = targetInfo.gitopsBranch;
  target.namespace = targetInfo.namespace;
  target.endpoint = targetInfo.publicUrl;
  target.publicEndpoints = {
    ...(target.publicEndpoints ?? {}),
    frontend: targetInfo.publicUrl,
    api: targetInfo.publicUrl,
    legacyFrontend: `http://${options.host}:${targetInfo.webPort}`,
    legacyApi: `http://${options.host}:${targetInfo.apiPort}`
  };
  target.artifactCatalog = `deploy/artifact-catalog.${options.lane}.json`;
  target.runtimePath = `runtime-${options.lane}`;
  target.imageTagMode = target.imageTagMode ?? "full";

  deploy.lanes[options.lane] = target;
  const copiedFiles = options.copyLaneFiles ? await copiedLaneFiles(target, replacements, sourceInfo, targetInfo) : [];
  return { lane: options.lane, fromLane: options.fromLane, config: target, copiedFiles };
}

function assertLane(value, label) {
  if (!/^v[0-9]{2,}$/u.test(String(value ?? ""))) throw new Error(`${label} must look like v03, v04, v05`);
}

function laneInfo(lane, options = {}) {
  assertLane(lane, "lane");
  const minor = Number.parseInt(lane.slice(1), 10);
  const webPort = options.webPort ?? 17666 + minor * 1000;
  const apiPort = options.apiPort ?? webPort + 1;
  const versionName = `v0.${minor}`;
  const host = options.host ?? defaultHost;
  const hostLabel = host.replaceAll(".", "-");
  return {
    lane,
    minor,
    host,
    versionName,
    sourceBranch: versionName,
    gitopsBranch: `${versionName}-gitops`,
    namespace: `hwlab-${lane}`,
    webPort,
    apiPort,
    publicUrl: options.publicUrl ?? `https://hwlab-${lane}.${hostLabel}.nip.io`
  };
}

function laneReplacements(sourceConfig, sourceInfo, targetInfo) {
  const sourceLegacy = sourceConfig?.publicEndpoints ?? {};
  return [
    [sourceInfo.versionName, targetInfo.versionName],
    [sourceInfo.sourceBranch, targetInfo.sourceBranch],
    [sourceInfo.gitopsBranch, targetInfo.gitopsBranch],
    [sourceInfo.namespace, targetInfo.namespace],
    [sourceInfo.lane, targetInfo.lane],
    [`runtime-${sourceInfo.lane}`, `runtime-${targetInfo.lane}`],
    [`artifact-catalog.${sourceInfo.lane}.json`, `artifact-catalog.${targetInfo.lane}.json`],
    [`usr_${sourceInfo.lane}_admin`, `usr_${targetInfo.lane}_admin`],
    [`${sourceInfo.lane}-redacted-presence-only`, `${targetInfo.lane}-redacted-presence-only`],
    [String(sourceLegacy.legacyFrontend ?? `http://${sourceInfo.host}:${sourceInfo.webPort}`), `http://${targetInfo.host}:${targetInfo.webPort}`],
    [String(sourceLegacy.legacyApi ?? `http://${sourceInfo.host}:${sourceInfo.apiPort}`), `http://${targetInfo.host}:${targetInfo.apiPort}`],
    [String(sourceLegacy.frontend ?? sourceConfig?.endpoint ?? ""), targetInfo.publicUrl],
    [String(sourceLegacy.api ?? sourceConfig?.endpoint ?? ""), targetInfo.publicUrl]
  ].filter(([from]) => from);
}

function deepTransform(value, replacements) {
  if (typeof value === "string") return replaceAll(value, replacements);
  if (Array.isArray(value)) return value.map((item) => deepTransform(item, replacements));
  if (value && typeof value === "object") {
    return Object.fromEntries(Object.entries(value).map(([key, item]) => [replaceAll(key, replacements), deepTransform(item, replacements)]));
  }
  return value;
}

function replaceAll(value, replacements) {
  let text = String(value);
  for (const [from, to] of replacements) text = text.split(from).join(to);
  return text;
}

async function copiedLaneFiles(target, replacements, sourceInfo, targetInfo) {
  const paths = new Set();
  collectRepoPaths(target, paths);
  const copied = [];
  for (const targetPath of paths) {
    if (!targetPath.includes(targetInfo.lane) && !targetPath.includes(targetInfo.versionName)) continue;
    const sourcePath = targetPath
      .split(targetInfo.lane).join(sourceInfo.lane)
      .split(targetInfo.versionName).join(sourceInfo.versionName);
    const sourceAbs = path.join(repoRoot, sourcePath);
    const targetAbs = path.join(repoRoot, targetPath);
    if (sourceAbs === targetAbs) continue;
    let raw;
    try {
      raw = await readFile(sourceAbs, "utf8");
    } catch (error) {
      if (error?.code === "ENOENT") continue;
      throw error;
    }
    try {
      await readFile(targetAbs, "utf8");
      continue;
    } catch (error) {
      if (error?.code !== "ENOENT") throw error;
    }
    copied.push({ from: sourcePath, to: targetPath, content: replaceAll(raw, replacements) });
  }
  return copied;
}

function collectRepoPaths(value, result) {
  if (typeof value === "string") {
    if (/^[A-Za-z0-9_.-]+(?:\/[A-Za-z0-9_.-]+)+$/u.test(value) && !value.startsWith("http")) result.add(value);
    return;
  }
  if (Array.isArray(value)) {
    for (const item of value) collectRepoPaths(item, result);
    return;
  }
  if (value && typeof value === "object") {
    for (const item of Object.values(value)) collectRepoPaths(item, result);
  }
}

function laneSummary(config) {
  return {
    name: config.name,
    sourceBranch: config.sourceBranch,
    gitopsBranch: config.gitopsBranch,
    namespace: config.namespace,
    artifactCatalog: config.artifactCatalog,
    runtimePath: config.runtimePath,
    publicEndpoints: config.publicEndpoints,
    serviceCount: Array.isArray(config.envReuseServices) ? config.envReuseServices.length : null
  };
}

function clone(value) {
  return JSON.parse(JSON.stringify(value));
}
