/**
 * Membership client: Google login, multi-tier entitlements, Creem checkout.
 */

import {
  MEMBERSHIP_API_BASE,
  PRODUCT_ID
} from './membership-config.js';

export const MEMBERSHIP_STORAGE_KEY = 'supermarkMembership';
export const MEMBERSHIP_SESSION_KEY = 'supermarkSessionToken';
export const MEMBERSHIP_CATALOG_KEY = 'supermarkMembershipCatalog';

/** Plan-gated feature ids used in client checks (must match API feature ids). */
export const PRO_FEATURES = {
  AI_ORGANIZE: 'ai_organize',
  AI_RECOMMEND: 'ai_recommend',
  AI_INTRODUCE: 'ai_introduce',
  PLUGIN_BAR: 'plugin_bar',
  BING_WALLPAPER: 'bing_wallpaper',
  COMPARE_SEARCH: 'compare_search',
  QUICK_LINKS_DRAG_TO_REORDER: 'quick_links_drag_to_reorder',
  CUSTOMIZE_MODEL_PROVIDERS: 'customize_model_providers'
};

/** Default false for gated features until API / catalog says otherwise. */
const DEFAULT_ENTITLEMENTS = {
  [PRO_FEATURES.AI_ORGANIZE]: false,
  [PRO_FEATURES.AI_RECOMMEND]: false,
  [PRO_FEATURES.AI_INTRODUCE]: false,
  [PRO_FEATURES.PLUGIN_BAR]: false,
  [PRO_FEATURES.BING_WALLPAPER]: false,
  [PRO_FEATURES.COMPARE_SEARCH]: false,
  [PRO_FEATURES.QUICK_LINKS_DRAG_TO_REORDER]: false,
  [PRO_FEATURES.CUSTOMIZE_MODEL_PROVIDERS]: false
};

/**
 * Last-resort offline defaults for gated features only (API / catalog unavailable).
 * Free-baseline features are never gated in client code — do not hardcode them here.
 */
const OFFLINE_GATED_FALLBACK = {
  free: {},
  pro: {
    [PRO_FEATURES.COMPARE_SEARCH]: true,
    [PRO_FEATURES.QUICK_LINKS_DRAG_TO_REORDER]: true,
    [PRO_FEATURES.PLUGIN_BAR]: true,
    [PRO_FEATURES.BING_WALLPAPER]: true,
    [PRO_FEATURES.AI_ORGANIZE]: true
  },
  premium: {
    [PRO_FEATURES.COMPARE_SEARCH]: true,
    [PRO_FEATURES.QUICK_LINKS_DRAG_TO_REORDER]: true,
    [PRO_FEATURES.PLUGIN_BAR]: true,
    [PRO_FEATURES.BING_WALLPAPER]: true,
    [PRO_FEATURES.AI_ORGANIZE]: true,
    [PRO_FEATURES.AI_RECOMMEND]: true,
    [PRO_FEATURES.AI_INTRODUCE]: true,
    [PRO_FEATURES.CUSTOMIZE_MODEL_PROVIDERS]: true
  }
};

/** @deprecated Use API entitlements; kept as alias of offline gated fallback. */
export const PLAN_FEATURE_MATRIX = OFFLINE_GATED_FALLBACK;

/**
 * Lowest paid plan that includes a gated feature (catalog first, then offline fallback).
 */
export function resolveMinimumPlanForFeature(featureId, catalog = cachedCatalog) {
  const id = String(featureId || '').trim();
  if (!id) {
    return { id: 'pro', name: 'Pro', rank: 1 };
  }

  const tiers = getCatalogTiers(catalog)
    .filter((t) => !(t?.isFree || t?.id === 'free' || Number(t?.rank) <= 0))
    .slice()
    .sort((a, b) => (Number(a.rank) || 0) - (Number(b.rank) || 0));

  for (const tier of tiers) {
    const features = Array.isArray(tier.features) ? tier.features : [];
    if (features.some((f) => String(f || '').trim() === id)) {
      return {
        id: String(tier.id || 'pro').toLowerCase(),
        name: String(tier.name || tier.id || 'Pro').replace(/\s*Plan$/i, '').trim() || 'Pro',
        rank: Number(tier.rank) || 0
      };
    }
  }

  const offlineOrder = ['pro', 'premium'];
  for (const planId of offlineOrder) {
    if (OFFLINE_GATED_FALLBACK[planId]?.[id]) {
      return {
        id: planId,
        name: planId === 'premium' ? 'Premium' : 'Pro',
        rank: planId === 'premium' ? 2 : 1
      };
    }
  }

  return { id: 'pro', name: 'Pro', rank: 1 };
}

function localizedPlanLabel(plan) {
  const id = String(plan?.id || '').toLowerCase();
  if (id === 'premium') {
    return chrome.i18n.getMessage('accountPlanPremium') || plan?.name || 'Premium';
  }
  if (id === 'pro') {
    return chrome.i18n.getMessage('accountPlanPro') || plan?.name || 'Pro';
  }
  if (id === 'free') {
    return chrome.i18n.getMessage('accountPlanFree') || plan?.name || 'Free';
  }
  return plan?.name || id || 'Pro';
}

let cachedStatus = null;
let cachedCatalog = null;
let refreshPromise = null;
let catalogPromise = null;

function storageGet(keys) {
  return new Promise((resolve) => {
    chrome.storage.local.get(keys, (result) => resolve(result || {}));
  });
}

function storageSet(values) {
  return new Promise((resolve) => {
    chrome.storage.local.set(values, () => resolve());
  });
}

function storageRemove(keys) {
  return new Promise((resolve) => {
    chrome.storage.local.remove(keys, () => resolve());
  });
}

function productQuery() {
  const params = new URLSearchParams();
  if (PRODUCT_ID) params.set('product_id', PRODUCT_ID);
  const qs = params.toString();
  return qs ? `?${qs}` : '';
}

function emptyStatus() {
  return {
    signedIn: false,
    user: null,
    productId: PRODUCT_ID,
    creemProductId: null,
    plan: 'free',
    planStatus: 'inactive',
    isPro: false,
    isPremium: false,
    tier: null,
    trialEligible: false,
    trialUsed: false,
    entitlements: { ...DEFAULT_ENTITLEMENTS },
    ai: null,
    updatedAt: Date.now()
  };
}

function entitlementsFromFeatureIds(featureIds = []) {
  const map = { ...DEFAULT_ENTITLEMENTS };
  for (const raw of featureIds) {
    const id = String(raw || '').trim();
    if (id) map[id] = true;
  }
  return map;
}

/**
 * Resolve entitlements:
 * 1) /v1/me entitlements from API (source of truth)
 * 2) cached catalog tier.features for this plan
 * 3) slim offline gated fallback (never free-baseline features)
 */
