import { spawn } from "node:child_process";
import { randomBytes } from "node:crypto";
import { existsSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
import { createServer } from "node:net";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";

const currentDir = dirname(fileURLToPath(import.meta.url));
const workspaceRoot = resolve(currentDir, "../../..");
const webRoot = resolve(currentDir, "..");
const runtimeEnvPath = resolve(currentDir, ".playwright-runtime.json");
const webEnvLocal = resolve(webRoot, ".env.local");
const webEnvBackup = resolve(webRoot, ".env.local.e2e-backup");
const webDistDir = ".next-e2e";
const webDistDirPath = resolve(webRoot, webDistDir);
const managedEnvBanner = "# Managed by apps/web/e2e/test-server.mjs";
const e2ePort = process.env.PLAYWRIGHT_TEST_PORT ?? "3110";
const e2eBaseUrl = process.env.PLAYWRIGHT_TEST_BASE_URL ?? `http://localhost:${e2ePort}`;
const e2eAuthSecret = process.env.PLAYWRIGHT_AUTH_SECRET ?? `capakraken-e2e-${randomBytes(24).toString("hex")}`;
const manageWebEnvFile = process.env.PLAYWRIGHT_MANAGE_WEB_ENV_FILE === "true";
const composeProjectName = `capakraken-e2e-${process.pid}`;
const managedEnvKeys = [
  "DATABASE_URL",
  "REDIS_URL",
  "NEXTAUTH_URL",
  "NEXTAUTH_SECRET",
  "AUTH_SECRET",
  "E2E_TEST_MODE",
  "NODE_ENV",
  "PORT",
];
const e2eComposePrefix = "capakraken-e2e-";

function dockerComposeArgs(...args) {
  return ["compose", "-p", composeProjectName, ...args];
}

function loadEnvFile(filePath) {
  const env = {};

  try {
    const contents = readFileSync(filePath, "utf8");
    for (const rawLine of contents.split(/\r?\n/u)) {
      const line = rawLine.trim();
      if (!line || line.startsWith("#")) {
        continue;
      }

      const separatorIndex = line.indexOf("=");
      if (separatorIndex <= 0) {
        continue;
      }

      const key = line.slice(0, separatorIndex).trim();
      const rawValue = line.slice(separatorIndex + 1).trim();
      const quoted =
        (rawValue.startsWith("\"") && rawValue.endsWith("\"")) ||
        (rawValue.startsWith("'") && rawValue.endsWith("'"));

      env[key] = quoted ? rawValue.slice(1, -1) : rawValue;
    }
  } catch {
    // Keep local runs working even when no workspace .env is present.
  }

  return env;
}

function applyEnv(env) {
  for (const [key, value] of Object.entries(env)) {
    process.env[key] = value;
  }
}

function writeManagedWebEnv(rootEnv) {
  if (!manageWebEnvFile) {
    restoreWebEnv();
    return;
  }

  if (existsSync(webEnvBackup) && isManagedEnvFile(webEnvBackup)) {
    rmSync(webEnvBackup, { force: true });
  }

  if (existsSync(webEnvLocal)) {
    if (isManagedEnvFile(webEnvLocal)) {
      rmSync(webEnvLocal, { force: true });
    } else {
      if (existsSync(webEnvBackup)) {
        rmSync(webEnvBackup, { force: true });
      }
      renameSync(webEnvLocal, webEnvBackup);
    }
  }

  const contents = managedEnvKeys
    .map((key) => {
      const value = process.env[key] ?? rootEnv[key];
      return value ? `${key}=${value}` : null;
    })
    .filter(Boolean)
    .join("\n");

  writeFileSync(webEnvLocal, `${managedEnvBanner}\n${contents}\n`, "utf8");
}

function restoreWebEnv() {
  if (existsSync(webEnvLocal) && isManagedEnvFile(webEnvLocal)) {
    rmSync(webEnvLocal, { force: true });
  }

  if (existsSync(webEnvBackup)) {
    if (isManagedEnvFile(webEnvBackup)) {
      rmSync(webEnvBackup, { force: true });
    } else {
      renameSync(webEnvBackup, webEnvLocal);
    }
  }
}

let restoredManagedEnv = false;

function restoreWebEnvOnce() {
  if (restoredManagedEnv) {
    return;
  }

  restoredManagedEnv = true;
  restoreWebEnv();
  rmSync(runtimeEnvPath, { force: true });
}

function isManagedEnvFile(filePath) {
  try {
    const contents = readFileSync(filePath, "utf8");
    return contents.includes(managedEnvBanner) || contents.includes("E2E_TEST_MODE=true");
  } catch {
    return false;
  }
}

function run(command, args, cwd) {
  return new Promise((resolvePromise, rejectPromise) => {
    const child = spawn(command, args, {
      cwd,
      env: process.env,
      stdio: "inherit",
    });

    child.on("error", rejectPromise);
    child.on("exit", (code) => {
      if (code === 0) {
        resolvePromise();
        return;
      }

      rejectPromise(new Error(`${command} ${args.join(" ")} exited with code ${code ?? "null"}`));
    });
  });
}

function runQuiet(command, args, cwd) {
  return new Promise((resolvePromise, rejectPromise) => {
    const child = spawn(command, args, {
      cwd,
      env: process.env,
      stdio: "ignore",
    });

    child.on("error", rejectPromise);
    child.on("exit", (code) => {
      if (code === 0) {
        resolvePromise();
        return;
      }

      rejectPromise(new Error(`${command} ${args.join(" ")} exited with code ${code ?? "null"}`));
    });
  });
}

function runCapture(command, args, cwd) {
  return new Promise((resolvePromise, rejectPromise) => {
    let stdout = "";
    let stderr = "";
    const child = spawn(command, args, {
      cwd,
      env: process.env,
      stdio: ["ignore", "pipe", "pipe"],
    });

    child.stdout.on("data", (chunk) => {
      stdout += chunk.toString();
    });
    child.stderr.on("data", (chunk) => {
      stderr += chunk.toString();
    });

    child.on("error", rejectPromise);
    child.on("exit", (code) => {
      if (code === 0) {
        resolvePromise(stdout);
        return;
      }

      rejectPromise(
        new Error(
          `${command} ${args.join(" ")} exited with code ${code ?? "null"}${stderr ? `: ${stderr.trim()}` : ""}`,
        ),
      );
    });
  });
}

async function cleanupStaleE2eArtifacts() {
  try {
    const containerOutput = await runCapture("docker", ["ps", "-a", "--format", "{{.Names}}"], workspaceRoot);
    const staleContainers = containerOutput
      .split(/\r?\n/u)
      .map((value) => value.trim())
      .filter((name) => name.startsWith(e2eComposePrefix));

    if (staleContainers.length > 0) {
      await runQuiet("docker", ["rm", "-f", ...staleContainers], workspaceRoot);
    }
  } catch {
    // Best-effort cleanup only.
  }

  try {
    const networkOutput = await runCapture("docker", ["network", "ls", "--format", "{{.Name}}"], workspaceRoot);
    const staleNetworks = networkOutput
      .split(/\r?\n/u)
      .map((value) => value.trim())
      .filter((name) => name.startsWith(e2eComposePrefix));

    if (staleNetworks.length > 0) {
      await runQuiet("docker", ["network", "rm", ...staleNetworks], workspaceRoot);
    }
  } catch {
    // Best-effort cleanup only.
  }
}

async function ensureE2eDatabaseContainer() {
  try {
    await runQuiet("docker", dockerComposeArgs("rm", "-sf", "postgres-test"), workspaceRoot);
  } catch {
    // No previous test container to remove.
  }

  await run("docker", dockerComposeArgs("--profile", "test", "up", "-d", "--force-recreate", "postgres-test"), workspaceRoot);

  const maxAttempts = 30;
  for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
    try {
      await runQuiet(
        "docker",
        dockerComposeArgs("exec", "-T", "postgres-test", "pg_isready", "-U", "capakraken", "-d", "capakraken_test", "-q"),
        workspaceRoot,
      );
      return;
    } catch (error) {
      if (attempt === maxAttempts) {
        throw error;
      }
      await new Promise((resolvePromise) => setTimeout(resolvePromise, 1000));
    }
  }
}

