import http from "node:http";

import { handleAdmin } from "./admin.mjs";
import {
  json,
  normalizeHeaders,
  readBody,
  requestOrigin,
  setCors,
  tryParseJson,
} from "./http.mjs";
import { handleAuth } from "./routes/auth.mjs";
import { handleAudio } from "./routes/audio.mjs";
import { handleConversations } from "./routes/conversations.mjs";
import { handleCron } from "./routes/cron.mjs";
import { handleIntegrations } from "./routes/integrations.mjs";
import { handleInvites } from "./routes/invites.mjs";
import { handleLlmCompletions } from "./routes/llm.mjs";
import { handleOAuth } from "./routes/oauth.mjs";
import { handlePayments } from "./routes/payments.mjs";
import { handleUser } from "./routes/user.mjs";
import { handleVersion } from "./routes/version.mjs";
import { handleWebhooks } from "./routes/webhooks.mjs";
import { handleSocketRequest, handleWebSocketUpgrade } from "./socket.mjs";
import {
  appendRequest,
  behavior,
  DEFAULT_PORT,
  hashString,
  MAX_PORT_RETRY_ATTEMPTS,
  openSockets,
  parseBehaviorJson,
  sleep,
} from "./state.mjs";

let server = null;

// Order matters: admin & socket.io short-circuit early; the rest fall through
// in domain order so the cheapest predicates run first.
const ROUTE_HANDLERS = [
  handleOAuth,
  handleAuth,
  handleUser,
  handleInvites,
  handlePayments,
  handleAudio,
  // LLM completions must run before the catch-all stub in
  // `handleIntegrations` so keyword-driven test scripts can override
  // the default "Hello from e2e mock agent" reply.
  handleLlmCompletions,
  handleIntegrations,
  handleWebhooks,
  handleCron,
  handleConversations,
  handleVersion,
];

async function handleRequest(req, res) {
  const method = req.method ?? "GET";
  const url = req.url ?? "/";
  const body = await readBody(req);
  const parsedBody = tryParseJson(body);
  const origin = requestOrigin(req);

  appendRequest({
    method,
    url,
    body,
    headers: normalizeHeaders(req.headers),
    timestamp: Date.now(),
  });

  if (method === "OPTIONS") {
    setCors(res);
    res.writeHead(204);
    res.end();
    return;
  }

  const ctx = {
    method,
    url,
    body,
    parsedBody,
    origin,
    req,
    res,
    getPort: getMockServerPort,
  };

  if (handleAdmin(ctx)) return;

  const maybeShortCircuit = await maybeApplyGlobalBehavior(ctx);
  if (maybeShortCircuit) return;

  if (url.startsWith("/socket.io/")) {
    handleSocketRequest(ctx);
    return;
  }

  for (const handler of ROUTE_HANDLERS) {
    if (await handler(ctx)) return;
  }

  // Catch-all: fail fast so tests notice missing mock endpoints.
  console.log(`[MockServer] UNHANDLED ${method} ${url}`);
  json(res, 404, {
    success: false,
    error: `Mock server: no handler for ${method} ${url}`,
  });
}

function requestHash(ctx) {
  return hashString(`${ctx.method}:${ctx.url}:${ctx.body || ""}`);
}

function ruleMatches(rule, ctx) {
  if (!rule || typeof rule !== "object") return false;
  if (rule.method && String(rule.method).toUpperCase() !== ctx.method)
    return false;
  if (typeof rule.path === "string" && rule.path !== ctx.url) return false;
  if (typeof rule.pathRegex === "string") {
    try {
      const regex = new RegExp(rule.pathRegex);
      if (!regex.test(ctx.url)) return false;
    } catch {
      return false;
    }
  }
  if (typeof rule.contains === "string" && !ctx.url.includes(rule.contains)) {
    return false;
  }
  return true;
}