function entitlementsForPlan(plan, apiEntitlements = {}, catalog = null) {
  if (apiEntitlements && typeof apiEntitlements === 'object' && Object.keys(apiEntitlements).length > 0) {
    return { ...DEFAULT_ENTITLEMENTS, ...apiEntitlements };
  }

  const planId = String(plan || 'free').toLowerCase();
  const tiers = catalog?.tiers || cachedCatalog?.tiers || [];
  const tier = tiers.find((t) => String(t.id || '').toLowerCase() === planId)
    || (planId === 'free' ? tiers.find((t) => t.isFree || Number(t.rank) === 0) : null);
  if (Array.isArray(tier?.features) && tier.features.length) {
    return entitlementsFromFeatureIds(tier.features);
  }

  const offline = OFFLINE_GATED_FALLBACK[planId]
    || (planId !== 'free' && planId !== 'pro' ? OFFLINE_GATED_FALLBACK.premium : null)
    || OFFLINE_GATED_FALLBACK.free;
  return { ...DEFAULT_ENTITLEMENTS, ...offline };
}

function statusFromUser(user, meta = {}) {
  if (!user?.id) {
    return emptyStatus();
  }
  const isPro = Boolean(user?.isPro ?? meta.isPro);
  const isPremium = Boolean(user?.isPremium ?? meta.isPremium);
  const plan = String(user?.plan || meta.plan || (isPro ? 'pro' : 'free')).toLowerCase();
  const planStatus = String(user?.planStatus ?? meta.planStatus ?? 'inactive').toLowerCase();
  const apiEntitlements = user?.entitlements || meta.entitlements || {};
  const entitlements = entitlementsForPlan(plan, apiEntitlements, meta.catalog || cachedCatalog);
  return {
    signedIn: true,
    user: {
      id: user.id,
      email: user.email,
      name: user.name,
      picture: user.picture,
      planStatus,
      currentPeriodEnd: user.currentPeriodEnd ?? meta.currentPeriodEnd
    },
    productId: meta.product?.id || user.productId || PRODUCT_ID,
    creemProductId: meta.product?.creemProductId
      || meta.tier?.creemProductId
      || null,
    productName: meta.product?.name || null,
    plan,
    planStatus,
    isPro,
    isPremium,
    tier: user?.tier || meta.tier || null,
    trialEligible: Boolean(meta.trialEligible ?? user?.trialEligible),
    trialUsed: Boolean(meta.trialUsed ?? user?.trialUsed),
    currentPeriodEnd: user?.currentPeriodEnd ?? meta.currentPeriodEnd ?? null,
    entitlements,
    ai: meta.ai || user?.ai || null,
    updatedAt: Date.now()
  };
}

export async function getCachedMembershipStatus() {
  if (cachedStatus) return cachedStatus;
  const stored = await storageGet([MEMBERSHIP_STORAGE_KEY]);
  cachedStatus = stored[MEMBERSHIP_STORAGE_KEY] || emptyStatus();
  return cachedStatus;
}

export async function isFeatureEntitled(featureId) {
  const status = await getCachedMembershipStatus();
  const entitlements = status?.entitlements || {};
  return Boolean(entitlements[featureId]);
}

export async function requireAiIntroduceFeature({ openSettings = true } = {}) {
  const entitled = await isFeatureEntitled(PRO_FEATURES.AI_INTRODUCE);
  if (entitled) return true;
  return requireProFeature(PRO_FEATURES.AI_INTRODUCE, { openSettings });
}

export async function requireProFeature(featureId, { openSettings = true } = {}) {
  const entitled = await isFeatureEntitled(featureId);
  if (entitled) return true;

  const status = await getCachedMembershipStatus();

  if (openSettings && typeof window.settingsManager?.openAccountSettings === 'function') {
    window.settingsManager.openAccountSettings({ highlightUpgrade: true });
  }

  // Fire after opening settings so switchTab's updateUILanguage cannot overwrite toast text
  window.dispatchEvent(new CustomEvent('supermark:pro-required', {
    detail: { featureId, signedIn: Boolean(status.signedIn) }
  }));

  return false;
}

export async function refreshMembershipStatus({ force = false } = {}) {
  if (refreshPromise && !force) return refreshPromise;

  // Keep a stable reference so a concurrent force-refresh cannot clear
  // another in-flight promise via finally.
  const current = (async () => {
    const sessionStore = await storageGet([MEMBERSHIP_SESSION_KEY]);
    const token = sessionStore[MEMBERSHIP_SESSION_KEY];
    if (!token) {
      cachedStatus = emptyStatus();
      await storageSet({ [MEMBERSHIP_STORAGE_KEY]: cachedStatus });
      return cachedStatus;
    }

    try {
      const res = await fetch(`${MEMBERSHIP_API_BASE}/v1/me${productQuery()}`, {
        headers: { Authorization: `Bearer ${token}` }
      });
      if (res.status === 401) {
        await clearMembershipSession();
        return emptyStatus();
      }
      if (!res.ok) {
        throw new Error(`me_failed_${res.status}`);
      }
      const data = await res.json();
      cachedStatus = statusFromUser(data.user, data);
      await storageSet({ [MEMBERSHIP_STORAGE_KEY]: cachedStatus });
      return cachedStatus;
    } catch (error) {
      console.warn('Failed to refresh membership:', error);
      return getCachedMembershipStatus();
    } finally {
      if (refreshPromise === current) {
        refreshPromise = null;
      }
    }
  })();

  refreshPromise = current;
  return current;
}

export async function fetchProductCatalog({ force = false } = {}) {
  if (catalogPromise && !force) return catalogPromise;
  if (cachedCatalog && !force) return cachedCatalog;

  const current = (async () => {
    try {
      if (!force) {
        const stored = await storageGet([MEMBERSHIP_CATALOG_KEY]);
        const cached = stored[MEMBERSHIP_CATALOG_KEY];
        // Only reuse cache when it already includes multi-tier data
        if (
          cached?.fetchedAt
          && Date.now() - cached.fetchedAt < 10 * 60 * 1000
          && Array.isArray(cached.tiers)
          && cached.tiers.length > 0
        ) {
          cachedCatalog = cached;
          return cachedCatalog;
        }
      }

      const res = await fetch(`${MEMBERSHIP_API_BASE}/v1/products${productQuery()}`, {
        cache: 'no-store'
      });
      if (!res.ok) throw new Error(`catalog_failed_${res.status}`);
      const data = await res.json();
      cachedCatalog = { ...data, fetchedAt: Date.now() };
      await storageSet({ [MEMBERSHIP_CATALOG_KEY]: cachedCatalog });
      return cachedCatalog;
    } catch (error) {
      console.warn('Failed to load product catalog:', error);
      const stored = await storageGet([MEMBERSHIP_CATALOG_KEY]);
      cachedCatalog = stored[MEMBERSHIP_CATALOG_KEY] || null;
      return cachedCatalog;
    } finally {
      if (catalogPromise === current) {
        catalogPromise = null;
      }
    }
  })();

  catalogPromise = current;
  return current;
}

