/**
 * Broadcast REST routes — admin CRUD + public user endpoints
 * @module broadcasts/routes
 */

import { randomUUID } from 'node:crypto';
import type { FastifyInstance } from 'fastify';
import {
  UnauthorizedError,
  ForbiddenError,
  NotFoundError,
  BadRequestError,
} from '../../lib/errors.js';
import { getRequestProductId } from '../../lib/request-context.js';
import { list as listUsers } from '../auth/repository.js';
import { dispatchEmail } from '../delivery/dispatcher.js';
import * as repo from './repository.js';
import {
  CreateBroadcastSchema,
  UpdateBroadcastSchema,
  BroadcastStatus,
  type Broadcast,
} from './types.js';

// =============================================================================
// Auth Helpers
// =============================================================================

function requireAuth(req: { jwtPayload?: { sub: string } }): string {
  if (!req.jwtPayload?.sub) throw new UnauthorizedError('Authentication required');
  return req.jwtPayload.sub;
}

function requireAdmin(req: { jwtPayload?: { sub: string; role?: string } }): string {
  const userId = requireAuth(req);
  if (req.jwtPayload?.role !== 'admin') {
    throw new ForbiddenError('Admin access required');
  }
  return userId;
}

// =============================================================================
// Admin Routes
// =============================================================================

