'use strict';

/**
 * DC-055 + DC-057 Billing routes — `/api/v1/billing/*`
 *
 * Two public endpoints:
 *
 *   POST /api/v1/billing/checkout
 *     Body: { productId: 'pro-30d'|'pro-90d'|'pro-180d'|'pro-365d', customerEmail?: string }
 *     Returns: { id, url } — url is the Stripe-hosted Checkout page.
 *     Auth: PUBLIC (customer hasn't paid yet → no session). CSRF-exempt.
 *
 *   GET /api/v1/billing/lookup/:sessionId
 *     Returns: { status: 'not_found'|'expired'|'processing'|'pending_email'|'delivered', code?, codeId?, durationDays?, productId?, deliveredVia? }
 *     Auth: PUBLIC. CSRF-exempt.
 *
 *     The lookup serves the persisted license in BOTH `delivered` AND
 *     `pending_email` states. This is the documented SMTP-failure recovery
 *     path (the customer pastes their key even if email failed).
 *
 *     Security model:
 *       - sessionId is a bearer-style secret returned by Stripe ONLY to the
 *         customer who completed payment (single-use, expires after 24h).
 *       - The endpoint enforces a 24h TTL (LOOKUP_TTL_MS) — after that,
 *         404 even with a valid sessionId.
 *       - Cache-Control: no-store on all responses.
 *       - Rate-limited via the general limiter (10/min/IP).
 *
 * The inbound webhook side lives in scripts/stripe-license-bridge.js
 * (DC-054 + DC-057). That runs as its own process on port 3010 so the
 * merchant webhook secret never enters the API host's process tree.
 * The bridge writes to the SAME fulfillment-store file the lookup endpoint
 * reads (platformPaths.dataDir + 'stripe-fulfillments.json'), so the API
 * sees the license as soon as the bridge saves it.
 */

const express = require('express');
const { ok, errorResponse } = require('../src/utils/responses');
const { ValidationError } = require('../src/utilities/errors');
const { createCheckoutSession } = require('../src/billing/stripe-client');
const { createFulfillmentStore } = require('../src/billing/fulfillment-store');

// One fulfillment-store instance per process. Reads from the same file the
// bridge writes to — IPC via the bind-mounted data dir. Override path via
// STRIPE_BRIDGE_FULFILLMENT_STORE_FILE (the bridge reads the same env var
// at startup) — both processes target the same file. In tests we point
// at a tmp dir.
const fulfillmentStore = createFulfillmentStore({
  filePath: process.env.STRIPE_BRIDGE_FULFILLMENT_STORE_FILE,
});

const LOOKUP_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours — matches Stripe's
                                             // default Checkout session expiry.

/**
 * Compute the public origin to embed in Stripe Checkout success/cancel
 * URLs.
 *
 * SECURITY: the success_url is what Stripe redirects the customer's
 * browser to after payment. If we let the request's Host header
 * influence it unchecked, a header-injection attacker could redirect
 * customers to their own origin — and the session_id in the URL is
 * the bearer token for /api/v1/billing/lookup/:sessionId (the customer
 * would then leak their own license to the attacker). So we derive
 * the origin only from TRUSTED SOURCES:
 *
 *   1. `STRIPE_PUBLIC_ORIGIN` env var (preferred — operator-declared)
 *   2. `STRIPE_ALLOWED_HOSTS` allowlist + request Host header
 *      (fallback for operators who don't set the env var)
 *   3. `undefined` → Stripe Checkout falls back to its default
 *      success/cancel URL behavior (still safe; just loses the
 *      success-page reveal flow).
 *
 * We also enforce scheme allowlist (https only by default; http only
 * for explicit dev mode) to prevent javascript:/file:/data: smuggling.
 */
function _resolvePublicOrigin(req) {
  // 1. Explicit env-var override (canonical deployment shape).
  if (process.env.STRIPE_PUBLIC_ORIGIN) {
    const raw = process.env.STRIPE_PUBLIC_ORIGIN.trim();
    try {
      const u = new URL(raw);
      if (u.protocol === 'https:' || (u.protocol === 'http:' && process.env.NODE_ENV !== 'production')) {
        return `${u.protocol}//${u.host}`;
      }
    } catch (_) { /* fall through to header-based resolution */ }
  }

  // 2. Header-based fallback, gated by STRIPE_ALLOWED_HOSTS allowlist.
  const allowedHosts = (process.env.STRIPE_ALLOWED_HOSTS || '')
    .split(',').map((h) => h.trim().toLowerCase()).filter(Boolean);
  if (allowedHosts.length === 0) return undefined;

  const host = (req.get('x-forwarded-host') || req.get('host') || '').toLowerCase();
  // Strip :port for comparison; port is added back when building the URL.
  const hostNoPort = host.split(':')[0];
  if (!hostNoPort) return undefined;
  if (!allowedHosts.includes(hostNoPort) && !allowedHosts.includes(host)) return undefined;

  const rawProto = (req.get('x-forwarded-proto') || req.protocol || 'https').toLowerCase();
  const proto = rawProto === 'http' && process.env.NODE_ENV !== 'production' ? 'http' : 'https';
  return `${proto}://${host}`;
}

