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_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',
};

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

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;

  // Replay attack prevention
  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();
}

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 }
  );
}

// ---------------------------------------------------------------------------
// POST /payment/create-checkout
// Returns the Paddle priceId for the requested plan/billingCycle.
// The FE uses this to open the Paddle overlay — the price is always
// determined server-side; the FE never picks it on its own.
// ---------------------------------------------------------------------------
router.post('/create-checkout', verifyToken, async (req, res) => {
  try {
    const { plan, billingCycle } = req.body as { plan: UserPlan; billingCycle: BillingCycle };

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

    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 });
  } 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/update-subscription
// For users who already have an active Paddle subscription:
// changes plan or billing cycle without requiring a new checkout.
// Upgrades are applied with proration immediately;
// downgrades and billing-cycle switches take effect next billing period.
// ---------------------------------------------------------------------------

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

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;
    }

    // If subscription ID isn't stored locally, look it up from Paddle via customer ID
    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;
    }

    // Determine proration mode based on what is changing.
    // Paddle only allows prorated_immediately / full_immediately / do_not_bill
    // when the billing cycle changes — full_next_billing_period is rejected in that case.
    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) {
      // Upgrade or billing-cycle switch: charge prorated difference immediately.
      // This credits the unused portion of the current period so the subscriber
      // doesn't get free time on top of the new cycle.
      prorationMode = 'prorated_immediately';
    } else {
      // Same cycle, downgrade: take effect at next renewal
      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('Subscription updated', { email, plan, billingCycle, prorationMode });
    res.json({ token, plan, planBillingCycle: billingCycle });
  } 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/verify-transaction
// Called by the FE after the Paddle overlay fires onCompleted.
// Security checks:
//   1. Transaction exists and status === 'completed' (Paddle API)
//   2. transaction.custom_data.userId === JWT email (ownership)
//   3. paddleTransactionId not already used (idempotency)
//   4. plan derived from transaction.items[0].price.id (never from user input)
// ---------------------------------------------------------------------------
router.post('/verify-transaction', verifyToken, async (req, res) => {
  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;
    }

    // 1. Fetch and verify transaction from Paddle
    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,
    });

    // 'completed' = one-time purchase; 'billed' = subscription charged; 'paid' = sandbox/alternate term
    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;
    }

    // 2. Ownership check — custom_data.userId must match authenticated user
    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;
    }

    // 3. Idempotency — reject if this transaction was already claimed
    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;
    }

    // 4. Map price ID → plan (plan is derived from Paddle data, never from user)
    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;

    // Try to get subscriptionId from the transaction; if absent, look it up via customer_id
    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) });
      }
    }

    // 5. Update user
    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('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/webhook
// Receives Paddle subscription lifecycle events.
// Raw body is captured by the express.json() verify callback in index.ts
// (stored as req.rawBody) and used here for HMAC signature verification.
// Always returns 200 immediately; processing is async after the response.
// ---------------------------------------------------------------------------
router.post('/webhook', async (req, res) => {
    // Respond immediately — Paddle retries on non-200
    res.status(200).json({ received: true });

    const sigHeader = req.headers['paddle-signature'] as string | undefined;
    const rawBody: Buffer | undefined = (req as any).rawBody;

    if (!rawBody || !sigHeader || !verifyPaddleSignature(rawBody, sigHeader)) {
      logger.warn('Invalid or missing Paddle webhook signature');
      return;
    }

    // req.body is already the parsed JSON object (from express.json() in middleware)
    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),
      });
    }
  }
);

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(),
          // Plan stays active until subscriptionCurrentPeriodEnd
        }
      );

      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 profile page.
// ---------------------------------------------------------------------------
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'
    );

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

    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;
    }

    // If subscription ID isn't stored locally, look it up from Paddle via customer ID
    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',
    });

    // Optimistic local update — webhook will confirm when it fires
    await UserModel.updateOne(
      { _id: user._id },
      { subscriptionCanceledAt: new Date() }
    );

    logger.info('Subscription cancellation scheduled', { email });
    res.json({ message: 'Subscription will cancel at end of billing period' });
  } catch (error) {
    logger.error('cancel failed', {
      error: error instanceof Error ? error.message : String(error),
    });
    res.status(500).json({ error: 'Failed to cancel subscription' });
  }
});

export default router;