export function findPaymentPlan(tierOrCatalog, planId) {
  const id = String(planId || '').trim().toLowerCase();
  if (!id) return null;
  const plans = tierOrCatalog?.paymentPlans || [];
  return plans.find((p) => String(p.id || '').toLowerCase() === id) || null;
}

export function formatBillingPlanLabel(planId, tierOrCatalog = null) {
  const id = String(planId || '').trim().toLowerCase();
  const plan = findPaymentPlan(tierOrCatalog, id);
  if (plan?.kind === 'free') return 'Free';
  if (plan?.kind === 'one_time' || id === 'once' || id === 'onetime' || id === 'one_time') {
    return 'One-time';
  }
  if (id === 'yearly' || id === 'year' || plan?.billingPeriod === 'every-year') return 'Yearly';
  if (id === 'monthly' || id === 'month' || plan?.billingPeriod === 'every-month') return 'Monthly';
  if (plan?.billingPeriod === 'every-three-months') return 'Every 3 months';
  if (plan?.billingPeriod === 'every-six-months') return 'Every 6 months';
  if (plan?.billingPeriod === 'every-day') return 'Daily';
  if (plan?.kind === 'subscription') return 'Subscription';
  return id ? id.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase()) : 'Plan';
}

function pricingFromPaymentPlan(plan) {
  if (!plan || plan.priceCents == null || !Number.isFinite(Number(plan.priceCents))) return null;
  const priceCents = Number(plan.priceCents);
  return {
    currency: plan.currency || 'USD',
    billingPeriod: plan.billingPeriod || (plan.kind === 'subscription' ? 'every-month' : 'once'),
    priceCents,
    price: priceCents / 100,
    compareAtPriceCents: null,
    compareAtPrice: null,
    discountPercent: null,
    discountLabel: null,
    discountExpiryDate: null,
    discountDuration: null,
    discountDurationInMonths: null,
    billingType: plan.kind === 'subscription' ? 'recurring' : 'onetime'
  };
}

/** Mean Gregorian month length — used to normalize daily plans to /mo. */
const DAYS_PER_MONTH = 30.436875;

/**
 * How many months one charge covers for a Creem billing period.
 * Returns null for one-time / free (no monthly normalization).
 */
export function billingPeriodMonthCount(billingPeriod, kind = 'subscription') {
  if (kind === 'one_time' || kind === 'free' || billingPeriod === 'once') return null;
  switch (String(billingPeriod || '').toLowerCase()) {
    case 'every-day':
      return 1 / DAYS_PER_MONTH;
    case 'every-month':
      return 1;
    case 'every-three-months':
      return 3;
    case 'every-six-months':
      return 6;
    case 'every-year':
      return 12;
    default:
      return 1;
  }
}

/** Convert a period charge into a monthly-equivalent list price. */
export function toMonthlyEquivalent(price, billingPeriod, kind = 'subscription') {
  const months = billingPeriodMonthCount(billingPeriod, kind);
  const value = Number(price);
  if (months == null || !Number.isFinite(value)) {
    return { monthlyPrice: Number.isFinite(value) ? value : null, months: null, normalized: false };
  }
  return {
    monthlyPrice: value / months,
    months,
    normalized: true
  };
}

function formatMoneyAmount(value, currency = 'USD') {
  try {
    return new Intl.NumberFormat(undefined, {
      style: 'currency',
      currency,
      minimumFractionDigits: value % 1 === 0 ? 0 : 2,
      maximumFractionDigits: 2
    }).format(value);
  } catch {
    return `$${Number(value).toFixed(2)}`;
  }
}

function billedLabelForPeriod(billingPeriod, kind) {
  if (kind === 'one_time' || billingPeriod === 'once') return 'One-time payment';
  switch (String(billingPeriod || '').toLowerCase()) {
    case 'every-year':
      return 'billed annually';
    case 'every-three-months':
      return 'billed every 3 months';
    case 'every-six-months':
      return 'billed every 6 months';
    case 'every-day':
      return 'billed daily';
    case 'every-month':
    default:
      return 'billed monthly';
  }
}

function savingsPhrase(savingsAmountText, months) {
  if (!savingsAmountText || !Number.isFinite(Number(months)) || months <= 1) return null;
  if (months >= 12) return `Save ${savingsAmountText} a year`;
  if (months === 6) return `Save ${savingsAmountText} every 6 months`;
  if (months === 3) return `Save ${savingsAmountText} every 3 months`;
  return `Save ${savingsAmountText}`;
}

/**
 * Resolve the charge amount for one billing interval (before /mo conversion).
 * @returns {{ price: number, compareAtPrice: number|null, currency: string, billingPeriod: string, kind: string, discountLabel: string|null, discountDuration: string|null, discountDurationInMonths: number|null, discountExpiryDate: string|null, pricing: object } | null}
 */
function resolveIntervalCharge(source, interval, campaignOn) {
  const resolvedInterval = String(interval || '').trim().toLowerCase() || 'monthly';
  const slot = source?.intervals?.[resolvedInterval];
  const plan = findPaymentPlan(source, resolvedInterval);
  const pricing = slot?.pricing
    || pricingFromPaymentPlan(plan)
    || (resolvedInterval === 'monthly' ? source?.pricing : null);
  if (!pricing || pricing.price == null) return null;

  let displayPrice = Number(pricing.price);
  let compareAtPrice = pricing.compareAtPrice != null ? Number(pricing.compareAtPrice) : null;
  let discountLabel = null;
  let discountDuration = pricing.discountDuration || null;
  let discountDurationInMonths = pricing.discountDurationInMonths != null
    ? Number(pricing.discountDurationInMonths)
    : null;
  let discountExpiryDate = pricing.discountExpiryDate || null;

  if (campaignOn) {
    discountLabel = pricing.discountLabel || (
      pricing.discountAmountCents != null && Number.isFinite(Number(pricing.discountAmountCents))
        ? (() => {
            const major = Number(pricing.discountAmountCents) / 100;
            const cur = pricing.currency || 'USD';
            return `${formatMoneyAmount(major, cur)} off`;
          })()
        : (pricing.discountPercent != null ? `${pricing.discountPercent}% off` : null)
    );
  } else if (Number.isFinite(compareAtPrice) && compareAtPrice > displayPrice) {
    displayPrice = compareAtPrice;
    compareAtPrice = null;
    discountLabel = null;
    discountDuration = null;
    discountDurationInMonths = null;
    discountExpiryDate = null;
  } else {
    compareAtPrice = null;
    discountLabel = null;
    discountDuration = null;
    discountDurationInMonths = null;
    discountExpiryDate = null;
  }

  const billingPeriod = pricing.billingPeriod
    || plan?.billingPeriod
    || (resolvedInterval === 'yearly' ? 'every-year'
      : (resolvedInterval === 'once' ? 'once' : 'every-month'));
  const isSubscription = (plan?.kind || pricing.billingType) === 'subscription'
    || pricing.billingType === 'recurring'
    || (!plan && (resolvedInterval === 'monthly' || resolvedInterval === 'yearly'));
  const kind = plan?.kind || (isSubscription ? 'subscription' : 'one_time');

  return {
    price: displayPrice,
    compareAtPrice: campaignOn
      && Number.isFinite(compareAtPrice)
      && compareAtPrice > displayPrice
      ? compareAtPrice
      : null,
    currency: pricing.currency || 'USD',
    billingPeriod,
    kind,
    isSubscription,
    discountLabel,
    discountDuration,
    discountDurationInMonths,
    discountExpiryDate,
    pricing
  };
}

