import { spawn, type ChildProcess } from "node:child_process";
import { readFileSync } from "node:fs";
import { access } from "node:fs/promises";
import net from "node:net";
import path from "node:path";
import process from "node:process";

interface ManagedProcess {
  readonly name: string;
  readonly child: ChildProcess;
}

const ROOT_DIR = process.cwd();
const AI_ENGINE_DIR = path.join(ROOT_DIR, "ai-engine");
loadEnvFile(path.join(ROOT_DIR, ".env"));
const DEFAULT_AI_URL = process.env.AI_ENGINE_URL ?? "http://127.0.0.1:8000";
const DEFAULT_API_PORT = Number(process.env.PORT ?? "3005");
const AI_ENGINE_PORT = resolveAiPort(DEFAULT_AI_URL);
const AI_START_TIMEOUT_MS = 120_000;
const NEST_START_TIMEOUT_MS = 90_000;
const HEALTH_POLL_INTERVAL_MS = 1_000;

let nestProcess: ManagedProcess | null = null;
let aiProcess: ManagedProcess | null = null;
let shuttingDown = false;

async function main(): Promise<void> {
  ensureWindowsOrUnixShellAwareness();

  const aiHealthUrl = `${DEFAULT_AI_URL}/health`;
  const aiHost = resolveHost(DEFAULT_AI_URL);
  const aiAlreadyHealthy = await isHealthy(aiHealthUrl);

  if (aiAlreadyHealthy) {
    log(`AI engine already running at ${aiHealthUrl}`);
  } else {
    const aiPortBusy = await isPortInUse(aiHost, AI_ENGINE_PORT);
    if (aiPortBusy) {
      throw new Error(
        `AI engine port ${AI_ENGINE_PORT} is already in use but ${aiHealthUrl} is not healthy`,
      );
    }

    const pythonCommand = await resolvePythonCommand();
    aiProcess = {
      name: "ai-engine",
      child: spawn(
        pythonCommand.command,
        [
          ...pythonCommand.args,
          "-m",
          "uvicorn",
          "main:app",
          "--host",
          "0.0.0.0",
          "--port",
          String(AI_ENGINE_PORT),
          ...resolveAiExtraArgs(),
        ],
        {
          cwd: AI_ENGINE_DIR,
          stdio: "inherit",
          env: {
            ...process.env,
            PYTHONUNBUFFERED: "1",
            PORT: String(AI_ENGINE_PORT),
          },
        },
      ),
    };

    attachExitHandlers(aiProcess);

    log(`Waiting for AI engine health at ${aiHealthUrl}`);
    await waitForHealth(aiHealthUrl, AI_START_TIMEOUT_MS);
    log("AI engine is ready");
  }

  const nestHealthUrl = `http://127.0.0.1:${DEFAULT_API_PORT}/api/health/live`;
  const nestAlreadyHealthy = await isHealthy(nestHealthUrl);

  if (nestAlreadyHealthy) {
    log(`NestJS already running at ${nestHealthUrl}`);
  } else {
    const nestPortBusy = await isPortInUse("127.0.0.1", DEFAULT_API_PORT);
    if (nestPortBusy) {
      throw new Error(
        `NestJS port ${DEFAULT_API_PORT} is already in use but ${nestHealthUrl} is not healthy`,
      );
    }

    nestProcess = {
      name: "nest",
      child: spawnNestProcess(),
    };

    attachExitHandlers(nestProcess);

    log(`Waiting for NestJS health at ${nestHealthUrl}`);
    await waitForHealth(nestHealthUrl, NEST_START_TIMEOUT_MS);
    log("NestJS is ready");
  }

  log("Full stack is running");
}

function ensureWindowsOrUnixShellAwareness(): void {
  process.on("SIGINT", () => {
    void shutdown("SIGINT");
  });

  process.on("SIGTERM", () => {
    void shutdown("SIGTERM");
  });

  process.on("uncaughtException", (error: Error) => {
    console.error("[full:run] Uncaught exception:", error);
    void shutdown("uncaughtException", 1);
  });

  process.on("unhandledRejection", (reason: unknown) => {
    console.error("[full:run] Unhandled rejection:", reason);
    void shutdown("unhandledRejection", 1);
  });
}

function resolveNestStartScript(): string {
  return process.env.FULL_RUN_NEST_SCRIPT ?? "start:dev";
}

function resolveAiExtraArgs(): string[] {
  return process.env.FULL_RUN_AI_RELOAD === "true" ? ["--reload"] : [];
}

function spawnNestProcess(): ChildProcess {
  const nestScript = resolveNestStartScript();

  if (process.platform === "win32") {
    return spawn("cmd.exe", ["/d", "/s", "/c", `npm run ${nestScript}`], {
      cwd: ROOT_DIR,
      stdio: "inherit",
      env: process.env,
    });
  }

  return spawn("npm", ["run", nestScript], {
    cwd: ROOT_DIR,
    stdio: "inherit",
    env: process.env,
  });
}

