import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { type UniDeskConfig, repoRoot, rootPath } from "./config";
import { runCommand } from "./command";
import { runProviderTriage } from "./provider-triage";

interface ProviderAttachOptions {
  providerId: string;
  masterServer: string;
  envFile: string;
  composeFile: string;
  logDir: string;
  hostSshKeyDir: string;
  token: string | null;
  force: boolean;
  up: boolean;
}

interface ProviderAttachContainerValidation {
  ok: boolean;
  containerName: string;
  restartPolicy: string;
  restartPolicyOk: boolean;
  pidMode: string;
  pidModeOk: boolean;
  state: string;
  command: string[];
  exitCode: number | null;
  stderr: string;
  hostSshKeyPath: string;
  hostSshKeyPresent: boolean;
  hostSshKeyCommand: string[];
  hostSshKeyExitCode: number | null;
  hostSshTarget: string;
  hostSshProbeOk: boolean;
  hostSshProbeCommand: string[];
  hostSshProbeExitCode: number | null;
  hostSshProbeStdout: string;
  hostSshProbeStderr: string;
}

function optionValue(args: string[], name: string): string | null {
  const index = args.indexOf(name);
  if (index === -1) return null;
  const value = args[index + 1];
  if (value === undefined || value.length === 0) throw new Error(`${name} requires a non-empty value`);
  return value;
}

function hasFlag(args: string[], name: string): boolean {
  return args.includes(name);
}

function safeProviderSlug(providerId: string): string {
  return providerId.replace(/[^a-zA-Z0-9_.-]/g, "-").replace(/^-+|-+$/g, "").toLowerCase() || "provider";
}

function defaultHostSshUser(hostProjectRoot: string): string {
  const homeMatch = hostProjectRoot.match(/^\/home\/([^/]+)/u);
  if (homeMatch?.[1]) return homeMatch[1];
  return "root";
}

function defaultHostRemoteCwd(user: string): string {
  return user === "root" ? "/root" : `/home/${user}`;
}

function defaultMasterServer(config: UniDeskConfig): string {
  return `http://${config.network.publicHost}/`;
}

function defaultHostSshKeyDir(): string {
  const home = process.env.HOME || "/root";
  return join(home, ".unidesk", "host-ssh");
}

function parseAttachOptions(config: UniDeskConfig, args: string[]): ProviderAttachOptions {
  const valueOptions = new Set(["--env-file", "--compose-file", "--log-dir", "--master-server", "--master", "--host-ssh-key-dir", "--provider-token", "--token"]);
  let providerId: string | undefined;
  for (let index = 0; index < args.length; index += 1) {
    const arg = args[index] ?? "";
    if (valueOptions.has(arg)) {
      index += 1;
      continue;
    }
    if (!arg.startsWith("--")) {
      providerId = arg;
      break;
    }
  }
  if (providerId === undefined) {
    throw new Error("provider attach requires provider id, for example: bun scripts/cli.ts provider attach D518");
  }
  const envFile = optionValue(args, "--env-file") ?? rootPath(".state", `provider-${providerId}.env`);
  const composeFile = optionValue(args, "--compose-file") ?? rootPath(`provider-${providerId}.yml`);
  const logDir = optionValue(args, "--log-dir") ?? rootPath(".state", "logs", `provider-${providerId}`);
  return {
    providerId,
    masterServer: optionValue(args, "--master-server") ?? optionValue(args, "--master") ?? defaultMasterServer(config),
    envFile,
    composeFile,
    logDir,
    hostSshKeyDir: optionValue(args, "--host-ssh-key-dir") ?? defaultHostSshKeyDir(),
    token: optionValue(args, "--provider-token") ?? optionValue(args, "--token"),
    force: hasFlag(args, "--force"),
    up: hasFlag(args, "--up"),
  };
}

