// App Store integration routes.
//
// /verify  — client-initiated verify. Payload schema mirrors aistro
//            (`{ receipt?, signedTransaction?, ... }`, `.or('receipt','signedTransaction')`)
//            so aistro iOS clients can hit this endpoint without modification.
//            JWS path takes priority; falls back to legacy receipt verifyReceipt.
// /webhook — App Store Server Notifications V2. Stubbed; full state machine
//            lands in A1-followup.

import { Request, Response, Router } from 'express';
import Joi from 'joi';

import handleAppStoreNotification from '../../integrations/app-store/handlers';
import { ingestVerifiedAppStorePurchase } from '../../integrations/app-store/handlers/subscription';
import { peekNotificationRouting } from '../../integrations/app-store/notification-routing';
import logger from '../../libs/logger';
import { authenticate } from '../../libs/security';
import { Customer, PaymentMethod } from '../../store/models';

const router = Router();
const userAuth = authenticate<Customer>({ component: false, ensureLogin: true });

const verifyBodySchema = Joi.object<{
  platform?: 'ios';
  receipt?: string;
  signedTransaction?: string;
  // legacy fields tolerated for aistro-shape compatibility (ignored server-side)
  developerPayload?: string;
  language?: string;
}>({
  platform: Joi.string().valid('ios'),
  receipt: Joi.string().empty(['', null]),
  signedTransaction: Joi.string().empty(['', null]),
  developerPayload: Joi.string().empty(['', null]),
  language: Joi.string().empty(['', null]),
}).or('receipt', 'signedTransaction');

router.post('/verify', userAuth, async (req: Request, res: Response) => {
  try {
    const did = (req as any).user?.did;
    if (!did) {
      res.status(401).json({ error: 'unauthenticated' });
      return;
    }
    const input = await verifyBodySchema.validateAsync(req.body, { stripUnknown: true });

    const method = await PaymentMethod.findOne({
      where: { type: 'app_store', active: true, livemode: !!req.livemode },
    });
    if (!method) {
      res.status(503).json({ error: 'app_store PaymentMethod not configured' });
      return;
    }
    const client = method.getAppStoreClient();

    const result = await ingestVerifiedAppStorePurchase({
      customerDid: did,
      paymentMethod: method,
      client,
      signedTransaction: input.signedTransaction,
      receipt: input.receipt,
    });

    res.json({
      success: true,
      subscription_id: result.subscription.id,
      isFirstSubscribe: result.isFirstSubscribe,
      active: result.subscription.status === 'active',
      expires_at: result.subscription.current_period_end,
      transaction: {
        original_transaction_id: result.transaction.originalTransactionId,
        transaction_id: result.transaction.transactionId,
        product_id: result.transaction.productId,
        environment: result.transaction.environment,
      },
    });
  } catch (err: any) {
    const message = err?.message || (typeof err === 'string' ? err : null) || 'verify failed';
    logger.error('app_store verify failed', { message, stack: err?.stack });
    res.status(400).json({ success: false, error: { message, code: err?.code } });
  }
});

// Restore-side input caps. A real client's `Transaction.currentEntitlements`
// holds one entry per active subscription — practical ceiling is well under
// 10 for any single Apple ID. Cap an order of magnitude above realistic to
// catch buggy clients without blocking legitimate uses, and cap per-item
// length so a 1MB JSON body can't pack hundreds of strings.
// Apple JWS is typically 1-3 KB; legacy base64 receipts can run 5-50 KB.
const RESTORE_MAX_ITEMS = 50;
const JWS_MAX_LENGTH = 8 * 1024;
const RECEIPT_MAX_LENGTH = 64 * 1024;
// Verify pool size. Each restore item triggers an Apple JWS verify + DB
// upsert. Promise.all over an unbounded list lets an authenticated caller
// fan out into many concurrent Apple verifications; bound the pool so the
// worst case is still recoverable on a single Worker invocation.
const RESTORE_CONCURRENCY = 5;

const restoreBodySchema = Joi.object<{
  receipts?: string[];
  signedTransactions?: string[];
}>({
  receipts: Joi.array().items(Joi.string().max(RECEIPT_MAX_LENGTH)).max(RESTORE_MAX_ITEMS).default([]),
  signedTransactions: Joi.array().items(Joi.string().max(JWS_MAX_LENGTH)).max(RESTORE_MAX_ITEMS).default([]),
}).or('receipts', 'signedTransactions');

/**
 * Restore purchases (StoreKit-style).
 *
 * Mobile client posts when the user reinstalls / switches device. We re-verify
 * each receipt/signedTransaction and either return the existing Subscription
 * or create one. Partial success is allowed — per-item failures land in the
 * `errors` array; per-item successes land in `restored`.
 */
