import type { ConfigurationDefinition, EffectiveConfiguration } from "@awp/contracts";
import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
import { readFile } from "node:fs/promises";
import { fileURLToPath } from "node:url";
import { renderRequest } from "./app.js";
import {
  renderApprovedLiveAccounts,
  renderApprovedLiveFactory,
  renderApprovedLiveProject,
  renderApprovedLiveProjectList,
  renderApprovedLiveQueue,
  renderApprovedLiveReview,
  type LiveHierarchy,
} from "./approved-live.js";
import type { AccountListItem } from "./render-accounts.js";
import type { ProjectConfigurationViewItem } from "./render-project.js";
import {
  LoginRateLimiter,
  clearSessionCookie,
  internalSessionHeaders,
  loginPage,
  mutationIsCrossSite,
  sessionFromCookie,
  setSessionCookie,
} from "./session-gateway.js";

const port = Number.parseInt(process.env.AWP_WEB_PORT ?? "4173", 10);
const host = process.env.AWP_WEB_HOST ?? "127.0.0.1";
const fixtureMode = process.env.AWP_WEB_FIXTURE_MODE === "1";
const controlPlaneUrl = process.env.AWP_CONTROL_PLANE_URL ?? "http://127.0.0.1:8787";
const selfRepositoryUrl = process.env.AWP_SELF_REPOSITORY_URL;
const acceptanceStateFile = process.env.AWP_ACCEPTANCE_STATE_FILE;
const loginRateLimiter = new LoginRateLimiter();

class InvalidSessionError extends Error {}

async function acceptanceProgress(): Promise<{ checked: number; total: number } | undefined> {
  if (!acceptanceStateFile) return undefined;
  const source = await readFile(acceptanceStateFile, "utf8");
  const criteria = [...source.matchAll(/^- \[([ xX])\] AC-\d+:/gm)];
  return {
    checked: criteria.filter((match) => match[1]?.toLowerCase() === "x").length,
    total: criteria.length,
  };
}

async function controlFetch(
  path: string,
  token: string,
  init: RequestInit = {},
): Promise<Response> {
  const upstream = await fetch(`${controlPlaneUrl}${path}`, {
    ...init,
    headers: internalSessionHeaders(
      token,
      init.headers instanceof Headers ? (init.headers.get("content-type") ?? undefined) : undefined,
    ),
  });
  if (upstream.status === 401) throw new InvalidSessionError("Operator session is invalid");
  return upstream;
}

async function projectHierarchy(
  projectId: string,
  token: string,
): Promise<LiveHierarchy | undefined> {
  const upstream = await controlFetch(`/internal/projects/${encodeURIComponent(projectId)}`, token);
  if (upstream.status === 404) return undefined;
  if (!upstream.ok) throw new Error(`Control plane returned ${upstream.status}`);
  return (await upstream.json()) as LiveHierarchy;
}

async function projectConfiguration(
  projectId: string,
  token: string,
): Promise<readonly ProjectConfigurationViewItem[]> {
  const definitionsResponse = await controlFetch("/internal/configuration/definitions", token);
  if (definitionsResponse.status === 404) return [];
  if (!definitionsResponse.ok) {
    throw new Error(`Configuration definitions returned ${definitionsResponse.status}`);
  }
  const payload = (await definitionsResponse.json()) as {
    definitions?: readonly ConfigurationDefinition<unknown>[];
  };
  if (!Array.isArray(payload.definitions)) {
    throw new Error("Configuration definitions response is invalid");
  }
  return Promise.all(
    payload.definitions.map(async (definition) => {
      const effectiveResponse = await controlFetch(
        `/internal/configuration/effective/${encodeURIComponent(definition.key)}?projectId=${encodeURIComponent(projectId)}`,
        token,
      );
      if (!effectiveResponse.ok) {
        throw new Error(
          `Effective configuration ${definition.key} returned ${effectiveResponse.status}`,
        );
      }
      const effectivePayload = (await effectiveResponse.json()) as {
        effective?: EffectiveConfiguration<unknown> | null;
      };
      return {
        definition,
        ...(effectivePayload.effective == null ? {} : { effective: effectivePayload.effective }),
      };
    }),
  );
}

