import { GoogleAuth } from "google-auth-library";

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

const SYNC_PATH = "/internal/google-play/voided-purchases/sync";
const API_BASE = "https://androidpublisher.googleapis.com/androidpublisher/v3/applications";
const ANDROID_PUBLISHER_SCOPE = "https://www.googleapis.com/auth/androidpublisher";
const PACKAGE_NAME = "com.makecdev.coderemotemanager";
const PRODUCT_ID = "pro_multi_account";
const CURSOR = /^[!-~]{1,2048}$/;
const PURCHASE_TOKEN = /^[!-~]{1,4096}$/;

export function createVoidedPurchasesClient({
  auth = new GoogleAuth({ scopes: [ANDROID_PUBLISHER_SCOPE] }),
  fetch = globalThis.fetch,
  packageName,
}) {
  if (packageName !== PACKAGE_NAME || typeof auth?.getRequestHeaders !== "function" || typeof fetch !== "function") {
    throw unavailable();
  }

  return {
    async list({ cursor } = {}) {
      try {
        if (cursor !== undefined && !CURSOR.test(cursor)) throw new Error();
        const url = new URL(`${API_BASE}/${encodeURIComponent(packageName)}/purchases/voidedpurchases`);
        url.searchParams.set("pageSelection.maxResults", "1000");
        url.searchParams.set("type", "0");
        url.searchParams.set("includeQuantityBasedPartialRefund", "true");
        if (cursor !== undefined) url.searchParams.set("pageSelection.token", cursor);
        const authorization = await auth.getRequestHeaders(url.href);
        const response = await fetch(url.href, { method: "GET", headers: new Headers(authorization) });
        if (!response?.ok) throw new Error();
        return normalizeResponse(await response.json());
      } catch {
        throw unavailable();
      }
    },
  };
}

export function createVoidedPurchasesRoute({ oidc, client, purchaseStore, config }) {
  if (
    config?.packageName !== PACKAGE_NAME
    || config?.productId !== PRODUCT_ID
    || typeof oidc?.verifyRequest !== "function"
    || typeof client?.list !== "function"
    || typeof purchaseStore?.invalidate !== "function"
  ) throw new Error("invalid_voided_sync_route_config");

  return async function resolveVoidedPurchases({ req, url, readJson }) {
    if (url.pathname !== SYNC_PATH) return null;
    if (req.method !== "POST") return { status: 405, body: { error: "method_not_allowed" } };
    await oidc.verifyRequest(req, config.schedulerOidc);
    const input = await readJson(req, 4 * 1024);
    if (!validInput(input)) throw publicError(400, "invalid_voided_sync_request");

    try {
      const page = await client.list(input);
      for (const purchaseToken of page.purchaseTokens) {
        await purchaseStore.invalidate({
          packageName: PACKAGE_NAME,
          productId: PRODUCT_ID,
          purchaseToken,
          reason: "voided",
        });
      }
      return {
        status: 200,
        body: { processed: page.purchaseTokens.length, nextCursor: page.nextCursor },
      };
    } catch {
      throw publicError(503, "voided_sync_unavailable");
    }
  };
}

function validInput(value) {
  if (value === null || typeof value !== "object" || Array.isArray(value) || Object.getPrototypeOf(value) !== Object.prototype) return false;
  const keys = Object.keys(value);
  return (keys.length === 0 || (keys.length === 1 && keys[0] === "cursor"))
    && (value.cursor === undefined || CURSOR.test(value.cursor));
}

function normalizeResponse(value) {
  if (value === null || typeof value !== "object" || Array.isArray(value)) throw new Error();
  const purchases = value.voidedPurchases ?? [];
  if (!Array.isArray(purchases) || purchases.length > 1000) throw new Error();
  const purchaseTokens = purchases.map((purchase) => {
    if (
      purchase === null
      || typeof purchase !== "object"
      || Array.isArray(purchase)
      || !PURCHASE_TOKEN.test(purchase.purchaseToken ?? "")
      || (purchase.voidedQuantity !== undefined && (!Number.isSafeInteger(purchase.voidedQuantity) || purchase.voidedQuantity < 1))
    ) throw new Error();
    return purchase.purchaseToken;
  });
  const nextCursor = value.tokenPagination?.nextPageToken ?? null;
  if (nextCursor !== null && !CURSOR.test(nextCursor)) throw new Error();
  return { purchaseTokens, nextCursor };
}

function unavailable() {
  const error = new Error("Voided purchase synchronization is unavailable.");
  error.code = "voided_purchases_unavailable";
  error.status = 503;
  return error;
}
