import { Router } from 'express';
import { createHmac, timingSafeEqual } from 'crypto';
import { config } from '../config';
import { createLogger } from '../utils/logger';
import { verifyToken } from '../middleware/auth';
import { UserModel, UserPlan, BillingCycle, SubscriptionStatus } from '../models/User';

const router = Router();
const logger = createLogger('PaymentRoutes');

const PADDLE_ENABLED = config.features.paddle;
const CREEM_ENABLED  = config.features.creem;

// ---------------------------------------------------------------------------
// Shared helpers
// ---------------------------------------------------------------------------

function issueJwt(user: { email: string; role: string; plan: UserPlan; planBillingCycle: string }): string {
  const jwt = require('jsonwebtoken');
  return jwt.sign(
    { userId: user.email, email: user.email, role: user.role, plan: user.plan, planBillingCycle: user.planBillingCycle || 'monthly' },
    config.jwt.secret,
    { expiresIn: config.jwt.expiresIn }
  );
}

const PLAN_TIER: Record<string, number> = {
  free: 0, basic: 1, premium: 2, max: 3, business: 4,
};

// ---------------------------------------------------------------------------
// Paddle helpers (kept but only active when ENABLE_PADDLE=true)
// ---------------------------------------------------------------------------

const PADDLE_BASE =
  config.paddle.environment === 'production'
    ? 'https://api.paddle.com'
    : 'https://sandbox-api.paddle.com';

const PADDLE_HEADERS = {
  Authorization: `Bearer ${config.paddle.apiKey}`,
  'Content-Type': 'application/json',
};

type PlanKey = keyof typeof config.paddle.priceIds;

function mapPlanToPriceId(plan: UserPlan, billingCycle: BillingCycle): string {
  if (plan === 'free' || plan === 'business') {
    throw new Error(`Plan '${plan}' does not have a Paddle price`);
  }
  const key = `${plan}_${billingCycle}` as PlanKey;
  const priceId = config.paddle.priceIds[key];
  if (!priceId) {
    throw new Error(`Paddle price ID not configured for ${key}`);
  }
  return priceId;
}

function mapPriceIdToPlan(priceId: string): { plan: UserPlan; billingCycle: BillingCycle } {
  const { priceIds } = config.paddle;
  const mapping: Record<string, { plan: UserPlan; billingCycle: BillingCycle }> = {
    [priceIds.basic_monthly]:   { plan: 'basic',   billingCycle: 'monthly' },
    [priceIds.basic_yearly]:    { plan: 'basic',   billingCycle: 'yearly'  },
    [priceIds.premium_monthly]: { plan: 'premium', billingCycle: 'monthly' },
    [priceIds.premium_yearly]:  { plan: 'premium', billingCycle: 'yearly'  },
    [priceIds.max_monthly]:     { plan: 'max',     billingCycle: 'monthly' },
    [priceIds.max_yearly]:      { plan: 'max',     billingCycle: 'yearly'  },
  };
  const result = mapping[priceId];
  if (!result) {
    throw new Error(`Unknown Paddle price ID: ${priceId}`);
  }
  return result;
}

const MAX_WEBHOOK_AGE_MS = 5 * 60 * 1000; // 5 minutes

function verifyPaddleSignature(rawBody: Buffer, header: string): boolean {
  if (!config.paddle.webhookSecret) return false;

  const parts = header.split(';').reduce<Record<string, string>>((acc, part) => {
    const idx = part.indexOf('=');
    if (idx !== -1) acc[part.slice(0, idx)] = part.slice(idx + 1);
    return acc;
  }, {});

  const { ts, h1 } = parts;
  if (!ts || !h1) return false;

  const age = Date.now() - parseInt(ts, 10) * 1000;
  if (age > MAX_WEBHOOK_AGE_MS) {
    logger.warn('Paddle webhook timestamp too old', { age_ms: age });
    return false;
  }

  const signed = `${ts}:${rawBody.toString('utf8')}`;
  const expected = createHmac('sha256', config.paddle.webhookSecret)
    .update(signed)
    .digest('hex');

  try {
    return timingSafeEqual(Buffer.from(h1, 'hex'), Buffer.from(expected, 'hex'));
  } catch {
    return false;
  }
}

async function paddleGet(path: string): Promise<any> {
  const res = await fetch(`${PADDLE_BASE}${path}`, {
    method: 'GET',
    headers: PADDLE_HEADERS,
  });
  if (!res.ok) {
    const body = await res.text();
    throw new Error(`Paddle API error ${res.status}: ${body}`);
  }
  return res.json();
}

async function paddlePost(path: string, body: unknown): Promise<any> {
  const res = await fetch(`${PADDLE_BASE}${path}`, {
    method: 'POST',
    headers: PADDLE_HEADERS,
    body: JSON.stringify(body),
  });
  if (!res.ok) {
    const text = await res.text();
    throw new Error(`Paddle API error ${res.status}: ${text}`);
  }
  return res.json();
}

async function paddlePatch(path: string, body: unknown): Promise<any> {
  const res = await fetch(`${PADDLE_BASE}${path}`, {
    method: 'PATCH',
    headers: PADDLE_HEADERS,
    body: JSON.stringify(body),
  });
  if (!res.ok) {
    const text = await res.text();
    throw new Error(`Paddle API error ${res.status}: ${text}`);
  }
  return res.json();
}

// ---------------------------------------------------------------------------
// Creem helpers (active MOR)
// ---------------------------------------------------------------------------

const CREEM_BASE =
  config.creem.environment === 'test'
    ? 'https://test-api.creem.io/v1'
    : 'https://api.creem.io/v1';

const CREEM_HEADERS = {
  'x-api-key': config.creem.apiKey,
  'Content-Type': 'application/json',
};

type CreemProductKey = keyof typeof config.creem.productIds;

