import { spawn } from "node:child_process";
import { createServer } from "node:net";
import * as Clock from "effect/Clock";
import * as Effect from "effect/Effect";

// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------

export interface ParsedDaemonBaseUrl {
  readonly hostname: string;
  readonly port: number;
}

export interface DaemonSpawnSpec {
  readonly command: string;
  readonly args: ReadonlyArray<string>;
}

export interface SpawnedDetachedProcess {
  readonly pid: number;
}

export interface ExecutorServerReachabilityInput {
  readonly baseUrl: string;
}

type ProbeServer = ReturnType<typeof createServer> & {
  removeAllListeners: () => void;
  once: (event: "error" | "listening", listener: (...args: unknown[]) => void) => void;
};

// ---------------------------------------------------------------------------
// Base URL parsing
// ---------------------------------------------------------------------------

export const parseDaemonBaseUrl = (baseUrl: string, defaultPort: number): ParsedDaemonBaseUrl => {
  const parsed = new URL(baseUrl);

  if (parsed.protocol !== "http:") {
    throw new Error(`Only http:// base URLs are supported for daemon auto-start: ${baseUrl}`);
  }

  const port = Number(parsed.port) || defaultPort;
  if (!Number.isFinite(port) || port <= 0 || port > 65535) {
    throw new Error(`Invalid daemon port in base URL: ${baseUrl}`);
  }

  return {
    hostname: parsed.hostname || "localhost",
    port,
  };
};

// ---------------------------------------------------------------------------
// Local-host checks
// ---------------------------------------------------------------------------

const LOCAL_DAEMON_HOSTNAMES = new Set(["localhost", "127.0.0.1", "::1", "[::1]"]);

export const canAutoStartLocalDaemonForHost = (hostname: string): boolean =>
  LOCAL_DAEMON_HOSTNAMES.has(hostname.toLowerCase());

/**
 * Bun's compiled-binary embedded filesystem root, drive-rooted on Windows
 * (`B:\~BUN\root\...`, argv normalized to `B:/~BUN/root/...`). Anchored to a
 * drive prefix so a dev checkout that merely *contains* a `~BUN` directory
 * isn't misread as a compiled binary.
 */
const WINDOWS_BUNFS_ENTRYPOINT = /^[a-z]:\/~BUN\//i;

/**
 * Whether the process is running from the dev source (`bun run src/main.ts`)
 * rather than a compiled single-file binary. A compiled binary runs from Bun's
 * embedded filesystem, whose entrypoint is `/$bunfs/root/main.js` on Unix but
 * `B:\~BUN\root\main.js` (argv like `B:/~BUN/root/main.js`) on Windows — match
 * BOTH. Missing the Windows form made a real `executor.exe` look like a dev
 * checkout, so `service install` refused on Windows. (Found by a real EC2
 * Windows test.)
 */
export const isDevCliEntrypoint = (scriptPath: string | undefined): boolean => {
  if (!scriptPath) return false;
  const normalized = scriptPath.replaceAll("\\", "/");
  if (normalized.startsWith("/$bunfs/") || WINDOWS_BUNFS_ENTRYPOINT.test(normalized)) return false;
  return normalized.endsWith(".ts") || normalized.endsWith(".js");
};

export const isExecutorServerReachable = (
  input: ExecutorServerReachabilityInput,
): Effect.Effect<boolean> =>
  Effect.tryPromise(async () => {
    // The unauthenticated liveness probe — never forwards a credential, so a
    // misconfigured base URL can't leak the bearer token to a third-party host.
    const url = new URL("/api/health", input.baseUrl);
    const response = await fetch(url, { signal: AbortSignal.timeout(2000) });
    const body = await response.text();
    return response.ok && body.trim() === "ok";
  }).pipe(Effect.catchCause(() => Effect.succeed(false)));

// ---------------------------------------------------------------------------
// Process spec
// ---------------------------------------------------------------------------

export const buildDaemonSpawnSpec = (input: {
  readonly port: number;
  readonly hostname: string;
  readonly isDevMode: boolean;
  readonly scriptPath: string | undefined;
  readonly executablePath: string;
  readonly allowedHosts?: ReadonlyArray<string>;
}): DaemonSpawnSpec => {
  const daemonArgs = [
    "daemon",
    "run",
    "--port",
    String(input.port),
    "--hostname",
    input.hostname,
    "--foreground",
    ...(input.allowedHosts ?? []).flatMap((h) => ["--allowed-host", h]),
  ];

  if (input.isDevMode) {
    if (!input.scriptPath) {
      throw new Error("Cannot auto-start daemon in dev mode without a CLI script path");
    }
    return {
      command: "bun",
      args: ["run", input.scriptPath, ...daemonArgs],
    };
  }

  return {
    command: input.executablePath,
    args: daemonArgs,
  };
};

// ---------------------------------------------------------------------------
// Spawn + wait
// ---------------------------------------------------------------------------

export const spawnDetached = (input: {
  readonly command: string;
  readonly args: ReadonlyArray<string>;
  readonly env: Record<string, string | undefined>;
}): Effect.Effect<SpawnedDetachedProcess, Error> =>
  Effect.try({
    try: () => {
      const child = spawn(input.command, [...input.args], {
        detached: true,
        stdio: "ignore",
        env: input.env,
      });
      child.unref();
      if (typeof child.pid !== "number") {
        child.kill();
        throw new Error("Failed to spawn daemon process: child pid was not assigned");
      }
      return { pid: child.pid };
    },
    catch: (cause) =>
      cause instanceof Error
        ? cause
        : new Error(`Failed to spawn daemon process: ${String(cause)}`),
  });

