import { cpSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { createHash } from "node:crypto";
import { join } from "node:path";
import type { UniDeskConfig } from "./config";
import { rootPath, stateRoot } from "./config";
import { runCommand, runCommandObserved } from "./command";

const configLabel = "config/platform-infra/hyperv-vms.yaml";
const configPath = rootPath(configLabel);
const bootstrapScriptPath = rootPath("scripts/assets/platform-infra/hyperv-vm-bootstrap.ps1");
const providerInstallTemplatePath = rootPath("scripts/assets/platform-infra/install-provider.sh");

interface HyperVConfig {
  defaults: { targetId: string };
  targets: Record<string, HyperVTarget>;
}

interface HyperVTarget {
  enabled: boolean;
  host: { route: string; computerName: string; stateDir: string; vmRoot: string; minimumFreeBytesBeforeInstall: number };
  vm: Record<string, unknown> & { name: string; processorCount: number; memoryStartupBytes: number; memoryMinimumBytes: number; memoryMaximumBytes: number; hostReservedMemoryBytes: number; diskBytes: number };
  image: Record<string, unknown> & { fileName: string; format: string; sha256: string; sources: Array<{ id: string; region: string; url: string }> };
  network: Record<string, unknown> & { address: string };
  ubuntu: Record<string, unknown> & { user: string };
  docker: Record<string, unknown>;
  k3s: Record<string, unknown> & { nodeName: string };
  provider: Record<string, unknown> & {
    id: string;
    name: string;
    masterServer: string;
    root: string;
    stateRoot: string;
    composeProject: string;
    containerName: string;
    image: string;
    systemdUnit: string;
  };
  verification: { probeTimeoutSeconds: number; repairReadyTimeoutSeconds: number; repairRetryIntervalSeconds: number };
  status: Record<string, unknown>;
}

interface Options {
  action: "plan" | "apply" | "status" | "logs" | "verify";
  targetId: string;
  confirm: boolean;
  repairProviderChannel: boolean;
  tail: number;
}

interface VerifyRoutesResult extends Record<string, unknown> {
  ok: boolean;
  repairable: boolean;
  failureKind: string | null;
}

export async function runPlatformInfraHyperVVmCommand(_config: UniDeskConfig, args: string[]): Promise<Record<string, unknown>> {
  const parsed = readConfig();
  const options = parseOptions(args, parsed.defaults.targetId);
  const target = parsed.targets[options.targetId];
  if (!target) throw new Error(`${configLabel}.targets.${options.targetId} is missing`);
  validateTarget(options.targetId, target);
  if (options.action === "plan") return plan(options.targetId, target);
  if (options.action === "status") return status(options.targetId, target);
  if (options.action === "logs") return logs(options.targetId, target, options.tail);
  if (options.action === "verify") return await verify(options.targetId, target, options);
  if (!options.confirm) return { ...plan(options.targetId, target), mode: "dry-run", mutation: false };
  return apply(options.targetId, target);
}

function readConfig(): HyperVConfig {
  const raw = record(Bun.YAML.parse(readFileSync(configPath, "utf8")), configLabel);
  if (raw.kind !== "platform-infra-hyperv-vms") throw new Error(`${configLabel}.kind must be platform-infra-hyperv-vms`);
  const defaults = record(raw.defaults, `${configLabel}.defaults`);
  const targets = record(raw.targets, `${configLabel}.targets`) as unknown as Record<string, HyperVTarget>;
  return { defaults: { targetId: stringField(defaults, "targetId", `${configLabel}.defaults`) }, targets };
}

function parseOptions(args: string[], defaultTargetId: string): Options {
  const action = args[0] ?? "plan";
  if (action !== "plan" && action !== "apply" && action !== "status" && action !== "logs" && action !== "verify") {
    throw new Error("platform-infra hyperv-vm usage: plan|apply|status|logs|verify --target <id> [--repair-provider-channel --confirm] [--tail 120]");
  }
  return {
    action,
    targetId: option(args, "--target") ?? defaultTargetId,
    confirm: args.includes("--confirm"),
    repairProviderChannel: args.includes("--repair-provider-channel"),
    tail: integerOption(args, "--tail", 120, 1, 500),
  };
}

function validateTarget(id: string, target: HyperVTarget): void {
  if (target.enabled !== true) throw new Error(`${configLabel}.targets.${id}.enabled must be true`);
  if (!target.host.route.includes(":win/")) throw new Error(`${configLabel}.targets.${id}.host.route must select a Windows workspace`);
  if (target.provider.id !== id) throw new Error(`${configLabel}.targets.${id}.provider.id must equal target id`);
  if (target.vm.memoryMinimumBytes <= 0 || target.vm.memoryStartupBytes < target.vm.memoryMinimumBytes || target.vm.memoryMaximumBytes < target.vm.memoryStartupBytes || target.vm.hostReservedMemoryBytes <= 0) {
    throw new Error(`${configLabel}.targets.${id}.vm dynamic memory values are invalid`);
  }
  if (target.vm.diskBytes !== 274_877_906_944) throw new Error(`${configLabel}.targets.${id}.vm.diskBytes must describe the requested 256 GiB disk`);
  if (!/^[a-f0-9]{64}$/u.test(target.image.sha256)) throw new Error(`${configLabel}.targets.${id}.image.sha256 must be lowercase SHA-256`);
  if (target.image.sources.length < 2) throw new Error(`${configLabel}.targets.${id}.image.sources must include mirror and canonical fallback`);
  const k3sManagement = record(target.k3s.management, `${configLabel}.targets.${id}.k3s.management`);
  if (k3sManagement.mode !== "external-cluster" && k3sManagement.mode !== "standalone") {
    throw new Error(`${configLabel}.targets.${id}.k3s.management.mode must be external-cluster or standalone`);
  }
  if (typeof k3sManagement.configRef !== "string" || !k3sManagement.configRef.startsWith("config/platform-infra/k3s-clusters.yaml#")) {
    throw new Error(`${configLabel}.targets.${id}.k3s.management.configRef must reference the k3s cluster owning YAML`);
  }
  for (const value of [target.provider.id, target.provider.name, target.provider.composeProject, target.provider.containerName, target.provider.image, target.ubuntu.user, target.k3s.nodeName]) {
    if (!/^[A-Za-z0-9._:/-]+$/u.test(value)) throw new Error(`${configLabel}.targets.${id} contains a shell-unsafe provider or user value`);
  }
  if (!/^[A-Za-z0-9_.@-]+$/u.test(target.provider.systemdUnit)) throw new Error(`${configLabel}.targets.${id}.provider.systemdUnit is invalid`);
  for (const [key, value] of Object.entries(target.verification)) {
    if (!Number.isInteger(value) || value <= 0) throw new Error(`${configLabel}.targets.${id}.verification.${key} must be a positive integer`);
  }
}

function plan(id: string, target: HyperVTarget): Record<string, unknown> {
  return {
    ok: true,
    mutation: false,
    targetId: id,
    source: configLabel,
    host: { route: target.host.route, computerName: target.host.computerName, vmRoot: target.host.vmRoot },
    vm: target.vm,
    network: target.network,
    image: { fileName: target.image.fileName, format: target.image.format, sha256: target.image.sha256, sources: target.image.sources, conversion: target.image.conversion },
    runtime: { ubuntu: target.ubuntu, docker: target.docker, k3s: target.k3s, provider: target.provider },
    constraints: {
      gpu: "unsupported-on-windows-11-hyperv-geforce-linux-guest",
      wslDependency: false,
      hostMemoryReservedBytes: target.vm.hostReservedMemoryBytes,
      guestMemoryMaximumBytes: target.vm.memoryMaximumBytes,
    },
    next: {
      apply: `bun scripts/cli.ts platform-infra hyperv-vm apply --target ${id} --confirm`,
      status: `bun scripts/cli.ts platform-infra hyperv-vm status --target ${id}`,
      logs: `bun scripts/cli.ts platform-infra hyperv-vm logs --target ${id} --tail 120`,
      verify: `bun scripts/cli.ts platform-infra hyperv-vm verify --target ${id}`,
    },
  };
}

function apply(id: string, target: HyperVTarget): Record<string, unknown> {
  const localDir = join(stateRoot, "hyperv-vm", id);
  const stagingDir = join(localDir, "bundle");
  const generatedConfigPath = join(localDir, "bootstrap-config.json");
  const generatedScriptPath = join(localDir, "hyperv-vm-bootstrap.ps1");
  const bundlePath = join(localDir, "unidesk-provider-bundle.tar.gz");
  const k3sDir = join(localDir, "k3s");
  mkdirSync(k3sDir, { recursive: true });
  const k3sBinaryPath = join(k3sDir, "k3s");
  const k3sInstallerPath = join(k3sDir, "install-k3s.sh");
  ensureDownloadedArtifact(String(target.k3s.binaryUrl), String(target.k3s.binarySha256), k3sBinaryPath);
  ensureDownloadedArtifact(String(target.k3s.installerUrl), "", k3sInstallerPath);
  mkdirSync(localDir, { recursive: true });
  rmSync(stagingDir, { recursive: true, force: true });
  mkdirSync(stagingDir, { recursive: true });
  cpSync(rootPath("src/components/provider-gateway"), join(stagingDir, "src/components/provider-gateway"), { recursive: true });
  cpSync(rootPath("src/components/shared"), join(stagingDir, "src/components/shared"), { recursive: true });
  const renderedInstaller = renderProviderInstaller(target);
  writeFileSync(join(stagingDir, "install-provider.sh"), renderedInstaller, { encoding: "utf8", mode: 0o755 });
  const tar = runCommand(["tar", "-czf", bundlePath, "-C", stagingDir, "."], rootPath());
  if (tar.exitCode !== 0) throw new Error(`provider bundle creation failed: ${tar.stderr.trim()}`);

  const remoteScriptPath = `${target.host.stateDir}\\hyperv-vm-bootstrap.ps1`;
  const remoteConfigPath = `${target.host.stateDir}\\bootstrap-config.json`;
  const remoteBundlePath = `${target.host.stateDir}\\unidesk-provider-bundle.tar.gz`;
  const generated = {
    targetId: id,
    ...target,
    artifacts: {
      providerBundlePath: remoteBundlePath,
      k3sBinaryPath: `${target.host.stateDir}\\k3s`,
      k3sInstallerPath: `${target.host.stateDir}\\install-k3s.sh`,
    },
  };
  writeFileSync(generatedConfigPath, `${JSON.stringify(generated, null, 2)}\n`, "utf8");
  cpSync(bootstrapScriptPath, generatedScriptPath);

  upload(target.host.route, generatedScriptPath, remoteScriptPath, true);
  upload(target.host.route, generatedConfigPath, remoteConfigPath, true);
  upload(target.host.route, bundlePath, remoteBundlePath, false);
  upload(target.host.route, k3sBinaryPath, `${target.host.stateDir}\\k3s`, false);
  upload(target.host.route, k3sInstallerPath, `${target.host.stateDir}\\install-k3s.sh`, true);

  const launchSource = [
    `$script=${psQuote(remoteScriptPath)}`,
    `$config=${psQuote(remoteConfigPath)}`,
    "$arguments=@('-NoProfile','-ExecutionPolicy','Bypass','-File',$script,'-ConfigPath',$config)",
    "$process=Start-Process -FilePath 'powershell.exe' -Verb RunAs -ArgumentList $arguments -PassThru",
    `[ordered]@{ok=$true;mutation=$true;state='uac-pending';processId=$process.Id;script=$script;config=$config} | ConvertTo-Json -Compress`,
  ].join("; ");
  const launch = runCommand(["trans", target.host.route, "ps", launchSource], rootPath(), { timeoutMs: 30_000 });
  if (launch.exitCode !== 0) throw new Error(`failed to launch elevated Hyper-V bootstrap: ${launch.stderr.trim() || launch.stdout.trim()}`);
  return {
    ok: true,
    mutation: true,
    state: "uac-pending",
    targetId: id,
    message: "Approve the Windows UAC prompt on D601-WIN; installation then continues in the background.",
    remote: { scriptPath: remoteScriptPath, configPath: remoteConfigPath, bundlePath: remoteBundlePath },
    statusCommand: `bun scripts/cli.ts platform-infra hyperv-vm status --target ${id}`,
    launch: parseFirstJson(launch.stdout),
  };
}

function ensureDownloadedArtifact(url: string, expectedSha256: string, destination: string): void {
  if (existsSync(destination) && (!expectedSha256 || createHash("sha256").update(readFileSync(destination)).digest("hex") === expectedSha256)) return;
  const download = runCommand(["curl", "-fsSL", url, "-o", destination], rootPath(), { timeoutMs: 120_000 });
  if (download.exitCode !== 0) throw new Error(`k3s artifact download failed: ${url}: ${bounded(download.stderr || download.stdout)}`);
  if (expectedSha256) {
    const actual = createHash("sha256").update(readFileSync(destination)).digest("hex");
    if (actual !== expectedSha256) throw new Error(`k3s artifact hash mismatch: expected=${expectedSha256} actual=${actual}`);
  }
}

function status(id: string, target: HyperVTarget): Record<string, unknown> {
  const statusPath = `${target.host.stateDir}\\status.json`;
  const source = `$p=${psQuote(statusPath)}; if(Test-Path -LiteralPath $p){Get-Content -Raw -LiteralPath $p}else{[ordered]@{ok=$false;state='not-started';stage='none';message='status.json is not present'}|ConvertTo-Json -Compress}`;
  const result = runCommand(["trans", target.host.route, "ps", source], rootPath(), { timeoutMs: 30_000 });
  if (result.exitCode !== 0) return { ok: false, mutation: false, targetId: id, state: "read-failed", error: bounded(result.stderr || result.stdout) };
  const payload = parseFirstJson(result.stdout);
  return {
    ...payload,
    mutation: false,
    targetId: id,
    address: target.network.address,
    providerId: target.provider.id,
    next: payload.state === "failed"
      ? { logs: `bun scripts/cli.ts platform-infra hyperv-vm logs --target ${id} --tail 200`, retry: `bun scripts/cli.ts platform-infra hyperv-vm apply --target ${id} --confirm` }
      : { status: `bun scripts/cli.ts platform-infra hyperv-vm status --target ${id}` },
  };
}

function logs(id: string, target: HyperVTarget, tail: number): Record<string, unknown> {
  const logPath = `${target.host.stateDir}\\install.log`;
  const source = `$p=${psQuote(logPath)}; if(Test-Path -LiteralPath $p){Get-Content -LiteralPath $p -Tail ${tail}}else{'install.log is not present'}`;
  const result = runCommand(["trans", target.host.route, "ps", source], rootPath(), { timeoutMs: 30_000 });
  return { ok: result.exitCode === 0, mutation: false, targetId: id, tail, logPath, lines: bounded(result.stdout, 30_000), error: bounded(result.stderr) };
}

async function verify(id: string, target: HyperVTarget, options: Options): Promise<Record<string, unknown>> {
  const before = await verifyProviderRoutes(target);
  if (before.ok) return {
    ok: true,
    mutation: false,
    targetId: id,
    providerId: target.provider.id,
    repaired: false,
    before,
    after: before,
  };
  const repairCommand = `bun scripts/cli.ts platform-infra hyperv-vm verify --target ${id} --repair-provider-channel --confirm`;
  if (!before.repairable) return {
    ok: false,
    mutation: false,
    targetId: id,
    providerId: target.provider.id,
    repaired: false,
    before,
    next: { retry: `bun scripts/cli.ts platform-infra hyperv-vm verify --target ${id}` },
  };
  if (!options.repairProviderChannel || !options.confirm) return {
    ok: false,
    mutation: false,
    targetId: id,
    providerId: target.provider.id,
    repaired: false,
    before,
    next: { repair: repairCommand },
  };
  const repair = await restartProviderChannel(target);
  if (!repair.ok) return {
    ok: false,
    mutation: true,
    targetId: id,
    providerId: target.provider.id,
    repaired: false,
    before,
    repair,
  };
  const deadline = Date.now() + target.verification.repairReadyTimeoutSeconds * 1_000;
  let after = await verifyProviderRoutes(target);
  while (!after.ok && Date.now() < deadline) {
    await Bun.sleep(target.verification.repairRetryIntervalSeconds * 1_000);
    after = await verifyProviderRoutes(target);
  }
  return {
    ok: after.ok,
    mutation: true,
    targetId: id,
    providerId: target.provider.id,
    repaired: after.ok,
    before,
    repair,
    after,
  };
}

async function verifyProviderRoutes(target: HyperVTarget): Promise<VerifyRoutesResult> {
  const timeoutMs = target.verification.probeTimeoutSeconds * 1_000;
  const [host, k3s] = await Promise.all([
    runCommandObserved(["trans", target.provider.id, "argv", "hostname"], rootPath(), { timeoutMs, maxCaptureChars: 4_000 }),
    runCommandObserved(["trans", `${target.provider.id}:k3s`, "kubectl", "get", "node", target.k3s.nodeName, "-o", "name"], rootPath(), { timeoutMs, maxCaptureChars: 4_000 }),
  ]);
  const failureKind = providerRouteFailureKind(`${host.stdout}\n${host.stderr}\n${k3s.stdout}\n${k3s.stderr}`);
  return {
    ok: host.exitCode === 0 && k3s.exitCode === 0,
    repairable: failureKind === "provider-data-channel-missing",
    failureKind,
    host: commandEvidence(host),
    k3s: commandEvidence(k3s),
  };
}

function providerRouteFailureKind(text: string): string | null {
  const normalized = text.toLowerCase();
  if (normalized.includes("requested ssh tcp data channel is not ready") || normalized.includes("ssh tcp data channel is not available")) return "provider-data-channel-missing";
  if (normalized.includes("ssh tcp data channel closed")) return "provider-data-channel-closed";
  if (normalized.includes("provider ssh tcp data pool has no idle channel")) return "provider-data-pool-exhausted";
  return null;
}

async function restartProviderChannel(target: HyperVTarget): Promise<Record<string, unknown> & { ok: boolean }> {
  const keyPath = `${target.host.stateDir}\\id_ed25519`;
  const guest = `${target.ubuntu.user}@${target.network.address}`;
  const source = [
    `$key=${psQuote(keyPath)}`,
    `$guest=${psQuote(guest)}`,
    `$unit=${psQuote(target.provider.systemdUnit)}`,
    "$arguments=@('-i',$key,'-o','BatchMode=yes','-o','ConnectTimeout=8',$guest,'sudo','systemctl','restart',$unit)",
    "& ssh.exe @arguments 2>&1",
    "$code=$LASTEXITCODE",
    "[ordered]@{ok=($code -eq 0);exitCode=$code}|ConvertTo-Json -Compress",
    "exit $code",
  ].join("; ");
  const result = await runCommandObserved(["trans", target.host.route, "ps", source], rootPath(), {
    timeoutMs: target.verification.probeTimeoutSeconds * 1_000,
    maxCaptureChars: 4_000,
  });
  return { ok: result.exitCode === 0, ...commandEvidence(result) };
}

function commandEvidence(result: { exitCode: number | null; stdout: string; stderr: string; timedOut: boolean }): Record<string, unknown> {
  return {
    exitCode: result.exitCode,
    timedOut: result.timedOut,
    stdout: bounded(result.stdout, 1_000),
    stderr: bounded(result.stderr, 2_000),
  };
}

function renderProviderInstaller(target: HyperVTarget): string {
  let source = readFileSync(providerInstallTemplatePath, "utf8");
  const replacements: Record<string, string> = {
    "@@PROVIDER_ROOT@@": target.provider.root,
    "@@STATE_ROOT@@": target.provider.stateRoot,
    "@@PROVIDER_ID@@": target.provider.id,
    "@@PROVIDER_NAME@@": target.provider.name,
    "@@MASTER_SERVER@@": target.provider.masterServer,
    "@@COMPOSE_PROJECT@@": target.provider.composeProject,
    "@@PROVIDER_IMAGE@@": target.provider.image,
    "@@PROVIDER_CONTAINER@@": target.provider.containerName,
    "@@UBUNTU_USER@@": target.ubuntu.user,
    "@@K3S_NODE_NAME@@": target.k3s.nodeName,
  };
  for (const [placeholder, value] of Object.entries(replacements)) source = source.replaceAll(placeholder, value);
  if (/@@[A-Z0-9_]+@@/u.test(source)) throw new Error("provider installer contains unresolved YAML placeholder");
  return source;
}

function upload(route: string, localPath: string, remotePath: string, allowText: boolean): void {
  if (!existsSync(localPath)) throw new Error(`upload source is missing: ${localPath}`);
  const command = ["trans", route, "upload", localPath, remotePath];
  if (allowText) command.push("--allow-text-transfer");
  const result = runCommand(command, rootPath(), { timeoutMs: 60_000 });
  if (result.exitCode !== 0) throw new Error(`upload failed for ${remotePath}: ${bounded(result.stderr || result.stdout)}`);
}

function parseFirstJson(output: string): Record<string, unknown> {
  for (const line of output.split(/\r?\n/u)) {
    const trimmed = line.trim();
    if (!trimmed.startsWith("{")) continue;
    try { return record(JSON.parse(trimmed), "remote JSON"); } catch { /* continue */ }
  }
  const start = output.indexOf("{");
  const end = output.lastIndexOf("}");
  if (start >= 0 && end > start) {
    try { return record(JSON.parse(output.slice(start, end + 1)), "remote JSON"); } catch { /* fall through */ }
  }
  throw new Error(`remote command returned no JSON: ${bounded(output)}`);
}

function psQuote(value: string): string {
  return `'${value.replaceAll("'", "''")}'`;
}

function option(args: string[], name: string): string | null {
  const index = args.indexOf(name);
  if (index < 0) return null;
  const value = args[index + 1];
  if (!value || value.startsWith("--")) throw new Error(`${name} requires a value`);
  return value;
}

function integerOption(args: string[], name: string, fallback: number, minimum: number, maximum: number): number {
  const raw = option(args, name);
  if (raw === null) return fallback;
  const value = Number(raw);
  if (!Number.isInteger(value) || value < minimum || value > maximum) throw new Error(`${name} must be ${minimum}..${maximum}`);
  return value;
}

function record(value: unknown, label: string): Record<string, any> {
  if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`${label} must be an object`);
  return value as Record<string, any>;
}

function stringField(value: Record<string, unknown>, key: string, label: string): string {
  const field = value[key];
  if (typeof field !== "string" || field.length === 0) throw new Error(`${label}.${key} must be a non-empty string`);
  return field;
}

function bounded(value: string, max = 4_000): string {
  const text = value.trim();
  return text.length <= max ? text : `${text.slice(0, max)}\n...[truncated ${text.length - max} chars]`;
}
