import { existsSync } from "fs";
import { dirname, join } from "path";
import { fileURLToPath } from "url";
import { clearAuthBlockByIp, listAuthBlocks } from "./auth-control.js";
import { printCapabilities, printIdentify } from "./automation-client.js";
import { callCoderStudioCommand } from "./automation-command-client.js";
import { openBrowser } from "./browser.js";
import { type CliConfig, readCliConfig, writeCliConfig } from "./config-store.js";
import { readLogExcerpt } from "./log-excerpt.js";
import { assertSupportedNodeVersion } from "./node-version.js";
import { getCliVersion } from "./package-manifest.js";
import { parseArgs } from "./parse-args.js";
import { startManagedServer } from "./pm2-control.js";
import { confirmYesNo, isInteractiveSession } from "./prompts.js";
import { getServerStatus, type ServerStatus, stopRunningServer } from "./server-control.js";
import { prepareLocalStateStorage, startServer } from "./server-runner.js";
import { getBrowserUrl, getListenIp, getListenUrl } from "./server-url.js";

const MANAGED_SERVER_WAIT_MS = 5000;
const DEFAULT_LOG_TAIL_LINES = 40;

function formatConfig(config: CliConfig | null): string {
  return JSON.stringify(config ?? {}, null, 2);
}

function formatStatus(status: ServerStatus): string {
  const listenUrl = getListenUrl(status) ?? "n/a";
  const browserUrl = getBrowserUrl(status) ?? "n/a";
  const startedAt = status.startedAt === null ? "n/a" : new Date(status.startedAt).toISOString();

  return [
    `Status: ${status.status}`,
    `Listen host: ${status.host ?? "n/a"}`,
    `Listen IP: ${getListenIp(status) ?? "n/a"}`,
    `Port: ${status.port ?? "n/a"}`,
    `Listen URL: ${listenUrl}`,
    `Local URL: ${browserUrl}`,
    `PID: ${status.pid ?? "n/a"}`,
    `Started: ${startedAt}`,
    `Restarts: ${status.restartCount}`,
    `Out log: ${status.outFile}`,
    `Error log: ${status.errFile}`,
  ].join("\n");
}

function showLogs(
  status: ServerStatus,
  {
    tail = DEFAULT_LOG_TAIL_LINES,
    errorsOnly = false,
  }: { tail?: number; errorsOnly?: boolean } = {}
): void {
  const paths = errorsOnly ? [status.errFile] : [status.outFile, status.errFile];
  const contents = paths
    .filter((path, index, paths) => paths.indexOf(path) === index)
    .flatMap((path) => {
      const content = readLogExcerpt(path, { maxLines: tail, maxChars: null });
      return content ? [content] : [];
    });

  console.log(contents.length === 0 ? "No logs available." : contents.join("\n"));
}

function showHelp(): void {
  console.log(`
@spencer-kit/coder-studio - Coder Studio CLI

USAGE:
  coder-studio [COMMAND]

COMMANDS:
  serve    Start the Coder Studio server in background (default)
  server   Alias for serve
  open     Start the server if needed and open Coder Studio in a browser
  auth     Manage auth login blocks in local server storage
  config   Persist CLI host/port/state-dir/password settings
  stop     Stop the managed Coder Studio server
  status   Show the managed server status
  logs     Show the managed server logs
  help     Show this help message
  identify      Print Coder Studio agent runtime context
  capabilities  Print agent-facing automation capabilities
  workspace     Read workspace automation data
  session       Read session automation data
  terminal      Read terminal automation data
  git           Read git automation data
  version  Show version

OPTIONS:
  --host <string>          Save server host for future runs
  --port, -p <number>      Save server port for future runs
  --state-dir, --data-dir, -d <path>  Save state directory for future runs
  --password <string>      Save auth password for future runs
  --restart                Restart an already running managed server for serve/open
  --help                   Show help
  --version, -v            Show version

EXAMPLES:
  coder-studio
  coder-studio serve
  coder-studio server
  coder-studio auth ban-list
  coder-studio auth unblock --ip 198.51.100.24
  coder-studio serve --foreground
  coder-studio serve --restart
  coder-studio open
  coder-studio open --restart
  coder-studio status
  coder-studio logs
  coder-studio identify --json
  coder-studio capabilities --json
  coder-studio workspace list --json
  coder-studio session list --workspace ws_123 --json
  coder-studio terminal read --terminal term_123 --bytes 4096 --json
  coder-studio git status --workspace ws_123 --json
  coder-studio git diff --workspace ws_123 --path src/a.ts --json
  coder-studio stop
  coder-studio config --host 0.0.0.0 --port 8080
`);
}