async function accountInventory(token: string): Promise<readonly AccountListItem[]> {
  const upstream = await controlFetch("/internal/providers/accounts", token);
  if (upstream.status === 404) return [];
  if (!upstream.ok) throw new Error(`Account provider returned ${upstream.status}`);
  const payload = (await upstream.json()) as { accounts?: unknown };
  if (!Array.isArray(payload.accounts)) throw new Error("Account provider response is invalid");
  return payload.accounts.map((value) => {
    if (typeof value !== "object" || value === null || Array.isArray(value)) {
      throw new Error("Account provider response entry is invalid");
    }
    const record = value as Record<string, unknown>;
    if (
      typeof record.accountKey !== "string" ||
      typeof record.label !== "string" ||
      !Array.isArray(record.capabilities) ||
      record.capabilities.some((item) => typeof item !== "string")
    ) {
      throw new Error("Account provider response entry has an invalid shape");
    }
    return {
      accountKey: record.accountKey,
      label: record.label,
      capabilities: record.capabilities as string[],
    };
  });
}

async function hierarchyForEntity(
  kind: "factory-run" | "change-set",
  entityId: string,
  token: string,
  projectHint?: string,
): Promise<LiveHierarchy | undefined> {
  if (projectHint) {
    const hinted = await projectHierarchy(projectHint, token);
    if (
      hinted &&
      (kind === "factory-run"
        ? hinted.factoryRuns.some((run) => run.id === entityId)
        : hinted.changeSets.some((changeSet) => changeSet.id === entityId))
    ) {
      return hinted;
    }
  }
  const upstream = await controlFetch("/internal/projects", token);
  if (!upstream.ok) throw new Error(`Control plane returned ${upstream.status}`);
  const payload = (await upstream.json()) as { projects: readonly { id: string }[] };
  for (const project of payload.projects) {
    const hierarchy = await projectHierarchy(project.id, token);
    if (!hierarchy) continue;
    const found =
      kind === "factory-run"
        ? hierarchy.factoryRuns.some((run) => run.id === entityId)
        : hierarchy.changeSets.some((changeSet) => changeSet.id === entityId);
    if (found) return hierarchy;
  }
  return undefined;
}

async function requestBody(request: IncomingMessage, limit = 65_536): Promise<string> {
  const chunks: Buffer[] = [];
  let size = 0;
  for await (const chunk of request) {
    const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
    size += buffer.length;
    if (size > limit) throw new Error("Request body is too large");
    chunks.push(buffer);
  }
  return Buffer.concat(chunks).toString("utf8");
}

function crossSite(request: IncomingMessage): boolean {
  return mutationIsCrossSite({
    ...(request.method === undefined ? {} : { method: request.method }),
    ...(request.headers.origin === undefined ? {} : { origin: request.headers.origin }),
    ...(request.headers["sec-fetch-site"] === undefined
      ? {}
      : { secFetchSite: request.headers["sec-fetch-site"] }),
    ...(request.headers.host === undefined ? {} : { host: request.headers.host }),
  });
}

function redirectToLogin(response: ServerResponse, clear = false): void {
  response.writeHead(303, {
    location: "/login",
    "cache-control": "no-store",
    ...(clear ? { "set-cookie": clearSessionCookie() } : {}),
  });
  response.end();
}

function asset(name: string): string {
  return fileURLToPath(
    new URL(name.endsWith(".js") ? `./${name}` : `../src/${name}`, import.meta.url),
  );
}

function contentType(name: string): string {
  return name.endsWith(".css") ? "text/css; charset=utf-8" : "text/javascript; charset=utf-8";
}

