/**
 * Waitlist REST endpoints — pre-launch signup collection.
 *
 * Public (no auth, rate-limited):
 *   POST   /public/waitlist/:productId          — join waitlist
 *   POST   /public/waitlist/:productId/status   — check position (email + token)
 *   GET    /public/waitlist/:productId/count    — total signups (social proof)
 *   GET    /public/waitlist/:productId/config   — prelaunch config (custom fields)
 *   POST   /public/waitlist/unsubscribe         — unsubscribe via token
 *
 * Admin (JWT auth, role=admin):
 *   GET    /waitlist                            — list entries
 *   GET    /waitlist/stats                      — signup analytics
 *   GET    /waitlist/:id                        — get single entry
 *   PUT    /waitlist/:id                        — update entry
 *   DELETE /waitlist/:id                        — delete entry
 *   POST   /waitlist/invite                     — batch invite
 *   POST   /waitlist/export                     — CSV export
 */

import crypto from 'node:crypto';
import type { FastifyInstance, FastifyRequest } from 'fastify';
import rateLimit from '@fastify/rate-limit';
import { getRequestProductId } from '../../lib/request-context.js';
import { BadRequestError, ForbiddenError, NotFoundError } from '../../lib/errors.js';
import { getProduct } from '../products/cache.js';
import { dispatchWaitlistJoined } from '../../lib/webhooks.js';
import * as auditRepo from '../audit/repository.js';
import * as invitationsRepo from '../invitations/repository.js';
import type { InvitationCodeDoc } from '../invitations/types.js';
import type { CustomField } from '../products/types.js';
import * as repo from './repository.js';
import {
  JoinWaitlistSchema,
  CheckStatusSchema,
  UnsubscribeSchema,
  UpdateWaitlistEntrySchema,
  WaitlistQuerySchema,
  BatchInviteSchema,
  type WaitlistEntryDoc,
} from './types.js';

// ── Helpers ──

function requireAdmin(req: FastifyRequest): void {
  if (!req.jwtPayload || req.jwtPayload.role !== 'admin') {
    throw new ForbiddenError('Admin access required');
  }
}

function hashIp(ip: string): string {
  return crypto.createHash('sha256').update(ip).digest('hex').slice(0, 16);
}

/**
 * Validate customData keys against the product's customFields schema.
 * Rejects unknown keys and enforces required fields.
 */
function validateCustomData(
  customData: Record<string, unknown>,
  customFields: CustomField[]
): string | null {
  const allowedKeys = new Set(customFields.map(f => f.key));

  // Reject unknown keys
  for (const key of Object.keys(customData)) {
    if (!allowedKeys.has(key)) {
      return `Unknown custom field: "${key}"`;
    }
  }

  // Check required fields
  for (const field of customFields) {
    if (field.required && (customData[field.key] === undefined || customData[field.key] === '')) {
      return `Custom field "${field.key}" is required`;
    }
  }

  return null; // valid
}

// ── Route registration ──