function showConfigHelp(): void {
  console.log(`
@spencer-kit/coder-studio - config

USAGE:
  coder-studio config [OPTIONS]
  coder-studio config help

BEHAVIOR:
  Without options, prints the current saved config.
  Bare serve reads this saved config for future runs.

OPTIONS:
  --host <string>          Save server host for future runs
  --port, -p <number>      Save server port for future runs
  --state-dir, --data-dir, -d <path>  Save state directory for future runs
  --password <string>      Save auth password for future runs
  --help                   Show config help

EXAMPLES:
  coder-studio config
  coder-studio config --host 0.0.0.0
  coder-studio config --port 8080
  coder-studio config --state-dir /tmp/cs-data
  coder-studio config --password sekrit
  coder-studio config --host 0.0.0.0 --port 8080
`);
}

function showVersion(): void {
  console.log(`@spencer-kit/coder-studio v${getCliVersion(import.meta.url)}`);
}

function printCommandResult(result: unknown, options: { json?: boolean } = {}): void {
  if (options.json) {
    console.log(JSON.stringify(result, null, 2));
    return;
  }

  if (
    typeof result === "object" &&
    result !== null &&
    "text" in result &&
    typeof (result as { text?: unknown }).text === "string"
  ) {
    console.log((result as { text: string }).text);
    return;
  }

  if (
    typeof result === "object" &&
    result !== null &&
    "diff" in result &&
    typeof (result as { diff?: unknown }).diff === "string"
  ) {
    console.log((result as { diff: string }).diff);
    return;
  }

  console.log(JSON.stringify(result, null, 2));
}

function formatAuthBlocks(blocks: Awaited<ReturnType<typeof listAuthBlocks>>): string {
  if (blocks.length === 0) {
    return "No blocked IPs.";
  }

  return JSON.stringify(blocks, null, 2);
}

function resolveManagedScriptPath(): string {
  const currentFile = fileURLToPath(import.meta.url);
  const currentDir = dirname(currentFile);
  const candidates = [
    join(currentDir, "server-runner.js"),
    join(currentDir, "server-runner.mjs"),
    join(currentDir, "../src/server-runner.ts"),
  ];

  const scriptPath = candidates.find((candidate) => existsSync(candidate));
  if (!scriptPath) {
    throw new Error("Unable to locate the managed server entry script");
  }

  return scriptPath;
}

function isRunningStatus(status: ServerStatus): boolean {
  return status.status === "running" || status.status === "starting";
}

interface ManagedStartupDecision {
  existingStatus: ServerStatus | null;
  restartRequested: boolean;
}

async function shouldRestartRunningServer(status: ServerStatus): Promise<boolean> {
  const currentUrl = getBrowserUrl(status) ?? getListenUrl(status) ?? "the existing server";

  if (!isInteractiveSession()) {
    return false;
  }

  return confirmYesNo(`Coder Studio is already running at ${currentUrl}. Restart it? [y/N] `);
}

async function prepareManagedStartup(forceRestart = false): Promise<ManagedStartupDecision> {
  const status = await getServerStatus();
  if (!isRunningStatus(status)) {
    return {
      existingStatus: null,
      restartRequested: false,
    };
  }

  const restart = forceRestart ? true : await shouldRestartRunningServer(status);
  if (!restart) {
    const currentUrl = getBrowserUrl(status) ?? getListenUrl(status) ?? "n/a";
    if (!isInteractiveSession()) {
      console.log(
        `Coder Studio is already running at ${currentUrl}. Service already exists and was not restarted.`
      );
    } else {
      console.log(`Leaving the existing Coder Studio server running at ${currentUrl}.`);
    }
    return {
      existingStatus: status,
      restartRequested: false,
    };
  }

  console.log("Restarting the managed Coder Studio server...");
  return {
    existingStatus: null,
    restartRequested: true,
  };
}

async function startManagedServerFlow(): Promise<void> {
  await startManagedServer({
    script: resolveManagedScriptPath(),
    cwd: process.cwd(),
    waitMs: MANAGED_SERVER_WAIT_MS,
  });
}

async function openManagedServerInBrowser(existingStatus?: ServerStatus | null): Promise<void> {
  const status = existingStatus ?? (await getServerStatus());
  const browserUrl = getBrowserUrl(status);

  if (browserUrl === null) {
    throw new Error("Unable to determine the running Coder Studio URL.");
  }

  console.log(`Opening Coder Studio in your browser: ${browserUrl}`);
  await openBrowser(browserUrl);
}

