import express from 'express';
import crypto from 'crypto';
import { pathToFileURL } from 'url';
import { config } from './config.js';
import { getPlan, listPlans, PREMIUM_FEATURES } from './plans.js';
import { getStripe } from './stripe.js';
import {
  getStoreSnapshot,
  upsertCustomer,
  upsertSubscription,
  findLicenseByCustomerId,
  findCheckoutResult,
  claimWebhookEvent,
  finishWebhookEvent,
  claimBusinessObject,
  finishBusinessObject,
  cancelLicenseBySubscription,
  markLicensePaymentFailed,
  updateLicense
} from './store.js';
import {
  syncLicenseFromSubscription,
  grantOneTimeLicense,
  validateLicense,
  deactivateLicense
} from './licenseLogic.js';
import { sendLicenseEmail } from './email.js';

const app = express();
app.disable('x-powered-by');
app.set('trust proxy', 'loopback');
const allowedOrigins = new Set([
  config.websiteUrl.replace(/\/$/, ''),
  'https://dashcaddy.net',
  'https://www.dashcaddy.net'
]);

app.use((req, res, next) => {
  const origin = req.headers.origin;
  if (origin && allowedOrigins.has(origin)) {
    res.setHeader('Access-Control-Allow-Origin', origin);
    res.setHeader('Vary', 'Origin');
    res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
    res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, Stripe-Signature');
    res.setHeader('Access-Control-Max-Age', '600');
  }
  if (req.method === 'OPTIONS') {
    return origin && allowedOrigins.has(origin) ? res.sendStatus(204) : res.sendStatus(403);
  }
  return next();
});

app.use('/api/stripe/webhook', express.raw({ type: 'application/json', limit: '1mb' }));
app.use(express.json({ limit: '16kb' }));

const checkoutAttempts = new Map();
const CHECKOUT_WINDOW_MS = 15 * 60 * 1000;
const CHECKOUT_LIMIT = 20;
function checkoutRateLimit(req, res, next) {
  const now = Date.now();
  const key = req.ip || req.socket.remoteAddress || 'unknown';
  const isNewKey = !checkoutAttempts.has(key);
  const recent = (checkoutAttempts.get(key) || []).filter((time) => now - time < CHECKOUT_WINDOW_MS);
  if (recent.length >= CHECKOUT_LIMIT) {
    res.setHeader('Retry-After', String(Math.ceil((CHECKOUT_WINDOW_MS - (now - recent[0])) / 1000)));
    return res.status(429).json({ ok: false, error: 'Too many checkout attempts. Try again later.' });
  }
  recent.push(now);
  checkoutAttempts.set(key, recent);
  if (isNewKey) {
    const timer = setTimeout(() => checkoutAttempts.delete(key), CHECKOUT_WINDOW_MS + 1000);
    timer.unref?.();
  }
  return next();
}

function normalizeCustomerEmail(value) {
  const email = String(value || '').trim().toLowerCase();
  if (email.length > 254 || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) return null;
  return email;
}

function stripeObjectId(value) {
  if (!value) return null;
  return typeof value === 'string' ? value : value.id || null;
}

function getInvoiceSubscriptionId(invoice) {
  return stripeObjectId(invoice?.parent?.subscription_details?.subscription)
    || stripeObjectId(invoice?.subscription);
}

function getSubscriptionPeriodEnd(subscription) {
  const itemEnds = subscription?.items?.data
    ?.map((item) => Number(item.current_period_end || 0))
    .filter(Boolean) || [];
  const timestamp = itemEnds.length ? Math.max(...itemEnds) : Number(subscription?.current_period_end || 0);
  return timestamp ? new Date(timestamp * 1000).toISOString() : null;
}

function requireAdmin(req, res, next) {
  if (!config.adminToken) return res.status(404).json({ ok: false, error: 'Not found' });
  const supplied = String(req.headers.authorization || '').replace(/^Bearer\s+/i, '');
  const expected = Buffer.from(config.adminToken);
  const actual = Buffer.from(supplied);
  if (actual.length !== expected.length || !crypto.timingSafeEqual(actual, expected)) {
    return res.status(401).json({ ok: false, error: 'Unauthorized' });
  }
  return next();
}

