#!/usr/bin/env bun
import { spawnSync } from "node:child_process";
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { resolve } from "node:path";

function option(name) {
  const index = process.argv.indexOf(name);
  if (index === -1) return null;
  const value = process.argv[index + 1];
  if (value === undefined || value.length === 0 || value.startsWith("--")) throw new Error(`${name} requires a value`);
  return value;
}

function record(value, path) {
  if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`${path} must be an object`);
  return value;
}

function required(value, path) {
  if (typeof value !== "string" || value.length === 0 || value.includes("\n")) throw new Error(`${path} must be a non-empty single-line string`);
  return value;
}

function safeRelativePath(value, path) {
  const result = required(value, path);
  if (result.startsWith("/") || result.split("/").includes("..")) throw new Error(`${path} must be a safe relative path`);
  return result;
}

function stringArray(value, path) {
  if (!Array.isArray(value) || value.length === 0) throw new Error(`${path} must be a non-empty string array`);
  return value.map((item, index) => safeRelativePath(item, `${path}[${index}]`));
}

function run(command, args, cwd, allowFailure = false) {
  const result = spawnSync(command, args, { cwd, encoding: "utf8", maxBuffer: 4 * 1024 * 1024 });
  if (result.error !== undefined) throw result.error;
  if (result.status !== 0 && !allowFailure) {
    const detail = `${result.stderr || result.stdout || "command failed"}`.trim().slice(-2000);
    throw new Error(`${command} ${args.join(" ")} failed (${result.status}): ${detail}`);
  }
  return result;
}

function valueAt(root, reference) {
  return reference.split(".").reduce((value, key) => record(value, reference)[key], root);
}

function writeValue(outputDir, name, value) {
  writeFileSync(resolve(outputDir, name), `${value}\n`, { encoding: "utf8", mode: 0o644 });
}

function deliveryServices(config, delivery) {
  if (!Array.isArray(delivery.services) || delivery.services.length === 0) throw new Error("delivery.services must be a non-empty array");
  const ids = new Set();
  return delivery.services.map((value, index) => {
    const path = `delivery.services[${index}]`;
    const item = record(value, path);
    const id = required(item.id, `${path}.id`);
    if (!/^[a-z0-9](?:[-a-z0-9]{0,62}[a-z0-9])?$/u.test(id) || ids.has(id)) throw new Error(`${path}.id must be unique DNS label`);
    ids.add(id);
    const serviceRef = required(item.serviceRef, `${path}.serviceRef`);
    const service = record(valueAt(config, serviceRef), serviceRef);
    const image = record(item.image, `${path}.image`);
    const gitops = record(item.gitops, `${path}.gitops`);
    return {
      id,
      serviceRef,
      service,
      imagePaths: stringArray(item.imagePaths, `${path}.imagePaths`),
      imageRepository: required(image.repository, `${path}.image.repository`),
      manifestPath: safeRelativePath(gitops.manifestPath, `${path}.gitops.manifestPath`),
      releaseStatePath: safeRelativePath(gitops.releaseStatePath, `${path}.gitops.releaseStatePath`),
    };
  });
}

function baselineForService(sourceRoot, gitopsRef, service) {
  const result = run("git", ["show", `${gitopsRef}:${service.releaseStatePath}`], sourceRoot, true);
  if (result.status !== 0) return null;
  const state = record(JSON.parse(result.stdout), service.releaseStatePath);
  if (state.kind !== "UniDeskHostReleaseState") throw new Error(`${service.releaseStatePath}.kind must be UniDeskHostReleaseState`);
  if (state.serviceRef !== service.serviceRef) throw new Error(`${service.releaseStatePath}.serviceRef must match ${service.serviceRef}`);
  const sourceCommit = required(state.sourceCommit, `${service.releaseStatePath}.sourceCommit`);
  if (!/^[0-9a-f]{40}$/u.test(sourceCommit)) throw new Error(`${service.releaseStatePath}.sourceCommit must be a full Git commit SHA`);
  return sourceCommit;
}