function findMonthlyBaselineCharge(source, campaignOn) {
  const intervals = getAvailableBillingIntervals(source);
  const monthlyId = intervals.find((id) => {
    const plan = findPaymentPlan(source, id);
    return id === 'monthly'
      || id === 'month'
      || plan?.billingPeriod === 'every-month';
  });
  if (!monthlyId) return null;
  const charge = resolveIntervalCharge(source, monthlyId, campaignOn);
  if (!charge || charge.kind === 'one_time' || charge.kind === 'free') return null;
  if (billingPeriodMonthCount(charge.billingPeriod, charge.kind) !== 1) return null;
  return charge;
}

export function formatCatalogPrice(catalogOrTier, { interval = null, promoActive = null } = {}) {
  const source = catalogOrTier || {};
  const resolvedInterval = interval
    || source?.defaultInterval
    || source?.availableIntervals?.[0]
    || 'monthly';
  const campaignOn = promoActive == null
    ? true
    : promoActive !== false;
  const charge = resolveIntervalCharge(source, resolvedInterval, campaignOn);
  if (!charge) return null;

  const { currency, billingPeriod, kind, isSubscription } = charge;
  const monthly = toMonthlyEquivalent(charge.price, billingPeriod, kind);
  const compareMonthly = charge.compareAtPrice != null
    ? toMonthlyEquivalent(charge.compareAtPrice, billingPeriod, kind)
    : null;

  const showPerMonth = Boolean(monthly.normalized && isSubscription);
  const displayMonthly = showPerMonth ? monthly.monthlyPrice : charge.price;
  const compareDisplay = showPerMonth && compareMonthly?.monthlyPrice != null
    ? compareMonthly.monthlyPrice
    : charge.compareAtPrice;

  const amountText = formatMoneyAmount(displayMonthly, currency);
  const compareAtAmountText = Number.isFinite(compareDisplay) && compareDisplay > displayMonthly
    ? formatMoneyAmount(compareDisplay, currency)
    : null;
  const perMonthSuffix = showPerMonth ? '/mo' : (kind === 'one_time' ? '' : null);
  const billedLabel = billedLabelForPeriod(billingPeriod, kind);
  const periodAmountText = formatMoneyAmount(charge.price, currency);

  let savingsAmount = null;
  let savingsAmountText = null;
  let savingsText = null;
  let savingsTip = null;
  if (showPerMonth && monthly.months != null && monthly.months > 1) {
    const baseline = findMonthlyBaselineCharge(source, campaignOn);
    if (baseline && Number.isFinite(baseline.price)) {
      const fullAtMonthly = baseline.price * monthly.months;
      const saved = fullAtMonthly - charge.price;
      if (saved > 0.009) {
        savingsAmount = saved;
        savingsAmountText = formatMoneyAmount(saved, currency);
        savingsText = savingsPhrase(savingsAmountText, monthly.months);
        savingsTip = `Compared to ${formatMoneyAmount(baseline.price, currency)}/mo billed monthly`;
      }
    }
  }

  const priceText = showPerMonth
    ? `${amountText}/mo`
    : (kind === 'one_time' ? `${amountText} once` : amountText);

  const plan = findPaymentPlan(source, resolvedInterval);
  const slot = source?.intervals?.[resolvedInterval];
  const trialRaw = slot?.trialDays ?? plan?.trialDays ?? source?.trialDays;
  const trialDays = isSubscription && Number(trialRaw) > 0 ? Number(trialRaw) : 0;

  return {
    interval: resolvedInterval,
    planId: resolvedInterval,
    kind,
    label: formatBillingPlanLabel(resolvedInterval, source),
    isSubscription,
    amountText,
    perMonthSuffix,
    priceText,
    periodAmountText,
    billedLabel,
    billingPeriod,
    monthsPerCharge: monthly.months,
    savingsAmount,
    savingsAmountText,
    savingsText,
    savingsTip,
    compareAtAmountText,
    compareAtText: compareAtAmountText
      ? (showPerMonth ? `${compareAtAmountText}/mo` : compareAtAmountText)
      : null,
    discountLabel: charge.discountLabel,
    discountExpiryDate: charge.discountExpiryDate,
    discountDuration: charge.discountDuration,
    discountDurationInMonths: charge.discountDurationInMonths,
    trialDays,
    currency,
    promoActive: campaignOn,
    raw: charge.pricing
  };
}

/** Human-readable Creem discount duration (once / forever / repeating N months). */
export function formatDiscountDurationLabel(duration, durationInMonths = null) {
  const d = String(duration || '').trim().toLowerCase();
  if (!d) return null;
  if (d === 'once') return 'Promo applies to first charge only';
  if (d === 'forever') return 'Promo applies to every renewal';
  if (d === 'repeating') {
    const months = Number(durationInMonths);
    if (Number.isFinite(months) && months > 0) {
      return `Promo applies for ${months} month${months === 1 ? '' : 's'}`;
    }
    return 'Promo applies for a limited number of renewals';
  }
  return null;
}

/**
 * Product-level discount campaign switch from /v1/products (`promoActive`).
 * Explicit false pauses; missing field stays active (older cached catalogs).
 */
export function isCatalogPromoActive(catalog) {
  if (!catalog) return false;
  return catalog.promoActive !== false;
}

/** True when discounts should apply (switch on + inside start/end window). */
export function isCatalogPromoLive(catalog) {
  if (!isCatalogPromoActive(catalog)) return false;
  if (catalog.promoLive === false) return false;
  if (catalog.promoLive === true) return true;
  const now = Date.now();
  const promo = catalog?.promo;
  if (promo?.startsAt) {
    const ms = Date.parse(promo.startsAt);
    if (Number.isFinite(ms) && now < ms) return false;
  }
  if (promo?.endsAt) {
    const ms = Date.parse(promo.endsAt);
    if (Number.isFinite(ms) && now >= ms) return false;
  }
  if (promo?.phase === 'scheduled') return false;
  return true;
}