async function deliverAndTrackLicenseEmail({ license, customerEmail, code, durationDays, planCode, extended, deliveryId }, sender = sendLicenseEmail) {
  if (deliveryId && license.lastEmailDeliveryId === deliveryId) {
    return { delivered: true, via: license.emailDeliveryVia || 'recorded', skipped: true };
  }
  if (!customerEmail) {
    updateLicense(license.id, { emailDeliveryStatus: 'failed', emailDeliveryError: 'missing customer email' });
    throw new Error('License fulfillment is missing customer email');
  }
  updateLicense(license.id, { emailDeliveryStatus: 'pending', emailDeliveryError: null });
  try {
    const result = await sender({
      to: customerEmail,
      code,
      durationDays,
      planCode,
      expiresAt: license.expiresAt,
      extended
    });
    if (!result.delivered) throw new Error('SMTP did not confirm delivery');
    updateLicense(license.id, {
      emailDeliveryStatus: 'delivered',
      emailDeliveryVia: result.via || null,
      emailDeliveredAt: new Date().toISOString(),
      lastEmailDeliveryId: deliveryId || license.lastEmailDeliveryId || null,
      emailDeliveryError: null
    });
    return result;
  } catch (error) {
    updateLicense(license.id, {
      emailDeliveryStatus: 'failed',
      emailDeliveryError: String(error.message || error).slice(0, 500)
    });
    console.error('License email failed', error.message);
    throw error;
  }
}

app.get('/health', (_req, res) => {
  res.json({ ok: true, service: 'dashcaddy-license-server' });
});

app.get('/api/public/config', (_req, res) => {
  res.json({
    ok: true,
    publishableKey: config.stripePublishableKey || null,
    websiteUrl: config.websiteUrl
  });
});

app.get('/api/public/plans', (_req, res) => {
  res.json({ ok: true, tier: 'premium', features: PREMIUM_FEATURES, plans: listPlans() });
});

app.get('/api/checkout/session/:sessionId', (req, res) => {
  res.setHeader('Cache-Control', 'no-store');
  const sessionId = String(req.params.sessionId || '');
  if (!/^cs_[A-Za-z0-9_]+$/.test(sessionId)) {
    return res.status(404).json({ status: 'not_found' });
  }
  const result = findCheckoutResult(sessionId);
  if (!result) return res.status(404).json({ status: 'not_found' });
  if (!result.license) return res.json({ status: 'processing' });

  const plan = getPlan(result.license.planCode);
  const expired = result.license.expiresAt && new Date(result.license.expiresAt).getTime() <= Date.now();
  const deliveryStatus = result.license.emailDeliveryStatus;
  const status = expired
    ? 'expired'
    : deliveryStatus === 'pending' || !deliveryStatus
      ? 'processing'
      : deliveryStatus === 'failed' ? 'pending_email' : 'delivered';
  return res.json({
    status,
    code: result.license.key,
    productId: result.license.planCode?.replace(/^premium_/, 'pro-') || null,
    durationDays: plan?.durationDays || null,
    deliveredVia: result.license.emailDeliveryVia || null
  });
});

app.get('/api/admin/debug/store', requireAdmin, (_req, res) => {
  res.json({ ok: true, store: getStoreSnapshot() });
});

/**
 * Subscription checkout. Customer subscribes and is auto-renewed on the
 * plan's interval. The license is created on the first webhook event and
 * extended on every renewal.
 */