const signalPid = (pid: number): Effect.Effect<void, Error> =>
  Effect.try({
    try: () => {
      process.kill(pid, "SIGTERM");
    },
    catch: (cause) =>
      cause instanceof Error
        ? cause
        : new Error(`Failed to terminate daemon process ${pid}: ${String(cause)}`),
  });

export const terminateSpawnedDetachedProcess = (
  child: SpawnedDetachedProcess,
): Effect.Effect<void, Error> =>
  process.platform === "win32"
    ? signalPid(child.pid)
    : signalPid(-child.pid).pipe(Effect.catch(() => signalPid(child.pid)));

const waitForCondition = <E, R>(input: {
  readonly check: Effect.Effect<boolean, E, R>;
  readonly expected: boolean;
  readonly timeoutMs: number;
  readonly intervalMs: number;
}): Effect.Effect<boolean, E, R> =>
  Effect.gen(function* () {
    const startedAt = yield* Clock.currentTimeMillis;
    while (true) {
      const reachable = yield* input.check;
      if (reachable === input.expected) return true;

      const now = yield* Clock.currentTimeMillis;
      if (now - startedAt >= input.timeoutMs) return false;

      yield* Effect.sleep(input.intervalMs);
    }
  });

export const waitForReachable = <E, R>(input: {
  readonly check: Effect.Effect<boolean, E, R>;
  readonly timeoutMs: number;
  readonly intervalMs: number;
}): Effect.Effect<boolean, E, R> =>
  waitForCondition({
    check: input.check,
    expected: true,
    timeoutMs: input.timeoutMs,
    intervalMs: input.intervalMs,
  });

export const waitForUnreachable = <E, R>(input: {
  readonly check: Effect.Effect<boolean, E, R>;
  readonly timeoutMs: number;
  readonly intervalMs: number;
}): Effect.Effect<boolean, E, R> =>
  waitForCondition({
    check: input.check,
    expected: false,
    timeoutMs: input.timeoutMs,
    intervalMs: input.intervalMs,
  });

const toProbeHost = (hostname: string): string => {
  const normalized = hostname.trim().toLowerCase();
  if (normalized === "localhost" || normalized === "0.0.0.0") {
    return "127.0.0.1";
  }
  return hostname;
};

const isPortAvailable = (input: {
  hostname: string;
  port: number;
}): Effect.Effect<boolean, Error> =>
  Effect.tryPromise({
    try: () =>
      new Promise<boolean>((resolve) => {
        const server = createServer() as ProbeServer;
        const cleanup = () => {
          if (typeof server.removeAllListeners === "function") {
            server.removeAllListeners();
          }
        };

        server.once("error", () => {
          cleanup();
          resolve(false);
        });

        server.once("listening", () => {
          cleanup();
          server.close(() => resolve(true));
        });

        server.listen({ port: input.port, host: toProbeHost(input.hostname) });
      }),
    catch: (cause) =>
      cause instanceof Error
        ? cause
        : new Error(`Failed probing port availability: ${String(cause)}`),
  });

const pickEphemeralPort = (hostname: string): Effect.Effect<number, Error> =>
  Effect.tryPromise({
    try: () =>
      new Promise<number>((resolve, reject) => {
        const server = createServer() as ProbeServer;

        server.once("error", (error: unknown) => {
          reject(error);
        });

        server.once("listening", () => {
          const address = server.address();
          const port = typeof address === "object" && address !== null ? address.port : 0;
          server.close(() => resolve(port));
        });

        server.listen({ port: 0, host: toProbeHost(hostname) });
      }),
    catch: (cause) =>
      cause instanceof Error
        ? cause
        : new Error(`Failed selecting ephemeral port: ${String(cause)}`),
  });

export const chooseDaemonPort = (input: {
  preferredPort: number;
  hostname: string;
}): Effect.Effect<number, Error> =>
  Effect.gen(function* () {
    const preferredAvailable = yield* isPortAvailable({
      hostname: input.hostname,
      port: input.preferredPort,
    });
    if (preferredAvailable) return input.preferredPort;

    const fallbackPort = yield* pickEphemeralPort(input.hostname);
    if (!Number.isFinite(fallbackPort) || fallbackPort <= 0 || fallbackPort > 65535) {
      return yield* Effect.fail(
        new Error(`Could not find an available daemon port for host ${input.hostname}`),
      );
    }
    return fallbackPort;
  });

// ---------------------------------------------------------------------------
// Service-install planning (pure)
// ---------------------------------------------------------------------------

export type ServiceInstallPlan = "noop" | "reinstall" | "takeover-then-install";

export const planServiceInstall = (input: {
  readonly registered: boolean;
  readonly running: boolean;
  readonly activeKind: "cli-daemon" | "desktop-sidecar" | "foreground" | null;
  readonly activePid?: number | null;
  readonly servicePid?: number | null;
  readonly activeVersion: string | null;
  readonly activeExecutablePath?: string | null;
  readonly activePort: number | null;
  readonly requestedPort: number;
  readonly currentVersion: string;
  readonly currentExecutablePath?: string | null;
}): ServiceInstallPlan => {
  if (input.activeKind !== null && input.activeKind !== "cli-daemon") {
    return "takeover-then-install";
  }

  if (input.registered && input.running) {
    if (
      input.activeKind === "cli-daemon" &&
      input.activePid !== undefined &&
      input.activePid !== null &&
      input.servicePid !== undefined &&
      input.servicePid !== null &&
      input.activePid !== input.servicePid
    ) {
      return "takeover-then-install";
    }

    const executableMatches =
      !input.activeExecutablePath ||
      !input.currentExecutablePath ||
      input.activeExecutablePath === input.currentExecutablePath;
    return input.activeVersion === input.currentVersion &&
      input.activePort === input.requestedPort &&
      executableMatches
      ? "noop"
      : "reinstall";
  }

  return "takeover-then-install";
};