/** Maps a UserPlan + BillingCycle to its Creem product ID. */
function mapPlanToCreemProduct(plan: UserPlan, billingCycle: BillingCycle = 'monthly'): string {
  if (plan === 'free') return config.creem.productIds.free;
  if (plan === 'business') {
    throw new Error(`Plan 'business' does not have a Creem product`);
  }
  const key = `${plan}_${billingCycle}` as CreemProductKey;
  const productId = config.creem.productIds[key];
  if (!productId) {
    throw new Error(`Creem product ID not configured for ${key}`);
  }
  return productId;
}

/** Maps a Creem product ID back to { plan, billingCycle }. */
function mapCreemProductToPlan(productId: string): { plan: UserPlan; billingCycle: BillingCycle } {
  const { productIds } = config.creem;
  const mapping: Record<string, { plan: UserPlan; billingCycle: BillingCycle }> = {
    [productIds.free]:            { plan: 'free',    billingCycle: 'monthly' },
    [productIds.basic_monthly]:   { plan: 'basic',   billingCycle: 'monthly' },
    [productIds.basic_yearly]:    { plan: 'basic',   billingCycle: 'yearly'  },
    [productIds.premium_monthly]: { plan: 'premium', billingCycle: 'monthly' },
    [productIds.premium_yearly]:  { plan: 'premium', billingCycle: 'yearly'  },
    [productIds.max_monthly]:     { plan: 'max',     billingCycle: 'monthly' },
    [productIds.max_yearly]:      { plan: 'max',     billingCycle: 'yearly'  },
  };
  const result = mapping[productId];
  if (!result) {
    throw new Error(`Unknown Creem product ID: ${productId}`);
  }
  return result;
}

function verifyCreemSignature(rawBody: Buffer, header: string): boolean {
  if (!config.creem.webhookSecret) return false;

  // Creem signature format: "t=<timestamp>,v1=<hmac_hex>"
  const parts = header.split(',').reduce<Record<string, string>>((acc, part) => {
    const idx = part.indexOf('=');
    if (idx !== -1) acc[part.slice(0, idx)] = part.slice(idx + 1);
    return acc;
  }, {});

  const { t: ts, v1 } = parts;
  if (!ts || !v1) return false;

  const age = Date.now() - parseInt(ts, 10) * 1000;
  if (age > MAX_WEBHOOK_AGE_MS) {
    logger.warn('Creem webhook timestamp too old', { age_ms: age });
    return false;
  }

  const signed = `${ts}.${rawBody.toString('utf8')}`;
  const expected = createHmac('sha256', config.creem.webhookSecret)
    .update(signed)
    .digest('hex');

  try {
    return timingSafeEqual(Buffer.from(v1, 'hex'), Buffer.from(expected, 'hex'));
  } catch {
    return false;
  }
}

async function creemGet(path: string): Promise<any> {
  const res = await fetch(`${CREEM_BASE}${path}`, {
    method: 'GET',
    headers: CREEM_HEADERS,
  });
  if (!res.ok) {
    const text = await res.text();
    throw new Error(`Creem API error ${res.status}: ${text}`);
  }
  return res.json();
}

async function creemPost(path: string, body: unknown): Promise<any> {
  const res = await fetch(`${CREEM_BASE}${path}`, {
    method: 'POST',
    headers: CREEM_HEADERS,
    body: JSON.stringify(body),
  });
  if (!res.ok) {
    const text = await res.text();
    throw new Error(`Creem API error ${res.status}: ${text}`);
  }
  return res.json();
}


// ---------------------------------------------------------------------------
// POST /payment/create-checkout
//
// Creem:  creates a hosted checkout session and returns { checkoutUrl }.
//         The FE redirects the browser to checkoutUrl.
// Paddle: returns { priceId } for the Paddle JS overlay (disabled by default).
// ---------------------------------------------------------------------------
router.post('/create-checkout', verifyToken, async (req, res) => {
  try {
    const { plan, billingCycle } = req.body as { plan: UserPlan; billingCycle: BillingCycle };
    const { email } = (req as any).user;

    if (!plan || !billingCycle) {
      res.status(400).json({ error: 'plan and billingCycle are required' });
      return;
    }

    if (CREEM_ENABLED) {
      if (!config.creem.apiKey) {
        logger.error('CREEM_API_KEY is not configured');
        res.status(503).json({ error: 'Payment provider is not configured. Set CREEM_API_KEY in .env.' });
        return;
      }

      let productId: string;
      try {
        productId = mapPlanToCreemProduct(plan, billingCycle);
      } catch (e) {
        res.status(400).json({ error: e instanceof Error ? e.message : 'Invalid plan' });
        return;
      }

      const success_url = `${config.creem.appUrl}/payment/success`;
      const checkout = await creemPost('/checkouts', {
        product_id: productId,
        success_url,
        metadata: { userId: email },
      });

      // Creem returns camelCase; fall back to snake_case and other common variants
      const checkoutUrl: string | undefined =
        checkout.checkoutUrl ??
        checkout.checkout_url ??
        checkout.url ??
        checkout.redirect_url ??
        checkout.payment_url;

      if (!checkoutUrl) {
        logger.error('Creem checkout response missing URL field', { checkout });
        res.status(500).json({ error: 'Creem did not return a checkout URL' });
        return;
      }

      res.json({ checkoutUrl });
      return;
    }

    if (PADDLE_ENABLED) {
      let priceId: string;
      try {
        priceId = mapPlanToPriceId(plan, billingCycle);
      } catch (e) {
        res.status(400).json({ error: e instanceof Error ? e.message : 'Invalid plan' });
        return;
      }
      res.json({ priceId });
      return;
    }

    res.status(503).json({ error: 'No payment provider is enabled' });
  } catch (error) {
    logger.error('create-checkout failed', {
      error: error instanceof Error ? error.message : String(error),
    });
    res.status(500).json({ error: 'Failed to create checkout' });
  }
});

