import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync } from "node:fs";
import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
import { runCommand } from "./command";
import { repoRoot, rootPath } from "./config";
import { k3sGuardShellLines, defaultNativeKubeconfig } from "./k3s-target-guard";
import { startJob } from "./jobs";

const defaultManifest = "src/components/microservices/k3sctl-adapter/k3s/dev/unidesk-dev-foundation.k8s.yaml";
const devNamespace = "unidesk-dev";
const prodNamespace = "unidesk";
const defaultProviderId = "D601";
const defaultProxyUrl = "http://127.0.0.1:18789";
const defaultPrewarmImages = [
  "postgres:16-alpine",
  "rancher/mirrored-library-busybox:1.36.1",
];
const defaultWorktreeCopyFile = ".worktreecopy";
const defaultWorktreeFromRef = "origin/master";
const maxWorktreeCopyBytes = 1024 * 1024;
const refusedWorktreeCopyPrefixes = [
  ".git/",
  ".worktree/",
  "node_modules/",
  "dist/",
  "build/",
  "coverage/",
  ".state/",
  "tmp/",
  ".tmp/",
];
const foundationRequiredKinds = new Set([
  "Namespace/unidesk-dev",
  "Secret/unidesk-dev-runtime-secrets",
  "ConfigMap/unidesk-dev-runtime-config",
  "ConfigMap/unidesk-dev-db-guard",
  "ConfigMap/unidesk-dev-db-init",
  "Service/postgres-dev",
  "StatefulSet/postgres-dev",
  "Job/unidesk-dev-db-migrate",
]);
const coreRequiredKinds = new Set([
  "Service/backend-core-dev",
  "Deployment/backend-core-dev",
  "Service/frontend-dev",
  "Deployment/frontend-dev",
]);
const codeQueueRequiredKinds = new Set([
  "Service/code-queue-scheduler-dev",
  "Service/code-queue-read-dev",
  "Service/code-queue-write-dev",
  "Service/d601-dev-provider-egress-proxy",
  "ConfigMap/d601-dev-provider-egress-proxy",
  "Deployment/d601-dev-provider-egress-proxy",
  "Deployment/code-queue-scheduler-dev",
  "Deployment/code-queue-read-dev",
  "Deployment/code-queue-write-dev",
]);
const mdtodoRequiredKinds = new Set([
  "Service/mdtodo-dev",
  "Deployment/mdtodo-dev",
]);
const claudeqqRequiredKinds = new Set([
  "Service/claudeqq-dev",
  "Deployment/claudeqq-dev",
]);

interface ManifestDocument {
  index: number;
  raw: string;
  kind: string;
  name: string;
  namespace: string | null;
}

interface ValidateOptions {
  manifestPath: string;
  kubectlDryRun: boolean;
}

interface PrewarmImagesOptions {
  providerId: string;
  images: string[];
  proxyUrl: string;
  pullMissing: boolean;
  pullTimeoutMs: number;
  dryRun: boolean;
}

interface WorktreeAddOptions {
  targetPath: string;
  branch: string | null;
  fromRef: string;
  copyLocal: boolean;
  overwriteLocal: boolean;
  dryRun: boolean;
  json: boolean;
}

interface WorktreeCopyPattern {
  raw: string;
  pattern: string;
  include: boolean;
}

interface WorktreeCopyRow {
  path: string;
  status: "copied" | "skipped" | "refused";
  reason: string;
  bytes: number | null;
}

function isHelpArg(arg: string | undefined): boolean {
  return arg === "help" || arg === "--help" || arg === "-h";
}

function positiveInteger(value: string | undefined, option: string): number {
  const parsed = Number(value);
  if (!Number.isInteger(parsed) || parsed <= 0) throw new Error(`${option} must be a positive integer`);
  return parsed;
}

function rejectUnsafeToken(value: string, option: string): void {
  if (/[\s\x00-\x1f\x7f]/u.test(value)) throw new Error(`${option} must not contain whitespace or control characters`);
}

function parseValidateOptions(args: string[]): ValidateOptions {
  const options: ValidateOptions = { manifestPath: defaultManifest, kubectlDryRun: false };
  for (let index = 0; index < args.length; index += 1) {
    const arg = args[index];
    if (arg === "--manifest") {
      const value = args[index + 1];
      if (value === undefined || value.length === 0) throw new Error("--manifest requires a path");
      options.manifestPath = value;
      index += 1;
    } else if (arg === "--kubectl-dry-run") {
      options.kubectlDryRun = true;
    } else if (!isHelpArg(arg)) {
      throw new Error(`unknown dev-env option: ${arg}`);
    }
  }
  return options;
}