app.post('/api/checkout/subscription', checkoutRateLimit, async (req, res) => {
  try {
    const { planCode, customerEmail } = req.body || {};
    const plan = getPlan(planCode);
    if (!plan) return res.status(400).json({ ok: false, error: 'Invalid planCode' });
    const normalizedEmail = normalizeCustomerEmail(customerEmail);
    if (!normalizedEmail) return res.status(400).json({ ok: false, error: 'Valid customerEmail is required' });

    const stripe = getStripe();
    const session = await stripe.checkout.sessions.create({
      mode: 'subscription',
      payment_method_types: ['card'],
      line_items: [{
        price_data: {
          currency: 'usd',
          product_data: { name: plan.label, description: 'DashCaddy Premium subscription' },
          recurring: { interval: plan.interval, interval_count: plan.intervalCount },
          unit_amount: plan.amountUsd * 100
        },
        quantity: 1
      }],
      success_url: `${config.websiteUrl}/success?session_id={CHECKOUT_SESSION_ID}&tier=premium`,
      cancel_url: `${config.websiteUrl}/pricing`,
      allow_promotion_codes: true,
      billing_address_collection: 'required',
      customer_email: normalizedEmail,
      metadata: {
        source: 'dashcaddy.net',
        planCode: plan.code,
        tier: plan.tier,
        mode: 'subscription',
        customerEmail: normalizedEmail
      },
      subscription_data: {
        metadata: {
          source: 'dashcaddy.net',
          planCode: plan.code,
          tier: plan.tier,
          customerEmail: normalizedEmail
        }
      }
    });

    return res.json({ ok: true, url: session.url, sessionId: session.id });
  } catch (error) {
    console.error('Subscription checkout error:', error);
    return res.status(500).json({ ok: false, error: 'Checkout is temporarily unavailable' });
  }
});

/**
 * One-time purchase checkout. Customer pays once for a license of the
 * plan duration. If they buy again, time is added to their existing license.
 */
app.post('/api/checkout/one-time', checkoutRateLimit, async (req, res) => {
  try {
    const { planCode, customerEmail } = req.body || {};
    const plan = getPlan(planCode);
    if (!plan) return res.status(400).json({ ok: false, error: 'Invalid planCode' });
    const normalizedEmail = normalizeCustomerEmail(customerEmail);
    if (!normalizedEmail) return res.status(400).json({ ok: false, error: 'Valid customerEmail is required' });

    const stripe = getStripe();
    const session = await stripe.checkout.sessions.create({
      mode: 'payment',
      customer_creation: 'always',
      payment_method_types: ['card'],
      line_items: [{
        price_data: {
          currency: 'usd',
          product_data: { name: plan.label, description: 'DashCaddy Premium one-time license' },
          unit_amount: plan.amountUsd * 100
        },
        quantity: 1
      }],
      success_url: `${config.websiteUrl}/success?session_id={CHECKOUT_SESSION_ID}&tier=premium`,
      cancel_url: `${config.websiteUrl}/pricing`,
      allow_promotion_codes: true,
      billing_address_collection: 'required',
      customer_email: normalizedEmail,
      metadata: {
        source: 'dashcaddy.net',
        planCode: plan.code,
        tier: plan.tier,
        mode: 'one-time',
        durationDays: String(plan.durationDays),
        customerEmail: normalizedEmail
      },
      payment_intent_data: {
        metadata: {
          source: 'dashcaddy.net',
          planCode: plan.code,
          tier: plan.tier,
          mode: 'one-time',
          durationDays: String(plan.durationDays),
          customerEmail: normalizedEmail
        }
      }
    });

    return res.json({ ok: true, url: session.url, sessionId: session.id });
  } catch (error) {
    console.error('One-time checkout error:', error);
    return res.status(500).json({ ok: false, error: 'Checkout is temporarily unavailable' });
  }
});

/**
 * Stripe webhook endpoint. Handles subscription lifecycle events AND
 * one-time payment completion events. The same handler routes based on
 * event type and updates the customer's license accordingly.
 */
