import AsyncStorage from '@react-native-async-storage/async-storage';
import {
  BackendApiError,
  BillingProvider,
  BillingSummary,
  HealthCheckRequest,
  HealthCheckResponse,
  PlanId,
  PurchaseProductId,
  RevenueCatCustomerInfo,
  RevenueCatEntitlementInfo,
  RevenueCatNonSubscriptionTransaction,
  RevenueCatSyncSource,
  ScanPlantRequest,
  ScanPlantResponse,
  SemanticSearchRequest,
  SemanticSearchResponse,
  SimulatePurchaseRequest,
  SimulatePurchaseResponse,
  SimulateWebhookRequest,
  SimulateWebhookResponse,
  SyncRevenueCatStateResponse,
  isBackendApiError,
} from './contracts';
import { getMockPlantByImage, searchMockCatalog } from './mockCatalog';
import { openAiScanService } from './openAiScanService';
import { IdentificationResult, PlantHealthCheck } from '../../types';

const MOCK_ACCOUNT_STORE_KEY = 'greenlens_mock_backend_accounts_v1';
const MOCK_IDEMPOTENCY_STORE_KEY = 'greenlens_mock_backend_idempotency_v1';

const FREE_MONTHLY_CREDITS = 15;
const GUEST_TRIAL_CREDITS = 5;
const PRO_MONTHLY_CREDITS = 250;

const SCAN_PRIMARY_COST = 1;
const SCAN_REVIEW_COST = 1;
const SEMANTIC_SEARCH_COST = 2;
const HEALTH_CHECK_COST = 2;

const LOW_CONFIDENCE_REVIEW_THRESHOLD = 0.8;
const FREE_SIMULATED_DELAY_MS = 1100;
const PRO_SIMULATED_DELAY_MS = 280;

const TOPUP_DEFAULT_CREDITS = 60;

const TOPUP_CREDITS_BY_PRODUCT: Record<PurchaseProductId, number> = {
  monthly_pro: 0,
  yearly_pro: 0,
  topup_small: 25,
  topup_medium: 120,
  topup_large: 300,
};

const REVENUECAT_PRO_ENTITLEMENT_ID = (process.env.EXPO_PUBLIC_REVENUECAT_PRO_ENTITLEMENT_ID || 'pro').trim() || 'pro';
const SUPPORTED_REVENUECAT_SUBSCRIPTION_PRODUCTS = new Set<PurchaseProductId>(['monthly_pro', 'yearly_pro']);

interface MockAccountRecord {
  userId: string;
  plan: PlanId;
  provider: BillingProvider;
  cycleStartedAt: string;
  cycleEndsAt: string;
  monthlyAllowance: number;
  usedThisCycle: number;
  topupBalance: number;
  renewsAt: string | null;
  updatedAt: string;
}

interface IdempotencyRecord {
  response: unknown;
  createdAt: string;
}

type AccountStore = Record<string, MockAccountRecord>;
type IdempotencyStore = Record<string, IdempotencyRecord>;

const userLocks = new Map<string, Promise<void>>();

const nowIso = (): string => new Date().toISOString();

const startOfUtcMonth = (date: Date): Date => {
  return new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), 1, 0, 0, 0, 0));
};

const addUtcMonths = (date: Date, months: number): Date => {
  return new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth() + months, 1, 0, 0, 0, 0));
};

const addDays = (date: Date, days: number): Date => {
  const result = new Date(date.getTime());
  result.setUTCDate(result.getUTCDate() + days);
  return result;
};

const getCycleBounds = (now: Date) => {
  const cycleStartedAt = startOfUtcMonth(now);
  const cycleEndsAt = addUtcMonths(cycleStartedAt, 1);
  return { cycleStartedAt, cycleEndsAt };
};

const getMonthlyAllowanceForPlan = (plan: PlanId, userId?: string): number => {
  if (userId === 'guest') return GUEST_TRIAL_CREDITS;
  return plan === 'pro' ? PRO_MONTHLY_CREDITS : FREE_MONTHLY_CREDITS;
};

const getSimulatedDelay = (plan: PlanId): number => {
  return plan === 'pro' ? PRO_SIMULATED_DELAY_MS : FREE_SIMULATED_DELAY_MS;
};

const sleep = async (ms: number): Promise<void> => {
  if (ms <= 0) return;
  await new Promise(resolve => setTimeout(resolve, ms));
};

const withUserLock = async <T>(userId: string, worker: () => Promise<T>): Promise<T> => {
  const previousLock = userLocks.get(userId) || Promise.resolve();
  let releaseLock: () => void = () => {};
  const activeLock = new Promise<void>((resolve) => {
    releaseLock = resolve;
  });
  userLocks.set(userId, activeLock);

  await previousLock;
  try {
    return await worker();
  } finally {
    releaseLock();
    if (userLocks.get(userId) === activeLock) {
      userLocks.delete(userId);
    }
  }
};