/**
 * Unified plans promo countdown from catalog.promo (or earliest per-interval expiry).
 * Returns null when campaign is paused or no schedule/label is available.
 */
export function getCatalogPromo(catalog) {
  if (!isCatalogPromoActive(catalog)) return null;
  const now = Date.now();
  const resolveLabel = (label, percent) => (
    label
    || (percent != null && Number.isFinite(Number(percent)) ? `${Number(percent)}% off` : null)
  );

  const fromRoot = catalog?.promo;
  if (fromRoot) {
    const title = String(fromRoot.title || '').trim() || null;
    const label = resolveLabel(fromRoot.discountLabel || title, fromRoot.discountPercent);
    const startsMs = fromRoot.startsAt ? Date.parse(fromRoot.startsAt) : null;
    const endsMs = fromRoot.endsAt ? Date.parse(fromRoot.endsAt) : null;
    if (Number.isFinite(startsMs) && now < startsMs && label) {
      return {
        phase: 'scheduled',
        startsAtMs: startsMs,
        endsAtMs: Number.isFinite(endsMs) ? endsMs : null,
        discountLabel: label,
        title
      };
    }
    if (Number.isFinite(endsMs) && endsMs > now && label) {
      return {
        phase: 'live',
        startsAtMs: Number.isFinite(startsMs) ? startsMs : null,
        endsAtMs: endsMs,
        discountLabel: label,
        title
      };
    }
    if (fromRoot.phase === 'live' && label && !Number.isFinite(endsMs)) {
      return {
        phase: 'live',
        startsAtMs: Number.isFinite(startsMs) ? startsMs : null,
        endsAtMs: null,
        discountLabel: label,
        title
      };
    }
    // Match Admin Preview: live campaign with a label but no usable end timestamp.
    if (label && isCatalogPromoLive(catalog) && !(Number.isFinite(startsMs) && now < startsMs)) {
      return {
        phase: 'live',
        startsAtMs: Number.isFinite(startsMs) ? startsMs : null,
        endsAtMs: Number.isFinite(endsMs) && endsMs > now ? endsMs : null,
        discountLabel: label,
        title
      };
    }
  }

  if (!isCatalogPromoLive(catalog)) return null;

  let endsAtMs = null;
  let discountLabel = null;
  for (const tier of getCatalogTiers(catalog)) {
    if (tier?.isFree) continue;
    const intervals = tier?.intervals && typeof tier.intervals === 'object'
      ? Object.values(tier.intervals)
      : [];
    for (const slot of intervals) {
      const pricing = slot?.pricing;
      if (!pricing) continue;
      const hasDiscount = Boolean(
        pricing.discountLabel
        || pricing.discountPercent != null
        || pricing.compareAtPriceCents != null
      );
      if (!hasDiscount) continue;
      const expiry = pricing.discountExpiryDate;
      if (!expiry) continue;
      const ms = Date.parse(expiry);
      if (!Number.isFinite(ms) || ms <= now) continue;
      if (endsAtMs == null || ms < endsAtMs) {
        endsAtMs = ms;
        discountLabel = resolveLabel(pricing.discountLabel, pricing.discountPercent);
      }
    }
  }
  if (endsAtMs == null || !discountLabel) return null;
  return { phase: 'live', endsAtMs, discountLabel, title: null, startsAtMs: null };
}

export function formatPromoCountdown(remainingMs) {
  const ms = Math.max(0, Number(remainingMs) || 0);
  const totalSec = Math.floor(ms / 1000);
  const days = Math.floor(totalSec / 86400);
  const hours = Math.floor((totalSec % 86400) / 3600);
  const minutes = Math.floor((totalSec % 3600) / 60);
  const seconds = totalSec % 60;
  const pad = (n) => String(n).padStart(2, '0');
  if (days > 0) {
    return `${days}d ${pad(hours)}:${pad(minutes)}:${pad(seconds)}`;
  }
  return `${pad(hours)}:${pad(minutes)}:${pad(seconds)}`;
}

export function getAvailableBillingIntervals(catalogOrTier) {
  const fromApi = Array.isArray(catalogOrTier?.availableIntervals)
    ? catalogOrTier.availableIntervals.map(String).filter(Boolean)
    : [];
  if (fromApi.length) return fromApi;

  const fromPlans = (catalogOrTier?.paymentPlans || [])
    .filter((p) => p?.creemProductId || p?.kind === 'free' || Number(p?.priceCents) > 0)
    .map((p) => p.id)
    .filter(Boolean);
  if (fromPlans.length) return fromPlans;

  const intervals = [];
  if (catalogOrTier?.intervals?.monthly || catalogOrTier?.pricing) intervals.push('monthly');
  if (catalogOrTier?.intervals?.yearly) intervals.push('yearly');
  // Any other interval keys on the tier
  for (const key of Object.keys(catalogOrTier?.intervals || {})) {
    if (!intervals.includes(key)) intervals.push(key);
  }
  return intervals;
}

export function planHasCheckout(tier, planId) {
  if (!tier || !planId) return false;
  if (tier.intervals?.[planId]?.creemProductId) return true;
  const plan = findPaymentPlan(tier, planId);
  if (plan?.creemProductId) return true;
  if (planId === 'yearly' && tier.creemProductIdYearly) return true;
  if ((planId === 'monthly' || !planId) && tier.creemProductId) return true;
  return false;
}

export function getCatalogTiers(catalog) {
  if (Array.isArray(catalog?.tiers) && catalog.tiers.length) {
    return catalog.tiers.slice().sort((a, b) => (Number(a.rank) || 0) - (Number(b.rank) || 0));
  }
  // Legacy flat catalog → synthesize Free + Pro
  const intervals = getAvailableBillingIntervals(catalog);
  return [
    {
      id: 'free',
      name: 'Free',
      rank: 0,
      isFree: true,
      isPaid: false,
      features: [],
      availableIntervals: [],
      intervals: {}
    },
    {
      id: 'pro',
      name: catalog?.name || 'Pro',
      rank: 10,
      isFree: false,
      isPaid: true,
      features: catalog?.features || [],
      availableIntervals: intervals.length ? intervals : ['monthly'],
      defaultInterval: catalog?.defaultInterval || 'monthly',
      intervals: catalog?.intervals || {},
      pricing: catalog?.pricing || null,
      creemProductId: catalog?.creemProductId || null,
      creemProductIdYearly: catalog?.creemProductIdYearly || null
    }
  ];
}

export function resolvePlanIdFromStatus(status = null) {
  const plan = String(status?.plan || status?.tier || status?.planId || 'free')
    .trim()
    .toLowerCase();
  if (!plan || plan === 'none') return 'free';
  return plan;
}