// ---------------------------------------------------------------------------
// POST /payment/verify-checkout
// Called by the FE success page after Creem redirects back.
//
// Creem appends to the success_url:
//   checkout_id, order_id, customer_id, subscription_id, product_id,
//   request_id (optional), signature
//
// Security checks:
//   1. Redirect signature verified (SHA-256 of sorted params + salt=apiKey)
//   2. product_id → plan mapping (never trust raw plan from client)
//   3. creemCheckoutId not already used (idempotency)
// ---------------------------------------------------------------------------

/**
 * Verify the SHA-256 redirect signature Creem appends to the success URL.
 *
 * Creem's canonical string = URL params in their original insertion order
 * (excluding the `signature` param itself and any empty/null values),
 * joined as "key=value|key=value|...", then "|salt={apiKey}" appended.
 * The whole string is SHA-256 hashed.
 *
 * @param rawQuery  The raw query string from window.location.search (e.g. "?checkout_id=...&signature=...")
 * @param providedSig  The `signature` value extracted from the query string
 */
function verifyCreemRedirectSignature(rawQuery: string, providedSig: string): boolean {
  const { createHash, timingSafeEqual } = require('crypto');

  // URLSearchParams preserves insertion order, so we get the same sequence Creem used
  const qs = rawQuery.startsWith('?') ? rawQuery.slice(1) : rawQuery;
  const sp = new URLSearchParams(qs);

  const parts: string[] = [];
  for (const [key, value] of sp.entries()) {
    if (key === 'signature') continue;          // exclude signature itself
    if (value == null || value === '') continue; // exclude empty values
    parts.push(`${key}=${value}`);
  }

  const canonical = parts.join('|') + `|salt=${config.creem.apiKey}`;

  // Diagnostic logging — helps trace signature mismatches in production
  const apiKeyHint = config.creem.apiKey
    ? `${config.creem.apiKey.slice(0, 12)}…` : '(empty)';
  logger.info('Creem signature debug', {
    rawQuery,
    canonical,
    apiKeyHint,
    providedSig,
  });

  const expected = createHash('sha256').update(canonical).digest('hex');
  try {
    return timingSafeEqual(Buffer.from(expected, 'hex'), Buffer.from(providedSig, 'hex'));
  } catch {
    return false;
  }
}

router.post('/verify-checkout', verifyToken, async (req, res) => {
  try {
    const {
      checkoutId,
      orderId,
      customerId,
      subscriptionId,
      productId: productIdFromParams,
      signature,
      rawQuery,
    } = req.body as {
      checkoutId: string;
      orderId?: string;
      customerId?: string;
      subscriptionId?: string;
      productId?: string;
      requestId?: string;
      signature?: string;
      rawQuery?: string;
    };
    const { email } = (req as any).user;

    if (!checkoutId || typeof checkoutId !== 'string') {
      res.status(400).json({ error: 'checkoutId is required' });
      return;
    }

    if (!CREEM_ENABLED) {
      res.status(503).json({ error: 'Creem is not enabled' });
      return;
    }

    logger.info('Creem verify-checkout called', {
      checkoutId, orderId, customerId, subscriptionId,
      productId: productIdFromParams, email,
    });

    // 1. Verify redirect signature (when provided)
    if (signature && rawQuery) {
      if (!verifyCreemRedirectSignature(rawQuery, signature)) {
        logger.warn('Creem redirect signature mismatch', { checkoutId, email });
        res.status(403).json({ error: 'Invalid redirect signature' });
        return;
      }
      logger.info('Creem redirect signature verified', { checkoutId, email });
    } else {
      logger.warn('Creem verify-checkout: no signature/rawQuery provided', { checkoutId, email });
    }

    // 2. Idempotency — reject if already used
    const alreadyUsed = await UserModel.findOne({ creemCheckoutId: checkoutId });
    if (alreadyUsed) {
      // If already used by this user, re-issue JWT (idempotent success)
      if (alreadyUsed.email === email) {
        const token = issueJwt(alreadyUsed);
        res.json({ token, plan: alreadyUsed.plan, planBillingCycle: alreadyUsed.planBillingCycle });
        return;
      }
      logger.warn('Creem checkout already used by another user', { checkoutId, email });
      res.status(409).json({ error: 'Checkout has already been applied' });
      return;
    }

    // 3. Map product ID → plan
    const productId = productIdFromParams ?? '';
    if (!productId) {
      res.status(400).json({ error: 'product_id is required' });
      return;
    }

    let plan: UserPlan;
    let billingCycle: BillingCycle;
    try {
      ({ plan, billingCycle } = mapCreemProductToPlan(productId));
    } catch (e) {
      logger.error('Unknown Creem product ID in verify-checkout', { productId, checkoutId });
      res.status(400).json({ error: 'Unrecognised product in checkout' });
      return;
    }

    // 4. Update user
    const user = await UserModel.findOneAndUpdate(
      { email },
      {
        plan,
        planBillingCycle: billingCycle,
        creemCheckoutId: checkoutId,
        ...(customerId     ? { creemCustomerId:     customerId }     : {}),
        ...(subscriptionId ? { creemSubscriptionId: subscriptionId } : {}),
        subscriptionStatus: 'active' as SubscriptionStatus,
        $unset: { subscriptionCanceledAt: 1 },
      },
      { new: true }
    );

    if (!user) {
      res.status(404).json({ error: 'User not found' });
      return;
    }

    const token = issueJwt(user);
    logger.info('Creem checkout verified, plan activated', { email, plan, billingCycle, checkoutId });

    res.json({ token, plan: user.plan, planBillingCycle: user.planBillingCycle });
  } catch (error) {
    logger.error('verify-checkout failed', {
      error: error instanceof Error ? error.message : String(error),
    });
    res.status(500).json({ error: 'Failed to verify checkout' });
  }
});