router.post('/restore', userAuth, async (req: Request, res: Response) => {
  try {
    const did = (req as any).user?.did;
    if (!did) {
      res.status(401).json({ error: 'unauthenticated' });
      return;
    }
    const input = await restoreBodySchema.validateAsync(req.body, { stripUnknown: true });

    const method = await PaymentMethod.findOne({
      where: { type: 'app_store', active: true, livemode: !!req.livemode },
    });
    if (!method) {
      res.status(503).json({ error: 'app_store PaymentMethod not configured' });
      return;
    }
    const client = method.getAppStoreClient();

    // Dedupe by raw value before verifying. A buggy client that posts the
    // same JWS twice (or duplicates between `signedTransactions` and
    // `receipts`) shouldn't double-charge Apple's verifier or write twice
    // through `ingestVerifiedAppStorePurchase`.
    const seen = new Set<string>();
    const items: Array<{ kind: 'jws' | 'receipt'; value: string }> = [
      ...(input.signedTransactions ?? []).map((v) => ({ kind: 'jws' as const, value: v })),
      ...(input.receipts ?? []).map((v) => ({ kind: 'receipt' as const, value: v })),
    ].filter((item) => {
      if (seen.has(item.value)) return false;
      seen.add(item.value);
      return true;
    });

    // Bounded concurrency: process in fixed-size batches. Each item hits
    // Apple's verifier + at least one DB write, and Promise.all over an
    // arbitrary list lets a single authenticated request fan out into
    // many concurrent Apple calls. Batching of `RESTORE_CONCURRENCY`
    // caps the worst case while keeping a typical (1-3 item) restore
    // single-batch fast.
    type ItemResult =
      | {
          ok: true;
          subscription_id: string;
          isFirstSubscribe: boolean;
          original_transaction_id: string;
          product_id: string;
        }
      | { ok: false; error: string };
    const results: ItemResult[] = [];
    for (let i = 0; i < items.length; i += RESTORE_CONCURRENCY) {
      const batch = items.slice(i, i + RESTORE_CONCURRENCY);
      // eslint-disable-next-line no-await-in-loop -- intentional: batches must complete sequentially to bound concurrency
      const batchResults = await Promise.all(
        batch.map(async (item): Promise<ItemResult> => {
          try {
            const r = await ingestVerifiedAppStorePurchase({
              customerDid: did,
              paymentMethod: method,
              client,
              signedTransaction: item.kind === 'jws' ? item.value : undefined,
              receipt: item.kind === 'receipt' ? item.value : undefined,
            });
            return {
              ok: true,
              subscription_id: r.subscription.id,
              isFirstSubscribe: r.isFirstSubscribe,
              original_transaction_id: r.transaction.originalTransactionId,
              product_id: r.transaction.productId,
            };
          } catch (err: any) {
            return { ok: false, error: err?.message ?? 'restore failed' };
          }
        })
      );
      results.push(...batchResults);
    }

    res.json({
      restored: results.filter((r) => r.ok),
      errors: results.filter((r) => !r.ok),
    });
  } catch (err: any) {
    logger.error('app_store restore failed', { error: err?.message, stack: err?.stack });
    res.status(400).json({ error: err?.message ?? 'restore failed' });
  }
});

/**
 * App Store Server Notifications V2 — Apple S2S webhook.
 *
 * Request body: `{ signedPayload: "<JWS>" }` per Apple's spec. Selection +
 * verification failures (malformed / forged / not-for-us) are acked with 2xx so
 * Apple doesn't retry-storm; a failure while PROCESSING a verified notification
 * is transient and returns 5xx so Apple retries.
 */
router.post('/webhook', async (req: Request, res: Response) => {
  const signedPayload = (req.body?.signedPayload as string | undefined) ?? '';
  if (!signedPayload) {
    logger.warn('app_store webhook missing signedPayload');
    res.json({ skipped: true, reason: 'no signedPayload' });
    return;
  }

  // --- Select the matching method + verify. Failures here are NOT retryable. ---
  let notification: any;
  let client: any;
  try {
    const methods = await PaymentMethod.findAll({ where: { type: 'app_store' } });
    if (methods.length === 0) {
      logger.warn('app_store webhook: no PaymentMethod configured');
      res.json({ skipped: true, reason: 'no app_store PaymentMethod' });
      return;
    }

    // Read bundleId/environment from the UNVERIFIED payload to pick the method.
    // Verifying against the wrong env/bundle client (e.g. methods[0]) would throw
    // before the correct method is ever tried, and the old catch then 200'd —
    // silently discarding valid notifications (PR #1381 review P1).
    const routing = peekNotificationRouting(signedPayload);
    const matched = methods.find((m) => {
      const settings = PaymentMethod.decryptSettings(m.settings);
      if (settings.app_store?.bundle_id !== routing?.bundleId) return false;
      if (!routing?.environment) return true;
      return settings.app_store?.environment === routing.environment.toLowerCase();
    });
    if (!matched) {
      logger.warn('app_store webhook: no matching PaymentMethod', {
        bundleId: routing?.bundleId,
        environment: routing?.environment,
      });
      res.json({ skipped: true, reason: 'no matching PaymentMethod' });
      return;
    }

    client = matched.getAppStoreClient();
    notification = await client.verifyNotificationPayload(signedPayload);
  } catch (err: any) {
    // Malformed / forged / not-for-us → ack so Apple stops retrying.
    logger.warn('app_store webhook: verification/selection failed — acking', { error: err?.message });
    res.json({ skipped: true, reason: 'verification failed' });
    return;
  }

  // --- Process the verified notification. Failures here ARE transient. ---
  try {
    await handleAppStoreNotification(notification, client);
    res.json({ received: true });
  } catch (err: any) {
    logger.error('app_store webhook processing failed — will retry', { error: err?.message, stack: err?.stack });
    res.status(500).json({ error: err?.message ?? 'processing failed' });
  }
});

export default router;