async function resolvePythonCommand(): Promise<{
  command: string;
  args: string[];
}> {
  const explicitPython = process.env.AI_ENGINE_PYTHON?.trim();
  if (explicitPython) {
    return { command: explicitPython, args: [] };
  }

  const localVenvPython =
    process.platform === "win32"
      ? path.join(AI_ENGINE_DIR, "venv", "Scripts", "python.exe")
      : path.join(AI_ENGINE_DIR, "venv", "bin", "python");

  if (await pathExists(localVenvPython)) {
    return { command: localVenvPython, args: [] };
  }

  return {
    command: process.platform === "win32" ? "python" : "python3",
    args: [],
  };
}

function resolveAiPort(aiUrl: string): number {
  const parsedUrl = new URL(aiUrl);

  if (parsedUrl.port) {
    return Number(parsedUrl.port);
  }

  return parsedUrl.protocol === "https:" ? 443 : 80;
}

async function pathExists(targetPath: string): Promise<boolean> {
  try {
    await access(targetPath);
    return true;
  } catch {
    return false;
  }
}

function resolveHost(url: string): string {
  const parsedUrl = new URL(url);
  return parsedUrl.hostname;
}

function attachExitHandlers(managedProcess: ManagedProcess): void {
  managedProcess.child.on(
    "exit",
    (code: number | null, signal: NodeJS.Signals | null) => {
      if (shuttingDown) {
        return;
      }

      const detail =
        signal !== null ? `signal=${signal}` : `code=${code ?? "unknown"}`;

      console.error(
        `[full:run] ${managedProcess.name} exited unexpectedly (${detail})`,
      );
      void shutdown(`${managedProcess.name}-exit`, code ?? 1);
    },
  );

  managedProcess.child.on("error", (error: Error) => {
    if (shuttingDown) {
      return;
    }

    console.error(`[full:run] Failed to start ${managedProcess.name}:`, error);
    void shutdown(`${managedProcess.name}-error`, 1);
  });
}

async function waitForHealth(url: string, timeoutMs: number): Promise<void> {
  const startedAt = Date.now();

  while (Date.now() - startedAt < timeoutMs) {
    try {
      const response = await fetch(url);
      if (response.ok) {
        return;
      }
    } catch {
      // Service is still booting.
    }

    await delay(HEALTH_POLL_INTERVAL_MS);
  }

  throw new Error(`Health check timed out: ${url}`);
}

async function isHealthy(url: string): Promise<boolean> {
  try {
    const response = await fetch(url);
    return response.ok;
  } catch {
    return false;
  }
}

async function isPortInUse(host: string, port: number): Promise<boolean> {
  return new Promise((resolve) => {
    const socket = net.createConnection({ host, port });

    socket.once("connect", () => {
      socket.destroy();
      resolve(true);
    });

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

function delay(ms: number): Promise<void> {
  return new Promise((resolve) => {
    setTimeout(resolve, ms);
  });
}

async function shutdown(reason: string, exitCode = 0): Promise<void> {
  if (shuttingDown) {
    return;
  }

  shuttingDown = true;
  log(`Shutting down stack (${reason})`);

  await stopProcess(nestProcess);
  await stopProcess(aiProcess);

  process.exit(exitCode);
}

async function stopProcess(
  managedProcess: ManagedProcess | null,
): Promise<void> {
  if (!managedProcess) {
    return;
  }

  const { child, name } = managedProcess;
  if (child.killed || child.exitCode !== null) {
    return;
  }

  child.kill("SIGTERM");
  const stopped = await waitForProcessExit(child, 10_000);
  if (!stopped) {
    console.warn(
      `[full:run] ${name} did not stop gracefully, forcing termination`,
    );
    child.kill("SIGKILL");
    await waitForProcessExit(child, 5_000);
  }
}

function waitForProcessExit(
  child: ChildProcess,
  timeoutMs: number,
): Promise<boolean> {
  return new Promise((resolve) => {
    const timeout = setTimeout(() => {
      cleanup();
      resolve(false);
    }, timeoutMs);

    const onExit = () => {
      cleanup();
      resolve(true);
    };

    const cleanup = () => {
      clearTimeout(timeout);
      child.off("exit", onExit);
    };

    child.once("exit", onExit);
  });
}

function log(message: string): void {
  console.log(`[full:run] ${message}`);
}

function loadEnvFile(envPath: string): void {
  try {
    const content = readFileSync(envPath, "utf8");
    const lines = content.split(/\r?\n/u);

    lines.forEach((line) => {
      const trimmed = line.trim();
      if (!trimmed || trimmed.startsWith("#")) {
        return;
      }

      const separatorIndex = trimmed.indexOf("=");
      if (separatorIndex === -1) {
        return;
      }

      const key = trimmed.slice(0, separatorIndex).trim();
      const rawValue = trimmed.slice(separatorIndex + 1).trim();
      const normalizedValue = rawValue.replace(/^['"]|['"]$/gu, "");

      if (!process.env[key]) {
        process.env[key] = normalizedValue;
      }
    });
  } catch {
    // .env is optional for this script.
  }
}

void main().catch((error: Error) => {
  console.error("[full:run] Startup failed:", error);
  void shutdown("startup-failed", 1);
});