// ---------------------------------------------------------------------------
// POST /payment/verify-transaction  (Paddle only — disabled by default)
// ---------------------------------------------------------------------------
router.post('/verify-transaction', verifyToken, async (req, res) => {
  if (!PADDLE_ENABLED) {
    res.status(503).json({ error: 'Paddle is not enabled' });
    return;
  }

  try {
    const { transactionId } = req.body as { transactionId: string };
    const { email } = (req as any).user;

    if (!transactionId || typeof transactionId !== 'string') {
      res.status(400).json({ error: 'transactionId is required' });
      return;
    }

    let txnResponse: any;
    try {
      txnResponse = await paddleGet(`/transactions/${transactionId}`);
    } catch (e) {
      logger.warn('Paddle transaction fetch failed', { transactionId, email });
      res.status(400).json({ error: 'Transaction not found' });
      return;
    }

    const txn = txnResponse.data;

    logger.info('Paddle transaction fetched', {
      transactionId,
      status: txn.status,
      origin: txn.origin,
      customData: txn.custom_data,
      itemPriceId: txn.items?.[0]?.price?.id,
    });

    if (txn.status !== 'completed' && txn.status !== 'billed' && txn.status !== 'paid') {
      logger.warn('Transaction not completed', { transactionId, status: txn.status, email });
      res.status(402).json({ error: 'Transaction is not completed' });
      return;
    }

    const txnUserId = txn.custom_data?.userId;
    if (!txnUserId || txnUserId !== email) {
      logger.warn('Transaction ownership mismatch', { transactionId, txnUserId, email });
      res.status(403).json({ error: 'Transaction does not belong to this user' });
      return;
    }

    const alreadyUsed = await UserModel.findOne({ paddleTransactionId: transactionId });
    if (alreadyUsed) {
      logger.warn('Transaction already used', { transactionId, email });
      res.status(409).json({ error: 'Transaction has already been applied' });
      return;
    }

    const priceId = txn.items?.[0]?.price?.id;
    if (!priceId) {
      res.status(400).json({ error: 'Transaction has no price item' });
      return;
    }

    let planInfo: { plan: UserPlan; billingCycle: BillingCycle };
    try {
      planInfo = mapPriceIdToPlan(priceId);
    } catch (e) {
      logger.error('Unknown price ID in verified transaction', { priceId, transactionId });
      res.status(400).json({ error: 'Unrecognised price in transaction' });
      return;
    }

    const periodEnd = txn.billing_period?.ends_at
      ? new Date(txn.billing_period.ends_at)
      : undefined;

    let subscriptionId: string | undefined = txn.subscription_id ?? undefined;
    if (!subscriptionId && txn.customer_id) {
      try {
        const subsResponse = await paddleGet(`/subscriptions?customer_id=${txn.customer_id}&status=active`);
        const activeSub = subsResponse.data?.[0];
        if (activeSub?.id) {
          subscriptionId = activeSub.id;
          logger.info('Resolved subscriptionId from Paddle API during verify', { subscriptionId, transactionId });
        }
      } catch (e) {
        logger.warn('Could not look up subscription during verify-transaction', { error: e instanceof Error ? e.message : String(e) });
      }
    }

    const user = await UserModel.findOneAndUpdate(
      { email },
      {
        plan: planInfo.plan,
        planBillingCycle: planInfo.billingCycle,
        paddleCustomerId: txn.customer_id ?? undefined,
        ...(subscriptionId ? { paddleSubscriptionId: subscriptionId } : {}),
        paddleTransactionId: transactionId,
        subscriptionStatus: 'active' as SubscriptionStatus,
        ...(periodEnd ? { subscriptionCurrentPeriodEnd: periodEnd } : {}),
        $unset: { subscriptionCanceledAt: 1 },
      },
      { new: true }
    );

    if (!user) {
      res.status(404).json({ error: 'User not found' });
      return;
    }

    const token = issueJwt(user);
    logger.info('Paddle transaction verified, plan activated', {
      email,
      plan: planInfo.plan,
      billingCycle: planInfo.billingCycle,
      transactionId,
    });

    res.json({ token, plan: user.plan, planBillingCycle: user.planBillingCycle });
  } catch (error) {
    logger.error('verify-transaction failed', {
      error: error instanceof Error ? error.message : String(error),
    });
    res.status(500).json({ error: 'Failed to verify transaction' });
  }
});