function parseDatabaseName(databaseUrl) {
  const parsed = new URL(databaseUrl);
  return parsed.pathname.replace(/^\/+/u, "");
}

async function canBindPort(port) {
  return new Promise((resolvePromise) => {
    const server = createServer();

    server.once("error", () => {
      resolvePromise(false);
    });
    server.once("listening", () => {
      server.close(() => resolvePromise(true));
    });

    server.listen(port, "127.0.0.1");
  });
}

async function selectAvailablePort(preferredPort) {
  const candidates = [
    preferredPort,
    ...Array.from({ length: 50 }, (_, index) => preferredPort + index + 1),
  ];

  for (const candidate of candidates) {
    if (await canBindPort(candidate)) {
      return candidate;
    }
  }

  throw new Error(`No free host port available for postgres-test near ${preferredPort}.`);
}

function replaceDatabasePort(databaseUrl, port) {
  const parsed = new URL(databaseUrl);
  parsed.port = String(port);
  return parsed.toString();
}

let cleanedUpComposeProject = false;

async function cleanupComposeProject() {
  if (cleanedUpComposeProject) {
    return;
  }

  cleanedUpComposeProject = true;

  try {
    await runQuiet("docker", dockerComposeArgs("down", "--remove-orphans"), workspaceRoot);
  } catch {
    // Best-effort cleanup only.
  }
}