const readJson = async <T,>(key: string, fallbackValue: T): Promise<T> => {
  try {
    const raw = await AsyncStorage.getItem(key);
    if (!raw) return fallbackValue;
    return JSON.parse(raw) as T;
  } catch (error) {
    console.error(`Failed to read mock backend key ${key}`, error);
    return fallbackValue;
  }
};

const writeJson = async <T,>(key: string, value: T): Promise<void> => {
  try {
    await AsyncStorage.setItem(key, JSON.stringify(value));
  } catch (error) {
    console.error(`Failed to write mock backend key ${key}`, error);
  }
};

const loadStores = async (): Promise<{ accounts: AccountStore; idempotency: IdempotencyStore }> => {
  const [accounts, idempotency] = await Promise.all([
    readJson<AccountStore>(MOCK_ACCOUNT_STORE_KEY, {}),
    readJson<IdempotencyStore>(MOCK_IDEMPOTENCY_STORE_KEY, {}),
  ]);
  return { accounts, idempotency };
};

const persistStores = async (stores: { accounts: AccountStore; idempotency: IdempotencyStore }): Promise<void> => {
  await Promise.all([
    writeJson(MOCK_ACCOUNT_STORE_KEY, stores.accounts),
    writeJson(MOCK_IDEMPOTENCY_STORE_KEY, stores.idempotency),
  ]);
};

const buildDefaultAccount = (userId: string, now: Date): MockAccountRecord => {
  const { cycleStartedAt, cycleEndsAt } = getCycleBounds(now);
  const allowance = getMonthlyAllowanceForPlan('free', userId);
  return {
    userId,
    plan: 'free',
    provider: 'mock',
    cycleStartedAt: cycleStartedAt.toISOString(),
    cycleEndsAt: cycleEndsAt.toISOString(),
    monthlyAllowance: allowance,
    usedThisCycle: 0,
    topupBalance: 0,
    renewsAt: null,
    updatedAt: nowIso(),
  };
};

const alignAccountToCurrentCycle = (account: MockAccountRecord, now: Date): MockAccountRecord => {
  const next = { ...account };
  const expectedMonthlyAllowance = getMonthlyAllowanceForPlan(next.plan, next.userId);
  if (next.monthlyAllowance !== expectedMonthlyAllowance) {
    next.monthlyAllowance = expectedMonthlyAllowance;
  }

  if (!next.renewsAt && next.plan === 'pro' && next.provider === 'mock') {
    next.renewsAt = addDays(now, 30).toISOString();
  }

  const cycleEndsAtMs = new Date(next.cycleEndsAt).getTime();
  if (Number.isNaN(cycleEndsAtMs) || now.getTime() >= cycleEndsAtMs) {
    const { cycleStartedAt, cycleEndsAt } = getCycleBounds(now);
    next.cycleStartedAt = cycleStartedAt.toISOString();
    next.cycleEndsAt = cycleEndsAt.toISOString();
    next.usedThisCycle = 0;
    next.monthlyAllowance = expectedMonthlyAllowance;
  }

  return next;
};

const getOrCreateAccount = (stores: { accounts: AccountStore }, userId: string): MockAccountRecord => {
  const now = new Date();
  const existing = stores.accounts[userId] || buildDefaultAccount(userId, now);
  const aligned = alignAccountToCurrentCycle(existing, now);
  stores.accounts[userId] = aligned;
  return aligned;
};

const getAvailableCredits = (account: MockAccountRecord): number => {
  const monthlyRemaining = Math.max(0, account.monthlyAllowance - account.usedThisCycle);
  return monthlyRemaining + Math.max(0, account.topupBalance);
};

const buildBillingSummary = (account: MockAccountRecord): BillingSummary => {
  return {
    entitlement: {
      plan: account.plan,
      provider: account.provider,
      status: account.plan === 'pro' ? 'active' : 'inactive',
      renewsAt: account.renewsAt,
    },
    credits: {
      monthlyAllowance: account.monthlyAllowance,
      usedThisCycle: account.usedThisCycle,
      topupBalance: account.topupBalance,
      available: getAvailableCredits(account),
      cycleStartedAt: account.cycleStartedAt,
      cycleEndsAt: account.cycleEndsAt,
    },
    availableProducts: ['monthly_pro', 'yearly_pro', 'topup_small', 'topup_medium', 'topup_large'],
  };
};

const normalizeRevenueCatTransactions = (
  customerInfo: RevenueCatCustomerInfo,
): RevenueCatNonSubscriptionTransaction[] => {
  const nonSubscriptions = customerInfo?.nonSubscriptions || {};
  return Object.values(nonSubscriptions).flatMap((entries) => Array.isArray(entries) ? entries : []);
};