function findCatalogFeatureDef(catalog, featureId) {
  const id = String(featureId || '').trim();
  if (!id) return null;
  const defs = Array.isArray(catalog?.featureDefs) ? catalog.featureDefs : [];
  return defs.find((d) => d && String(d.id || '').trim() === id) || null;
}

function resolveCatalogTier(catalog, planId) {
  const tiers = getCatalogTiers(catalog);
  return tiers.find((t) => String(t.id || '').toLowerCase() === planId)
    || (planId === 'free'
      ? tiers.find((t) => t.isFree || Number(t.rank) === 0)
      : null)
    || null;
}

/** Prefer tier quotas[limitKey]; else presets by plan rank. */
function fallbackLimitFromPresets(def, planId, tier) {
  const presets = (Array.isArray(def?.presets) ? def.presets : [])
    .map((n) => Math.floor(Number(n)))
    .filter((n) => Number.isFinite(n) && n >= 0);
  if (!presets.length) return null;
  if (planId === 'premium' || Number(tier?.rank) >= 20) {
    return presets[presets.length - 1];
  }
  if (planId === 'pro' || Number(tier?.rank) >= 10) {
    return presets[Math.min(1, presets.length - 1)];
  }
  return presets[0];
}

/**
 * Read an adjustable plan limit for a feature via its schema `limitKey`
 * (values live in `tier.quotas`).
 */
export function getPlanLimitForFeature(featureId, {
  status = cachedStatus,
  catalog = cachedCatalog,
  defaultValue = 0
} = {}) {
  const def = findCatalogFeatureDef(catalog, featureId);
  const limitKey = String(def?.limitKey || '').trim();
  if (!limitKey) return defaultValue;

  const planId = resolvePlanIdFromStatus(status);
  const tier = resolveCatalogTier(catalog, planId);
  const quotas = tier?.quotas && typeof tier.quotas === 'object' ? tier.quotas : {};
  const raw = quotas[limitKey];
  if (raw != null && raw !== '') {
    const n = Math.floor(Number(raw));
    if (Number.isFinite(n) && n >= 0) return n;
  }

  const fromPresets = fallbackLimitFromPresets(def, planId, tier);
  return fromPresets == null ? defaultValue : fromPresets;
}

/** Clear-404 concurrent scan multiplier (feature `clear_404_bookmarks` → its limitKey). */
export function getClear404ScanSpeedMultiplier(opts = {}) {
  const n = getPlanLimitForFeature('clear_404_bookmarks', {
    ...opts,
    defaultValue: 1
  });
  return Math.max(1, Math.min(8, n || 1));
}

/** Config-only / storage keys — hide as Account plan bullets. */
export function isPlanConfigFeatureId(featureId) {
  const id = String(featureId || '').trim();
  return id.endsWith('_scan_speed')
    || id.endsWith('_limit')
    || id.endsWith('_quota');
}

/** Immediate previous tier by rank (sorted ascending). */
export function getPreviousTierByRank(tiers, tier) {
  const sorted = (tiers || [])
    .slice()
    .sort((a, b) => (Number(a.rank) || 0) - (Number(b.rank) || 0));
  const idx = sorted.findIndex((t) => t.id === tier?.id);
  if (idx <= 0) return null;
  return sorted[idx - 1] || null;
}

/** Active UI language code (e.g. en, zh_CN), including system → browser locale. */
export function resolveActiveUiLanguage() {
  try {
    if (typeof window.getCurrentUILanguage === 'function') {
      const selected = window.getCurrentUILanguage();
      if (selected && selected !== 'system') return selected;
    }
  } catch {
    // ignore
  }
  return (chrome.i18n.getUILanguage() || 'en').replace('-', '_');
}

export function preferZhFeatureLabels(lang = resolveActiveUiLanguage()) {
  return String(lang || '').toLowerCase().replace('-', '_').startsWith('zh');
}

/**
 * Pick feature display name from catalog featureDefs.labels.
 * Prefer exact UI locale, then zh_CN/zh for Chinese UIs, then en, then any zh, then label.
 */
export function labelFromFeatureDef(def, { lang = resolveActiveUiLanguage() } = {}) {
  if (!def) return null;
  const labels = def.labels && typeof def.labels === 'object' ? def.labels : {};
  const normalized = String(lang || 'en').replace('-', '_');
  const lower = normalized.toLowerCase();

  const pick = (...keys) => {
    for (const key of keys) {
      const text = String(labels[key] || '').trim();
      if (text) return text;
    }
    return null;
  };

  const exact = pick(normalized)
    || (lower === 'zh_cn' ? pick('zh_CN', 'zh') : null)
    || (lower === 'zh_tw' ? pick('zh_TW', 'zh_CN', 'zh') : null)
    || (lower === 'zh_hk' ? pick('zh_HK', 'zh_TW', 'zh_CN', 'zh') : null)
    || (lower.startsWith('pt') ? pick('pt_BR') : null)
    || ((lower === 'nb' || lower === 'no' || lower === 'nn') ? pick('nb') : null);

  if (exact) return exact;
  if (lower.startsWith('zh')) {
    return pick('zh_CN', 'zh', 'zh_TW', 'zh_HK', 'en') || def.labelZh || def.label || null;
  }
  return pick('en', normalized) || def.label || pick('zh_CN', 'zh') || def.labelZh || null;
}

export function featureLabelFromCatalog(featureId, catalog = cachedCatalog, options = {}) {
  const id = String(featureId || '').trim();
  if (!id) return null;
  const defs = Array.isArray(catalog?.featureDefs) ? catalog.featureDefs : [];
  const def = defs.find((d) => String(d?.id || '').trim() === id);
  return labelFromFeatureDef(def, options);
}

export async function clearMembershipSession() {
  const sessionStore = await storageGet([MEMBERSHIP_SESSION_KEY]);
  const token = sessionStore[MEMBERSHIP_SESSION_KEY];
  if (token) {
    try {
      await fetch(`${MEMBERSHIP_API_BASE}/v1/logout`, {
        method: 'POST',
        headers: { Authorization: `Bearer ${token}` }
      });
    } catch (_) {
      // ignore network errors on logout
    }
  }
  cachedStatus = emptyStatus();
  await storageRemove([MEMBERSHIP_SESSION_KEY]);
  await storageSet({ [MEMBERSHIP_STORAGE_KEY]: cachedStatus });
  return cachedStatus;
}