function envContent(options: ProviderAttachOptions): string {
  const slug = safeProviderSlug(options.providerId);
  const hostSshUser = defaultHostSshUser(repoRoot);
  const lines = [
    "# Generated by: bun scripts/cli.ts provider attach",
    "# Required attach inputs are intentionally limited to master server, provider id, and optional provider token.",
    `UNIDESK_MASTER_SERVER=${options.masterServer}`,
    `PROVIDER_ID=${options.providerId}`,
    `PROVIDER_NAME=${options.providerId}`,
    `PROVIDER_UPGRADE_HOST_PROJECT_ROOT=${repoRoot}`,
    "PROVIDER_UPGRADE_WORKSPACE_PATH=/workspace",
    `PROVIDER_UPGRADE_COMPOSE_FILE=provider-${options.providerId}.yml`,
    `PROVIDER_UPGRADE_ENV_FILE=${options.envFile}`,
    `PROVIDER_UPGRADE_COMPOSE_PROJECT=unidesk-${slug}`,
    "PROVIDER_UPGRADE_SERVICE=provider-gateway",
    `PROVIDER_UPGRADE_RUNNER_IMAGE=unidesk_provider-gateway:${slug}`,
    "HOST_SSH_HOST=host.docker.internal",
    "HOST_SSH_PORT=22",
    `HOST_SSH_USER=${hostSshUser}`,
    "HOST_SSH_KEY=/run/host-ssh/id_ed25519",
    `HOST_REMOTE_CWD=${defaultHostRemoteCwd(hostSshUser)}`,
    "HOST_LOGIN_SHELL=/bin/bash",
  ];
  if (options.token !== null) lines.push(`PROVIDER_TOKEN=${options.token}`);
  return `${lines.join("\n")}\n`;
}

function composeContent(options: ProviderAttachOptions): string {
  const slug = safeProviderSlug(options.providerId);
  const relativeEnv = pathForCompose(options.envFile);
  const relativeLogDir = pathForCompose(options.logDir);
  return [
    "services:",
    "  provider-gateway:",
    `    image: unidesk_provider-gateway:${slug}`,
    "    build:",
    "      context: .",
    "      dockerfile: src/components/provider-gateway/Dockerfile",
    "      args:",
    '        ALPINE_REPOSITORY: "${PROVIDER_GATEWAY_ALPINE_REPOSITORY:-}"',
    `    container_name: unidesk-provider-gateway-${slug}`,
    "    restart: always",
    '    pid: "host"',
    "    env_file:",
    `      - ${relativeEnv}`,
    "    ports:",
    '      - "127.0.0.1:${PROVIDER_EGRESS_PROXY_HOST_PORT:-18789}:${PROVIDER_EGRESS_PROXY_PORT:-18789}"',
    "    volumes:",
    "      - /var/run/docker.sock:/var/run/docker.sock",
    "      - .:/workspace:ro",
    `      - ${relativeLogDir}:/var/log/unidesk`,
    `      - ${options.hostSshKeyDir}:/run/host-ssh:ro`,
    "    extra_hosts:",
    '      - "host.docker.internal:host-gateway"',
    "",
  ].join("\n");
}

function pathForCompose(path: string): string {
  if (path.startsWith(repoRoot)) {
    const relative = path.slice(repoRoot.length).replace(/^\/+/, "");
    return relative.length === 0 ? "." : `./${relative}`;
  }
  return path;
}

function writeGeneratedFile(path: string, content: string, force: boolean): "created" | "updated" | "unchanged" | "skipped" {
  if (existsSync(path)) {
    const existing = readFileSync(path, "utf8");
    if (existing === content) return "unchanged";
    if (!force) return "skipped";
    writeFileSync(path, content, "utf8");
    return "updated";
  }
  mkdirSync(dirname(path), { recursive: true });
  writeFileSync(path, content, "utf8");
  return "created";
}