const summarizeRevenueCatCustomerInfo = (customerInfo: RevenueCatCustomerInfo) => {
  const activeEntitlements = customerInfo?.entitlements?.active || {};
  return {
    appUserId: customerInfo?.appUserId ?? null,
    originalAppUserId: customerInfo?.originalAppUserId ?? null,
    activeEntitlements: Object.entries(activeEntitlements).map(([id, entitlement]) => ({
      id,
      productIdentifier: entitlement?.productIdentifier ?? null,
      expirationDate: entitlement?.expirationDate || entitlement?.expiresDate || null,
    })),
    allPurchasedProductIdentifiers: customerInfo?.allPurchasedProductIdentifiers ?? [],
    nonSubscriptionTransactions: normalizeRevenueCatTransactions(customerInfo).map((transaction) => ({
      productIdentifier: transaction?.productIdentifier ?? null,
      transactionIdentifier: transaction?.transactionIdentifier || transaction?.transactionId || null,
    })),
  };
};

const getValidProEntitlement = (customerInfo: RevenueCatCustomerInfo): RevenueCatEntitlementInfo | null => {
  const activeEntitlements = customerInfo?.entitlements?.active || {};
  const proEntitlement = activeEntitlements[REVENUECAT_PRO_ENTITLEMENT_ID];
  if (!proEntitlement) {
    return null;
  }

  if (
    proEntitlement.productIdentifier
    && SUPPORTED_REVENUECAT_SUBSCRIPTION_PRODUCTS.has(proEntitlement.productIdentifier as PurchaseProductId)
  ) {
    return proEntitlement;
  }

  console.warn('[Billing][Mock] Ignoring unsupported RevenueCat pro entitlement', summarizeRevenueCatCustomerInfo(customerInfo));
  return null;
};

const readIdempotentResponse = <T,>(store: IdempotencyStore, key: string): T | null => {
  const record = store[key];
  if (!record) return null;
  return record.response as T;
};

const writeIdempotentResponse = <T,>(store: IdempotencyStore, key: string, value: T): void => {
  store[key] = {
    response: value,
    createdAt: nowIso(),
  };
};

const consumeCredits = (account: MockAccountRecord, cost: number): number => {
  if (cost <= 0) return 0;

  const available = getAvailableCredits(account);
  if (available < cost) {
    throw new BackendApiError(
      'INSUFFICIENT_CREDITS',
      `Insufficient credits. Required ${cost}, available ${available}.`,
      402,
      { required: cost, available },
    );
  }

  let remaining = cost;
  const monthlyRemaining = Math.max(0, account.monthlyAllowance - account.usedThisCycle);
  if (monthlyRemaining > 0) {
    const monthlyUsage = Math.min(monthlyRemaining, remaining);
    account.usedThisCycle += monthlyUsage;
    remaining -= monthlyUsage;
  }

  if (remaining > 0 && account.topupBalance > 0) {
    const topupUsage = Math.min(account.topupBalance, remaining);
    account.topupBalance -= topupUsage;
    remaining -= topupUsage;
  }

  return cost;
};

const consumeCreditsWithIdempotency = (
  account: MockAccountRecord,
  idempotencyStore: IdempotencyStore,
  key: string,
  cost: number,
): number => {
  const existing = readIdempotentResponse<{ charged: number }>(idempotencyStore, key);
  if (existing) return existing.charged;

  const charged = consumeCredits(account, cost);
  writeIdempotentResponse(idempotencyStore, key, { charged });
  return charged;
};

const endpointKey = (scope: string, userId: string, idempotencyKey: string): string => {
  return `endpoint:${scope}:${userId}:${idempotencyKey}`;
};

const chargeKey = (scope: string, userId: string, idempotencyKey: string): string => {
  return `charge:${scope}:${userId}:${idempotencyKey}`;
};

const hashString = (value: string): number => {
  let hash = 0;
  for (let i = 0; i < value.length; i += 1) {
    hash = ((hash << 5) - hash + value.charCodeAt(i)) | 0;
  }
  return Math.abs(hash);
};

const clampConfidence = (value: number): number => {
  return Math.max(0.05, Math.min(0.99, Number(value.toFixed(2))));
};