export async function signInWithGoogle() {
  if (!chrome.identity?.launchWebAuthFlow) {
    throw new Error('IDENTITY_UNAVAILABLE');
  }

  const redirectUrl = chrome.identity.getRedirectURL();
  const authUrl = `${MEMBERSHIP_API_BASE}/v1/auth/google/start?extension_redirect=${encodeURIComponent(redirectUrl)}`;

  const responseUrl = await new Promise((resolve, reject) => {
    chrome.identity.launchWebAuthFlow(
      { url: authUrl, interactive: true },
      (callbackUrl) => {
        if (chrome.runtime.lastError) {
          reject(new Error(chrome.runtime.lastError.message));
          return;
        }
        if (!callbackUrl) {
          reject(new Error('LOGIN_CANCELLED'));
          return;
        }
        resolve(callbackUrl);
      }
    );
  });

  const parsed = new URL(responseUrl);
  const session = parsed.searchParams.get('session');
  if (!session) {
    throw new Error('SESSION_MISSING');
  }

  await storageSet({ [MEMBERSHIP_SESSION_KEY]: session });
  return refreshMembershipStatus({ force: true });
}

export async function startProCheckout({
  billingInterval = null,
  planId = null,
  tierId = 'pro'
} = {}) {
  const sessionStore = await storageGet([MEMBERSHIP_SESSION_KEY]);
  const token = sessionStore[MEMBERSHIP_SESSION_KEY];
  if (!token) {
    throw new Error('NOT_SIGNED_IN');
  }

  const resolvedPlanId = String(planId || billingInterval || 'monthly')
    .trim()
    .toLowerCase() || 'monthly';
  const resolvedTierId = String(tierId || 'pro').trim().toLowerCase() || 'pro';

  const res = await fetch(`${MEMBERSHIP_API_BASE}/v1/checkout`, {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${token}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      productId: PRODUCT_ID,
      tierId: resolvedTierId,
      planId: resolvedPlanId,
      billingInterval: resolvedPlanId,
      successUrl: `${MEMBERSHIP_API_BASE}/v1/checkout/success`
    })
  });

  const data = await res.json().catch(() => ({}));
  if (!res.ok) {
    const err = new Error(data.error || 'CHECKOUT_FAILED');
    err.detail = data.message;
    throw err;
  }

  if (!data.checkoutUrl) {
    throw new Error('CHECKOUT_URL_MISSING');
  }

  await chrome.tabs.create({ url: data.checkoutUrl });
  return data;
}

export async function openBillingPortal() {
  const sessionStore = await storageGet([MEMBERSHIP_SESSION_KEY]);
  const token = sessionStore[MEMBERSHIP_SESSION_KEY];
  if (!token) {
    throw new Error('NOT_SIGNED_IN');
  }

  const res = await fetch(`${MEMBERSHIP_API_BASE}/v1/billing-portal`, {
    method: 'POST',
    headers: { Authorization: `Bearer ${token}` }
  });
  const data = await res.json().catch(() => ({}));
  if (!res.ok || !data.portalUrl) {
    throw new Error(data.error || 'PORTAL_FAILED');
  }
  await chrome.tabs.create({ url: data.portalUrl });
  return data;
}

export async function redeemActivationCode(code) {
  const sessionStore = await storageGet([MEMBERSHIP_SESSION_KEY]);
  const token = sessionStore[MEMBERSHIP_SESSION_KEY];
  if (!token) {
    throw new Error('NOT_SIGNED_IN');
  }

  const trimmed = String(code || '').trim();
  if (!trimmed) {
    throw new Error('invalid_code');
  }

  const res = await fetch(`${MEMBERSHIP_API_BASE}/v1/activate`, {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${token}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      code: trimmed,
      productId: PRODUCT_ID
    })
  });

  const data = await res.json().catch(() => ({}));
  if (!res.ok) {
    const err = new Error(data.error || 'ACTIVATE_FAILED');
    err.detail = data.message;
    throw err;
  }

  cachedStatus = statusFromUser(data.user, data);
  await storageSet({ [MEMBERSHIP_STORAGE_KEY]: cachedStatus });
  return { status: cachedStatus, data };
}

export function getMembershipErrorMessage(error) {
  const code = error?.message || '';
  switch (code) {
    case 'NOT_SIGNED_IN':
      return chrome.i18n.getMessage('accountSignInRequired') || 'Please sign in with Google first';
    case 'LOGIN_CANCELLED':
      return chrome.i18n.getMessage('accountLoginCancelled') || 'Sign-in cancelled';
    case 'IDENTITY_UNAVAILABLE':
      return chrome.i18n.getMessage('accountIdentityUnavailable') || 'Google sign-in is unavailable in this browser';
    case 'already_pro':
      return chrome.i18n.getMessage('accountAlreadyPro') || 'You already have Pro for this product';
    case 'product_not_found':
      return chrome.i18n.getMessage('accountProductNotFound') || 'Membership product is not configured';
    case 'invalid_code':
      return chrome.i18n.getMessage('accountInvalidCode') || 'Invalid activation code';
    case 'code_revoked':
      return chrome.i18n.getMessage('accountCodeRevoked') || 'This activation code has been revoked';
    case 'code_expired':
      return chrome.i18n.getMessage('accountCodeExpired') || 'This activation code has expired';
    case 'code_exhausted':
      return chrome.i18n.getMessage('accountCodeExhausted') || 'This activation code has already been used up';
    case 'already_redeemed':
      return chrome.i18n.getMessage('accountCodeAlreadyRedeemed') || 'You have already redeemed this code';
    case 'tier_downgrade_blocked':
      return chrome.i18n.getMessage('accountTierDowngradeBlocked')
        || (error.detail || 'You already have a higher membership tier');
    case 'code_product_mismatch':
      return chrome.i18n.getMessage('accountCodeProductMismatch')
        || (error.detail || 'This code is for a different product');
    case 'yearly_not_configured':
      return chrome.i18n.getMessage('accountYearlyNotConfigured')
        || 'Yearly billing is not available for this product yet';
    case 'tier_not_configured':
    case 'invalid_tier':
      return error.detail
        || chrome.i18n.getMessage('accountTierNotConfigured')
        || 'This membership tier is not available yet';
    case 'CHECKOUT_FAILED':
    case 'checkout_failed':
      return chrome.i18n.getMessage('accountCheckoutFailed')
        || `Checkout failed${error.detail ? `: ${error.detail}` : ''}`;
    case 'PORTAL_FAILED':
    case 'portal_failed':
      return chrome.i18n.getMessage('accountPortalFailed')
        || 'Could not open billing portal. Please try again.';
    case 'no_creem_customer':
      return chrome.i18n.getMessage('accountNoBillingCustomer')
        || 'No billing account found. Complete a purchase first.';
    case 'ACTIVATE_FAILED':
      return chrome.i18n.getMessage('accountActivateFailed') || 'Could not activate with this code';
    default:
      return chrome.i18n.getMessage('accountGenericError')
        || `Account error: ${String(code).slice(0, 120)}`;
  }
}