// ---------------------------------------------------------------------------
// POST /payment/update-subscription
// Changes plan for a user who already has an active subscription.
//
// Creem:  POSTs /subscriptions/:id/upgrade with the new product_id. Charges
//         immediately for upgrades, no charge for downgrades. Falls back to a
//         new checkout URL if the direct upgrade is rejected by Creem.
// Paddle: PATCHes /subscriptions/:id with proration (disabled by default).
// ---------------------------------------------------------------------------
router.post('/update-subscription', verifyToken, async (req, res) => {
  try {
    const { plan, billingCycle } = req.body as { plan: UserPlan; billingCycle: BillingCycle };
    const { email } = (req as any).user;

    if (!plan || !billingCycle) {
      res.status(400).json({ error: 'plan and billingCycle are required' });
      return;
    }

    const user = await UserModel.findOne({ email });
    if (!user) {
      res.status(404).json({ error: 'User not found' });
      return;
    }

    // ---- Creem path ----
    if (CREEM_ENABLED) {
      let productId: string;
      try {
        productId = mapPlanToCreemProduct(plan, billingCycle);
      } catch (e) {
        res.status(400).json({ error: e instanceof Error ? e.message : 'Invalid plan' });
        return;
      }

      // If no subscription ID stored, create a new checkout instead
      if (!user.creemSubscriptionId) {
        const success_url = `${config.creem.appUrl}/payment/success`;
        const checkout = await creemPost('/checkouts', {
          product_id: productId,
          success_url,
          metadata: { userId: email },
        });
        const checkoutUrl: string | undefined =
          checkout.checkoutUrl ?? checkout.checkout_url ?? checkout.url;
        res.json({ checkoutUrl });
        return;
      }

      // Determine whether this is an upgrade or downgrade so we can apply
      // the correct proration behavior.
      // Upgrades: charge the prorated difference immediately.
      // Downgrades: switch immediately and generate a credit note for the
      //   remaining days on the higher plan. The credit is applied to the
      //   next invoice, so if the user re-upgrades right away they won't
      //   be charged twice for the same period.
      const PLAN_RANK: Record<UserPlan, number> = {
        free: 0, basic: 1, premium: 2, max: 3, business: 4,
      };
      const currentRank = PLAN_RANK[user.plan ?? 'free'];
      const newRank = PLAN_RANK[plan];
      const cycleUpgrade =
        newRank === currentRank &&
        user.planBillingCycle === 'monthly' &&
        billingCycle === 'yearly';
      const isUpgrade = newRank > currentRank || cycleUpgrade;
      const update_behavior = isUpgrade
        ? 'proration-charge-immediately'
        : 'proration-charge';

      // Update existing subscription (upgrade or downgrade).
      // Do NOT fall back to a new checkout here — the user already has an active
      // subscription and creating a second checkout would produce duplicate billing.
      // If the upgrade API fails, surface the error so the issue can be fixed
      // (e.g. currency mismatch between products in the Creem dashboard).
      try {
        await creemPost(`/subscriptions/${user.creemSubscriptionId}/upgrade`, {
          product_id: productId,
          update_behavior,
        });
      } catch (upgradeErr) {
        const msg = upgradeErr instanceof Error ? upgradeErr.message : String(upgradeErr);
        logger.error('Creem subscription upgrade failed', {
          email,
          subscriptionId: user.creemSubscriptionId,
          error: msg,
        });
        // Surface a specific code when a prior change is still settling in Creem.
        // The frontend uses this to show a targeted "please wait a moment" message.
        if (msg.includes('previous change has finished')) {
          res.status(409).json({ error: 'pending_change' });
          return;
        }
        res.status(422).json({
          error: 'Plan change failed. Please contact support if the issue persists.',
          detail: msg,
        });
        return;
      }

      const updatedUser = await UserModel.findOneAndUpdate(
        { email },
        { plan, planBillingCycle: billingCycle },
        { new: true }
      );

      if (!updatedUser) {
        res.status(404).json({ error: 'User not found' });
        return;
      }

      const token = issueJwt(updatedUser);
      logger.info('Creem subscription updated', { email, plan, billingCycle });
      res.json({ token, plan, planBillingCycle: billingCycle });
      return;
    }

    // ---- Paddle path (disabled by default) ----
    if (PADDLE_ENABLED) {
      if (!user.paddleSubscriptionId && user.paddleCustomerId) {
        try {
          const subsResponse = await paddleGet(`/subscriptions?customer_id=${user.paddleCustomerId}&status=active`);
          const activeSub = subsResponse.data?.[0];
          if (activeSub?.id) {
            user.paddleSubscriptionId = activeSub.id;
            await UserModel.updateOne({ email }, { paddleSubscriptionId: activeSub.id });
            logger.info('Backfilled paddleSubscriptionId from Paddle API', { email, subscriptionId: activeSub.id });
          }
        } catch (e) {
          logger.warn('Failed to look up Paddle subscription', { email, error: e instanceof Error ? e.message : String(e) });
        }
      }

      if (!user.paddleSubscriptionId) {
        res.status(400).json({ error: 'No active Paddle subscription' });
        return;
      }

      let priceId: string;
      try {
        priceId = mapPlanToPriceId(plan, billingCycle);
      } catch (e) {
        res.status(400).json({ error: e instanceof Error ? e.message : 'Invalid plan' });
        return;
      }

      const currentTier = PLAN_TIER[user.plan] ?? 0;
      const newTier = PLAN_TIER[plan] ?? 0;
      const cycleChanging = user.planBillingCycle !== billingCycle;
      let prorationMode: string;
      if (newTier > currentTier || cycleChanging) {
        prorationMode = 'prorated_immediately';
      } else {
        prorationMode = 'full_next_billing_period';
      }

      await paddlePatch(`/subscriptions/${user.paddleSubscriptionId}`, {
        items: [{ price_id: priceId, quantity: 1 }],
        proration_billing_mode: prorationMode,
      });

      const updatedUser = await UserModel.findOneAndUpdate(
        { email },
        { plan, planBillingCycle: billingCycle },
        { new: true }
      );

      if (!updatedUser) {
        res.status(404).json({ error: 'User not found' });
        return;
      }

      const token = issueJwt(updatedUser);
      logger.info('Paddle subscription updated', { email, plan, billingCycle });
      res.json({ token, plan, planBillingCycle: billingCycle });
      return;
    }

    res.status(503).json({ error: 'No payment provider is enabled' });
  } catch (error) {
    logger.error('update-subscription failed', {
      error: error instanceof Error ? error.message : String(error),
    });
    res.status(500).json({ error: 'Failed to update subscription' });
  }
});