function inspectAttachedContainer(options: ProviderAttachOptions): ProviderAttachContainerValidation {
  const containerName = `unidesk-provider-gateway-${safeProviderSlug(options.providerId)}`;
  const hostSshKeyPath = "/run/host-ssh/id_ed25519";
  const hostSshTarget = `${defaultHostSshUser(repoRoot)}@host.docker.internal`;
  const command = [
    "docker",
    "inspect",
    "--format",
    "{{.HostConfig.RestartPolicy.Name}}\t{{.HostConfig.PidMode}}\t{{.State.Status}}",
    containerName,
  ];
  const result = runCommand(command, repoRoot);
  const [restartPolicy = "", pidMode = "", state = ""] = result.stdout.trim().split("\t");
  const restartPolicyOk = restartPolicy === "always";
  const pidModeOk = pidMode === "host";
  const containerReady = result.exitCode === 0 && restartPolicyOk && pidModeOk && state === "running";
  const hostSshKeyCommand = ["docker", "exec", containerName, "test", "-s", hostSshKeyPath];
  const hostSshKeyResult = containerReady
    ? runCommand(hostSshKeyCommand, repoRoot, { timeoutMs: 10_000 })
    : null;
  const hostSshKeyPresent = hostSshKeyResult?.exitCode === 0;
  const hostSshProbeCommand = [
    "docker",
    "exec",
    containerName,
    "ssh",
    "-T",
    "-i",
    hostSshKeyPath,
    "-o",
    "BatchMode=yes",
    "-o",
    "StrictHostKeyChecking=accept-new",
    "-o",
    "ConnectTimeout=10",
    hostSshTarget,
    "true",
  ];
  const hostSshProbeResult = hostSshKeyPresent
    ? runCommand(hostSshProbeCommand, repoRoot, { timeoutMs: 15_000 })
    : null;
  const hostSshProbeOk = hostSshProbeResult?.exitCode === 0;
  return {
    ok: containerReady && hostSshKeyPresent && hostSshProbeOk,
    containerName,
    restartPolicy,
    restartPolicyOk,
    pidMode,
    pidModeOk,
    state,
    command,
    exitCode: result.exitCode,
    stderr: result.stderr.trim(),
    hostSshKeyPath,
    hostSshKeyPresent,
    hostSshKeyCommand,
    hostSshKeyExitCode: hostSshKeyResult?.exitCode ?? null,
    hostSshTarget,
    hostSshProbeOk,
    hostSshProbeCommand,
    hostSshProbeExitCode: hostSshProbeResult?.exitCode ?? null,
    hostSshProbeStdout: hostSshProbeResult?.stdout.trim() ?? "",
    hostSshProbeStderr: hostSshProbeResult?.stderr.trim() ?? "",
  };
}

export async function runProviderCommand(config: UniDeskConfig, args: string[]): Promise<unknown> {
  const [sub] = args;
  if (sub !== "attach") {
    if (sub === "triage") {
      const providerId = args[1];
      if (providerId === undefined || providerId.length === 0) {
        throw new Error("provider triage requires provider id, for example: bun scripts/cli.ts provider triage D601");
      }
      return runProviderTriage(config, providerId, args.slice(2));
    }
    throw new Error("provider requires subcommand: attach|triage");
  }
  const options = parseAttachOptions(config, args.slice(1));
  mkdirSync(options.logDir, { recursive: true });
  mkdirSync(options.hostSshKeyDir, { recursive: true, mode: 0o700 });
  const envStatus = writeGeneratedFile(options.envFile, envContent(options), options.force);
  const composeStatus = writeGeneratedFile(options.composeFile, composeContent(options), options.force);
  const composeCommand = [
    "docker",
    "compose",
    "--env-file",
    options.envFile,
    "-f",
    options.composeFile,
    "-p",
    `unidesk-${safeProviderSlug(options.providerId)}`,
    "up",
    "-d",
    "--build",
    "--remove-orphans",
  ];
  const result = options.up ? runCommand(composeCommand, repoRoot) : null;
  const containerValidation = options.up ? inspectAttachedContainer(options) : null;
  return {
    ok: result === null || (result.exitCode === 0 && containerValidation?.ok === true),
    providerId: options.providerId,
    masterServer: options.masterServer,
    envFile: options.envFile,
    envFileStatus: envStatus,
    composeFile: options.composeFile,
    composeFileStatus: composeStatus,
    logDir: options.logDir,
    hostSshKeyDir: options.hostSshKeyDir,
    hostSshKeyPath: join(options.hostSshKeyDir, "id_ed25519"),
    startCommand: composeCommand,
    started: options.up,
    result,
    containerValidation,
    notes: [
      "The generated env file records the master address, provider identity, upgrade coordinates, and Host SSH target; PROVIDER_TOKEN is added only when supplied.",
      "Place a maintenance SSH private key at hostSshKeyPath if this provider should expose host.ssh.",
      "When --up is used, containerValidation must show restartPolicy=always, pidMode=host, hostSshKeyPresent=true, and hostSshProbeOk=true before the node is accepted.",
      "For Docker Desktop with a WSL key directory, run provider attach from that WSL distro so the bind mount resolves to the distro filesystem.",
      envStatus === "skipped" || composeStatus === "skipped" ? "Existing generated files differed and were not overwritten; rerun with --force to replace them." : "Generated files are ready.",
    ],
  };
}