app.post('/api/stripe/webhook', async (req, res) => {
  let claimedEventId = null;
  let claimedBusiness = null;
  try {
    if (!config.stripeWebhookSecret) {
      return res.status(500).json({ ok: false, error: 'Missing STRIPE_WEBHOOK_SECRET' });
    }
    const signature = req.headers['stripe-signature'];
    if (!signature) {
      return res.status(400).json({ ok: false, error: 'Missing stripe-signature header' });
    }

    const stripe = getStripe();
    const event = stripe.webhooks.constructEvent(req.body, signature, config.stripeWebhookSecret);
    const eventClaim = claimWebhookEvent(event.id);
    if (!eventClaim.claimed) {
      if (eventClaim.status === 'completed') return res.json({ ok: true, duplicate: true });
      return res.status(409).json({ ok: false, retry: true, error: 'Webhook event is still processing' });
    }
    claimedEventId = event.id;

    switch (event.type) {
      case 'checkout.session.completed': {
        const session = event.data.object;
        const planCode = session.metadata?.planCode;
        const mode = session.metadata?.mode || 'subscription';
        const customerId = typeof session.customer === 'string' ? session.customer : session.customer?.id;
        const customerEmail = session.metadata?.customerEmail || session.customer_email || session.customer_details?.email || null;

        if (customerId) {
          upsertCustomer({ id: String(customerId), email: customerEmail, checkoutSessionId: session.id });
        }

        // For one-time payments, generate/extend the license and email it.
        if (mode === 'one-time' && (!planCode || session.payment_status !== 'paid')) {
          throw new Error('One-time checkout completed without a paid status or plan code');
        }
        if (mode === 'one-time' && planCode && session.payment_status === 'paid') {
          const plan = getPlan(planCode);
          if (!plan) throw new Error('Paid checkout references an unknown plan');
          if (plan) {
            const paymentIntentId = stripeObjectId(session.payment_intent);
            if (!paymentIntentId || !customerId || !customerEmail) {
              throw new Error('Paid checkout is missing payment intent, customer, or email');
            }
            const license = grantOneTimeLicense({
              paymentIntentId,
              customerId,
              customerEmail,
              planCode,
              durationDays: plan.durationDays
            });

            // The stored key is already human-readable and is the exact value
            // accepted by /api/license/validate. Never email a derived code
            // that the validation store cannot resolve.
            const code = license.key;

            if (!license.idempotent || license.emailDeliveryStatus !== 'delivered') {
              const delivery = await deliverAndTrackLicenseEmail({
                license,
                customerEmail,
                code,
                durationDays: plan.durationDays,
                planCode,
                extended: license.extended || false,
                deliveryId: paymentIntentId
              });
              console.log('License email', {
                delivered: delivery.delivered,
                via: delivery.via,
                extended: license.extended
              });
            }

            console.log('One-time license granted', {
              sessionId: session.id,
              extended: license.extended || false,
              addedDays: license.addedDays || plan.durationDays
            });
          }
        }
        break;
      }

      case 'invoice.paid': {
        // Subscription renewal. Extend the existing license once per Stripe invoice.
        const invoice = event.data.object;
        const invoiceClaim = claimBusinessObject('invoice.paid', invoice.id);
        if (!invoiceClaim.claimed) {
          if (invoiceClaim.status === 'completed') {
            console.log('Duplicate invoice.paid ignored', { invoiceId: invoice.id });
            break;
          }
          throw new Error('Invoice is already being processed; retry later');
        }
        claimedBusiness = { kind: 'invoice.paid', id: invoice.id };
        const subscriptionId = getInvoiceSubscriptionId(invoice);
        const customerId = typeof invoice.customer === 'string' ? invoice.customer : invoice.customer?.id;
        if (!subscriptionId || !customerId) throw new Error('Paid invoice is missing subscription or customer');
        if (subscriptionId && customerId) {
          const sub = await stripe.subscriptions.retrieve(subscriptionId);
          const planCode = sub.metadata?.planCode || 'premium_30d';
          const customerEmail = sub.metadata?.customerEmail || invoice.customer_email || null;
          const currentPeriodEnd = getSubscriptionPeriodEnd(sub);

          upsertSubscription({
            id: subscriptionId,
            customerId,
            status: sub.status,
            planCode,
            currentPeriodEnd,
            cancelAtPeriodEnd: Boolean(sub.cancel_at_period_end),
            lastStripeEventCreated: event.created,
            lastStripeEventId: event.id
          });

          const license = syncLicenseFromSubscription({
            subscriptionId,
            customerId,
            customerEmail,
            planCode,
            status: sub.status,
            currentPeriodEnd,
            eventCreated: event.created,
            eventId: event.id
          });

          const code = license.key;

          await deliverAndTrackLicenseEmail({
            license,
            customerEmail,
            code,
            durationDays: getPlan(planCode)?.durationDays || 30,
            planCode,
            extended: true,
            deliveryId: invoice.id
          });

          console.log('Subscription renewal extended license', {
            subscriptionId,
            expiresAt: license.expiresAt
          });
        }
        break;
      }

      case 'customer.subscription.created':
      case 'customer.subscription.updated': {
        const subscription = event.data.object;
        const customerId = typeof subscription.customer === 'string' ? subscription.customer : subscription.customer?.id;
        if (!subscription.id || !customerId) throw new Error('Subscription event is missing subscription or customer');
        let customerEmail = subscription.metadata?.customerEmail || null;
        if (!customerEmail && customerId) {
          try {
            const customer = await stripe.customers.retrieve(customerId);
            customerEmail = customer.email || null;
          } catch (err) {
            console.warn('Failed to fetch customer email', { customerId, error: err.message });
          }
        }
        const planCode = subscription.metadata?.planCode || 'premium_30d';
        const currentPeriodEnd = getSubscriptionPeriodEnd(subscription);

        upsertSubscription({
          id: subscription.id,
          customerId,
          status: subscription.status,
          planCode,
          currentPeriodEnd,
          cancelAtPeriodEnd: Boolean(subscription.cancel_at_period_end),
          lastStripeEventCreated: event.created,
          lastStripeEventId: event.id
        });

        const license = syncLicenseFromSubscription({
          subscriptionId: subscription.id,
          customerId,
          customerEmail,
          planCode,
          status: subscription.status,
          currentPeriodEnd,
          eventCreated: event.created,
          eventId: event.id
        });

        console.log('Subscription license synced', {
          subscriptionId: subscription.id,
          status: license.status,
          expiresAt: license.expiresAt
        });
        break;
      }

      case 'customer.subscription.deleted': {
        const subscription = event.data.object;
        upsertSubscription({
          id: subscription.id,
          status: 'canceled',
          currentPeriodEnd: getSubscriptionPeriodEnd(subscription),
          cancelAtPeriodEnd: true,
          lastStripeEventCreated: event.created,
          lastStripeEventId: event.id
        });
        const license = cancelLicenseBySubscription(subscription.id, event.created, event.id);
        console.log('Subscription cancelled', {
          subscriptionId: subscription.id,
          licenseActiveUntilExpiry: license?.active ?? null,
          expiresAt: license?.expiresAt || null
        });
        break;
      }

      case 'invoice.payment_failed': {
        const invoice = event.data.object;
        const failureClaim = claimBusinessObject('invoice.payment_failed', invoice.id);
        if (!failureClaim.claimed) {
          if (failureClaim.status === 'completed') {
            console.log('Duplicate invoice.payment_failed ignored', { invoiceId: invoice.id });
            break;
          }
          throw new Error('Failed invoice is already being processed; retry later');
        }
        claimedBusiness = { kind: 'invoice.payment_failed', id: invoice.id };
        const subscriptionId = getInvoiceSubscriptionId(invoice);
        if (!subscriptionId) throw new Error('Failed invoice is missing subscription');
        const failureAnchor = Number(invoice.due_date || invoice.period_end || invoice.created || event.created);
        const graceUntil = new Date((failureAnchor + 7 * 24 * 60 * 60) * 1000).toISOString();
        if (subscriptionId) {
          upsertSubscription({
            id: subscriptionId,
            status: 'past_due',
            graceUntil,
            customerId: typeof invoice.customer === 'string' ? invoice.customer : invoice.customer?.id || null,
            lastStripeEventCreated: event.created,
            lastStripeEventId: event.id
          });
          markLicensePaymentFailed(subscriptionId, graceUntil, event.created, event.id);
        }
        console.warn('Payment failed', { invoiceId: invoice.id, subscriptionId, graceUntil });
        break;
      }

      default:
        console.log('Unhandled Stripe event', event.type);
    }

    if (claimedBusiness) finishBusinessObject(claimedBusiness.kind, claimedBusiness.id);
    finishWebhookEvent(claimedEventId);
    return res.json({ ok: true });
  } catch (error) {
    if (claimedBusiness) {
      try { finishBusinessObject(claimedBusiness.kind, claimedBusiness.id, error.message || 'Processing failed'); } catch (_) {}
    }
    if (claimedEventId) {
      try { finishWebhookEvent(claimedEventId, error.message || 'Webhook failed'); } catch (_) {}
    }
    console.error('Webhook processing error:', error);
    return res.status(400).json({ ok: false, error: 'Webhook processing failed' });
  }
});