function parsePrewarmImagesOptions(args: string[]): PrewarmImagesOptions {
  const images: string[] = [];
  const options: PrewarmImagesOptions = {
    providerId: defaultProviderId,
    images,
    proxyUrl: defaultProxyUrl,
    pullMissing: true,
    pullTimeoutMs: 300_000,
    dryRun: false,
  };
  for (let index = 0; index < args.length; index += 1) {
    const arg = args[index];
    if (arg === "--provider-id" || arg === "--target") {
      const value = args[index + 1];
      if (value === undefined || value.length === 0) throw new Error(`${arg} requires a value`);
      rejectUnsafeToken(value, arg);
      options.providerId = value;
      index += 1;
    } else if (arg === "--image") {
      const value = args[index + 1];
      if (value === undefined || value.length === 0) throw new Error("--image requires a value");
      rejectUnsafeToken(value, "--image");
      images.push(value);
      index += 1;
    } else if (arg === "--proxy-url") {
      const value = args[index + 1];
      if (value === undefined || value.length === 0) throw new Error("--proxy-url requires a value");
      rejectUnsafeToken(value, "--proxy-url");
      options.proxyUrl = value;
      index += 1;
    } else if (arg === "--pull-timeout-ms") {
      options.pullTimeoutMs = positiveInteger(args[index + 1], "--pull-timeout-ms");
      index += 1;
    } else if (arg === "--no-pull") {
      options.pullMissing = false;
    } else if (arg === "--dry-run") {
      options.dryRun = true;
    } else if (!isHelpArg(arg)) {
      throw new Error(`unknown dev-env prewarm-images option: ${arg}`);
    }
  }
  if (options.images.length === 0) options.images = [...defaultPrewarmImages];
  return options;
}

function parseWorktreeAddOptions(args: string[]): WorktreeAddOptions {
  const positional: string[] = [];
  const options: WorktreeAddOptions = {
    targetPath: "",
    branch: null,
    fromRef: defaultWorktreeFromRef,
    copyLocal: true,
    overwriteLocal: false,
    dryRun: false,
    json: false,
  };
  for (let index = 0; index < args.length; index += 1) {
    const arg = args[index];
    if (arg === "--branch" || arg === "-b") {
      const value = args[index + 1];
      if (value === undefined || value.length === 0) throw new Error("--branch requires a branch name");
      rejectUnsafeToken(value, "--branch");
      options.branch = value;
      index += 1;
    } else if (arg === "--from") {
      const value = args[index + 1];
      if (value === undefined || value.length === 0) throw new Error("--from requires a git ref");
      rejectUnsafeToken(value, "--from");
      options.fromRef = value;
      index += 1;
    } else if (arg === "--copy-local") {
      options.copyLocal = true;
    } else if (arg === "--no-copy-local") {
      options.copyLocal = false;
    } else if (arg === "--overwrite-local") {
      options.overwriteLocal = true;
    } else if (arg === "--dry-run") {
      options.dryRun = true;
    } else if (arg === "--json") {
      options.json = true;
    } else if (!isHelpArg(arg)) {
      if (arg.startsWith("-")) throw new Error(`unknown dev-env worktree add option: ${arg}`);
      positional.push(arg);
    }
  }
  if (positional.length !== 1) throw new Error("dev-env worktree add usage: bun scripts/cli.ts dev-env worktree add .worktree/<task> --branch <branch> [--from origin/master]");
  options.targetPath = positional[0] ?? "";
  rejectUnsafeToken(options.targetPath, "worktree path");
  if (options.overwriteLocal) options.copyLocal = true;
  return options;
}

function scalarAfter(text: string, key: string): string | null {
  const match = text.match(new RegExp(`^\\s*${key}:\\s*"?([^"\\n#]+)"?\\s*(?:#.*)?$`, "mu"));
  return match?.[1]?.trim() ?? null;
}

