import { createHash } from "node:crypto";

import { publicError } from "./http.js";
import { hashPurchaseToken } from "./purchase-state.js";

const VERIFY_PATH = "/v1/entitlements/google-play/verify";
const PACKAGE_NAME = "com.makecdev.coderemotemanager";
const PRODUCT_ID = "pro_multi_account";
const BINDING = /^[A-Za-z0-9_-]{43}$/;
const PURCHASE_TOKEN = /^[!-~]{16,4096}$/;
const SAFE_FAILURES = new Map([
  ["purchase_pending", [409, "purchase_pending"]],
  ["purchase_canceled", [402, "purchase_canceled"]],
  ["purchase_refunded", [402, "purchase_refunded"]],
  ["purchase_consumed", [409, "purchase_consumed"]],
  ["purchase_not_valid", [400, "purchase_not_valid"]],
  ["purchase_invalidated", [402, "purchase_invalidated"]],
]);

export function createVerifyPurchaseRoute({
  googlePlay,
  purchaseStore,
  signer,
  rateLimiter = createVerificationRateLimiter(),
  packageName,
  productId,
}) {
  if (
    packageName !== PACKAGE_NAME
    || productId !== PRODUCT_ID
    || typeof googlePlay?.verifyPurchase !== "function"
    || typeof purchaseStore?.recordActive !== "function"
    || typeof signer?.sign !== "function"
    || typeof rateLimiter?.consume !== "function"
  ) throw new Error("invalid_verification_route_config");

  return async function resolveVerifyPurchase({ req, url, readJson, clientIp }) {
    if (url.pathname !== VERIFY_PATH) return null;
    if (req.method !== "POST") return { status: 405, body: { error: "method_not_allowed" } };
    const input = await readJson(req, 8 * 1024);
    if (!validInput(input)) throw publicError(400, "invalid_verification_request");

    const purchaseKey = hashPurchaseToken({
      packageName,
      productId,
      purchaseToken: input.purchaseToken,
    });
    try {
      const limit = rateLimiter.consume({ clientIp, purchaseKey });
      if (!limit?.allowed) throw publicError(429, "verification_rate_limited");
      const purchase = await googlePlay.verifyPurchase(input.purchaseToken);
      await purchaseStore.recordActive({
        packageName,
        productId,
        purchaseToken: input.purchaseToken,
        purchaseCompletionTime: purchase.purchaseCompletionTime,
      });
      const entitlement = signer.sign({
        backendKeyThumbprint: input.backendKeyThumbprint,
        challengeHash: input.challengeHash,
      });
      return {
        status: 200,
        body: {
          certificate: entitlement.certificate,
          expiresAt: entitlement.expiresAt,
        },
      };
    } catch (error) {
      const known = SAFE_FAILURES.get(error?.code);
      if (known) throw publicError(known[0], known[1]);
      if (error?.code === "verification_rate_limited") throw error;
      throw publicError(503, "verification_unavailable");
    }
  };
}

export function createVerificationRateLimiter({
  now = Date.now,
  windowMs = 60_000,
  ipLimit = 30,
  tokenLimit = 6,
  maxEntries = 10_000,
} = {}) {
  if (
    typeof now !== "function"
    || !positiveInteger(windowMs)
    || !positiveInteger(ipLimit)
    || !positiveInteger(tokenLimit)
    || !positiveInteger(maxEntries)
  ) throw new Error("invalid_rate_limiter_config");
  const ipCounters = new Map();
  const tokenCounters = new Map();

  return {
    consume({ clientIp, purchaseKey } = {}) {
      const timestamp = Number(now());
      if (
        !Number.isFinite(timestamp)
        || typeof clientIp !== "string"
        || clientIp.length === 0
        || clientIp.length > 128
        || !/^[a-f0-9]{64}$/.test(purchaseKey ?? "")
      ) return { allowed: false };
      pruneExpired(ipCounters, timestamp);
      pruneExpired(tokenCounters, timestamp);
      const ipKey = createHash("sha256").update(clientIp).digest("hex");
      const ip = currentCounter(ipCounters, ipKey, timestamp, windowMs);
      const token = currentCounter(tokenCounters, purchaseKey, timestamp, windowMs);
      if (!ip || !token || ip.count >= ipLimit || token.count >= tokenLimit) return { allowed: false };
      if (
        (ip.count === 0 && ipCounters.size >= maxEntries)
        || (token.count === 0 && tokenCounters.size >= maxEntries)
      ) return { allowed: false };
      ip.count += 1;
      token.count += 1;
      ipCounters.set(ipKey, ip);
      tokenCounters.set(purchaseKey, token);
      return { allowed: true };
    },
  };
}

function pruneExpired(counters, now) {
  for (const [key, counter] of counters) {
    if (now >= counter.resetAt) counters.delete(key);
  }
}

function currentCounter(counters, key, now, windowMs) {
  const existing = counters.get(key);
  if (!existing || now >= existing.resetAt) {
    if (existing) counters.delete(key);
    return { count: 0, resetAt: now + windowMs };
  }
  return existing;
}

function validInput(value) {
  if (!value || typeof value !== "object" || Array.isArray(value)) return false;
  const keys = Object.keys(value).sort();
  return keys.length === 3
    && keys[0] === "backendKeyThumbprint"
    && keys[1] === "challengeHash"
    && keys[2] === "purchaseToken"
    && PURCHASE_TOKEN.test(value.purchaseToken)
    && BINDING.test(value.backendKeyThumbprint)
    && BINDING.test(value.challengeHash);
}

function positiveInteger(value) {
  return Number.isSafeInteger(value) && value > 0;
}