export async function main(argv = process.argv.slice(2)): Promise<void> {
  assertSupportedNodeVersion();
  const args = parseArgs(argv);

  if (args.command === "config") {
    if (args.configHelp) {
      showConfigHelp();
      return;
    }

    if (
      args.host === undefined &&
      args.port === undefined &&
      args.stateDir === undefined &&
      args.password === undefined
    ) {
      console.log(formatConfig(readCliConfig()));
      return;
    }

    const savedConfig = readCliConfig();
    const nextConfig: CliConfig = {
      ...(savedConfig?.host !== undefined ? { host: savedConfig.host } : {}),
      ...(savedConfig?.port !== undefined && savedConfig.port > 0
        ? { port: savedConfig.port }
        : {}),
      ...(savedConfig?.stateDir !== undefined ? { stateDir: savedConfig.stateDir } : {}),
      ...(savedConfig?.password !== undefined ? { password: savedConfig.password } : {}),
      ...(args.host !== undefined ? { host: args.host } : {}),
      ...(args.port !== undefined ? { port: args.port } : {}),
      ...(args.stateDir !== undefined ? { stateDir: args.stateDir } : {}),
      ...(args.password !== undefined ? { password: args.password } : {}),
    };
    writeCliConfig(nextConfig);
    console.log(formatConfig(nextConfig));
    return;
  }

  if (args.command === "stop") {
    const stopped = await stopRunningServer();
    console.log(stopped ? "Stopped Coder Studio server." : "No running Coder Studio server found.");
    return;
  }

  if (args.command === "status") {
    console.log(formatStatus(await getServerStatus()));
    return;
  }

  if (args.command === "logs") {
    showLogs(await getServerStatus(), { tail: args.tail, errorsOnly: args.errorsOnly });
    return;
  }

  if (args.command === "help") {
    showHelp();
    return;
  }

  if (args.command === "version") {
    showVersion();
    return;
  }

  if (args.command === "identify") {
    printIdentify({ json: args.json });
    return;
  }

  if (args.command === "capabilities") {
    printCapabilities({ json: args.json });
    return;
  }

  if (args.command === "workspace" && args.workspaceCommand === "list") {
    printCommandResult(
      await callCoderStudioCommand({
        apiUrl: args.apiUrl,
        op: "workspace.list",
        args: {},
      }),
      { json: args.json }
    );
    return;
  }

  if (args.command === "session" && args.sessionCommand === "list") {
    printCommandResult(
      await callCoderStudioCommand({
        apiUrl: args.apiUrl,
        op: "session.list",
        args: { workspaceId: args.workspaceId! },
      }),
      { json: args.json }
    );
    return;
  }

  if (args.command === "terminal" && args.terminalCommand === "read") {
    printCommandResult(
      await callCoderStudioCommand({
        apiUrl: args.apiUrl,
        op: "terminal.read",
        args: {
          terminalId: args.terminalId!,
          ...(args.bytes !== undefined ? { bytes: args.bytes } : {}),
        },
      }),
      { json: args.json }
    );
    return;
  }

  if (args.command === "git" && args.gitCommand === "status") {
    printCommandResult(
      await callCoderStudioCommand({
        apiUrl: args.apiUrl,
        op: "git.status",
        args: { workspaceId: args.workspaceId! },
      }),
      { json: args.json }
    );
    return;
  }

  if (args.command === "git" && args.gitCommand === "diff") {
    printCommandResult(
      await callCoderStudioCommand({
        apiUrl: args.apiUrl,
        op: "git.diff",
        args: {
          workspaceId: args.workspaceId!,
          ...(args.path !== undefined ? { path: args.path } : {}),
          ...(args.staged === true ? { staged: true } : {}),
        },
      }),
      { json: args.json }
    );
    return;
  }

  if (args.command === "auth") {
    if (args.authCommand === "ban-list") {
      console.log(formatAuthBlocks(await listAuthBlocks()));
      return;
    }

    if (args.authCommand === "unblock") {
      const cleared = await clearAuthBlockByIp(args.ip!);
      console.log(cleared ? `Unblocked IP: ${args.ip}` : `No block found for IP: ${args.ip}`);
      return;
    }
  }

  if (args.command === "open") {
    const startup = await prepareManagedStartup(args.restart);
    if (startup.existingStatus === null) {
      prepareLocalStateStorage();
      await startManagedServerFlow();
    }

    await openManagedServerInBrowser(startup.existingStatus);
    return;
  }

  if (args.foreground) {
    const startup = await prepareManagedStartup(args.restart);
    if (startup.existingStatus !== null) {
      return;
    }

    if (startup.restartRequested) {
      await stopRunningServer();
    }

    console.log("Starting Coder Studio Server in foreground...");
    await startServer();
    return;
  }

  const startup = await prepareManagedStartup(args.restart);
  if (startup.existingStatus !== null) {
    return;
  }

  prepareLocalStateStorage();
  await startManagedServerFlow();

  console.log("Coder Studio server started in background.");
  console.log("Run `coder-studio status` to inspect the server.");
}