// ---------------------------------------------------------------------------
// POST /payment/webhook
// Accepts webhooks from Creem (active) and Paddle (disabled by default).
// Provider is detected by presence of the respective signature header.
// Always returns 200 immediately; processing is async.
// ---------------------------------------------------------------------------
router.post('/webhook', async (req, res) => {
  res.status(200).json({ received: true });

  const rawBody: Buffer | undefined = (req as any).rawBody;
  if (!rawBody) {
    logger.warn('Webhook: missing raw body');
    return;
  }

  // Creem webhook
  const creemSigHeader = req.headers['creem-signature'] as string | undefined;
  if (creemSigHeader && CREEM_ENABLED) {
    if (!verifyCreemSignature(rawBody, creemSigHeader)) {
      logger.warn('Invalid Creem webhook signature');
      return;
    }
    const event = req.body;
    logger.info('Creem webhook received', { event_type: event.type ?? event.eventType });
    try {
      await handleCreemEvent(event);
    } catch (error) {
      logger.error('Creem webhook handler failed', {
        event_type: event.type ?? event.eventType,
        error: error instanceof Error ? error.message : String(error),
      });
    }
    return;
  }

  // Paddle webhook (disabled by default)
  const paddleSigHeader = req.headers['paddle-signature'] as string | undefined;
  if (paddleSigHeader && PADDLE_ENABLED) {
    if (!verifyPaddleSignature(rawBody, paddleSigHeader)) {
      logger.warn('Invalid Paddle webhook signature');
      return;
    }
    const event = req.body;
    logger.info('Paddle webhook received', { event_type: event.event_type, event_id: event.event_id });
    try {
      await handlePaddleEvent(event);
    } catch (error) {
      logger.error('Paddle webhook handler failed', {
        event_type: event.event_type,
        event_id: event.event_id,
        error: error instanceof Error ? error.message : String(error),
      });
    }
    return;
  }

  logger.warn('Webhook: no recognised provider signature header');
});

// ---------------------------------------------------------------------------
// Creem event handler
// ---------------------------------------------------------------------------

async function findUserByCreemEvent(event: any): Promise<InstanceType<typeof UserModel> | null> {
  const data = event.object ?? event.data ?? {};
  const metaUserId = data.metadata?.userId;
  if (metaUserId) {
    const user = await UserModel.findOne({ email: metaUserId });
    if (user) return user;
  }
  const customerId = data.customer?.id ?? data.customerId ?? data.customer_id;
  if (customerId) {
    return UserModel.findOne({ creemCustomerId: customerId });
  }
  return null;
}

async function handleCreemEvent(event: any): Promise<void> {
  // Creem event payload shape: { type: 'subscription.active', object: { ... } }
  // or { eventType: 'subscription.active', data: { ... } }
  const eventType: string = event.type ?? event.eventType ?? '';
  const data = event.object ?? event.data ?? {};

  switch (eventType) {
    case 'subscription.active':
    case 'subscription.updated': {
      const user = await findUserByCreemEvent(event);
      if (!user) {
        logger.warn('Creem webhook: user not found', { event_type: eventType });
        return;
      }

      const productId: string =
        data.productId ?? data.product_id ?? data.product?.id ?? '';
      if (!productId) {
        logger.warn('Creem webhook: no product in event', { event_type: eventType });
        return;
      }

      let plan: UserPlan;
      let billingCycle: BillingCycle;
      try {
        ({ plan, billingCycle } = mapCreemProductToPlan(productId));
      } catch {
        logger.warn('Creem webhook: unknown product ID', { productId, event_type: eventType });
        return;
      }

      const periodEndRaw = data.currentPeriodEnd ?? data.current_period_end;
      const periodEnd = periodEndRaw ? new Date(periodEndRaw) : undefined;

      await UserModel.updateOne(
        { _id: user._id },
        {
          plan,
          planBillingCycle: billingCycle,
          creemCustomerId:    data.customer?.id ?? data.customerId ?? data.customer_id ?? user.creemCustomerId,
          creemSubscriptionId: data.id,
          subscriptionStatus: 'active' as SubscriptionStatus,
          ...(periodEnd ? { subscriptionCurrentPeriodEnd: periodEnd } : {}),
          $unset: { subscriptionCanceledAt: 1 },
        }
      );

      logger.info('Creem webhook: subscription activated/updated', { email: user.email, plan, event_type: eventType });
      break;
    }

    case 'subscription.renewed':
    case 'payment.succeeded': {
      const user = await findUserByCreemEvent(event);
      if (!user) return;

      const periodEndRaw2 = data.currentPeriodEnd ?? data.current_period_end;
      const periodEnd = periodEndRaw2 ? new Date(periodEndRaw2) : undefined;

      await UserModel.updateOne(
        { _id: user._id },
        {
          subscriptionStatus: 'active' as SubscriptionStatus,
          ...(periodEnd ? { subscriptionCurrentPeriodEnd: periodEnd } : {}),
          $unset: { subscriptionCanceledAt: 1 },
        }
      );

      logger.info('Creem webhook: subscription renewed / payment succeeded', { email: user.email });
      break;
    }

    case 'subscription.canceled':
    case 'subscription.expired': {
      const user = await findUserByCreemEvent(event);
      if (!user) return;

      await UserModel.updateOne(
        { _id: user._id },
        {
          subscriptionStatus: 'canceled' as SubscriptionStatus,
          subscriptionCanceledAt: new Date(),
        }
      );

      logger.info('Creem webhook: subscription canceled/expired', { email: user.email });
      break;
    }

    case 'payment.failed': {
      const user = await findUserByCreemEvent(event);
      if (!user) return;

      await UserModel.updateOne(
        { _id: user._id },
        { subscriptionStatus: 'past_due' as SubscriptionStatus }
      );

      logger.warn('Creem webhook: payment failed', { email: user.email });
      break;
    }

    default:
      logger.debug('Creem webhook: unhandled event type', { event_type: eventType });
  }
}

// ---------------------------------------------------------------------------
// Paddle event handler (kept; only called when Paddle is enabled)
// ---------------------------------------------------------------------------

async function findUserByPaddleEvent(event: any): Promise<InstanceType<typeof UserModel> | null> {
  const customUserId = event.data?.custom_data?.userId;
  if (customUserId) {
    const user = await UserModel.findOne({ email: customUserId });
    if (user) return user;
  }
  const customerId = event.data?.customer_id;
  if (customerId) {
    return UserModel.findOne({ paddleCustomerId: customerId });
  }
  return null;
}