async function adminRoutes(app: FastifyInstance): Promise<void> {
  // List all broadcasts
  app.get('/', async req => {
    const adminId = requireAdmin(req);
    const productId = getRequestProductId(req);

    const { status } = req.query as { status?: string };
    const { broadcasts, total } = await repo.listBroadcasts(productId, {
      status: status as Broadcast['status'],
    });

    req.log.info({ adminId, productId, count: broadcasts.length }, 'Listed broadcasts');
    return { broadcasts, total };
  });

  // Get single broadcast
  app.get<{ Params: { id: string } }>('/:id', async req => {
    requireAdmin(req);
    const productId = getRequestProductId(req);
    const { id } = req.params;

    const broadcast = await repo.getBroadcast(id, productId);
    if (!broadcast) throw new NotFoundError('Broadcast not found');

    return broadcast;
  });

  // Create broadcast
  app.post('/', async (req, reply) => {
    const adminId = requireAdmin(req);
    const productId = getRequestProductId(req);

    const input = CreateBroadcastSchema.parse(req.body);

    const now = new Date().toISOString();
    const broadcast: Broadcast = {
      id: `bcast_${randomUUID()}`,
      productId,
      ...input,
      status: input.scheduledAt ? BroadcastStatus.SCHEDULED : BroadcastStatus.DRAFT,
      metrics: {
        targetedCount: 0,
        sentCount: 0,
        deliveredCount: 0,
        openedCount: 0,
        clickedCount: 0,
        dismissedCount: 0,
        convertedCount: 0,
      },
      createdAt: now,
      updatedAt: now,
      createdBy: adminId,
    };

    const created = await repo.createBroadcast(broadcast);
    req.log.info({ broadcastId: created.id, adminId }, 'Created broadcast');

    reply.status(201);
    return created;
  });

  // Update broadcast (only if draft or scheduled)
  app.put<{ Params: { id: string } }>('/:id', async req => {
    const adminId = requireAdmin(req);
    const productId = getRequestProductId(req);
    const { id } = req.params;

    const existing = await repo.getBroadcast(id, productId);
    if (!existing) throw new NotFoundError('Broadcast not found');

    // Can only edit draft or scheduled broadcasts
    if (existing.status === BroadcastStatus.SENDING || existing.status === BroadcastStatus.SENT) {
      throw new BadRequestError('Cannot edit broadcast that has already been sent');
    }

    const updates = UpdateBroadcastSchema.parse(req.body);

    const updated = await repo.updateBroadcast(id, productId, updates);
    req.log.info({ broadcastId: id, adminId }, 'Updated broadcast');

    return updated;
  });

  // Delete broadcast
  app.delete<{ Params: { id: string } }>('/:id', async (req, reply) => {
    const adminId = requireAdmin(req);
    const productId = getRequestProductId(req);
    const { id } = req.params;

    const existing = await repo.getBroadcast(id, productId);
    if (!existing) throw new NotFoundError('Broadcast not found');

    // Cannot delete broadcasts that are being sent or already sent
    if (existing.status === BroadcastStatus.SENDING || existing.status === BroadcastStatus.SENT) {
      throw new BadRequestError('Cannot delete broadcast that has already been sent');
    }

    const deleted = await repo.deleteBroadcast(id, productId);
    if (!deleted) throw new NotFoundError('Broadcast not found');

    req.log.info({ broadcastId: id, adminId }, 'Deleted broadcast');
    reply.status(204);
    return;
  });

  // Estimate reach for targeting
  app.post<{ Params: { id: string } }>('/:id/estimate-reach', async req => {
    requireAdmin(req);
    const productId = getRequestProductId(req);
    const { id } = req.params;

    const broadcast = await repo.getBroadcast(id, productId);
    if (!broadcast) throw new NotFoundError('Broadcast not found');

    // QUESTION-1: Need integration with user subscriptions data
    // For now return mock data
    const result = await repo.estimateTargetReach(productId, broadcast.target);

    return {
      estimatedCount: result.count,
      sampleUserIds: result.sampleUserIds,
      target: broadcast.target,
    };
  });

  // Trigger send (for immediate or scheduled broadcasts)
  app.post<{ Params: { id: string } }>('/:id/send', async req => {
    const adminId = requireAdmin(req);
    const productId = getRequestProductId(req);
    const { id } = req.params;

    const broadcast = await repo.getBroadcast(id, productId);
    if (!broadcast) throw new NotFoundError('Broadcast not found');

    // Safety check for large broadcasts
    const reach = await repo.estimateTargetReach(productId, broadcast.target);
    if (reach.count > 10000) {
      // Log warning - in production, might require additional confirmation
      req.log.warn({ broadcastId: id, targetCount: reach.count }, 'Large broadcast send triggered');
    }

    // Update status to sending
    await repo.updateBroadcast(id, productId, {
      status: BroadcastStatus.SENDING,
      sentAt: new Date().toISOString(),
    });

    req.log.info(
      { broadcastId: id, adminId, targetCount: reach.count },
      'Triggered broadcast send'
    );

    // Deliver async — respond immediately, process in background
    const log = req.log;
    process.nextTick(async () => {
      try {
        const targetUserIds = broadcast.target.specificUserIds ?? [];
        let recipients: Array<{ id: string; email: string; displayName: string }>;
        if (targetUserIds.length > 0) {
          const allUsers = await listUsers(productId, 5000);
          recipients = allUsers
            .filter(u => targetUserIds.includes(u.id))
            .map(u => ({ id: u.id, email: u.email, displayName: u.displayName }));
        } else {
          const allUsers = await listUsers(productId, 5000);
          const pct = broadcast.target.percentageRollout ?? 100;
          const count = Math.ceil(allUsers.length * (pct / 100));
          recipients = allUsers
            .slice(0, count)
            .map(u => ({ id: u.id, email: u.email, displayName: u.displayName }));
        }

        let sent = 0;
        for (const user of recipients) {
          try {
            await dispatchEmail(
              {
                to: user.email,
                templateId: 'broadcast',
                variables: {
                  displayName: user.displayName,
                  subject: broadcast.title,
                  body: broadcast.body ?? '',
                  productName: productId,
                },
                productId,
                userId: user.id,
              },
              log
            );
            sent++;
          } catch {
            log.error(
              { userId: user.id, broadcastId: id },
              '[broadcasts] Failed to deliver to user'
            );
          }
        }

        await repo.updateBroadcast(id, productId, {
          status: BroadcastStatus.SENT,
        });
        await repo.updateBroadcastMetrics(id, productId, {
          sentCount: sent,
          targetedCount: recipients.length,
        });
        log.info(
          { broadcastId: id, sent, total: recipients.length },
          '[broadcasts] Delivery complete'
        );
      } catch (err) {
        log.error({ err, broadcastId: id }, '[broadcasts] Delivery failed');
        await repo
          .updateBroadcast(id, productId, {
            status: BroadcastStatus.DRAFT,
          })
          .catch(() => {});
      }
    });

    return { success: true, targetCount: reach.count };
  });

  // Pause sending
  app.post<{ Params: { id: string } }>('/:id/pause', async req => {
    const adminId = requireAdmin(req);
    const productId = getRequestProductId(req);
    const { id } = req.params;

    const broadcast = await repo.getBroadcast(id, productId);
    if (!broadcast) throw new NotFoundError('Broadcast not found');

    if (broadcast.status !== BroadcastStatus.SENDING) {
      throw new BadRequestError('Can only pause broadcasts that are currently sending');
    }

    await repo.updateBroadcast(id, productId, {
      status: BroadcastStatus.PAUSED,
    });

    req.log.info({ broadcastId: id, adminId }, 'Paused broadcast');
    return { success: true };
  });

  // Get metrics
  app.get<{ Params: { id: string } }>('/:id/metrics', async req => {
    requireAdmin(req);
    const productId = getRequestProductId(req);
    const { id } = req.params;

    const broadcast = await repo.getBroadcast(id, productId);
    if (!broadcast) throw new NotFoundError('Broadcast not found');

    return {
      broadcastId: id,
      metrics: broadcast.metrics,
      status: broadcast.status,
    };
  });

  // Clone broadcast (for A/B testing)
  app.post<{ Params: { id: string } }>('/:id/clone', async req => {
    const adminId = requireAdmin(req);
    const productId = getRequestProductId(req);
    const { id } = req.params;

    const existing = await repo.getBroadcast(id, productId);
    if (!existing) throw new NotFoundError('Broadcast not found');

    const body = req.body as { variant?: 'control' | 'treatment' };
    const { variant } = body;

    const now = new Date().toISOString();
    const cloned: Broadcast = {
      ...existing,
      id: `bcast_${randomUUID()}`,
      title: `${existing.title} (Clone)`,
      status: BroadcastStatus.DRAFT,
      variant: variant ?? 'treatment',
      parentBroadcastId: existing.id,
      experimentId: existing.experimentId ?? `exp_${randomUUID()}`,
      metrics: {
        targetedCount: 0,
        sentCount: 0,
        deliveredCount: 0,
        openedCount: 0,
        clickedCount: 0,
        dismissedCount: 0,
        convertedCount: 0,
      },
      createdAt: now,
      updatedAt: now,
      createdBy: adminId,
    };

    const created = await repo.createBroadcast(cloned);
    req.log.info({ broadcastId: created.id, parentId: id, adminId }, 'Cloned broadcast');

    return created;
  });
}