app.post('/api/license/validate', async (req, res) => {
  const { code, machine, machineId } = req.body || {};
  if (!code) return res.status(400).json({ ok: false, error: 'License code is required' });
  const hasMachineObject = machine && typeof machine === 'object'
    && ['hostname', 'platform', 'arch', 'cpu', 'mac'].some((field) => String(machine[field] || '').trim());
  if (!machineId && !hasMachineObject) {
    return res.status(400).json({ success: false, error: 'Machine identity is required' });
  }
  const machinePayload = machine || (machineId ? { hostname: String(machineId) } : {});
  const result = validateLicense({ code, machine: machinePayload });
  if (!result.success) {
    return res.status(400).json({ success: false, error: result.message, message: result.message });
  }
  const featureList = Object.entries(result.license.features || {})
    .filter(([, enabled]) => Boolean(enabled))
    .map(([feature]) => feature);
  const durationDays = result.license.expiresAt
    ? Math.max(0, Math.ceil((new Date(result.license.expiresAt).getTime() - Date.now()) / 86400000))
    : null;
  return res.json({
    ...result,
    codeId: result.license.id || null,
    durationDays,
    expiresAt: result.license.expiresAt,
    features: featureList,
    token: null
  });
});

app.post('/api/license/deactivate', async (req, res) => {
  const { code, machine, machineId } = req.body || {};
  if (!code) return res.status(400).json({ ok: false, error: 'License code is required' });
  const hasMachineObject = machine && typeof machine === 'object'
    && ['hostname', 'platform', 'arch', 'cpu', 'mac'].some((field) => String(machine[field] || '').trim());
  if (!machineId && !hasMachineObject) {
    return res.status(400).json({ success: false, error: 'Machine identity is required' });
  }
  const machinePayload = machine || (machineId ? { hostname: String(machineId) } : {});
  const result = deactivateLicense({ code, machine: machinePayload });
  return res.status(result.success ? 200 : 400).json(result);
});

function validateStartupConfig() {
  const missing = [];
  if (!config.stripeSecretKey) missing.push('STRIPE_SECRET_KEY');
  if (!config.stripeWebhookSecret) missing.push('STRIPE_WEBHOOK_SECRET');
  if (!/^[A-Fa-f0-9]{32,}$/.test(config.licenseSecret)) missing.push('DASHCADDY_LICENSE_SECRET (hex, at least 128 bits)');
  if (missing.length) throw new Error(`Missing or invalid production configuration: ${missing.join(', ')}`);
}

export { app, getInvoiceSubscriptionId, getSubscriptionPeriodEnd, validateStartupConfig, deliverAndTrackLicenseEmail };

if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
  validateStartupConfig();
  app.listen(config.port, () => {
    console.log(`dashcaddy-license-server listening on :${config.port}`);
  });
}