async function handlePaddleEvent(event: any): Promise<void> {
  const { event_type, data } = event;

  switch (event_type) {
    case 'subscription.activated':
    case 'subscription.updated': {
      const user = await findUserByPaddleEvent(event);
      if (!user) {
        logger.warn('Paddle webhook: user not found', { event_type, event_id: event.event_id });
        return;
      }

      const priceId = data.items?.[0]?.price?.id;
      if (!priceId) {
        logger.warn('Paddle webhook: no price item', { event_type });
        return;
      }

      let planInfo: { plan: UserPlan; billingCycle: BillingCycle };
      try {
        planInfo = mapPriceIdToPlan(priceId);
      } catch {
        logger.warn('Paddle webhook: unknown price ID', { priceId, event_type });
        return;
      }

      const periodEnd = data.current_billing_period?.ends_at
        ? new Date(data.current_billing_period.ends_at)
        : undefined;

      await UserModel.updateOne(
        { _id: user._id },
        {
          plan: planInfo.plan,
          planBillingCycle: planInfo.billingCycle,
          paddleCustomerId: data.customer_id ?? user.paddleCustomerId,
          paddleSubscriptionId: data.id,
          subscriptionStatus: (data.status as SubscriptionStatus) ?? 'active',
          ...(periodEnd ? { subscriptionCurrentPeriodEnd: periodEnd } : {}),
          $unset: { subscriptionCanceledAt: 1 },
        }
      );

      logger.info('Paddle webhook: subscription activated/updated', {
        email: user.email,
        plan: planInfo.plan,
        event_type,
      });
      break;
    }

    case 'subscription.renewed': {
      const user = await findUserByPaddleEvent(event);
      if (!user) return;

      const periodEnd = data.current_billing_period?.ends_at
        ? new Date(data.current_billing_period.ends_at)
        : undefined;

      await UserModel.updateOne(
        { _id: user._id },
        {
          subscriptionStatus: 'active' as SubscriptionStatus,
          ...(periodEnd ? { subscriptionCurrentPeriodEnd: periodEnd } : {}),
          $unset: { subscriptionCanceledAt: 1 },
        }
      );

      logger.info('Paddle webhook: subscription renewed', { email: user.email });
      break;
    }

    case 'subscription.canceled': {
      const user = await findUserByPaddleEvent(event);
      if (!user) return;

      await UserModel.updateOne(
        { _id: user._id },
        {
          subscriptionStatus: 'canceled' as SubscriptionStatus,
          subscriptionCanceledAt: new Date(),
        }
      );

      logger.info('Paddle webhook: subscription canceled', { email: user.email });
      break;
    }

    case 'transaction.payment_failed': {
      const user = await findUserByPaddleEvent(event);
      if (!user) return;

      await UserModel.updateOne(
        { _id: user._id },
        { subscriptionStatus: 'past_due' as SubscriptionStatus }
      );

      logger.warn('Paddle webhook: payment failed', { email: user.email });
      break;
    }

    default:
      logger.debug('Paddle webhook: unhandled event type', { event_type });
  }
}

// ---------------------------------------------------------------------------
// GET /payment/subscription
// Returns subscription state for the billing page (provider-agnostic).
// ---------------------------------------------------------------------------
router.get('/subscription', verifyToken, async (req, res) => {
  try {
    const { email } = (req as any).user;
    const user = await UserModel.findOne({ email }).select(
      'plan planBillingCycle subscriptionStatus subscriptionCurrentPeriodEnd subscriptionCanceledAt creemSubscriptionId'
    );

    if (!user) {
      res.status(404).json({ error: 'User not found' });
      return;
    }

    // Sync live subscription status from Creem to catch cases where webhooks
    // did not arrive (e.g. local dev without a tunnel).
    if (CREEM_ENABLED && user.creemSubscriptionId) {
      try {
        const creemSub = await creemGet(`/subscriptions/${user.creemSubscriptionId}`);
        // Creem status field — try common field names
        const creemStatus: string = creemSub.status ?? creemSub.subscription_status ?? '';

        // Map Creem status to our SubscriptionStatus.
        // 'scheduled_cancel' stays as 'active' in our model — subscriptionCanceledAt is the signal.
        const statusMap: Record<string, SubscriptionStatus> = {
          active: 'active',
          scheduled_cancel: 'active',
          canceled: 'canceled',
          expired: 'canceled',
          paused: 'paused',
          past_due: 'past_due',
          trialing: 'trialing',
        };
        const mappedStatus = statusMap[creemStatus];

        if (mappedStatus && mappedStatus !== user.subscriptionStatus) {
          const update: Record<string, any> = { subscriptionStatus: mappedStatus };

          // When Creem confirms a full cancellation, clear subscriptionCanceledAt
          // so it is only used for the scheduled-cancel window, not afterward.
          if (mappedStatus === 'canceled') {
            update.subscriptionCanceledAt = new Date();
          }

          // When Creem says it's active again (e.g. after resume), clear canceledAt.
          if (mappedStatus === 'active' && creemStatus === 'active') {
            update.$unset = { subscriptionCanceledAt: 1 };
            delete update.subscriptionCanceledAt;
          }

          await UserModel.updateOne({ _id: user._id }, update);
          user.subscriptionStatus = mappedStatus;
          if (mappedStatus === 'canceled') {
            user.subscriptionCanceledAt = update.subscriptionCanceledAt;
          }
        }

        // Also sync periodEnd if Creem provides it
        const periodEndRaw = creemSub.currentPeriodEnd ?? creemSub.current_period_end;
        if (periodEndRaw) {
          const periodEnd = new Date(periodEndRaw);
          await UserModel.updateOne({ _id: user._id }, { subscriptionCurrentPeriodEnd: periodEnd });
          user.subscriptionCurrentPeriodEnd = periodEnd;
        }
      } catch (syncErr) {
        const errMsg = syncErr instanceof Error ? syncErr.message : String(syncErr);
        if (errMsg.includes('Creem API error 404')) {
          // The subscription ID no longer exists in Creem.
          // Only treat this as a definitive cancellation when subscriptionCanceledAt
          // is set — meaning the user explicitly scheduled a cancellation and webhooks
          // didn't arrive (common in local dev). If subscriptionCanceledAt is null the
          // user likely just resubscribed and the stored ID is stale; preserve the
          // active status that verify-checkout already wrote.
          if (user.subscriptionCanceledAt) {
            logger.info('Creem subscription not found (404) with pending cancellation, marking as canceled', { email });
            await UserModel.updateOne(
              { _id: user._id },
              {
                subscriptionStatus: 'canceled' as SubscriptionStatus,
                subscriptionCanceledAt: new Date(),
              }
            );
            user.subscriptionStatus = 'canceled' as SubscriptionStatus;
          } else {
            logger.info('Creem subscription not found (404) but no cancellation signal — stale subscription ID, keeping current status', { email });
          }
        } else {
          // Non-fatal — fall back to DB data if Creem call fails
          logger.warn('Failed to sync subscription status from Creem', {
            email,
            error: errMsg,
          });
        }
      }
    }

    res.json({
      plan: user.plan,
      planBillingCycle: user.planBillingCycle,
      subscriptionStatus: user.subscriptionStatus ?? null,
      subscriptionCurrentPeriodEnd: user.subscriptionCurrentPeriodEnd ?? null,
      subscriptionCanceledAt: user.subscriptionCanceledAt ?? null,
    });
  } catch (error) {
    logger.error('subscription fetch failed', {
      error: error instanceof Error ? error.message : String(error),
    });
    res.status(500).json({ error: 'Failed to fetch subscription' });
  }
});