function main() {
  const configPath = option("--config") ?? "config/unidesk-host-k8s.yaml";
  const sourceRoot = resolve(option("--source-root") ?? process.cwd());
  const sourceCommit = required(option("--source-commit"), "--source-commit");
  const sourceSnapshotPrefix = required(option("--source-snapshot-prefix"), "--source-snapshot-prefix");
  const outputDir = resolve(required(option("--output-dir"), "--output-dir"));
  if (!/^[0-9a-f]{40}$/u.test(sourceCommit)) throw new Error("--source-commit must be a full Git commit SHA");
  if (run("git", ["check-ref-format", sourceSnapshotPrefix], sourceRoot, true).status !== 0) throw new Error("--source-snapshot-prefix must be a valid Git ref prefix");

  const config = record(Bun.YAML.parse(readFileSync(configPath, "utf8")), configPath);
  const delivery = record(config.delivery, "delivery");
  if (delivery.enabled !== true && delivery.enabled !== false) throw new Error("delivery.enabled must be boolean");
  const enabled = delivery.enabled === true;
  const changeDetection = record(delivery.changeDetection, "delivery.changeDetection");
  const sharedImagePaths = stringArray(changeDetection.sharedImagePaths, "delivery.changeDetection.sharedImagePaths");
  const services = deliveryServices(config, delivery);
  const build = record(delivery.build, "delivery.build");
  const proxy = record(build.proxy, "delivery.build.proxy");
  const gitops = record(delivery.gitops, "delivery.gitops");
  const readUrl = required(gitops.readUrl, "delivery.gitops.readUrl");
  const branch = required(gitops.branch, "delivery.gitops.branch");
  const noProxy = proxy.noProxy;
  if (!Array.isArray(noProxy) || noProxy.some((value) => typeof value !== "string" || value.length === 0)) throw new Error("delivery.build.proxy.noProxy must be a non-empty string array");

  const gitopsRef = "refs/unidesk/release-baseline/unidesk-host";
  const gitopsReady = enabled && run("git", ["fetch", "--depth=1", readUrl, `+refs/heads/${branch}:${gitopsRef}`], sourceRoot, true).status === 0;
  const plans = [];
  for (const service of services) {
    let action = enabled ? "build" : "disabled";
    let reason = enabled ? "gitops-baseline-unavailable" : "delivery-disabled";
    let baselineSourceCommit = null;
    let changedPaths = [];
    if (enabled && gitopsReady) {
      baselineSourceCommit = baselineForService(sourceRoot, gitopsRef, service);
      if (baselineSourceCommit === null) {
        reason = "release-state-missing";
      } else {
        const sourceRef = `refs/unidesk/release-baseline/unidesk-host-source-${service.id}`;
        const sourceReady = run("git", ["fetch", "--depth=1", "--filter=blob:none", "origin", `+${sourceSnapshotPrefix}/${baselineSourceCommit}:${sourceRef}`], sourceRoot, true).status === 0;
        if (sourceReady) {
          const paths = [...new Set([...sharedImagePaths, ...service.imagePaths])];
          const diff = run("git", ["diff", "--name-only", "--no-renames", sourceRef, sourceCommit, "--", ...paths], sourceRoot);
          changedPaths = diff.stdout.split(/\r?\n/u).filter(Boolean);
          action = changedPaths.length === 0 ? "skip" : "build";
          reason = changedPaths.length === 0 ? "image-inputs-unchanged" : "image-inputs-changed";
        } else {
          reason = "source-baseline-unavailable";
        }
      }
    }
    plans.push({ ...service, action, reason, baselineSourceCommit, changedPaths });
  }

  mkdirSync(resolve(outputDir, "services"), { recursive: true });
  for (const plan of plans) {
    const serviceDir = resolve(outputDir, "services", plan.id);
    mkdirSync(serviceDir, { recursive: true });
    writeValue(serviceDir, "action", plan.action);
    writeValue(serviceDir, "reason", plan.reason);
    writeValue(serviceDir, "service-ref", plan.serviceRef);
    writeValue(serviceDir, "baseline-source-commit", plan.baselineSourceCommit ?? "");
    writeValue(serviceDir, "dockerfile", required(record(plan.service.repository, `${plan.serviceRef}.repository`).dockerfile, `${plan.serviceRef}.repository.dockerfile`));
    writeValue(serviceDir, "image-repository", plan.imageRepository);
    writeFileSync(resolve(serviceDir, "changed-paths.json"), `${JSON.stringify(plan.changedPaths)}\n`, { encoding: "utf8", mode: 0o644 });
  }
  writeValue(outputDir, "action", plans.some((item) => item.action === "build") ? "build" : enabled ? "skip" : "disabled");
  writeValue(outputDir, "network-mode", required(build.networkMode, "delivery.build.networkMode"));
  writeValue(outputDir, "http-proxy", required(proxy.http, "delivery.build.proxy.http"));
  writeValue(outputDir, "https-proxy", required(proxy.https, "delivery.build.proxy.https"));
  writeValue(outputDir, "all-proxy", required(proxy.all, "delivery.build.proxy.all"));
  writeValue(outputDir, "no-proxy", noProxy.join(","));
  writeFileSync(resolve(outputDir, "plan.json"), `${JSON.stringify(plans.map(({ service: _service, ...item }) => item))}\n`, { encoding: "utf8", mode: 0o644 });
  process.stdout.write(`${JSON.stringify({
    ok: true,
    phase: "release-prepare",
    action: plans.some((item) => item.action === "build") ? "build" : enabled ? "skip" : "disabled",
    sourceCommit,
    affectedServices: plans.filter((item) => item.action === "build").map((item) => item.id),
    buildServices: plans.filter((item) => item.action === "build").map((item) => item.id),
    reusedServices: plans.filter((item) => item.action === "skip").map((item) => item.id),
    services: plans.map((item) => ({ id: item.id, serviceRef: item.serviceRef, action: item.action, reason: item.reason, baselineSourceCommit: item.baselineSourceCommit, changedPaths: item.changedPaths })),
    valuesPrinted: false,
  })}\n`);
}

if (!process.execArgv.includes("--check")) main();