const rootEnv = loadEnvFile(resolve(workspaceRoot, ".env"));
applyEnv(rootEnv);
let playwrightDatabaseUrl = process.env.PLAYWRIGHT_DATABASE_URL ?? process.env.DATABASE_URL_TEST;

if (!playwrightDatabaseUrl) {
  throw new Error("PLAYWRIGHT_DATABASE_URL or DATABASE_URL_TEST must be configured for E2E runs.");
}

const requestedTestDbPort = Number(new URL(playwrightDatabaseUrl).port || "5434");
const selectedTestDbPort = await selectAvailablePort(requestedTestDbPort);
playwrightDatabaseUrl = replaceDatabasePort(playwrightDatabaseUrl, selectedTestDbPort);

const playwrightDatabaseName = parseDatabaseName(playwrightDatabaseUrl);

if (!/(^|_)(test|e2e|ci)$/u.test(playwrightDatabaseName)) {
  throw new Error(
    `Refusing to run E2E destructive setup against non-test database '${playwrightDatabaseName}'. Set PLAYWRIGHT_DATABASE_URL to an isolated test database.`,
  );
}

process.env.DATABASE_URL = playwrightDatabaseUrl;
process.env.PLAYWRIGHT_DATABASE_URL = playwrightDatabaseUrl;
process.env.POSTGRES_TEST_PORT = String(selectedTestDbPort);
process.env.CAPAKRAKEN_EXPECTED_DB_NAME = playwrightDatabaseName;
process.env.ALLOW_DESTRUCTIVE_DB_TOOLS = "true";
process.env.CONFIRM_DESTRUCTIVE_DB_NAME = playwrightDatabaseName;
process.env.NODE_ENV = process.env.NODE_ENV ?? "development";
process.env.PORT = e2ePort;
process.env.NEXTAUTH_URL = e2eBaseUrl;
process.env.AUTH_URL = e2eBaseUrl;
process.env.NEXTAUTH_SECRET = e2eAuthSecret;
process.env.AUTH_SECRET = e2eAuthSecret;
process.env.NEXT_DIST_DIR = webDistDir;
process.env.E2E_TEST_MODE = "true";
writeFileSync(
  runtimeEnvPath,
  JSON.stringify(
    {
      DATABASE_URL: process.env.DATABASE_URL,
      PLAYWRIGHT_DATABASE_URL: process.env.PLAYWRIGHT_DATABASE_URL,
      POSTGRES_TEST_PORT: process.env.POSTGRES_TEST_PORT,
      BASE_URL: e2eBaseUrl,
    },
    null,
    2,
  ),
  "utf8",
);
writeManagedWebEnv(rootEnv);
process.on("exit", restoreWebEnvOnce);

try {
  await cleanupStaleE2eArtifacts();
  await ensureE2eDatabaseContainer();
  await run("pnpm", ["--filter", "@capakraken/db", "db:push"], workspaceRoot);
  await run("pnpm", ["--filter", "@capakraken/db", "db:seed"], workspaceRoot);
  await run("pnpm", ["--filter", "@capakraken/db", "db:seed:holidays"], workspaceRoot);
  rmSync(webDistDirPath, { recursive: true, force: true });

  const server = spawn("pnpm", ["exec", "next", "dev", "-p", e2ePort], {
    cwd: webRoot,
    env: process.env,
    stdio: "inherit",
  });

  for (const signal of ["SIGINT", "SIGTERM"]) {
    process.on(signal, () => {
      restoreWebEnvOnce();
      void cleanupComposeProject();
      server.kill(signal);
    });
  }

  server.on("exit", async (code) => {
    restoreWebEnvOnce();
    await cleanupComposeProject();
    process.exit(code ?? 0);
  });
} catch (error) {
  restoreWebEnvOnce();
  await cleanupComposeProject();
  throw error;
}