const buildMockHealthCheck = (request: HealthCheckRequest, creditsCharged: number): PlantHealthCheck => {
  const source = [
    request.imageUri,
    request.plantContext?.name || '',
    request.plantContext?.botanicalName || '',
    request.plantContext?.careInfo?.light || '',
    request.plantContext?.careInfo?.temp || '',
  ].join('|');
  const hash = hashString(source);
  const score = 40 + (hash % 56);

  const status: PlantHealthCheck['status'] = score >= 75
    ? 'healthy'
    : score >= 55
      ? 'watch'
      : 'critical';

  const confidenceBase = 0.45 + ((hash % 20) / 100);
  const confidenceMid = 0.35 + (((hash >> 2) % 20) / 100);
  const confidenceLow = 0.25 + (((hash >> 4) % 20) / 100);

  if (request.language === 'de') {
    const likelyIssues = status === 'critical'
      ? [
        {
          title: 'Moegliche Ueberwaesserung',
          confidence: clampConfidence(confidenceBase + 0.22),
          details: 'Gelbe, weiche Blaetter koennen auf zu nasse Erde hindeuten.',
        },
        {
          title: 'Wurzelstress',
          confidence: clampConfidence(confidenceMid + 0.15),
          details: 'Pruefe, ob das Substrat verdichtet ist oder unangenehm riecht.',
        },
        {
          title: 'Lichtmangel',
          confidence: clampConfidence(confidenceLow + 0.1),
          details: 'Zu wenig Licht kann zu Vergilbung und schwaecherem Wuchs fuehren.',
        },
      ]
      : status === 'watch'
        ? [
          {
            title: 'Leichter Naehrstoffmangel',
            confidence: clampConfidence(confidenceBase),
            details: 'Ein Teil der Vergilbung kann durch fehlende Naehrstoffe entstehen.',
          },
          {
            title: 'Unregelmaessiges Giessen',
            confidence: clampConfidence(confidenceMid),
            details: 'Zu grosse Schwankungen zwischen trocken und sehr nass belasten die Pflanze.',
          },
          {
            title: 'Standortstress',
            confidence: clampConfidence(confidenceLow),
            details: 'Zugluft oder haeufige Standortwechsel koennen Blattreaktionen ausloesen.',
          },
        ]
        : [
          {
            title: 'Leichte Lichtanpassung noetig',
            confidence: clampConfidence(confidenceBase - 0.1),
            details: 'Einige Blaetter zeigen milde Stressanzeichen, insgesamt wirkt die Pflanze stabil.',
          },
          {
            title: 'Naechsten Duengertermin beobachten',
            confidence: clampConfidence(confidenceMid - 0.1),
            details: 'Bei weiterem Vergilben Duengung in kleiner Dosis einplanen.',
          },
          {
            title: 'Normale Blattalterung',
            confidence: clampConfidence(confidenceLow - 0.05),
            details: 'Aeltere untere Blaetter duerfen gelegentlich gelb werden.',
          },
        ];

    const actionsNow = status === 'critical'
      ? [
        'Giessen fuer 3-5 Tage pausieren und Feuchtigkeit tief im Topf pruefen.',
        'Gelbe oder matschige Blaetter sauber entfernen.',
        'Topf auf gute Drainage pruefen; stehendes Wasser sofort entfernen.',
      ]
      : status === 'watch'
        ? [
          'Giessrhythmus fuer die naechsten 7 Tage konsistent halten.',
          'Pflanze heller, aber ohne harte Mittagssonne stellen.',
          'Auf Schaedlinge an Blattunterseiten kontrollieren.',
        ]
        : [
          'Aktuellen Pflegeplan beibehalten.',
          'Nur bei deutlich trockener Erde giessen.',
          'Gelbe Altblaetter nach Bedarf entfernen.',
        ];

    const plan7Days = status === 'critical'
      ? [
        'Tag 1: Feuchtigkeit messen und Uebertopf entleeren.',
        'Tag 3: Blattfarbe und Festigkeit erneut pruefen.',
        'Tag 5: Bei nasser Erde Umtopfen mit luftiger Mischung erwaegen.',
        'Tag 7: Neuen Foto-Health-Check ausfuehren.',
      ]
      : [
        'Tag 1: Standort und Lichtdauer notieren.',
        'Tag 3: Leichte Drehung fuer gleichmaessigen Wuchs.',
        'Tag 5: Bodenfeuchte vor Giessen kontrollieren.',
        'Tag 7: Vergleichsfoto erstellen.',
      ];

    return {
      generatedAt: nowIso(),
      overallHealthScore: score,
      status,
      likelyIssues,
      actionsNow,
      plan7Days,
      creditsCharged,
      imageUri: request.imageUri,
    };
  }

  if (request.language === 'es') {
    const likelyIssues = status === 'critical'
      ? [
        {
          title: 'Posible exceso de riego',
          confidence: clampConfidence(confidenceBase + 0.22),
          details: 'Hojas amarillas y blandas pueden indicar demasiada humedad.',
        },
        {
          title: 'Estres de raiz',
          confidence: clampConfidence(confidenceMid + 0.15),
          details: 'Revisa si el sustrato esta compacto o con mal olor.',
        },
        {
          title: 'Falta de luz',
          confidence: clampConfidence(confidenceLow + 0.1),
          details: 'La luz insuficiente puede causar amarilleo y crecimiento lento.',
        },
      ]
      : status === 'watch'
        ? [
          {
            title: 'Deficit leve de nutrientes',
            confidence: clampConfidence(confidenceBase),
            details: 'Parte del amarilleo puede venir de nutricion insuficiente.',
          },
          {
            title: 'Riego irregular',
            confidence: clampConfidence(confidenceMid),
            details: 'Cambios bruscos entre seco y muy humedo estresan la planta.',
          },
          {
            title: 'Estres de ubicacion',
            confidence: clampConfidence(confidenceLow),
            details: 'Corrientes de aire o cambios frecuentes pueden afectar las hojas.',
          },
        ]
        : [
          {
            title: 'Ajuste suave de luz',
            confidence: clampConfidence(confidenceBase - 0.1),
            details: 'Se observan signos leves, pero el estado general es bueno.',
          },
          {
            title: 'Monitorear fertilizacion',
            confidence: clampConfidence(confidenceMid - 0.1),
            details: 'Si continua el amarilleo, aplicar dosis pequena de fertilizante.',
          },
          {
            title: 'Envejecimiento normal',
            confidence: clampConfidence(confidenceLow - 0.05),
            details: 'Hojas inferiores viejas pueden amarillear de forma natural.',
          },
        ];

    const actionsNow = status === 'critical'
      ? [
        'Pausar riego 3-5 dias y comprobar humedad profunda.',
        'Retirar hojas amarillas o blandas con herramienta limpia.',
        'Verificar drenaje y eliminar agua acumulada.',
      ]
      : status === 'watch'
        ? [
          'Mantener riego estable durante 7 dias.',
          'Mover a una zona mas luminosa sin sol fuerte directo.',
          'Revisar plagas en el reverso de las hojas.',
        ]
        : [
          'Mantener rutina actual de cuidado.',
          'Regar solo cuando el sustrato este claramente seco.',
          'Retirar hojas amarillas antiguas si hace falta.',
        ];

    const plan7Days = status === 'critical'
      ? [
        'Dia 1: Medir humedad y vaciar agua retenida.',
        'Dia 3: Revisar color y firmeza de hojas.',
        'Dia 5: Si sigue muy humedo, considerar trasplante con mezcla aireada.',
        'Dia 7: Repetir health-check con foto nueva.',
      ]
      : [
        'Dia 1: Registrar ubicacion y horas de luz.',
        'Dia 3: Girar planta ligeramente para crecimiento uniforme.',
        'Dia 5: Comprobar humedad antes de regar.',
        'Dia 7: Tomar foto de comparacion.',
      ];

    return {
      generatedAt: nowIso(),
      overallHealthScore: score,
      status,
      likelyIssues,
      actionsNow,
      plan7Days,
      creditsCharged,
      imageUri: request.imageUri,
    };
  }

  const likelyIssues = status === 'critical'
    ? [
      {
        title: 'Possible overwatering',
        confidence: clampConfidence(confidenceBase + 0.22),
        details: 'Yellow and soft leaves can indicate excess moisture.',
      },
      {
        title: 'Root stress',
        confidence: clampConfidence(confidenceMid + 0.15),
        details: 'Check if the substrate is compacted or has a sour smell.',
      },
      {
        title: 'Low light stress',
        confidence: clampConfidence(confidenceLow + 0.1),
        details: 'Insufficient light can trigger yellowing and slower growth.',
      },
    ]
    : status === 'watch'
      ? [
        {
          title: 'Mild nutrient deficiency',
          confidence: clampConfidence(confidenceBase),
          details: 'Part of the yellowing may come from missing nutrients.',
        },
        {
          title: 'Inconsistent watering',
          confidence: clampConfidence(confidenceMid),
          details: 'Large swings between dry and wet can stress foliage.',
        },
        {
          title: 'Placement stress',
          confidence: clampConfidence(confidenceLow),
          details: 'Drafts or frequent location changes can affect leaves.',
        },
      ]
      : [
        {
          title: 'Minor light adjustment',
          confidence: clampConfidence(confidenceBase - 0.1),
          details: 'Mild stress signs are present, but overall condition looks stable.',
        },
        {
          title: 'Monitor next feeding',
          confidence: clampConfidence(confidenceMid - 0.1),
          details: 'If yellowing continues, apply a light fertilizer dose.',
        },
        {
          title: 'Normal leaf aging',
          confidence: clampConfidence(confidenceLow - 0.05),
          details: 'Older lower leaves can yellow naturally over time.',
        },
      ];

  const actionsNow = status === 'critical'
    ? [
      'Pause watering for 3-5 days and check deep soil moisture.',
      'Remove yellow or mushy leaves with clean tools.',
      'Ensure good drainage and empty standing water.',
    ]
    : status === 'watch'
      ? [
        'Keep watering schedule stable for 7 days.',
        'Move to brighter indirect light.',
        'Inspect leaf undersides for pests.',
      ]
      : [
        'Keep the current care routine.',
        'Water only when soil is clearly dry.',
        'Trim older yellow leaves if needed.',
      ];

  const plan7Days = status === 'critical'
    ? [
      'Day 1: Check moisture and remove excess water.',
      'Day 3: Re-check leaf color and firmness.',
      'Day 5: If still soggy, repot into an airy mix.',
      'Day 7: Run another health-check photo.',
    ]
    : [
      'Day 1: Note light duration and placement.',
      'Day 3: Rotate plant slightly for even growth.',
      'Day 5: Check soil moisture before watering.',
      'Day 7: Take a comparison photo.',
    ];

  return {
    generatedAt: nowIso(),
    overallHealthScore: score,
    status,
    likelyIssues,
    actionsNow,
    plan7Days,
    creditsCharged,
    imageUri: request.imageUri,
  };
};

