import { createHash } from "node:crypto";

import { publicError } from "./http.js";

const RTDN_PATH = "/internal/google-play/rtdn";
const PACKAGE_NAME = "com.makecdev.coderemotemanager";
const PRODUCT_ID = "pro_multi_account";
const PURCHASE_TOKEN = /^[!-~]{1,4096}$/;
const MESSAGE_ID = /^[!-~]{1,512}$/;
const RETENTION_MS = 30 * 24 * 60 * 60 * 1000;
const BASE64 = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;

export function createRtdnRoute({ oidc, purchaseStore, messageStore, config }) {
  if (
    config?.packageName !== PACKAGE_NAME
    || config?.productId !== PRODUCT_ID
    || typeof oidc?.verifyRequest !== "function"
    || typeof purchaseStore?.invalidate !== "function"
    || typeof messageStore?.record !== "function"
  ) throw new Error("invalid_rtdn_route_config");

  return async function resolveRtdn({ req, url, readJson }) {
    if (url.pathname !== RTDN_PATH) return null;
    if (req.method !== "POST") return { status: 405, body: { error: "method_not_allowed" } };
    await oidc.verifyRequest(req, config.pubsubOidc);
    const envelope = await readJson(req, 32 * 1024);
    const message = parseEnvelope(envelope);
    const notification = parseNotification(message.data, config);

    if (notification.action) {
      await purchaseStore.invalidate({
        packageName: PACKAGE_NAME,
        productId: PRODUCT_ID,
        purchaseToken: notification.purchaseToken,
        reason: notification.action,
      });
    }
    await messageStore.record(message.messageId);
    return { status: 204, body: null };
  };
}

export function createRtdnMessageStore({ firestore, now = Date.now }) {
  if (typeof firestore?.collection !== "function" || typeof firestore?.runTransaction !== "function" || typeof now !== "function") {
    throw new Error("invalid_rtdn_message_store_config");
  }
  const messages = firestore.collection("rtdnMessages");
  return {
    async record(messageId) {
      if (!MESSAGE_ID.test(messageId ?? "")) throw publicError(400, "invalid_rtdn_message");
      const timestamp = Number(now());
      if (!Number.isFinite(timestamp)) throw new Error("rtdn_message_store_unavailable");
      const key = createHash("sha256").update(messageId).digest("hex");
      const reference = messages.doc(key);
      return firestore.runTransaction(async (transaction) => {
        const snapshot = await transaction.get(reference);
        if (snapshot.exists) {
          validateReceipt(snapshot.data());
          return { duplicate: true };
        }
        transaction.set(reference, {
          schemaVersion: 1,
          processedAt: new Date(timestamp),
          deleteAfter: new Date(timestamp + RETENTION_MS),
        });
        return { duplicate: false };
      });
    },
  };
}

function parseEnvelope(value) {
  try {
    if (!plainObject(value) || !allowedKeys(value, ["deliveryAttempt", "message", "subscription"])) throw new Error();
    if (typeof value.subscription !== "string" || value.subscription.length < 1 || value.subscription.length > 1024) throw new Error();
    const message = value.message;
    if (!plainObject(message) || !allowedKeys(message, ["attributes", "data", "messageId", "message_id", "orderingKey", "publishTime", "publish_time"])) throw new Error();
    const messageId = message.messageId ?? message.message_id;
    if (!MESSAGE_ID.test(messageId ?? "") || (message.messageId && message.message_id)) throw new Error();
    if (typeof message.data !== "string" || message.data.length > 44 * 1024 || !BASE64.test(message.data)) throw new Error();
    const data = Buffer.from(message.data, "base64");
    if (data.length === 0 || data.length > 16 * 1024) throw new Error();
    return { messageId, data };
  } catch {
    throw publicError(400, "invalid_rtdn_message");
  }
}

function parseNotification(data, config) {
  try {
    const value = JSON.parse(data.toString("utf8"));
    if (!plainObject(value) || value.packageName !== config.packageName || typeof value.version !== "string") throw new Error();
    if (!/^\d+$/.test(value.eventTimeMillis ?? "")) throw new Error();
    const kinds = ["oneTimeProductNotification", "voidedPurchaseNotification", "testNotification"]
      .filter((key) => value[key] !== undefined);
    if (kinds.length !== 1 || !allowedKeys(value, ["eventTimeMillis", "oneTimeProductNotification", "packageName", "testNotification", "version", "voidedPurchaseNotification"])) throw new Error();
    if (kinds[0] === "testNotification") {
      if (!plainObject(value.testNotification) || !exactKeys(value.testNotification, ["version"]) || typeof value.testNotification.version !== "string") throw new Error();
      return {};
    }
    if (kinds[0] === "oneTimeProductNotification") {
      const item = value.oneTimeProductNotification;
      if (
        !plainObject(item)
        || !exactKeys(item, ["notificationType", "purchaseToken", "sku", "version"])
        || typeof item.version !== "string"
        || item.sku !== config.productId
        || !PURCHASE_TOKEN.test(item.purchaseToken ?? "")
        || ![1, 2].includes(item.notificationType)
      ) throw new Error();
      return item.notificationType === 2 ? { action: "canceled", purchaseToken: item.purchaseToken } : {};
    }
    const item = value.voidedPurchaseNotification;
    if (
      !plainObject(item)
      || !allowedKeys(item, ["orderId", "productType", "purchaseToken", "refundType"])
      || !PURCHASE_TOKEN.test(item.purchaseToken ?? "")
      || item.productType !== 2
      || ![1, 2].includes(item.refundType)
    ) throw new Error();
    return { action: "refunded", purchaseToken: item.purchaseToken };
  } catch {
    throw publicError(400, "invalid_rtdn_message");
  }
}

function validateReceipt(value) {
  if (
    !plainObject(value)
    || !exactKeys(value, ["deleteAfter", "processedAt", "schemaVersion"])
    || value.schemaVersion !== 1
    || !validTimestamp(value.processedAt)
    || !validTimestamp(value.deleteAfter)
  ) throw new Error("invalid_rtdn_receipt_state");
}

function validTimestamp(value) {
  try {
    const date = value instanceof Date ? value : value?.toDate?.();
    return date instanceof Date && Number.isFinite(date.getTime());
  } catch {
    return false;
  }
}

function plainObject(value) {
  return value !== null && typeof value === "object" && !Array.isArray(value) && Object.getPrototypeOf(value) === Object.prototype;
}

function allowedKeys(value, allowed) {
  return Object.keys(value).every((key) => allowed.includes(key));
}

function exactKeys(value, expected) {
  const keys = Object.keys(value).sort();
  return keys.length === expected.length && expected.every((key, index) => keys[index] === key);
}