export async function waitlistRoutes(app: FastifyInstance) {
  // ════════════════════════════════════════════════════════════
  // PUBLIC ROUTES — rate-limited, no auth
  // ════════════════════════════════════════════════════════════

  // Rate limiting for public waitlist routes
  await app.register(rateLimit, {
    max: 60,
    timeWindow: '1 minute',
    keyGenerator: req => req.ip,
  });

  // ── Join waitlist ──
  app.post(
    '/public/waitlist/:productId',
    { config: { rateLimit: { max: 10, timeWindow: '1 minute' } } },
    async (req, reply) => {
      const { productId } = req.params as { productId: string };

      // Validate product exists and is in pre_launch (or active/beta for flexibility)
      const product = getProduct(productId);
      if (!product) throw new NotFoundError('Product not found');
      if (
        product.status === 'draft' ||
        product.status === 'sunset' ||
        product.status === 'disabled'
      ) {
        throw new BadRequestError(`Product ${productId} is not accepting signups`);
      }

      // Check prelaunchConfig
      const plConfig = product.prelaunchConfig;
      if (!plConfig?.signupEnabled) {
        throw new BadRequestError('Waitlist signup is not enabled for this product');
      }

      // Parse input
      const parsed = JoinWaitlistSchema.safeParse(req.body);
      if (!parsed.success) {
        throw new BadRequestError(parsed.error.issues.map(i => i.message).join('; '));
      }
      const input = parsed.data;

      // TODO-1: CAPTCHA validation — when captchaEnabled, verify input.captchaToken
      // against the configured provider (Turnstile/hCaptcha/reCAPTCHA).
      // Skipped for now — requires provider API keys and HTTP calls.

      // Validate customData against product's custom fields
      if (plConfig.customFields.length > 0) {
        const err = validateCustomData(input.customData, plConfig.customFields);
        if (err) throw new BadRequestError(err);
      }

      // Dedupe check (returns 200, not 201, for existing entries)
      const emailNormalized = repo.normalizeEmail(input.email);
      const existing = await repo.getByEmail(emailNormalized, productId);
      if (existing) {
        return {
          id: existing.id,
          position: existing.position,
          alreadyRegistered: true,
          referralLink: `?ref=${existing.id}`,
        };
      }

      // Check max signups cap
      if (plConfig.maxSignups) {
        const currentCount = await repo.count(productId);
        if (currentCount >= plConfig.maxSignups) {
          throw new BadRequestError('Waitlist is full');
        }
      }

      // Assign position
      const position = await repo.getNextPosition(productId);

      // Handle referral
      let referredBy: string | undefined;
      let source = input.source;
      if (input.ref) {
        const referrer = await repo.getById(input.ref);
        if (referrer && referrer.productId === productId && referrer.status === 'pending') {
          referredBy = referrer.id;
          source = 'referral';
          // Bump referrer priority
          await repo.update(referrer.id, referrer.email, {
            priority: referrer.priority + 1,
          });
        }
      }

      const now = new Date().toISOString();
      const doc: WaitlistEntryDoc = {
        id: `wl_${crypto.randomUUID()}`,
        productId,
        email: input.email.trim(),
        emailNormalized,
        name: input.name,
        source,
        referredBy,
        status: 'pending',
        position,
        priority: 0,
        customData: input.customData,
        ipHash: hashIp(req.ip),
        utmSource: input.utmSource,
        utmMedium: input.utmMedium,
        utmCampaign: input.utmCampaign,
        unsubscribeToken: crypto.randomUUID(),
        createdAt: now,
        updatedAt: now,
      };

      const created = await repo.create(doc);

      // Fire webhook (async, non-blocking)
      dispatchWaitlistJoined({
        entryId: created.id,
        email: created.email,
        position: created.position,
        source: created.source,
      });

      reply.code(201);
      return {
        id: created.id,
        position: created.position,
        unsubscribeToken: created.unsubscribeToken,
        referralLink: `?ref=${created.id}`,
        message:
          plConfig.confirmationMessage?.replace('{{position}}', String(created.position)) ??
          `You're #${created.position} on the waitlist!`,
      };
    }
  );

  // ── Check status ──
  app.post(
    '/public/waitlist/:productId/status',
    { config: { rateLimit: { max: 30, timeWindow: '1 minute' } } },
    async req => {
      const { productId } = req.params as { productId: string };
      const parsed = CheckStatusSchema.safeParse(req.body);
      if (!parsed.success) {
        throw new BadRequestError(parsed.error.issues.map(i => i.message).join('; '));
      }

      const emailNormalized = repo.normalizeEmail(parsed.data.email);
      const entry = await repo.getByEmail(emailNormalized, productId);

      if (!entry || entry.unsubscribeToken !== parsed.data.unsubscribeToken) {
        throw new NotFoundError('Entry not found or token mismatch');
      }

      return {
        position: entry.position,
        status: entry.status,
        createdAt: entry.createdAt,
      };
    }
  );

  // ── Count (social proof) ──
  app.get('/public/waitlist/:productId/count', async req => {
    const { productId } = req.params as { productId: string };
    const product = getProduct(productId);
    if (!product) throw new NotFoundError('Product not found');
    if (
      product.status === 'draft' ||
      product.status === 'sunset' ||
      product.status === 'disabled'
    ) {
      throw new BadRequestError(`Product ${productId} is not available`);
    }

    const total = await repo.count(productId);
    return { count: total };
  });

  // ── Public config (custom fields for frontend rendering) ──
  app.get('/public/waitlist/:productId/config', async req => {
    const { productId } = req.params as { productId: string };
    const product = getProduct(productId);
    if (!product) throw new NotFoundError('Product not found');
    if (
      product.status === 'draft' ||
      product.status === 'sunset' ||
      product.status === 'disabled'
    ) {
      throw new BadRequestError(`Product ${productId} is not available`);
    }

    const plConfig = product.prelaunchConfig;
    if (!plConfig) {
      return {
        signupEnabled: false,
        customFields: [],
      };
    }

    // Return only public-safe fields (strip maxSignups, captcha provider details)
    return {
      signupEnabled: plConfig.signupEnabled,
      launchDate: plConfig.launchDate,
      tagline: plConfig.tagline,
      logoUrl: plConfig.logoUrl,
      customFields: plConfig.customFields,
      confirmationMessage: plConfig.confirmationMessage,
      redirectUrl: plConfig.redirectUrl,
      captchaEnabled: plConfig.captchaEnabled,
    };
  });

  // ── Unsubscribe ──
  app.post(
    '/public/waitlist/unsubscribe',
    { config: { rateLimit: { max: 10, timeWindow: '1 minute' } } },
    async req => {
      const parsed = UnsubscribeSchema.safeParse(req.body);
      if (!parsed.success) {
        throw new BadRequestError(parsed.error.issues.map(i => i.message).join('; '));
      }

      // Verify email matches the token (prevents blind token guessing)
      const entry = await repo.getByUnsubscribeToken(parsed.data.unsubscribeToken);
      if (!entry || repo.normalizeEmail(parsed.data.email) !== entry.emailNormalized) {
        throw new NotFoundError('Entry not found or token mismatch');
      }

      if (entry.status !== 'unsubscribed') {
        await repo.update(entry.id, entry.email, { status: 'unsubscribed' });
      }

      return { status: 'unsubscribed' };
    }
  );

  // ════════════════════════════════════════════════════════════
  // ADMIN ROUTES — JWT auth, role=admin
  // ════════════════════════════════════════════════════════════

  // ── List entries ──
  app.get('/waitlist', async req => {
    requireAdmin(req);
    const raw = req.query as Record<string, string>;
    // Default productId from request context
    if (!raw.productId) {
      try {
        raw.productId = getRequestProductId(req);
      } catch {
        // If no productId available, list across all products
      }
    }
    const parsed = WaitlistQuerySchema.safeParse(raw);
    if (!parsed.success) {
      throw new BadRequestError(parsed.error.issues.map(i => i.message).join('; '));
    }
    return repo.list(parsed.data);
  });

  // ── Stats ──
  app.get('/waitlist/stats', async req => {
    requireAdmin(req);
    const productId = getRequestProductId(req);
    return repo.stats(productId);
  });

  // ── Get single entry ──
  app.get('/waitlist/:id', async req => {
    requireAdmin(req);
    const { id } = req.params as { id: string };
    const entry = await repo.getById(id);
    if (!entry) throw new NotFoundError('Waitlist entry not found');
    return entry;
  });

  // ── Update entry ──
  app.put('/waitlist/:id', async req => {
    requireAdmin(req);
    const { id } = req.params as { id: string };
    const entry = await repo.getById(id);
    if (!entry) throw new NotFoundError('Waitlist entry not found');

    const parsed = UpdateWaitlistEntrySchema.safeParse(req.body);
    if (!parsed.success) {
      throw new BadRequestError(parsed.error.issues.map(i => i.message).join('; '));
    }

    const updated = await repo.update(id, entry.email, parsed.data);
    if (!updated) throw new NotFoundError('Update failed');
    return updated;
  });

  // ── Delete entry ──
  app.delete('/waitlist/:id', async (req, reply) => {
    requireAdmin(req);
    const { id } = req.params as { id: string };
    const entry = await repo.getById(id);
    if (!entry) throw new NotFoundError('Waitlist entry not found');

    const ok = await repo.remove(id, entry.email);
    if (!ok) throw new NotFoundError('Delete failed');

    // Create audit log entry for admin delete action
    await auditRepo.create({
      id: `audit_${crypto.randomUUID()}`,
      productId: entry.productId,
      userId: req.jwtPayload!.sub,
      action: 'waitlist.delete',
      category: 'waitlist',
      details: { entryId: id, email: entry.email },
      ipAddress: req.ip,
      userAgent: req.headers['user-agent'],
      createdAt: new Date().toISOString(),
    });

    reply.code(204);
    return;
  });

  // ── Batch invite ──
  app.post('/waitlist/invite', async req => {
    requireAdmin(req);
    const productId = getRequestProductId(req);

    const parsed = BatchInviteSchema.safeParse(req.body);
    if (!parsed.success) {
      throw new BadRequestError(parsed.error.issues.map(i => i.message).join('; '));
    }
    const { count: inviteCount, strategy } = parsed.data;

    // Fetch pending entries by strategy
    const sortBy = strategy === 'priority' ? 'priority' : 'position';
    let entries = await repo.getByStatus(productId, 'pending', sortBy, inviteCount);

    if (strategy === 'random') {
      // Shuffle using Fisher-Yates
      for (let i = entries.length - 1; i > 0; i--) {
        const j = Math.floor(Math.random() * (i + 1));
        [entries[i], entries[j]] = [entries[j], entries[i]];
      }
      entries = entries.slice(0, inviteCount);
    }

    const now = new Date().toISOString();
    let invited = 0;
    let failed = 0;

    for (const entry of entries) {
      try {
        // Generate invitation code for this waitlist entry
        const code = `WL-${crypto.randomUUID().slice(0, 8).toUpperCase()}`;
        const invDoc: InvitationCodeDoc = {
          id: `inv_${crypto.randomUUID()}`,
          productId,
          code,
          description: `Waitlist invite for ${entry.email}`,
          createdBy: req.jwtPayload!.sub,
          grantPlan: 'pro',
          grantTrialDays: 14,
          bonusTokens: 0,
          maxUses: 1,
          currentUses: 0,
          redeemedBy: [],
          status: 'active',
          expiresAt: new Date(Date.now() + 30 * 86_400_000).toISOString(),
          createdAt: now,
          updatedAt: now,
        };
        await invitationsRepo.create(invDoc);

        await repo.update(entry.id, entry.email, {
          status: 'invited',
          invitedAt: now,
          invitationCodeId: invDoc.id,
        });
        invited++;
      } catch {
        failed++;
      }
    }

    // Create audit log entry for batch invite
    await auditRepo.create({
      id: `audit_${crypto.randomUUID()}`,
      productId,
      userId: req.jwtPayload!.sub,
      action: 'waitlist.invite',
      category: 'waitlist',
      details: { invited, failed, total: entries.length, strategy },
      ipAddress: req.ip,
      userAgent: req.headers['user-agent'],
      createdAt: new Date().toISOString(),
    });

    return {
      invited,
      failed,
      total: entries.length,
    };
  });

  // ── Export (CSV) ──
  app.post('/waitlist/export', async (req, reply) => {
    requireAdmin(req);
    const productId = getRequestProductId(req);

    const { items } = await repo.list({
      productId,
      sortBy: 'position',
      sortOrder: 'asc',
      limit: 10000,
      offset: 0,
    });

    // Build CSV (exclude sensitive fields: ipHash, unsubscribeToken)
    const headers = [
      'id',
      'email',
      'name',
      'position',
      'priority',
      'status',
      'source',
      'referredBy',
      'utmSource',
      'utmMedium',
      'utmCampaign',
      'createdAt',
    ];
    const csvLines = [headers.join(',')];
    for (const item of items) {
      const row = headers.map(h => {
        const val = (item as unknown as Record<string, unknown>)[h];
        if (val === undefined || val === null) return '';
        const str = String(val);
        if (str.includes(',') || str.includes('"') || str.includes('\n') || str.includes('\r')) {
          return `"${str.replace(/"/g, '""')}"`;
        }
        return str;
      });
      csvLines.push(row.join(','));
    }

    // Create audit log entry for export action
    await auditRepo.create({
      id: `audit_${crypto.randomUUID()}`,
      productId,
      userId: req.jwtPayload!.sub,
      action: 'waitlist.export',
      category: 'waitlist',
      details: { recordCount: items.length },
      ipAddress: req.ip,
      userAgent: req.headers['user-agent'],
      createdAt: new Date().toISOString(),
    });

    reply.header('Content-Type', 'text/csv');
    reply.header(
      'Content-Disposition',
      `attachment; filename="waitlist-${productId}-${Date.now()}.csv"`
    );
    return csvLines.join('\n');
  });
}