export const mockBackendService = {
  getBillingSummary: async (userId: string): Promise<BillingSummary> => {
    return withUserLock(userId, async () => {
      const stores = await loadStores();
      const account = getOrCreateAccount(stores, userId);
      account.updatedAt = nowIso();
      await persistStores(stores);
      return buildBillingSummary(account);
    });
  },

  syncRevenueCatState: async (request: {
    userId: string;
    customerInfo: RevenueCatCustomerInfo;
    source?: RevenueCatSyncSource;
  }): Promise<SyncRevenueCatStateResponse> => {
    return withUserLock(request.userId, async () => {
      const stores = await loadStores();
      const account = getOrCreateAccount(stores, request.userId);
      const proEntitlement = getValidProEntitlement(request.customerInfo);
      const source = request.source || 'app_init';

      console.log('[Billing][Mock] Syncing RevenueCat customer info', {
        source,
        customerInfo: summarizeRevenueCatCustomerInfo(request.customerInfo),
      });

      if (source !== 'topup_purchase') {
        account.plan = proEntitlement ? 'pro' : 'free';
        account.provider = 'revenuecat';
        account.monthlyAllowance = getMonthlyAllowanceForPlan(account.plan, account.userId);
        account.renewsAt = proEntitlement?.expirationDate || proEntitlement?.expiresDate || null;
      }

      for (const transaction of normalizeRevenueCatTransactions(request.customerInfo)) {
        const productId = transaction.productIdentifier as PurchaseProductId | undefined;
        const transactionId = transaction.transactionIdentifier || transaction.transactionId;
        if (!productId || !transactionId || !productId.startsWith('topup_')) {
          continue;
        }

        const idempotencyKey = `revenuecat-topup:${transactionId}`;
        if (stores.idempotency[idempotencyKey]) {
          continue;
        }

        account.topupBalance += TOPUP_CREDITS_BY_PRODUCT[productId] || 0;
        writeIdempotentResponse(stores.idempotency, idempotencyKey, { transactionId, productId });
      }

      account.updatedAt = nowIso();
      await persistStores(stores);
      return {
        billing: buildBillingSummary(account),
        syncedAt: nowIso(),
      };
    });
  },

  scanPlant: async (request: ScanPlantRequest): Promise<ScanPlantResponse> => {
    const { response, simulatedDelayMs } = await withUserLock(request.userId, async () => {
      const stores = await loadStores();
      const account = getOrCreateAccount(stores, request.userId);

      const idemEndpointKey = endpointKey('scan', request.userId, request.idempotencyKey);
      const cachedResponse = readIdempotentResponse<ScanPlantResponse>(stores.idempotency, idemEndpointKey);
      if (cachedResponse) {
        return {
          response: cachedResponse,
          simulatedDelayMs: getSimulatedDelay(account.plan),
        };
      }

      let creditsCharged = 0;
      const modelPath: string[] = [];

      creditsCharged += consumeCreditsWithIdempotency(
        account,
        stores.idempotency,
        chargeKey('scan-primary', request.userId, request.idempotencyKey),
        SCAN_PRIMARY_COST,
      );

      let usedOpenAi = false;
      let result: IdentificationResult = getMockPlantByImage(request.imageUri, request.language, false);

      if (openAiScanService.isConfigured()) {
        const openAiPrimary = await openAiScanService.identifyPlant(
          request.imageUri,
          request.language,
          'primary',
          account.plan === 'pro' ? 'pro' : 'free',
        );
        if (openAiPrimary) {
          result = openAiPrimary;
          usedOpenAi = true;
          modelPath.push('openai-primary');
        } else {
          result = getMockPlantByImage(request.imageUri, request.language, false);
          modelPath.push('openai-primary-failed');
          modelPath.push('mock-primary-fallback');
        }
      } else {
        modelPath.push('mock-primary');
      }

      const shouldReview = result.confidence < LOW_CONFIDENCE_REVIEW_THRESHOLD;
      if (shouldReview && account.plan === 'pro') {
        try {
          creditsCharged += consumeCreditsWithIdempotency(
            account,
            stores.idempotency,
            chargeKey('scan-review', request.userId, request.idempotencyKey),
            SCAN_REVIEW_COST,
          );
          if (usedOpenAi) {
            const openAiReview = await openAiScanService.identifyPlant(
              request.imageUri,
              request.language,
              'review',
              account.plan === 'pro' ? 'pro' : 'free',
            );
            if (openAiReview) {
              result = openAiReview;
              modelPath.push('openai-review');
            } else {
              modelPath.push('openai-review-failed');
            }
          } else {
            result = getMockPlantByImage(request.imageUri, request.language, true);
            modelPath.push('mock-review');
          }
        } catch (error) {
          if (isBackendApiError(error) && error.code === 'INSUFFICIENT_CREDITS') {
            modelPath.push('review-skipped-insufficient-credits');
          } else {
            throw error;
          }
        }
      } else if (shouldReview) {
        modelPath.push('review-skipped-free-plan');
      }

      account.updatedAt = nowIso();
      const response: ScanPlantResponse = {
        result,
        creditsCharged,
        modelPath,
        billing: buildBillingSummary(account),
      };

      writeIdempotentResponse(stores.idempotency, idemEndpointKey, response);
      await persistStores(stores);

      return {
        response,
        simulatedDelayMs: getSimulatedDelay(account.plan),
      };
    });

    await sleep(simulatedDelayMs);
    return response;
  },

  semanticSearch: async (request: SemanticSearchRequest): Promise<SemanticSearchResponse> => {
    const { response, simulatedDelayMs } = await withUserLock(request.userId, async () => {
      const stores = await loadStores();
      const account = getOrCreateAccount(stores, request.userId);

      const idemEndpointKey = endpointKey('semantic-search', request.userId, request.idempotencyKey);
      const cachedResponse = readIdempotentResponse<SemanticSearchResponse>(stores.idempotency, idemEndpointKey);
      if (cachedResponse) {
        return {
          response: cachedResponse,
          simulatedDelayMs: getSimulatedDelay(account.plan),
        };
      }

      const normalizedQuery = request.query.trim();
      if (!normalizedQuery) {
        const noResultResponse: SemanticSearchResponse = {
          status: 'no_results',
          results: [],
          creditsCharged: 0,
          billing: buildBillingSummary(account),
        };
        writeIdempotentResponse(stores.idempotency, idemEndpointKey, noResultResponse);
        await persistStores(stores);
        return {
          response: noResultResponse,
          simulatedDelayMs: getSimulatedDelay(account.plan),
        };
      }

      const creditsCharged = consumeCreditsWithIdempotency(
        account,
        stores.idempotency,
        chargeKey('semantic-search', request.userId, request.idempotencyKey),
        SEMANTIC_SEARCH_COST,
      );

      const results = searchMockCatalog(request.query, request.language, 18);
      account.updatedAt = nowIso();

      const response: SemanticSearchResponse = {
        status: results.length > 0 ? 'success' : 'no_results',
        results,
        creditsCharged,
        billing: buildBillingSummary(account),
      };

      writeIdempotentResponse(stores.idempotency, idemEndpointKey, response);
      await persistStores(stores);

      return {
        response,
        simulatedDelayMs: getSimulatedDelay(account.plan),
      };
    });

    await sleep(simulatedDelayMs);
    return response;
  },

  healthCheck: async (request: HealthCheckRequest): Promise<HealthCheckResponse> => {
    const { response, simulatedDelayMs } = await withUserLock(request.userId, async () => {
      const stores = await loadStores();
      const account = getOrCreateAccount(stores, request.userId);

      const idemEndpointKey = endpointKey('health-check', request.userId, request.idempotencyKey);
      const cachedResponse = readIdempotentResponse<HealthCheckResponse>(stores.idempotency, idemEndpointKey);
      if (cachedResponse) {
        return {
          response: cachedResponse,
          simulatedDelayMs: getSimulatedDelay(account.plan),
        };
      }

      const normalizedImageUri = request.imageUri.trim();
      if (!normalizedImageUri) {
        throw new BackendApiError('BAD_REQUEST', 'Health check requires an image URI.', 400);
      }

      if (!openAiScanService.isConfigured()) {
        throw new BackendApiError(
          'PROVIDER_ERROR',
          'OpenAI health check is unavailable. Please configure EXPO_PUBLIC_OPENAI_API_KEY.',
          502,
        );
      }

      const aiAnalysis = await openAiScanService.analyzePlantHealth(
        normalizedImageUri,
        request.language,
        request.plantContext,
      );
      if (!aiAnalysis) {
        throw new BackendApiError(
          'PROVIDER_ERROR',
          'OpenAI health check failed. Please verify API key, network access, and image quality.',
          502,
        );
      }

      const creditsCharged = consumeCreditsWithIdempotency(
        account,
        stores.idempotency,
        chargeKey('health-check', request.userId, request.idempotencyKey),
        HEALTH_CHECK_COST,
      );

      const healthCheck: PlantHealthCheck = {
        generatedAt: nowIso(),
        overallHealthScore: aiAnalysis.overallHealthScore,
        status: aiAnalysis.status,
        likelyIssues: aiAnalysis.likelyIssues,
        actionsNow: aiAnalysis.actionsNow,
        plan7Days: aiAnalysis.plan7Days,
        creditsCharged,
        imageUri: normalizedImageUri,
      };
      account.updatedAt = nowIso();

      const response: HealthCheckResponse = {
        healthCheck,
        creditsCharged,
        billing: buildBillingSummary(account),
      };

      writeIdempotentResponse(stores.idempotency, idemEndpointKey, response);
      await persistStores(stores);

      return {
        response,
        simulatedDelayMs: getSimulatedDelay(account.plan),
      };
    });

    await sleep(simulatedDelayMs);
    return response;
  },

  simulatePurchase: async (request: SimulatePurchaseRequest): Promise<SimulatePurchaseResponse> => {
    return withUserLock(request.userId, async () => {
      const stores = await loadStores();
      const account = getOrCreateAccount(stores, request.userId);

      const idemEndpointKey = endpointKey('simulate-purchase', request.userId, request.idempotencyKey);
      const cachedResponse = readIdempotentResponse<SimulatePurchaseResponse>(stores.idempotency, idemEndpointKey);
      if (cachedResponse) return cachedResponse;

      if (request.productId === 'monthly_pro' || request.productId === 'yearly_pro') {
        const now = new Date();
        const { cycleStartedAt, cycleEndsAt } = getCycleBounds(now);
        account.plan = 'pro';
        account.provider = 'mock';
        account.monthlyAllowance = PRO_MONTHLY_CREDITS;
        account.usedThisCycle = 0;
        account.cycleStartedAt = cycleStartedAt.toISOString();
        account.cycleEndsAt = cycleEndsAt.toISOString();
        account.renewsAt = addDays(now, 30).toISOString();
      } else {
        const credits = TOPUP_CREDITS_BY_PRODUCT[request.productId];
        account.topupBalance += credits;
      }

      account.updatedAt = nowIso();

      const response: SimulatePurchaseResponse = {
        appliedProduct: request.productId,
        billing: buildBillingSummary(account),
      };

      writeIdempotentResponse(stores.idempotency, idemEndpointKey, response);
      await persistStores(stores);
      return response;
    });
  },

  simulateWebhook: async (request: SimulateWebhookRequest): Promise<SimulateWebhookResponse> => {
    return withUserLock(request.userId, async () => {
      const stores = await loadStores();
      const account = getOrCreateAccount(stores, request.userId);

      const idemEndpointKey = endpointKey('simulate-webhook', request.userId, request.idempotencyKey);
      const cachedResponse = readIdempotentResponse<SimulateWebhookResponse>(stores.idempotency, idemEndpointKey);
      if (cachedResponse) return cachedResponse;

      if (request.event === 'entitlement_granted') {
        const now = new Date();
        const { cycleStartedAt, cycleEndsAt } = getCycleBounds(now);
        account.plan = 'pro';
        account.provider = 'revenuecat';
        account.monthlyAllowance = PRO_MONTHLY_CREDITS;
        account.usedThisCycle = 0;
        account.cycleStartedAt = cycleStartedAt.toISOString();
        account.cycleEndsAt = cycleEndsAt.toISOString();
        account.renewsAt = addDays(now, 30).toISOString();
      }

      if (request.event === 'entitlement_revoked') {
        const now = new Date();
        const { cycleStartedAt, cycleEndsAt } = getCycleBounds(now);
        account.plan = 'free';
        account.provider = 'revenuecat';
        account.monthlyAllowance = FREE_MONTHLY_CREDITS;
        account.usedThisCycle = 0;
        account.cycleStartedAt = cycleStartedAt.toISOString();
        account.cycleEndsAt = cycleEndsAt.toISOString();
        account.renewsAt = null;
      }

      if (request.event === 'topup_granted') {
        const credits = Math.max(1, request.payload?.credits || TOPUP_DEFAULT_CREDITS);
        account.topupBalance += credits;
      }

      if (request.event === 'credits_depleted') {
        account.usedThisCycle = account.monthlyAllowance;
        account.topupBalance = 0;
      }

      account.updatedAt = nowIso();

      const response: SimulateWebhookResponse = {
        event: request.event,
        billing: buildBillingSummary(account),
      };

      writeIdempotentResponse(stores.idempotency, idemEndpointKey, response);
      await persistStores(stores);
      return response;
    });
  },
};