async function maybeApplyGlobalBehavior(ctx) {
  const mockBehavior = behavior();
  const baseDelay = Number(mockBehavior.globalDelayMs || 0);
  const jitterMax = Number(mockBehavior.globalJitterMs || 0);
  const jitter =
    Number.isFinite(jitterMax) && jitterMax > 0
      ? requestHash(ctx) % Math.min(jitterMax, 5000)
      : 0;
  const totalDelay = Math.max(0, baseDelay) + jitter;
  if (totalDelay > 0) {
    await sleep(Math.min(totalDelay, 30_000));
  }

  const rules = parseBehaviorJson("httpFaultRules", []);
  if (!Array.isArray(rules)) return false;

  for (const rule of rules) {
    if (!ruleMatches(rule, ctx)) continue;
    const status = Number(rule.status || 500);
    const body =
      rule.body && typeof rule.body === "object"
        ? rule.body
        : {
            success: false,
            error: rule.error || "Injected mock fault",
          };
    console.warn(
      `[MockServer] Injected fault ${ctx.method} ${ctx.url} -> ${status}`,
    );
    json(ctx.res, status, body);
    return true;
  }

  return false;
}

export function getMockServerPort() {
  const address = server?.address();
  return typeof address === "object" && address ? address.port : null;
}

function createServerInstance() {
  const nextServer = http.createServer((req, res) => {
    handleRequest(req, res).catch((err) => {
      console.error("[MockServer] Unhandled error:", err);
      json(res, 500, { success: false, error: "Internal mock error" });
    });
  });
  nextServer.on("connection", (socket) => {
    openSockets.add(socket);
    socket.on("close", () => openSockets.delete(socket));
  });
  nextServer.on("upgrade", (req, socket, head) =>
    handleWebSocketUpgrade(req, socket, head),
  );
  return nextServer;
}

function listen(serverInstance, port) {
  return new Promise((resolve, reject) => {
    const onError = (err) => {
      serverInstance.off("listening", onListening);
      reject(err);
    };
    const onListening = () => {
      serverInstance.off("error", onError);
      const address = serverInstance.address();
      const resolvedPort =
        typeof address === "object" && address ? address.port : port;
      resolve(resolvedPort);
    };
    serverInstance.once("error", onError);
    serverInstance.once("listening", onListening);
    serverInstance.listen(port, "127.0.0.1");
  });
}

export async function startMockServer(port = DEFAULT_PORT, options = {}) {
  if (server) {
    return { port: getMockServerPort() ?? port, alreadyRunning: true };
  }

  const preferredPort =
    Number.isInteger(port) && port > 0 ? port : DEFAULT_PORT;
  const retryIfInUse = options.retryIfInUse === true;
  const candidatePorts = retryIfInUse
    ? [
        preferredPort,
        ...Array.from(
          { length: MAX_PORT_RETRY_ATTEMPTS },
          (_, i) => preferredPort + i + 1,
        ),
        0,
      ]
    : [preferredPort];

  let lastError = null;
  for (const candidatePort of candidatePorts) {
    const nextServer = createServerInstance();
    try {
      const resolvedPort = await listen(nextServer, candidatePort);
      server = nextServer;
      const retryNote =
        resolvedPort === preferredPort
          ? ""
          : ` (preferred ${preferredPort} unavailable)`;
      console.log(
        `[MockServer] Listening on http://127.0.0.1:${resolvedPort}${retryNote}`,
      );
      return {
        port: resolvedPort,
        alreadyRunning: false,
        requestedPort: preferredPort,
        retried: resolvedPort !== preferredPort,
      };
    } catch (err) {
      try {
        nextServer.close();
      } catch {
        // The failed candidate may never have reached the listening state.
      }
      lastError = err;
      if (!retryIfInUse || err?.code !== "EADDRINUSE") {
        throw err;
      }
      console.warn(
        `[MockServer] Port ${candidatePort} unavailable; trying another local port`,
      );
    }
  }

  throw lastError ?? new Error("Mock server failed to start");
}

export function stopMockServer() {
  return new Promise((resolve) => {
    if (!server) {
      resolve();
      return;
    }
    for (const socket of openSockets) {
      socket.destroy();
    }
    openSockets.clear();
    server.close(() => {
      console.log("[MockServer] Stopped");
      server = null;
      resolve();
    });
  });
}
