import fs from "node:fs/promises";
import path from "node:path";

type GatewayProgramArgs = {
  programArguments: string[];
  workingDirectory?: string;
};

type GatewayRuntimePreference = "auto" | "node" | "bun";

function isNodeRuntime(execPath: string): boolean {
  const base = path.basename(execPath).toLowerCase();
  return base === "node" || base === "node.exe";
}

function isBunRuntime(execPath: string): boolean {
  const base = path.basename(execPath).toLowerCase();
  return base === "bun" || base === "bun.exe";
}

async function resolveCliEntrypointPathForService(): Promise<string> {
  const argv1 = process.argv[1];
  if (!argv1) throw new Error("Unable to resolve CLI entrypoint path");

  const normalized = path.resolve(argv1);
  const resolvedPath = await resolveRealpathSafe(normalized);
  const looksLikeDist = /[/\\]dist[/\\].+\.(cjs|js|mjs)$/.test(resolvedPath);
  if (looksLikeDist) {
    await fs.access(resolvedPath);
    // Prefer the original (possibly symlinked) path over the resolved realpath.
    // This keeps LaunchAgent/systemd paths stable across package version updates,
    // since symlinks like node_modules/clawdbot -> .pnpm/clawdbot@X.Y.Z/...
    // are automatically updated by pnpm, while the resolved path contains
    // version-specific directories that break after updates.
    const normalizedLooksLikeDist = /[/\\]dist[/\\].+\.(cjs|js|mjs)$/.test(normalized);
    if (normalizedLooksLikeDist && normalized !== resolvedPath) {
      try {
        await fs.access(normalized);
        return normalized;
      } catch {
        // Fall through to return resolvedPath
      }
    }
    return resolvedPath;
  }

  const distCandidates = buildDistCandidates(resolvedPath, normalized);

  for (const candidate of distCandidates) {
    try {
      await fs.access(candidate);
      return candidate;
    } catch {
      // keep going
    }
  }

  throw new Error(
    `Cannot find built CLI at ${distCandidates.join(" or ")}. Run "pnpm build" first, or use dev mode.`,
  );
}

async function resolveRealpathSafe(inputPath: string): Promise<string> {
  try {
    return await fs.realpath(inputPath);
  } catch {
    return inputPath;
  }
}

function buildDistCandidates(...inputs: string[]): string[] {
  const candidates: string[] = [];
  const seen = new Set<string>();

  for (const inputPath of inputs) {
    if (!inputPath) continue;
    const baseDir = path.dirname(inputPath);
    appendDistCandidates(candidates, seen, path.resolve(baseDir, ".."));
    appendDistCandidates(candidates, seen, baseDir);
    appendNodeModulesBinCandidates(candidates, seen, inputPath);
  }

  return candidates;
}

function appendDistCandidates(candidates: string[], seen: Set<string>, baseDir: string): void {
  const distDir = path.resolve(baseDir, "dist");
  const distEntries = [
    path.join(distDir, "index.js"),
    path.join(distDir, "index.mjs"),
    path.join(distDir, "entry.js"),
    path.join(distDir, "entry.mjs"),
  ];
  for (const entry of distEntries) {
    if (seen.has(entry)) continue;
    seen.add(entry);
    candidates.push(entry);
  }
}

function appendNodeModulesBinCandidates(
  candidates: string[],
  seen: Set<string>,
  inputPath: string,
): void {
  const parts = inputPath.split(path.sep);
  const binIndex = parts.lastIndexOf(".bin");
  if (binIndex <= 0) return;
  if (parts[binIndex - 1] !== "node_modules") return;
  const binName = path.basename(inputPath);
  const nodeModulesDir = parts.slice(0, binIndex).join(path.sep);
  const packageRoot = path.join(nodeModulesDir, binName);
  appendDistCandidates(candidates, seen, packageRoot);
}

function resolveRepoRootForDev(): string {
  const argv1 = process.argv[1];
  if (!argv1) throw new Error("Unable to resolve repo root");
  const normalized = path.resolve(argv1);
  const parts = normalized.split(path.sep);
  const srcIndex = parts.lastIndexOf("src");
  if (srcIndex === -1) {
    throw new Error("Dev mode requires running from repo (src/index.ts)");
  }
  return parts.slice(0, srcIndex).join(path.sep);
}

async function resolveBunPath(): Promise<string> {
  const bunPath = await resolveBinaryPath("bun");
  return bunPath;
}