function namespaceFromDoc(text: string): string | null {
  const metadataIndex = text.search(/^metadata:\s*$/mu);
  if (metadataIndex < 0) return null;
  const metadataBlock = text.slice(metadataIndex);
  const match = metadataBlock.match(/^ {2}namespace:\s*"?([^"\n#]+)"?\s*(?:#.*)?$/mu);
  return match?.[1]?.trim() ?? null;
}

function parseManifestDocuments(text: string): ManifestDocument[] {
  return text.split(/^---\s*$/mu)
    .map((raw, index) => ({ raw: raw.trim(), index }))
    .filter((doc) => doc.raw.length > 0)
    .map(({ raw, index }) => {
      const kind = scalarAfter(raw, "kind") ?? "";
      const name = (() => {
        const metadataIndex = raw.search(/^metadata:\s*$/mu);
        if (metadataIndex < 0) return "";
        const metadataBlock = raw.slice(metadataIndex);
        const match = metadataBlock.match(/^ {2}name:\s*"?([^"\n#]+)"?\s*(?:#.*)?$/mu);
        return match?.[1]?.trim() ?? "";
      })();
      return { index, raw, kind, name, namespace: namespaceFromDoc(raw) };
    });
}

function requiredResourcesFor(resources: string[]): Set<string> {
  const resourceSet = new Set(resources);
  if (resourceSet.has("Deployment/code-queue-scheduler-dev") || resourceSet.has("Service/code-queue-scheduler-dev")) return codeQueueRequiredKinds;
  if (resourceSet.has("Deployment/mdtodo-dev") || resourceSet.has("Service/mdtodo-dev")) return mdtodoRequiredKinds;
  if (resourceSet.has("Deployment/claudeqq-dev") || resourceSet.has("Service/claudeqq-dev")) return claudeqqRequiredKinds;
  if (resourceSet.has("Deployment/backend-core-dev") || resourceSet.has("Deployment/frontend-dev")) return coreRequiredKinds;
  return foundationRequiredKinds;
}

function databaseUrls(text: string): string[] {
  const urls: string[] = [];
  const pattern = /postgres(?:ql)?:\/\/[^\s"']+/gu;
  for (const match of text.matchAll(pattern)) urls.push(match[0] ?? "");
  return urls.filter((url) => url.length > 0 && !url.includes("*") && !url.includes("$"));
}

function validateDatabaseUrl(url: string): { ok: boolean; url: string; reason: string | null } {
  if (url.includes("d601-tcp-egress-gateway") || url.includes("74.48.78.17:15432") || url.includes("database:5432/unidesk")) {
    return { ok: false, url, reason: "matches production database route" };
  }
  let parsed: URL;
  try {
    parsed = new URL(url);
  } catch {
    return { ok: false, url, reason: "invalid URL" };
  }
  const hostOk = [
    "postgres-dev",
    "postgres-dev.unidesk-dev",
    "postgres-dev.unidesk-dev.svc",
    "postgres-dev.unidesk-dev.svc.cluster.local",
  ].includes(parsed.hostname);
  const database = parsed.pathname.replace(/^\/+/u, "");
  if (!hostOk) return { ok: false, url, reason: `host ${parsed.hostname} is not postgres-dev` };
  if (database !== "unidesk_dev") return { ok: false, url, reason: `database ${database} is not unidesk_dev` };
  return { ok: true, url, reason: null };
}

function normalizedRelativePath(path: string): string {
  return path.split(sep).join("/");
}

function resolveWorktreeTarget(targetPath: string): { absolutePath: string; displayPath: string } {
  const absolutePath = isAbsolute(targetPath) ? resolve(targetPath) : resolve(repoRoot, targetPath);
  const repoRelative = relative(repoRoot, absolutePath);
  if (repoRelative.length === 0) throw new Error("worktree path must not be the repository root");
  if (repoRelative.startsWith("..") || isAbsolute(repoRelative)) throw new Error("worktree path must stay under the UniDesk repository root");
  const displayPath = normalizedRelativePath(repoRelative);
  if (displayPath === ".git" || displayPath.startsWith(".git/")) throw new Error("worktree path must not be inside .git");
  if (displayPath.split("/").some((part) => part.length === 0 || part === "." || part === "..")) throw new Error("worktree path must be normalized");
  return { absolutePath, displayPath };
}

function parseWorktreeCopyPatterns(text: string): WorktreeCopyPattern[] {
  return text.split(/\r?\n/u)
    .map((line) => line.trim())
    .filter((line) => line.length > 0 && !line.startsWith("#"))
    .map((line) => {
      const include = !line.startsWith("!");
      const pattern = include ? line : line.slice(1);
      if (pattern.length === 0 || pattern.startsWith("/") || pattern.includes("\\") || pattern.split("/").some((part) => part === "..")) {
        throw new Error(`${defaultWorktreeCopyFile} contains unsupported pattern: ${line}`);
      }
      return { raw: line, pattern, include };
    });
}

function escapeRegExp(value: string): string {
  return value.replace(/[|\\{}()[\]^$+?.]/gu, "\\$&");
}

function globToRegExp(pattern: string): RegExp {
  let source = "^";
  for (let index = 0; index < pattern.length; index += 1) {
    const char = pattern[index] ?? "";
    const next = pattern[index + 1] ?? "";
    if (char === "*" && next === "*") {
      source += ".*";
      index += 1;
    } else if (char === "*") {
      source += "[^/]*";
    } else if (char === "?") {
      source += "[^/]";
    } else {
      source += escapeRegExp(char);
    }
  }
  return new RegExp(`${source}$`, "u");
}

function patternPrefix(pattern: string): string {
  const wildcardIndex = pattern.search(/[*?]/u);
  const prefix = wildcardIndex < 0 ? pattern : pattern.slice(0, wildcardIndex);
  const slashIndex = prefix.lastIndexOf("/");
  return slashIndex < 0 ? "" : prefix.slice(0, slashIndex);
}

function walkFiles(root: string, relativeRoot: string, out: string[]): void {
  if (!existsSync(root)) return;
  const stat = statSync(root);
  if (stat.isFile()) {
    out.push(relativeRoot);
    return;
  }
  if (!stat.isDirectory()) return;
  for (const entry of readdirSync(root, { withFileTypes: true })) {
    if (entry.name === ".git" || entry.name === "node_modules") continue;
    const childRelative = relativeRoot.length === 0 ? entry.name : `${relativeRoot}/${entry.name}`;
    const childPath = resolve(root, entry.name);
    if (entry.isDirectory()) walkFiles(childPath, childRelative, out);
    else if (entry.isFile()) out.push(childRelative);
  }
}

function candidateWorktreeCopyFiles(patterns: WorktreeCopyPattern[]): Set<string> {
  const candidates = new Set<string>();
  for (const pattern of patterns.filter((entry) => entry.include)) {
    if (!pattern.pattern.includes("*") && !pattern.pattern.includes("?")) {
      candidates.add(pattern.pattern);
      continue;
    }
    const prefix = patternPrefix(pattern.pattern);
    const files: string[] = [];
    walkFiles(resolve(repoRoot, prefix), prefix, files);
    const matcher = globToRegExp(pattern.pattern);
    for (const file of files) {
      if (matcher.test(file)) candidates.add(file);
    }
  }
  return candidates;
}

function includedByPatterns(path: string, patterns: WorktreeCopyPattern[]): boolean {
  let included = false;
  for (const pattern of patterns) {
    if (globToRegExp(pattern.pattern).test(path)) included = pattern.include;
  }
  return included;
}

function copyRefusalReason(path: string, bytes: number): string | null {
  const withSlash = path.endsWith("/") ? path : `${path}/`;
  if (refusedWorktreeCopyPrefixes.some((prefix) => path === prefix.slice(0, -1) || withSlash.startsWith(prefix))) return "refused-runtime-or-build-path";
  if (path.toLowerCase().includes("dump")) return "refused-dump-path";
  if (bytes > maxWorktreeCopyBytes) return `refused-large-file>${maxWorktreeCopyBytes}`;
  return null;
}

function copyWorktreeLocalFiles(targetAbsolutePath: string, overwriteLocal: boolean, dryRun = false): { configPath: string; patterns: string[]; rows: WorktreeCopyRow[] } {
  const configPath = resolve(repoRoot, defaultWorktreeCopyFile);
  if (!existsSync(configPath)) {
    return {
      configPath: defaultWorktreeCopyFile,
      patterns: [],
      rows: [{ path: defaultWorktreeCopyFile, status: "skipped", reason: "copy-config-missing", bytes: null }],
    };
  }
  const patterns = parseWorktreeCopyPatterns(readFileSync(configPath, "utf8"));
  const rows: WorktreeCopyRow[] = [];
  for (const candidate of Array.from(candidateWorktreeCopyFiles(patterns)).sort()) {
    if (!includedByPatterns(candidate, patterns)) continue;
    const sourcePath = resolve(repoRoot, candidate);
    const sourceRelative = normalizedRelativePath(relative(repoRoot, sourcePath));
    if (sourceRelative !== candidate || sourceRelative.startsWith("..")) {
      rows.push({ path: candidate, status: "refused", reason: "refused-outside-repo", bytes: null });
      continue;
    }
    if (!existsSync(sourcePath)) {
      rows.push({ path: candidate, status: "skipped", reason: "source-missing", bytes: null });
      continue;
    }
    const stat = statSync(sourcePath);
    if (!stat.isFile()) {
      rows.push({ path: candidate, status: "skipped", reason: "not-a-file", bytes: null });
      continue;
    }
    const refusalReason = copyRefusalReason(candidate, stat.size);
    if (refusalReason !== null) {
      rows.push({ path: candidate, status: "refused", reason: refusalReason, bytes: stat.size });
      continue;
    }
    const targetPath = resolve(targetAbsolutePath, candidate);
    const targetRelative = normalizedRelativePath(relative(targetAbsolutePath, targetPath));
    if (targetRelative !== candidate || targetRelative.startsWith("..")) {
      rows.push({ path: candidate, status: "refused", reason: "refused-target-outside-worktree", bytes: stat.size });
      continue;
    }
    if (existsSync(targetPath) && !overwriteLocal) {
      rows.push({ path: candidate, status: "skipped", reason: "target-exists", bytes: stat.size });
      continue;
    }
    if (dryRun) {
      rows.push({ path: candidate, status: "skipped", reason: existsSync(targetPath) ? "would-overwrite" : "would-copy", bytes: stat.size });
    } else {
      mkdirSync(dirname(targetPath), { recursive: true, mode: 0o700 });
      copyFileSync(sourcePath, targetPath);
      rows.push({ path: candidate, status: "copied", reason: overwriteLocal ? "overwritten" : "created", bytes: stat.size });
    }
  }
  return {
    configPath: defaultWorktreeCopyFile,
    patterns: patterns.map((pattern) => pattern.raw),
    rows,
  };
}

function kubectlDryRun(manifestPath: string): unknown {
  const kubeconfig = defaultNativeKubeconfig;
  const guardScript = k3sGuardShellLines(kubeconfig).join("\n");
  const guard = runCommand(["sh", "-lc", guardScript], repoRoot, {
    timeoutMs: 60_000,
    env: { ...process.env, KUBECONFIG: kubeconfig },
  });
  const guarded = guard.exitCode === 0;
  if (!guarded) {
    return {
      command: ["sh", "-lc", "d601 native k3s guard"],
      kubeconfig,
      exitCode: guard.exitCode,
      signal: guard.signal,
      timedOut: guard.timedOut,
      ok: false,
      guard: "d601-native-k3s",
      stdoutTail: guard.stdout.slice(-4000),
      stderrTail: guard.stderr.slice(-4000),
    };
  }
  const result = runCommand(["kubectl", "apply", "--dry-run=client", "--validate=false", "-f", manifestPath], repoRoot, {
    timeoutMs: 60_000,
    env: { ...process.env, KUBECONFIG: kubeconfig },
  });
  return {
    command: [`KUBECONFIG=${kubeconfig}`, ...result.command],
    kubeconfig,
    exitCode: result.exitCode,
    signal: result.signal,
    timedOut: result.timedOut,
    ok: result.exitCode === 0,
    stdoutTail: result.stdout.slice(-4000),
    stderrTail: result.stderr.slice(-4000),
  };
}

function shellQuote(value: string): string {
  return `'${value.replace(/'/g, "'\\''")}'`;
}

function prewarmImagesScript(options: PrewarmImagesOptions): string {
  const imageArray = options.images.map(shellQuote).join(" ");
  const pullTimeoutSeconds = Math.max(1, Math.ceil(options.pullTimeoutMs / 1000));
  return [
    "set -euo pipefail",
    "root_exec() {",
    "  if [ \"$(id -u)\" = \"0\" ]; then \"$@\"; return; fi",
    "  if sudo -n true >/dev/null 2>&1; then sudo -n \"$@\"; return; fi",
    "  if [ -x /mnt/c/Windows/System32/wsl.exe ]; then /mnt/c/Windows/System32/wsl.exe -u root -- \"$@\"; return; fi",
    "  echo 'dev_env_native_k3s_root_access=missing' >&2",
    "  return 1",
    "}",
    "normalize_image() {",
    "  image=\"$1\"",
    "  case \"$image\" in",
    "    *@*) printf '%s\\n' \"$image\" ;;",
    "    docker.io/*|ghcr.io/*|gcr.io/*|quay.io/*|cgr.dev/*|registry.*/*|localhost/*|*.*/*) printf '%s\\n' \"$image\" ;;",
    "    */*) printf 'docker.io/%s\\n' \"$image\" ;;",
    "    *) printf 'docker.io/library/%s\\n' \"$image\" ;;",
    "  esac",
    "}",
    `images=(${imageArray})`,
    `proxy_url=${shellQuote(options.proxyUrl)}`,
    `pull_missing=${options.pullMissing ? "1" : "0"}`,
    `pull_timeout_seconds=${pullTimeoutSeconds}`,
    "ctr_address=/run/k3s/containerd/containerd.sock",
    ...k3sGuardShellLines(),
    "export DOCKER_CONFIG=/tmp/unidesk-dev-env-docker-config",
    "mkdir -p \"$DOCKER_CONFIG\"",
    "printf '{}\\n' > \"$DOCKER_CONFIG/config.json\"",
    "printf 'dev_env_k3s_nodes='",
    "kubectl get nodes -o name | tr '\\n' ' '",
    "printf '\\n'",
    "for image in \"${images[@]}\"; do",
    "  if docker image inspect \"$image\" >/dev/null 2>&1; then",
    "    echo dev_env_image_cached=$image",
    "  elif [ \"$pull_missing\" = \"1\" ]; then",
    "    echo dev_env_image_pull=$image",
    "    timeout \"$pull_timeout_seconds\" env HTTP_PROXY=\"$proxy_url\" HTTPS_PROXY=\"$proxy_url\" ALL_PROXY=\"$proxy_url\" NO_PROXY=\"localhost,127.0.0.1,::1,host.docker.internal\" docker pull --platform linux/amd64 \"$image\"",
    "  else",
    "    echo dev_env_image_missing=$image >&2",
    "    exit 1",
    "  fi",
    "done",
    "archive=$(mktemp /tmp/unidesk-dev-env-images.XXXXXX.tar)",
    "list_file=$(mktemp /tmp/unidesk-dev-env-ctr-images.XXXXXX.txt)",
    "trap 'rm -f \"$archive\" \"$list_file\"' EXIT",
    "docker save \"${images[@]}\" -o \"$archive\"",
    "root_exec ctr --address \"$ctr_address\" -n k8s.io images import --digests --all-platforms \"$archive\"",
    "root_exec ctr --address \"$ctr_address\" -n k8s.io images ls > \"$list_file\"",
    "missing=0",
    "for image in \"${images[@]}\"; do",
    "  needle=$(normalize_image \"$image\")",
    "  if grep -F \"$needle\" \"$list_file\" >/dev/null || grep -F \"$image\" \"$list_file\" >/dev/null; then",
    "    echo dev_env_containerd_image_ready=$image",
    "  else",
    "    echo dev_env_containerd_image_missing=$image needle=$needle >&2",
    "    missing=1",
    "  fi",
    "done",
    "test \"$missing\" = \"0\"",
  ].join("\n");
}

function worktreeAddCommand(options: WorktreeAddOptions, targetAbsolutePath: string): string[] {
  const command = ["git", "worktree", "add"];
  if (options.branch !== null) command.push("-b", options.branch);
  command.push(targetAbsolutePath, options.fromRef);
  return command;
}

function runWorktreeAdd(options: WorktreeAddOptions): unknown {
  const target = resolveWorktreeTarget(options.targetPath);
  if (existsSync(target.absolutePath) && !options.dryRun) throw new Error(`worktree target already exists: ${target.displayPath}`);
  const command = worktreeAddCommand(options, target.absolutePath);
  const result = {
    ok: true,
    dryRun: options.dryRun,
    path: target.displayPath,
    branch: options.branch,
    fromRef: options.fromRef,
    command,
    copyLocal: options.copyLocal
      ? copyWorktreeLocalFiles(target.absolutePath, options.overwriteLocal, true)
      : { configPath: defaultWorktreeCopyFile, patterns: [], rows: [{ path: defaultWorktreeCopyFile, status: "skipped" as const, reason: "copy-local-disabled", bytes: null }] },
  };
  if (!options.dryRun) {
    const git = runCommand(command, repoRoot, { timeoutMs: 120_000 });
    if (git.exitCode !== 0) {
      throw new Error(`git worktree add failed: ${JSON.stringify({
        command: git.command,
        exitCode: git.exitCode,
        signal: git.signal,
        timedOut: git.timedOut,
        stdoutTail: git.stdout.slice(-2000),
        stderrTail: git.stderr.slice(-2000),
      })}`);
    }
    if (options.copyLocal) result.copyLocal = copyWorktreeLocalFiles(target.absolutePath, options.overwriteLocal);
  }
  if (options.json) return result;
  return {
    ok: true,
    command: "dev-env worktree add",
    contentType: "text/plain",
    renderedText: renderWorktreeAdd(result),
  };
}

function renderWorktreeAdd(result: {
  ok: boolean;
  dryRun: boolean;
  path: string;
  branch: string | null;
  fromRef: string;
  command: string[];
  copyLocal: { configPath: string; patterns: string[]; rows: WorktreeCopyRow[] };
}): string {
  const rows = result.copyLocal.rows;
  const counts = {
    copied: rows.filter((row) => row.status === "copied").length,
    skipped: rows.filter((row) => row.status === "skipped").length,
    refused: rows.filter((row) => row.status === "refused").length,
  };
  const pathWidth = Math.min(60, Math.max(4, ...rows.map((row) => row.path.length)));
  const statusWidth = Math.max(6, ...rows.map((row) => row.status.length));
  const reasonWidth = Math.min(38, Math.max(6, ...rows.map((row) => row.reason.length)));
  const truncate = (value: string, width: number): string => value.length <= width ? value : `${value.slice(0, Math.max(0, width - 1))}~`;
  const lines = [
    `worktree ${result.dryRun ? "dry-run" : "created"}: ${result.path}`,
    `from: ${result.fromRef}${result.branch === null ? "" : `  branch: ${result.branch}`}`,
    `copy config: ${result.copyLocal.configPath}  copied=${counts.copied} skipped=${counts.skipped} refused=${counts.refused}`,
    "",
    `${"STATUS".padEnd(statusWidth)}  ${"PATH".padEnd(pathWidth)}  ${"BYTES".padStart(8)}  REASON`,
  ];
  for (const row of rows) {
    lines.push([
      row.status.toUpperCase().padEnd(statusWidth),
      truncate(row.path, pathWidth).padEnd(pathWidth),
      (row.bytes === null ? "-" : String(row.bytes)).padStart(8),
      truncate(row.reason, reasonWidth),
    ].join("  "));
  }
  lines.push("");
  lines.push(`drill down: bun scripts/cli.ts dev-env worktree add ${result.path} --from ${result.fromRef}${result.branch === null ? "" : ` --branch ${result.branch}`} --json`);
  return lines.join("\n");
}

function devEnvHelp(): Record<string, unknown> {
  return {
    ok: true,
    command: "dev-env",
    usage: [
      "bun scripts/cli.ts dev-env validate [--manifest path] [--kubectl-dry-run]",
      "bun scripts/cli.ts dev-env prewarm-images [--image image] [--target D601] [--no-pull] [--proxy-url URL] [--pull-timeout-ms N] [--dry-run]",
      "bun scripts/cli.ts dev-env worktree add .worktree/<task> --branch <branch> [--from origin/master] [--copy-local] [--overwrite-local] [--json]",
    ],
    defaultManifest,
    defaultPrewarmImages,
    checks: [
      "all namespaced resources must target unidesk-dev",
      "required foundation resources, backend-core-dev/frontend-dev resources, or code-queue-dev resources must exist",
      "mdtodo-dev and claudeqq-dev service manifests may be validated independently when those dev workload manifests are selected",
      "dev DATABASE_URL values must target postgres-dev/unidesk_dev and not production routes",
      "--kubectl-dry-run optionally asks kubectl to client-dry-run the manifest without applying it",
      "prewarm-images imports dev foundation images from Docker into native k3s containerd on D601",
      "worktree add creates UniDesk task worktrees and copies local files allowed by .worktreecopy",
    ],
  };
}

export function runDevEnvCommand(args: string[]): unknown {
  const action = args[0];
  if (action === undefined || isHelpArg(action)) return devEnvHelp();
  if (action === "worktree") {
    const subaction = args[1];
    if (subaction === undefined || isHelpArg(subaction)) return devEnvHelp();
    if (subaction !== "add") throw new Error("dev-env worktree usage: bun scripts/cli.ts dev-env worktree add .worktree/<task> --branch <branch> [--from origin/master]");
    return runWorktreeAdd(parseWorktreeAddOptions(args.slice(2)));
  }
  if (action === "prewarm-images") {
    const options = parsePrewarmImagesOptions(args.slice(1));
    const script = prewarmImagesScript(options);
    const command = [process.execPath, "scripts/cli.ts", "ssh", options.providerId, "argv", "bash", "-lc", script];
    if (options.dryRun) {
      return {
        ok: true,
        dryRun: true,
        providerId: options.providerId,
        images: options.images,
        proxyUrl: options.proxyUrl,
        pullMissing: options.pullMissing,
        pullTimeoutMs: options.pullTimeoutMs,
        command,
      };
    }
    const job = startJob("dev_env_prewarm_images", command, `Prewarm ${options.images.length} dev foundation image(s) into ${options.providerId} native k3s containerd`);
    return {
      ok: true,
      providerId: options.providerId,
      images: options.images,
      proxyUrl: options.proxyUrl,
      pullMissing: options.pullMissing,
      pullTimeoutMs: options.pullTimeoutMs,
      job,
      statusCommand: `bun scripts/cli.ts job status ${job.id} --tail-bytes 12000`,
    };
  }
  if (action !== "validate") throw new Error("dev-env usage: bun scripts/cli.ts dev-env validate [--manifest path] [--kubectl-dry-run] OR dev-env prewarm-images OR dev-env worktree add");

  const options = parseValidateOptions(args.slice(1));
  const manifestPath = rootPath(options.manifestPath);
  const manifestText = readFileSync(manifestPath, "utf8");
  const docs = parseManifestDocuments(manifestText);
  const resources = docs.map((doc) => `${doc.kind}/${doc.name}`);
  const requiredKinds = requiredResourcesFor(resources);
  const namespacedViolations = docs
    .filter((doc) => doc.kind !== "Namespace")
    .filter((doc) => doc.namespace !== devNamespace)
    .map((doc) => ({ index: doc.index, kind: doc.kind, name: doc.name, namespace: doc.namespace }));
  const namespaceObjectViolations = docs
    .filter((doc) => doc.kind === "Namespace")
    .filter((doc) => doc.name !== devNamespace)
    .map((doc) => ({ index: doc.index, kind: doc.kind, name: doc.name }));
  const productionNamespaceTouches = docs
    .filter((doc) => doc.namespace === prodNamespace || (doc.kind === "Namespace" && doc.name === prodNamespace))
    .map((doc) => ({ kind: doc.kind, name: doc.name }));
  const missingRequiredResources = Array.from(requiredKinds).filter((resource) => !resources.includes(resource));
  const urlChecks = databaseUrls(manifestText).map(validateDatabaseUrl);
  const badUrls = urlChecks.filter((check) => !check.ok);
  const forbiddenProductionTextHits = [
    "namespace: unidesk\n",
    "d601-tcp-egress-gateway.unidesk.svc.cluster.local:15432/unidesk",
    "74.48.78.17:15432/unidesk",
  ].filter((needle) => manifestText.includes(needle));

  const staticOk = namespacedViolations.length === 0
    && namespaceObjectViolations.length === 0
    && productionNamespaceTouches.length === 0
    && missingRequiredResources.length === 0
    && badUrls.length === 0
    && forbiddenProductionTextHits.length === 0;
  const kubectl = options.kubectlDryRun ? kubectlDryRun(manifestPath) : { skipped: true, enableWith: "--kubectl-dry-run" };
  const kubectlOk = typeof kubectl === "object" && kubectl !== null && "ok" in kubectl ? (kubectl as { ok: boolean }).ok : true;

  return {
    ok: staticOk && kubectlOk,
    manifest: options.manifestPath,
    namespace: devNamespace,
    staticChecks: {
      ok: staticOk,
      resources,
      namespacedViolations,
      namespaceObjectViolations,
      productionNamespaceTouches,
      missingRequiredResources,
      databaseUrlChecks: urlChecks,
      forbiddenProductionTextHits,
    },
    kubectlDryRun: kubectl,
  };
}
