import { createServer as createHttpServer } from "node:http";
import { readFile as readMountedSecret } from "node:fs/promises";
import { pathToFileURL } from "node:url";
import { Firestore } from "@google-cloud/firestore";

import { loadVerifierConfig } from "./config.js";
import { createEntitlementSigner } from "./entitlement-signer.js";
import { createGoogleOidcVerifier } from "./google-oidc.js";
import { createGooglePlayClient } from "./google-play.js";
import { publicError, publicErrorResponse, readJson, sendJson } from "./http.js";
import { createPurchaseStateStore } from "./purchase-state.js";
import { createRtdnMessageStore, createRtdnRoute } from "./rtdn.js";
import { createVerificationRateLimiter, createVerifyPurchaseRoute } from "./verify-purchase.js";
import { createVoidedPurchasesClient, createVoidedPurchasesRoute } from "./voided-purchases.js";

export function createServer({ config, logger = console, resolveRoute = async () => null }) {
  return createHttpServer(async (req, res) => {
    try {
      const url = new URL(req.url ?? "/", "http://verifier.invalid");
      if (url.pathname === "/healthz") {
        if (req.method !== "GET") {
          sendJson(res, 405, { error: "method_not_allowed" });
          return;
        }
        sendJson(res, 200, { ok: true });
        return;
      }

      const route = await withTimeout(
        resolveRoute({ req, url, readJson, clientIp: req.socket?.remoteAddress }),
        config.requestTimeoutMs
      );
      if (!route) {
        sendJson(res, 404, { error: "not_found" });
        return;
      }
      if (!Number.isInteger(route.status) || route.status < 200 || route.status > 599 || !Object.hasOwn(route, "body")) {
        throw new Error("invalid_route_response");
      }
      sendJson(res, route.status, route.body);
    } catch (error) {
      const failure = publicErrorResponse(error);
      writeRequestFailure(logger, failure);
      if (!res.headersSent) sendJson(res, failure.status, { error: failure.code });
      else res.end();
    }
  });
}

function withTimeout(operation, timeoutMs) {
  let timeout;
  const expired = new Promise((_, reject) => {
    timeout = setTimeout(() => reject(publicError(504, "request_timeout")), timeoutMs);
    timeout.unref?.();
  });
  return Promise.race([operation, expired]).finally(() => clearTimeout(timeout));
}

function writeRequestFailure(logger, failure) {
  const level = failure.status >= 500 ? "error" : "warn";
  const line = JSON.stringify({
    level,
    code: "request_failed",
    status: failure.status,
    errorCode: failure.code,
  });
  try {
    const sink = typeof logger?.[level] === "function" ? logger[level] : logger?.log;
    sink?.call(logger, line);
  } catch {
    // Logging cannot expose request data or break the response path.
  }
}

export async function createProductionRoute(config, dependencies = {}) {
  const readFile = dependencies.readFile ?? readMountedSecret;
  const firestore = dependencies.firestore ?? new Firestore();
  const privateKey = await readFile(config.signingKeyFile);
  const googlePlay = dependencies.googlePlay ?? createGooglePlayClient({
    auth: dependencies.auth,
    fetch: dependencies.fetch ?? globalThis.fetch,
    packageName: config.packageName,
    productId: config.productId,
  });
  const purchaseStore = dependencies.purchaseStore ?? createPurchaseStateStore({
    firestore,
    now: dependencies.now ?? Date.now,
  });
  const signer = dependencies.signer ?? createEntitlementSigner({
    privateKey,
    keyId: config.signingKeyVersion,
    issuer: config.publicBaseUrl,
    now: dependencies.now ?? Date.now,
    randomUUID: dependencies.randomUUID,
  });
  const oidc = dependencies.oidc ?? createGoogleOidcVerifier({
    client: dependencies.oidcClient,
    now: dependencies.now ?? Date.now,
  });
  const routes = [createVerifyPurchaseRoute({
    googlePlay,
    purchaseStore,
    signer,
    rateLimiter: dependencies.rateLimiter ?? createVerificationRateLimiter({ now: dependencies.now ?? Date.now }),
    packageName: config.packageName,
    productId: config.productId,
  }), createRtdnRoute({
    oidc,
    purchaseStore,
    messageStore: dependencies.messageStore ?? createRtdnMessageStore({
      firestore,
      now: dependencies.now ?? Date.now,
    }),
    config,
  }), createVoidedPurchasesRoute({
    oidc,
    purchaseStore,
    client: dependencies.voidedPurchases ?? createVoidedPurchasesClient({
      auth: dependencies.auth,
      fetch: dependencies.fetch ?? globalThis.fetch,
      packageName: config.packageName,
    }),
    config,
  })];
  return async function resolveProductionRoute(context) {
    for (const route of routes) {
      const response = await route(context);
      if (response) return response;
    }
    return null;
  };
}

export async function startServer(env = process.env, dependencies = {}) {
  const config = loadVerifierConfig(env);
  const resolveRoute = dependencies.resolveRoute ?? await createProductionRoute(config, dependencies);
  const server = createServer({ config, ...dependencies, resolveRoute });
  server.listen(config.port, "0.0.0.0");
  return server;
}

if (process.argv[1] && pathToFileURL(process.argv[1]).href === import.meta.url) {
  try {
    await startServer();
  } catch {
    process.stderr.write(`${JSON.stringify({ level: "error", code: "startup_failed" })}\n`);
    process.exitCode = 1;
  }
}