// ---------------------------------------------------------------------------
// POST /payment/cancel
// Schedules subscription cancellation at end of current billing period.
// ---------------------------------------------------------------------------
router.post('/cancel', verifyToken, async (req, res) => {
  try {
    const { email } = (req as any).user;
    const user = await UserModel.findOne({ email });

    if (!user) {
      res.status(404).json({ error: 'User not found' });
      return;
    }

    // ---- Creem path ----
    if (CREEM_ENABLED) {
      if (!user.creemSubscriptionId) {
        res.status(400).json({ error: 'No active subscription found' });
        return;
      }

      await creemPost(`/subscriptions/${user.creemSubscriptionId}/cancel`, { mode: 'scheduled' });

      await UserModel.updateOne(
        { _id: user._id },
        { subscriptionCanceledAt: new Date() }
      );

      logger.info('Creem subscription cancellation scheduled', { email });
      res.json({ message: 'Subscription will cancel at end of billing period' });
      return;
    }

    // ---- Paddle path (disabled by default) ----
    if (PADDLE_ENABLED) {
      if (!user.paddleSubscriptionId && user.paddleCustomerId) {
        try {
          const subsResponse = await paddleGet(`/subscriptions?customer_id=${user.paddleCustomerId}&status=active`);
          const activeSub = subsResponse.data?.[0];
          if (activeSub?.id) {
            user.paddleSubscriptionId = activeSub.id;
            await UserModel.updateOne({ email }, { paddleSubscriptionId: activeSub.id });
            logger.info('Backfilled paddleSubscriptionId from Paddle API (cancel)', { email, subscriptionId: activeSub.id });
          }
        } catch (e) {
          logger.warn('Failed to look up Paddle subscription for cancel', { email, error: e instanceof Error ? e.message : String(e) });
        }
      }

      if (!user.paddleSubscriptionId) {
        res.status(400).json({ error: 'No active subscription found' });
        return;
      }

      await paddlePost(`/subscriptions/${user.paddleSubscriptionId}/cancel`, {
        effective_from: 'next_billing_period',
      });

      await UserModel.updateOne(
        { _id: user._id },
        { subscriptionCanceledAt: new Date() }
      );

      logger.info('Paddle subscription cancellation scheduled', { email });
      res.json({ message: 'Subscription will cancel at end of billing period' });
      return;
    }

    res.status(503).json({ error: 'No payment provider is enabled' });
  } catch (error) {
    logger.error('cancel failed', {
      error: error instanceof Error ? error.message : String(error),
    });
    res.status(500).json({ error: 'Failed to cancel subscription' });
  }
});

// ---------------------------------------------------------------------------
// POST /payment/reactivate
// Cancels a pending cancellation, keeping the subscription active.
// ---------------------------------------------------------------------------
router.post('/reactivate', verifyToken, async (req, res) => {
  try {
    const { email } = (req as any).user;
    const user = await UserModel.findOne({ email });

    if (!user) {
      res.status(404).json({ error: 'User not found' });
      return;
    }

    // ---- Creem path ----
    if (CREEM_ENABLED) {
      if (!user.creemSubscriptionId) {
        res.status(400).json({ error: 'No active subscription found' });
        return;
      }

      await creemPost(`/subscriptions/${user.creemSubscriptionId}/resume`, {});

      await UserModel.updateOne(
        { _id: user._id },
        { $unset: { subscriptionCanceledAt: '' } }
      );

      logger.info('Creem subscription reactivated', { email });
      res.json({ message: 'Subscription reactivated successfully' });
      return;
    }

    res.status(503).json({ error: 'No payment provider is enabled' });
  } catch (error) {
    logger.error('reactivate failed', {
      error: error instanceof Error ? error.message : String(error),
    });
    res.status(500).json({ error: 'Failed to reactivate subscription' });
  }
});

export default router;