createServer(async (request, response) => {
  try {
    const url = new URL(request.url ?? "/", `http://${request.headers.host ?? `${host}:${port}`}`);
    if (
      ["/u1.css", "/lifecycle.css", "/common.css", "/client.js", "/interaction.js"].includes(
        url.pathname,
      )
    ) {
      const name = url.pathname.slice(1);
      response.writeHead(200, { "content-type": contentType(name), "cache-control": "no-store" });
      response.end(await readFile(asset(name)));
      return;
    }

    if (!fixtureMode && url.pathname === "/login" && request.method === "GET") {
      response.writeHead(200, {
        "content-type": "text/html; charset=utf-8",
        "cache-control": "no-store",
      });
      response.end(loginPage());
      return;
    }

    if (!fixtureMode && url.pathname === "/auth/login" && request.method === "POST") {
      if (crossSite(request)) {
        response.writeHead(403, { "content-type": "text/plain; charset=utf-8" });
        response.end("Cross-site mutation rejected");
        return;
      }
      const remote = request.socket.remoteAddress ?? "unknown";
      if (!loginRateLimiter.allow(remote)) {
        response.writeHead(429, {
          "content-type": "text/html; charset=utf-8",
          "cache-control": "no-store",
        });
        response.end(loginPage("Too many sign-in attempts. Try again shortly."));
        return;
      }
      const form = new URLSearchParams(await requestBody(request));
      const password = form.get("password") ?? "";
      const upstream = await fetch(`${controlPlaneUrl}/internal/auth/sessions`, {
        method: "POST",
        headers: { "content-type": "application/json" },
        body: JSON.stringify({ password, userAgent: request.headers["user-agent"] ?? "" }),
      });
      if (upstream.status === 401) {
        response.writeHead(401, {
          "content-type": "text/html; charset=utf-8",
          "cache-control": "no-store",
        });
        response.end(loginPage("Invalid password."));
        return;
      }
      if (!upstream.ok) throw new Error(`Authentication service returned ${upstream.status}`);
      const payload = (await upstream.json()) as { token?: unknown };
      if (typeof payload.token !== "string" || !payload.token) {
        throw new Error("Authentication service returned an invalid session token");
      }
      loginRateLimiter.reset(remote);
      response.writeHead(303, {
        location: "/",
        "set-cookie": setSessionCookie(payload.token),
        "cache-control": "no-store",
      });
      response.end();
      return;
    }

    if (!fixtureMode && url.pathname.startsWith("/api/internal/execution/")) {
      if (request.method !== "POST") {
        response.writeHead(405, {
          "content-type": "application/json",
          allow: "POST",
          "cache-control": "no-store",
        });
        response.end(JSON.stringify({ error: "method-not-allowed" }));
        return;
      }
      const allowed = new Set([
        "/api/internal/execution/complete",
        "/api/internal/execution/fail",
        "/api/internal/execution/review",
      ]);
      if (!allowed.has(url.pathname)) {
        response.writeHead(404, {
          "content-type": "application/json",
          "cache-control": "no-store",
        });
        response.end(JSON.stringify({ error: "not-found" }));
        return;
      }
      const upstream = await fetch(`${controlPlaneUrl}${url.pathname.slice(4)}`, {
        method: "POST",
        headers: { "content-type": request.headers["content-type"] ?? "application/json" },
        body: await requestBody(request, 8 * 1024 * 1024),
      });
      response.writeHead(upstream.status, {
        "content-type": upstream.headers.get("content-type") ?? "application/json",
        "cache-control": "no-store",
      });
      response.end(Buffer.from(await upstream.arrayBuffer()));
      return;
    }

    const token = fixtureMode ? undefined : sessionFromCookie(request.headers.cookie);

    if (!fixtureMode && url.pathname === "/auth/logout" && request.method === "POST") {
      if (crossSite(request)) {
        response.writeHead(403, { "content-type": "text/plain; charset=utf-8" });
        response.end("Cross-site mutation rejected");
        return;
      }
      if (token) {
        await fetch(`${controlPlaneUrl}/internal/auth/session/revoke`, {
          method: "POST",
          headers: internalSessionHeaders(token, "application/json"),
          body: "{}",
        });
      }
      redirectToLogin(response, true);
      return;
    }

    if (!fixtureMode && url.pathname.startsWith("/api/internal/")) {
      if (!token) {
        response.writeHead(401, {
          "content-type": "application/json",
          "cache-control": "no-store",
        });
        response.end(JSON.stringify({ error: "unauthorized" }));
        return;
      }
      if (crossSite(request)) {
        response.writeHead(403, {
          "content-type": "application/json",
          "cache-control": "no-store",
        });
        response.end(JSON.stringify({ error: "cross-site-mutation" }));
        return;
      }
      const method = request.method ?? "GET";
      const init: RequestInit = {
        method,
        headers: internalSessionHeaders(
          token,
          request.headers["content-type"] ?? "application/json",
        ),
      };
      if (method !== "GET" && method !== "HEAD") init.body = await requestBody(request);
      const upstream = await fetch(`${controlPlaneUrl}${url.pathname.slice(4)}${url.search}`, init);
      response.writeHead(upstream.status, {
        "content-type": upstream.headers.get("content-type") ?? "application/json",
        "cache-control": "no-store",
        ...(upstream.status === 401 ? { "set-cookie": clearSessionCookie() } : {}),
      });
      response.end(Buffer.from(await upstream.arrayBuffer()));
      return;
    }

    let html: string;
    if (fixtureMode) {
      html = renderRequest(url, { fixtureMode: true });
    } else {
      if (!token) {
        redirectToLogin(response);
        return;
      }
      if (url.pathname === "/" || url.pathname === "/projects") {
        const upstream = await controlFetch("/internal/projects", token);
        if (!upstream.ok) throw new Error(`Control plane returned ${upstream.status}`);
        const payload = (await upstream.json()) as {
          projects: Parameters<typeof renderApprovedLiveProjectList>[0];
        };
        if (url.pathname === "/" && payload.projects.length === 1) {
          response.writeHead(302, {
            location: `/projects/${encodeURIComponent(payload.projects[0]!.id)}`,
          });
          response.end();
          return;
        }
        html = renderApprovedLiveProjectList(payload.projects);
      } else {
        const queueMatch = new RegExp("^/projects/([^/]+)/queue$").exec(url.pathname);
        const projectMatch = new RegExp("^/projects/([^/]+)$").exec(url.pathname);
        const accountsMatch = url.pathname === "/accounts";
        const factoryMatch = new RegExp("^/factory-runs/([^/]+)$").exec(url.pathname);
        const changeSetMatch = new RegExp("^/changesets/([^/]+)$").exec(url.pathname);
        if (accountsMatch) {
          let projectId = url.searchParams.get("project") ?? undefined;
          if (!projectId) {
            const projectsResponse = await controlFetch("/internal/projects", token);
            if (!projectsResponse.ok) {
              throw new Error(`Control plane returned ${projectsResponse.status}`);
            }
            const payload = (await projectsResponse.json()) as {
              projects: readonly { id: string }[];
            };
            if (payload.projects.length === 1) projectId = payload.projects[0]!.id;
          }
          if (!projectId) {
            response.writeHead(400, { "content-type": "text/plain; charset=utf-8" });
            response.end("Select a Project before managing accounts");
            return;
          }
          const hierarchy = await projectHierarchy(projectId, token);
          if (!hierarchy) {
            response.writeHead(404, { "content-type": "text/plain; charset=utf-8" });
            response.end("Project not found");
            return;
          }
          html = renderApprovedLiveAccounts(hierarchy, await accountInventory(token));
        } else if (queueMatch) {
          const projectId = decodeURIComponent(queueMatch[1]!);
          const hierarchy = await projectHierarchy(projectId, token);
          if (!hierarchy) {
            response.writeHead(404, { "content-type": "text/plain; charset=utf-8" });
            response.end("Project not found");
            return;
          }
          html = renderApprovedLiveQueue(hierarchy, url);
        } else if (projectMatch) {
          const projectId = decodeURIComponent(projectMatch[1]!);
          const hierarchy = await projectHierarchy(projectId, token);
          if (!hierarchy) {
            response.writeHead(404, { "content-type": "text/plain; charset=utf-8" });
            response.end("Project not found");
            return;
          }
          const acceptance =
            selfRepositoryUrl !== undefined && hierarchy.project.repositoryUrl === selfRepositoryUrl
              ? await acceptanceProgress()
              : undefined;
          const tab = url.searchParams.get("tab") ?? "overview";
          html = renderApprovedLiveProject(
            acceptance ? { ...hierarchy, acceptance } : hierarchy,
            tab,
            tab === "plans" ? await accountInventory(token) : [],
            tab === "settings" ? await projectConfiguration(projectId, token) : [],
          );
        } else if (factoryMatch) {
          const factoryRunId = decodeURIComponent(factoryMatch[1]!);
          const hierarchy = await hierarchyForEntity(
            "factory-run",
            factoryRunId,
            token,
            url.searchParams.get("project") ?? undefined,
          );
          if (!hierarchy) {
            response.writeHead(404, { "content-type": "text/plain; charset=utf-8" });
            response.end("FactoryRun not found");
            return;
          }
          html = renderApprovedLiveFactory(hierarchy, factoryRunId, url);
        } else if (changeSetMatch) {
          const changeSetId = decodeURIComponent(changeSetMatch[1]!);
          const hierarchy = await hierarchyForEntity(
            "change-set",
            changeSetId,
            token,
            url.searchParams.get("project") ?? undefined,
          );
          if (!hierarchy) {
            response.writeHead(404, { "content-type": "text/plain; charset=utf-8" });
            response.end("ChangeSet not found");
            return;
          }
          html = renderApprovedLiveReview(hierarchy, changeSetId, url);
        } else {
          response.writeHead(404, { "content-type": "text/plain; charset=utf-8" });
          response.end("AWP page not found");
          return;
        }
      }
    }
    response.writeHead(200, {
      "content-type": "text/html; charset=utf-8",
      "cache-control": "no-store",
    });
    response.end(html);
  } catch (error) {
    if (error instanceof InvalidSessionError) {
      redirectToLogin(response, true);
      return;
    }
    response.writeHead(500, { "content-type": "text/plain; charset=utf-8" });
    response.end(error instanceof Error ? error.message : "AWP web server error");
  }
}).listen(port, host, () => {
  console.log(`AWP web listening on http://${host}:${port}${fixtureMode ? " (fixture mode)" : ""}`);
});