// =============================================================================
// Public/User Routes
// =============================================================================

async function publicRoutes(app: FastifyInstance): Promise<void> {
  // List my active in-app messages
  app.get('/', async req => {
    const userId = requireAuth(req);
    const productId = getRequestProductId(req);

    // Clean expired messages first
    await repo.deleteExpiredInAppMessages(userId);

    // Get active messages
    const messages = await repo.getInAppMessagesForUser(userId, productId, {
      status: 'unread',
    });

    return { messages };
  });

  // Mark message as read
  app.post<{ Params: { id: string } }>('/:id/read', async req => {
    const userId = requireAuth(req);
    const productId = getRequestProductId(req);
    const { id } = req.params;

    // Find the message
    const messages = await repo.getInAppMessagesForUser(userId, productId);
    const message = messages.find(m => m.id === id);
    if (!message) throw new NotFoundError('Message not found');

    // Update status
    await repo.updateInAppMessageStatus(id, userId, 'read');

    // Record read receipt
    const recorded = await repo.recordReadReceipt(message.broadcastId, userId, productId, 'read');

    // Update broadcast metrics
    if (recorded) {
      await repo.updateBroadcastMetrics(message.broadcastId, productId, {
        openedCount: 1,
      });
    }

    return { success: true };
  });

  // Dismiss message
  app.post<{ Params: { id: string } }>('/:id/dismiss', async req => {
    const userId = requireAuth(req);
    const productId = getRequestProductId(req);
    const { id } = req.params;

    const messages = await repo.getInAppMessagesForUser(userId, productId);
    const message = messages.find(m => m.id === id);
    if (!message) throw new NotFoundError('Message not found');

    await repo.updateInAppMessageStatus(id, userId, 'dismissed');
    const recorded = await repo.recordReadReceipt(
      message.broadcastId,
      userId,
      productId,
      'dismiss'
    );

    if (recorded) {
      await repo.updateBroadcastMetrics(message.broadcastId, productId, {
        dismissedCount: 1,
      });
    }

    return { success: true };
  });

  // Track CTA click
  app.post<{ Params: { id: string } }>('/:id/click', async req => {
    const userId = requireAuth(req);
    const productId = getRequestProductId(req);
    const { id } = req.params;

    const messages = await repo.getInAppMessagesForUser(userId, productId);
    const message = messages.find(m => m.id === id);
    if (!message) throw new NotFoundError('Message not found');

    const recorded = await repo.recordReadReceipt(message.broadcastId, userId, productId, 'click');

    if (recorded) {
      await repo.updateBroadcastMetrics(message.broadcastId, productId, {
        clickedCount: 1,
      });
    }

    return { success: true, redirectUrl: message.ctaUrl };
  });
}

// =============================================================================
// Main Route Registration
// =============================================================================

export async function broadcastRoutes(app: FastifyInstance): Promise<void> {
  // Admin routes (prefix: /admin/broadcasts)
  await app.register(adminRoutes, { prefix: '/admin/broadcasts' });

  // Public routes (prefix: /broadcasts)
  await app.register(publicRoutes, { prefix: '/broadcasts' });
}
