import { existsSync, readdirSync } from "node:fs";
import { createServer, request as httpRequest } from "node:http";
import { createServer as createNetServer } from "node:net";
import { resolve, relative, join } from "node:path";
import { createMockServer, type MockServer } from "../../../testing/mock-server";
import { type CommandResult, silentFailure } from "../../lib/command-result";

interface MockEntry {
  provider: string;
  scenario: string;
  mockPath: string;
}

interface RunningMock {
  server: MockServer;
  route: string;
  provider: string;
  scenario: string;
}

function readdirSafe(dir: string): string[] {
  try {
    return readdirSync(dir, { withFileTypes: true })
      .filter((d) => d.isDirectory())
      .map((d) => d.name);
  } catch {
    return [];
  }
}

export function discoverMocks(mocksDir: string, filters: string[]): MockEntry[] {
  const mocks: MockEntry[] = [];

  for (const provider of readdirSafe(mocksDir)) {
    const providerDir = join(mocksDir, provider);
    for (const scenario of readdirSafe(providerDir)) {
      const mockPath = join(providerDir, scenario, "mock.json");
      if (!existsSync(mockPath)) continue;
      mocks.push({ provider, scenario, mockPath });
    }
  }

  if (filters.length === 0) return mocks;

  return mocks.filter(({ provider, scenario }) =>
    filters.some((f) => f === provider || f === `${provider}/${scenario}`),
  );
}

// Mockoon doesn't expose the actual listening port, so we can't use port 0 with readback.
// This has a minor TOCTOU window between closing this server and Mockoon binding.
function findFreePort(): Promise<number> {
  return new Promise((resolve, reject) => {
    const srv = createNetServer();
    srv.listen(0, "127.0.0.1", () => {
      const addr = srv.address();
      const port = typeof addr === "object" && addr ? addr.port : 0;
      srv.close(() => resolve(port));
    });
    srv.on("error", reject);
  });
}

async function startMock(mock: MockEntry): Promise<RunningMock> {
  const port = await findFreePort();
  const server = await createMockServer(mock.mockPath, port);
  const route = `/${mock.provider}/${mock.scenario}`;
  return { server, route, provider: mock.provider, scenario: mock.scenario };
}

export type RouteResolution =
  | { kind: "malformed" }
  | { kind: "unknownRoute"; key: string }
  | { kind: "matched"; key: string; downstream: string };

// Resolve `/{provider}/{scenario}/rest?query` to a known mock route, stripping
// the `/{provider}/{scenario}` prefix and keeping the rest (including query).
export function resolveRoute(url: string | undefined, knownRoutes: string[]): RouteResolution {
  const match = (url ?? "").match(/^\/([^/?]+)\/([^/?]+)(\/[^?]*)?(\?.*)?$/);
  if (!match) return { kind: "malformed" };

  const [, provider, scenario] = match;
  const key = `/${provider}/${scenario}`;
  if (!knownRoutes.includes(key)) return { kind: "unknownRoute", key };

  const downstream = (url ?? "/").slice(key.length) || "/";
  return { kind: "matched", key, downstream };
}

function createProxy(routeTable: Map<string, RunningMock>) {
  return createServer((req, res) => {
    const resolution = resolveRoute(req.url, [...routeTable.keys()]);

    if (resolution.kind === "malformed") {
      res.writeHead(404, { "Content-Type": "application/json" });
      res.end(JSON.stringify({ error: "Not found. Use /{provider}/{scenario}/..." }));
      return;
    }

    if (resolution.kind === "unknownRoute") {
      res.writeHead(404, { "Content-Type": "application/json" });
      res.end(
        JSON.stringify({
          error: `Unknown route: ${resolution.key}`,
          available: [...routeTable.keys()],
        }),
      );
      return;
    }

    const target = routeTable.get(resolution.key);
    // resolveRoute only returns "matched" for keys present in routeTable
    if (!target) return;

    const proxyReq = httpRequest(
      {
        hostname: "127.0.0.1",
        port: target.server.port,
        path: resolution.downstream,
        method: req.method,
        headers: { ...req.headers, host: `127.0.0.1:${target.server.port}` },
      },
      (proxyRes) => {
        res.writeHead(proxyRes.statusCode ?? 502, proxyRes.headers);
        proxyRes.pipe(res);
      },
    );

    proxyReq.on("error", (err) => {
      if (!res.headersSent) {
        res.writeHead(502, { "Content-Type": "application/json" });
      }
      res.end(JSON.stringify({ error: "Bad gateway", detail: err.message }));
    });

    req.pipe(proxyReq);
  });
}

export async function runMockStart(
  mocksRoot: string,
  filters: string[],
  port: number,
): Promise<CommandResult> {
  const mocksDir = resolve(mocksRoot);
  const mocks = discoverMocks(mocksDir, filters);

  if (mocks.length === 0) {
    console.error("No matching mocks found.");
    if (filters.length > 0) {
      console.error(`Filters: ${filters.join(", ")}`);
      console.error(`Available mocks are under: ${relative(process.cwd(), mocksDir)}/`);
    }
    return silentFailure();
  }

  console.log(`Starting ${mocks.length} mock(s)...\n`);

  const running: RunningMock[] = [];
  for (const mock of mocks) {
    const info = await startMock(mock);
    running.push(info);
  }

  const routeTable = new Map<string, RunningMock>();
  for (const r of running) {
    routeTable.set(r.route, r);
  }

  console.log("Route table:");
  console.log("\u2500".repeat(50));
  for (const [route, { server }] of routeTable) {
    console.log(`  ${route}  \u2192  127.0.0.1:${server.port}`);
  }
  console.log("\u2500".repeat(50));

  const proxy = createProxy(routeTable);
  proxy.listen(port, () => {
    console.log(`\nReverse proxy listening on http://localhost:${port}`);
    console.log("Press Ctrl+C to stop all mocks.\n");
  });

  function shutdown() {
    console.log("\nShutting down...");
    proxy.close();
    for (const { server } of running) {
      void server.stop();
    }
    process.exit(0);
  }

  process.on("SIGINT", shutdown);
  process.on("SIGTERM", shutdown);

  // Block until signal handler calls process.exit
  return new Promise<CommandResult>(() => void 0);
}