/**
 * Billing routes factory
 * @param {Object} deps
 * @param {Function} deps.asyncHandler - async route handler wrapper
 * @returns {express.Router}
 */
module.exports = function({ asyncHandler }) {
  const router = express.Router();

  /**
   * POST /api/v1/billing/checkout
   *
   * Body: { productId: 'pro-30d'|'pro-90d'|'pro-180d'|'pro-365d', customerEmail?: string }
   * Returns: { id, url }
   *
   * The customer is redirected to `url`. On success Stripe redirects to
   * {origin}/billing/success?session_id={CHECKOUT_SESSION_ID}. The
   * webhook bridge (scripts/stripe-license-bridge.js) generates the
   * license on `checkout.session.completed`, persists it to the
   * fulfillment store, emails it, and the success page polls the lookup
   * endpoint below to reveal it.
   *
   * SECURITY: The success_url is embedded in the Stripe Checkout Session
   * and shown to the customer in their browser. If an attacker can
   * control the `Host` / `X-Forwarded-Host` header, they can poison
   * Stripe's redirect to their own origin — and the customer's session
   * ID (which is the bearer token for /api/v1/billing/lookup/:sessionId)
   * lands in the attacker's URL bar. The lookup endpoint would then
   * serve the license to the attacker's browser.
   *
   * To prevent this, the API derives the origin from an explicit
   * `STRIPE_PUBLIC_ORIGIN` env var when set (the canonical deployment
   * shape). If unset, we fall back to the request's Host header, BUT
   * only when the Host is in the explicit `STRIPE_ALLOWED_HOSTS` allowlist
   * (comma-separated). This means a fresh operator MUST either set the
   * env var OR explicitly allowlist their hostname before checkout can
   * create sessions — a hostile header alone is not enough.
   */
  router.post('/checkout', asyncHandler(async (req, res) => {
    const { productId, customerEmail } = req.body || {};

    const origin = _resolvePublicOrigin(req);

    try {
      if (!productId) throw new ValidationError('productId is required', 'productId');
      const session = await createCheckoutSession({ productId, customerEmail, origin });
      ok(res, { data: session });
    } catch (err) {
      if (err.code === 'STRIPE_NOT_CONFIGURED' || err.code === 'INVALID_PRODUCT_ID') {
        return errorResponse(res, err.statusCode || 500, err.message);
      }
      if (err.code === 'DC-400' || err instanceof ValidationError) {
        return errorResponse(res, err.statusCode || 400, err.message);
      }
      // Stripe SDK throws Stripe-specific errors; surface message but don't leak
      // Stripe's full response (may include internal IDs we don't want exposed).
      if (err.type && err.type.startsWith('Stripe')) {
        return errorResponse(res, 502, 'Payment provider error. Please try again.');
      }
      throw err;
    }
  }, 'billing-checkout'));

  /**
   * GET /api/v1/billing/lookup/:sessionId
   *
   * Returns the fulfillment state for a Stripe Checkout session. The
   * success page polls this every 1.5s until `delivered` or `pending_email`.
   *
   *   - 200 + { status: 'processing', durationDays } — license being generated
   *   - 200 + { status: 'pending_email', code, codeId, durationDays, productId, ... }
   *        — license persisted (SMTP failure recovery)
   *   - 200 + { status: 'delivered', code, codeId, durationDays, productId, deliveredVia }
   *        — license delivered by email
   *   - 404 + { status: 'not_found' } — no payment record for this sessionId
   *   - 404 + { status: 'expired' } — record exists but past the 24h TTL
   *
   * Cache-Control: no-store. CSRF-exempt. PUBLIC_ROUTES allowlist.
   */
  router.get('/lookup/:sessionId', asyncHandler(async (req, res) => {
    const { sessionId } = req.params;
    res.set('Cache-Control', 'no-store');

    if (!sessionId || typeof sessionId !== 'string' || sessionId.length > 256) {
      return errorResponse(res, 400, 'invalid sessionId');
    }

    const record = fulfillmentStore.readBySession(sessionId);
    if (!record) {
      return errorResponse(res, 404, 'no record for that sessionId');
    }

    const createdAt = record.createdAt ? Date.parse(record.createdAt) : Date.now();
    const ageMs = Date.now() - createdAt;
    if (Number.isFinite(ageMs) && ageMs > LOOKUP_TTL_MS) {
      return errorResponse(res, 404, 'record past lookup TTL');
    }

    if (record.status === 'generating' || (!record.code && record.status !== 'delivered')) {
      return ok(res, { data: { status: 'processing', durationDays: record.durationDays, productId: record.productId } });
    }

    if (record.status === 'delivered') {
      return ok(res, { data: { status: 'delivered', durationDays: record.durationDays, productId: record.productId, code: record.code, codeId: record.codeId, deliveredVia: record.deliveredVia || 'unknown' } });
    }

    // pending_email OR delivering — license is durably persisted.
    return ok(res, {
      data: {
        status: 'pending_email',
        durationDays: record.durationDays,
        productId: record.productId,
        code: record.code,
        codeId: record.codeId,
        deliveredVia: record.deliveredVia,
        lastError: record.lastError,
      },
    });
  }, 'billing-lookup'));

  return router;
};