/** Show a lightweight upgrade / sign-in toast. */
export function showProRequiredToast(featureId, { signedIn } = {}) {
  const legacyLabels = {
    [PRO_FEATURES.AI_ORGANIZE]: chrome.i18n.getMessage('proFeatureAiOrganize') || 'AI Organize',
    [PRO_FEATURES.AI_RECOMMEND]: chrome.i18n.getMessage('proFeatureAiRecommend') || 'AI Recommend',
    [PRO_FEATURES.AI_INTRODUCE]: chrome.i18n.getMessage('proFeatureAiIntroduce') || 'AI Website Intro',
    [PRO_FEATURES.PLUGIN_BAR]: chrome.i18n.getMessage('proFeaturePluginBar') || 'Plugin Bar',
    [PRO_FEATURES.BING_WALLPAPER]: chrome.i18n.getMessage('proFeatureBingWallpaper') || 'Bing Wallpapers',
    [PRO_FEATURES.COMPARE_SEARCH]: chrome.i18n.getMessage('proFeatureCompareSearch') || 'Compare Search',
    [PRO_FEATURES.QUICK_LINKS_DRAG_TO_REORDER]: chrome.i18n.getMessage('proFeatureQuickLinksDrag') || 'Quick links drag to reorder',
    [PRO_FEATURES.CUSTOMIZE_MODEL_PROVIDERS]: chrome.i18n.getMessage('proFeatureCustomizeProviders') || 'Custom model providers'
  };

  const resolveAndShow = async (isSignedIn) => {
    let catalog = cachedCatalog;
    if (!catalog) {
      try {
        catalog = await fetchProductCatalog();
      } catch {
        catalog = null;
      }
    }
    const featureName = featureLabelFromCatalog(featureId, catalog)
      || legacyLabels[featureId]
      || featureId
      || 'This feature';

    if (!isSignedIn) {
      showMembershipToast(
        chrome.i18n.getMessage('proFeatureSignInRequired', [featureName])
        || `Sign in required to use ${featureName}`
      );
      return;
    }

    const required = resolveMinimumPlanForFeature(featureId, catalog);
    const planName = localizedPlanLabel(required);
    showMembershipToast(
      chrome.i18n.getMessage('proFeatureLocked', [featureName, planName])
      || `${featureName} requires ${planName}. Please upgrade to unlock.`
    );
  };

  if (typeof signedIn === 'boolean') {
    resolveAndShow(signedIn);
    return;
  }

  getCachedMembershipStatus()
    .then((status) => resolveAndShow(Boolean(status?.signedIn)))
    .catch(() => resolveAndShow(false));
}

function showMembershipToast(message, duration = 3200) {
  if (typeof window.Utilities?.showToast === 'function') {
    window.Utilities.showToast(message, duration);
    return;
  }

  const toast = document.getElementById('more-button-toast');
  if (!toast) return;
  const toastMessage = toast.querySelector('p');
  if (toastMessage) toastMessage.textContent = message;
  toast.classList.add('show');
  setTimeout(() => toast.classList.remove('show'), duration);
}

/**
 * Hosted OpenRouter AI usage for the signed-in member (from /v1/me cache).
 */
export async function getAiUsage({ refresh = false } = {}) {
  const status = refresh
    ? await refreshMembershipStatus({ force: true })
    : await getCachedMembershipStatus();
  return status?.ai || null;
}

export async function canUseHostedAi(feature = PRO_FEATURES.AI_ORGANIZE) {
  const status = await getCachedMembershipStatus();
  if (!status?.signedIn) return false;
  const featureId = String(feature || PRO_FEATURES.AI_ORGANIZE);
  const slot = status?.ai?.features?.[featureId];
  if (slot && Number.isFinite(Number(slot.remaining))) {
    return Number(slot.remaining) > 0;
  }
  // Legacy flat remaining (treat as organize bucket)
  if (featureId === PRO_FEATURES.AI_ORGANIZE) {
    const remaining = Number(status?.ai?.remaining);
    if (Number.isFinite(remaining)) return remaining > 0;
    return Number(status?.ai?.limit) > 0;
  }
  return false;
}

/**
 * Call membership Worker OpenRouter proxy (OpenAI-compatible response).
 */
export async function callHostedAiChat({
  messages,
  temperature = 0.2,
  maxTokens = 2048,
  model = null,
  feature = PRO_FEATURES.AI_ORGANIZE
} = {}) {
  const sessionStore = await storageGet([MEMBERSHIP_SESSION_KEY]);
  const token = sessionStore[MEMBERSHIP_SESSION_KEY];
  if (!token) {
    const error = new Error('NOT_SIGNED_IN');
    error.code = 'NOT_SIGNED_IN';
    throw error;
  }

  const modelId = String(model || '').trim();
  if (!modelId) {
    const error = new Error('MODEL_REQUIRED');
    error.code = 'MODEL_REQUIRED';
    throw error;
  }

  const featureId = String(feature || PRO_FEATURES.AI_ORGANIZE).trim() || PRO_FEATURES.AI_ORGANIZE;

  const res = await fetch(`${MEMBERSHIP_API_BASE}/v1/ai/chat`, {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${token}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      productId: PRODUCT_ID,
      messages,
      temperature,
      max_tokens: maxTokens,
      model: modelId,
      feature: featureId
    })
  });

  const data = await res.json().catch(() => ({}));
  if (data?.ai) {
    const status = await getCachedMembershipStatus();
    cachedStatus = { ...status, ai: data.ai, updatedAt: Date.now() };
    await storageSet({ [MEMBERSHIP_STORAGE_KEY]: cachedStatus });
  }

  if (!res.ok) {
    const error = new Error(data?.error || `HTTP_${res.status}`);
    error.status = res.status;
    error.code = data?.error || `HTTP_${res.status}`;
    error.ai = data?.ai || null;
    error.messageDetail = data?.message || '';
    throw error;
  }

  return data;
}

window.addEventListener('supermark:pro-required', (event) => {
  showProRequiredToast(event.detail?.featureId, {
    signedIn: event.detail?.signedIn
  });
});

window.SuperMarkMembership = {
  PRODUCT_ID,
  PRO_FEATURES,
  isFeatureEntitled,
  requireProFeature,
  resolveMinimumPlanForFeature,
  featureLabelFromCatalog,
  labelFromFeatureDef,
  refreshMembershipStatus,
  getCachedMembershipStatus,
  fetchProductCatalog,
  formatCatalogPrice,
  formatBillingPlanLabel,
  findPaymentPlan,
  planHasCheckout,
  getAvailableBillingIntervals,
  getCatalogTiers,
  getCatalogPromo,
  isCatalogPromoActive,
  isCatalogPromoLive,
  getPreviousTierByRank,
  getPlanLimitForFeature,
  getClear404ScanSpeedMultiplier,
  isPlanConfigFeatureId,
  signInWithGoogle,
  startProCheckout,
  redeemActivationCode,
  getAiUsage,
  canUseHostedAi,
  callHostedAiChat
};