async function resolveNodePath(): Promise<string> {
  const nodePath = await resolveBinaryPath("node");
  return nodePath;
}

async function resolveBinaryPath(binary: string): Promise<string> {
  const { execSync } = await import("node:child_process");
  const cmd = process.platform === "win32" ? "where" : "which";
  try {
    const output = execSync(`${cmd} ${binary}`, { encoding: "utf8" }).trim();
    const resolved = output.split(/\r?\n/)[0]?.trim();
    if (!resolved) throw new Error("empty");
    await fs.access(resolved);
    return resolved;
  } catch {
    if (binary === "bun") {
      throw new Error("Bun not found in PATH. Install bun: https://bun.sh");
    }
    throw new Error("Node not found in PATH. Install Node 22+.");
  }
}

async function resolveCliProgramArguments(params: {
  args: string[];
  dev?: boolean;
  runtime?: GatewayRuntimePreference;
  nodePath?: string;
}): Promise<GatewayProgramArgs> {
  const execPath = process.execPath;
  const runtime = params.runtime ?? "auto";

  if (runtime === "node") {
    const nodePath =
      params.nodePath ?? (isNodeRuntime(execPath) ? execPath : await resolveNodePath());
    const cliEntrypointPath = await resolveCliEntrypointPathForService();
    return {
      programArguments: [nodePath, cliEntrypointPath, ...params.args],
    };
  }

  if (runtime === "bun") {
    if (params.dev) {
      const repoRoot = resolveRepoRootForDev();
      const devCliPath = path.join(repoRoot, "src", "index.ts");
      await fs.access(devCliPath);
      const bunPath = isBunRuntime(execPath) ? execPath : await resolveBunPath();
      return {
        programArguments: [bunPath, devCliPath, ...params.args],
        workingDirectory: repoRoot,
      };
    }

    const bunPath = isBunRuntime(execPath) ? execPath : await resolveBunPath();
    const cliEntrypointPath = await resolveCliEntrypointPathForService();
    return {
      programArguments: [bunPath, cliEntrypointPath, ...params.args],
    };
  }

  if (!params.dev) {
    try {
      const cliEntrypointPath = await resolveCliEntrypointPathForService();
      return {
        programArguments: [execPath, cliEntrypointPath, ...params.args],
      };
    } catch (error) {
      // If running under bun or another runtime that can execute TS directly
      if (!isNodeRuntime(execPath)) {
        return { programArguments: [execPath, ...params.args] };
      }
      throw error;
    }
  }

  // Dev mode: use bun to run TypeScript directly
  const repoRoot = resolveRepoRootForDev();
  const devCliPath = path.join(repoRoot, "src", "index.ts");
  await fs.access(devCliPath);

  // If already running under bun, use current execPath
  if (isBunRuntime(execPath)) {
    return {
      programArguments: [execPath, devCliPath, ...params.args],
      workingDirectory: repoRoot,
    };
  }

  // Otherwise resolve bun from PATH
  const bunPath = await resolveBunPath();
  return {
    programArguments: [bunPath, devCliPath, ...params.args],
    workingDirectory: repoRoot,
  };
}

export async function resolveGatewayProgramArguments(params: {
  port: number;
  dev?: boolean;
  runtime?: GatewayRuntimePreference;
  nodePath?: string;
}): Promise<GatewayProgramArgs> {
  const gatewayArgs = ["gateway", "--port", String(params.port)];
  return resolveCliProgramArguments({
    args: gatewayArgs,
    dev: params.dev,
    runtime: params.runtime,
    nodePath: params.nodePath,
  });
}

export async function resolveNodeProgramArguments(params: {
  host: string;
  port: number;
  tls?: boolean;
  tlsFingerprint?: string;
  nodeId?: string;
  displayName?: string;
  dev?: boolean;
  runtime?: GatewayRuntimePreference;
  nodePath?: string;
}): Promise<GatewayProgramArgs> {
  const args = ["node", "run", "--host", params.host, "--port", String(params.port)];
  if (params.tls || params.tlsFingerprint) args.push("--tls");
  if (params.tlsFingerprint) args.push("--tls-fingerprint", params.tlsFingerprint);
  if (params.nodeId) args.push("--node-id", params.nodeId);
  if (params.displayName) args.push("--display-name", params.displayName);
  return resolveCliProgramArguments({
    args,
    dev: params.dev,
    runtime: params.runtime,
    nodePath: params.nodePath,
  });
}
