import { Injectable, NotFoundException } from '@nestjs/common';
import type { Campaign, CampaignRecipient, Prisma } from '@prisma/client';
import { randomUUID } from 'node:crypto';
import * as XLSX from 'xlsx';
import { AuthenticatedUser } from '../auth/auth.types';
import { normalizeText } from '../common/normalize';
import { JobsService } from '../jobs/jobs.service';
import { PrismaService } from '../prisma/prisma.service';
import { TemplatesService } from '../templates/templates.service';
import { CreateCampaignDto } from './dto/create-campaign.dto';
import { SendCampaignDto } from './dto/send-campaign.dto';
import { UpdateCampaignDto } from './dto/update-campaign.dto';

type SeedCampaign = {
  code: string;
  name: string;
  audienceLabel: string;
  audienceGroup: string;
  status: string;
  totalRecipients: number;
  deliveredCount: number;
  readCount: number;
  failedCount: number;
  deliveryRate: number | null;
  readRate: number | null;
  sentAt?: Date;
  scheduledAt?: Date;
  templateName: string;
  language: string;
  messageTitle: string;
  messageBody: string;
  primaryButton: string;
  secondaryButton: string;
  bannerImageUrl: string;
  recipients: Array<{
    phoneNumber: string;
    status: string;
    sentAt?: Date;
    errorReason?: string | null;
    deviceOs?: string | null;
  }>;
};

const summerSaleSentAt = new Date('2024-07-15T09:45:00.000Z');

const seededCampaigns: SeedCampaign[] = [
  {
    code: 'CAM-98231',
    name: 'Summer Sale 2024',
    audienceLabel: '45,200 recipients',
    audienceGroup: 'Retail subscribers',
    status: 'Sent',
    totalRecipients: 48250,
    deliveredCount: 47482,
    readCount: 30987,
    failedCount: 578,
    deliveryRate: 98.4,
    readRate: 64.2,
    sentAt: summerSaleSentAt,
    templateName: 'summer_promo_v2',
    language: 'English (US)',
    messageTitle: 'Hi {{name}}, ☀️',
    messageBody:
      'Our Summer Sale is here! Get up to 40% OFF on all new arrivals. Use code SUMMER40 at checkout.',
    primaryButton: 'Shop Collection',
    secondaryButton: 'View Catalog',
    bannerImageUrl:
      'https://lh3.googleusercontent.com/aida-public/AB6AXuDEStTHrI49NhOpgRMdXx3saVUtVNe9fBtTvDiMZMeuDcQNU8eJHfAxc5hS5M8ligofVNNpUi59-kOLD9peg5njH1bWmsrHGXIx7A37_pAFEfxEAGVbjVjWCD0mGWIHu4LIShS9yDlFmvznUPzlye_JNLPzs7S8LIULMi-bL7cP6qt-uzXKuoWUwj1sw0uq0UcJnkCb6Y-04pNG8iNd2MINCbLOSbmRyf8OSOe1b9-u-sA6p5Mq3CKRjP-Fvk0vk3ZKdritVLiB0U8',
    recipients: [
      { phoneNumber: '+1 (555) 012-3456', status: 'Read', sentAt: new Date('2024-07-15T10:12:00.000Z'), deviceOs: 'Android' },
      { phoneNumber: '+1 (555) 012-7890', status: 'Delivered', sentAt: new Date('2024-07-15T09:48:00.000Z'), deviceOs: 'iOS' },
      { phoneNumber: '+1 (555) 013-1122', status: 'Failed', sentAt: new Date('2024-07-15T09:45:00.000Z'), errorReason: 'Policy Violation', deviceOs: 'Android' },
      { phoneNumber: '+1 (555) 014-3344', status: 'Read', sentAt: new Date('2024-07-15T10:05:00.000Z'), deviceOs: 'Android' },
      { phoneNumber: '+1 (555) 015-5566', status: 'Delivered', sentAt: new Date('2024-07-15T09:52:00.000Z'), deviceOs: 'iOS' },
      { phoneNumber: '+1 (555) 016-7788', status: 'Read', sentAt: new Date('2024-07-15T10:16:00.000Z'), deviceOs: 'Android' },
      { phoneNumber: '+1 (555) 017-8899', status: 'Delivered', sentAt: new Date('2024-07-15T10:22:00.000Z'), deviceOs: 'Android' },
      { phoneNumber: '+1 (555) 018-9911', status: 'Read', sentAt: new Date('2024-07-15T10:31:00.000Z'), deviceOs: 'iOS' },
      { phoneNumber: '+1 (555) 019-2200', status: 'Failed', sentAt: new Date('2024-07-15T10:42:00.000Z'), errorReason: 'Invalid Template Parameter', deviceOs: 'Android' },
      { phoneNumber: '+1 (555) 020-3311', status: 'Delivered', sentAt: new Date('2024-07-15T10:51:00.000Z'), deviceOs: 'Web/Desktop' },
      { phoneNumber: '+1 (555) 021-4422', status: 'Read', sentAt: new Date('2024-07-15T11:09:00.000Z'), deviceOs: 'Android' },
      { phoneNumber: '+1 (555) 022-5533', status: 'Read', sentAt: new Date('2024-07-15T11:18:00.000Z'), deviceOs: 'iOS' },
    ],
  },
  {
    code: 'CAM-98244',
    name: 'Weekly Newsletter #42',
    audienceLabel: 'VIP Customer List',
    audienceGroup: 'High-value segment',
    status: 'Scheduled',
    totalRecipients: 18640,
    deliveredCount: 0,
    readCount: 0,
    failedCount: 0,
    deliveryRate: null,
    readRate: null,
    scheduledAt: new Date('2024-07-16T14:00:00.000Z'),
    templateName: 'vip_newsletter_v42',
    language: 'English (US)',
    messageTitle: 'Your insider update is almost here',
    messageBody: 'A curated digest for premium customers with fresh offers and product stories.',
    primaryButton: 'Open Newsletter',
    secondaryButton: 'Manage Preferences',
    bannerImageUrl:
      'https://images.unsplash.com/photo-1520607162513-77705c0f0d4a?auto=format&fit=crop&w=800&q=80',
    recipients: [],
  },
  {
    code: 'CAM-98255',
    name: 'Product Launch Promo',
    audienceLabel: 'New Leads Segment',
    audienceGroup: 'Cold outreach',
    status: 'Draft',
    totalRecipients: 9300,
    deliveredCount: 0,
    readCount: 0,
    failedCount: 0,
    deliveryRate: 0,
    readRate: 0,
    templateName: 'launch_teaser_v1',
    language: 'English (US)',
    messageTitle: 'Be first to see the launch',
    messageBody: 'A teaser sequence for high-intent prospects before the public product announcement.',
    primaryButton: 'Join Waitlist',
    secondaryButton: 'See Features',
    bannerImageUrl:
      'https://images.unsplash.com/photo-1516321318423-f06f85e504b3?auto=format&fit=crop&w=800&q=80',
    recipients: [],
  },
  {
    code: 'CAM-98201',
    name: 'Loyalty Program Update',
    audienceLabel: 'Dormant Users',
    audienceGroup: 'Reactivation list',
    status: 'Failed',
    totalRecipients: 60700,
    deliveredCount: 27315,
    readCount: 10844,
    failedCount: 33385,
    deliveryRate: 45,
    readRate: 17.9,
    sentAt: new Date('2024-07-10T08:30:00.000Z'),
    templateName: 'loyalty_reactivation_v1',
    language: 'English (US)',
    messageTitle: 'We saved your rewards for you',
    messageBody: 'A recovery campaign for inactive users with points reminder and welcome-back offer.',
    primaryButton: 'Claim Rewards',
    secondaryButton: 'Need Help',
    bannerImageUrl:
      'https://images.unsplash.com/photo-1515169067868-5387ec356754?auto=format&fit=crop&w=800&q=80',
    recipients: [],
  },
];

@Injectable()
export class CampaignsService {
  constructor(
    private readonly prisma: PrismaService,
    private readonly jobsService: JobsService,
    private readonly templatesService: TemplatesService,
  ) {}

  async findAll() {
    await this.ensureSeedData();

    const campaigns = await this.prisma.campaign.findMany();
    campaigns.sort((left, right) => this.getSortScore(right) - this.getSortScore(left));

    const totalMessages = campaigns.reduce((sum, campaign) => sum + campaign.totalRecipients, 0);
    const averageDeliveryRate = this.average(
      campaigns
        .map((campaign) => campaign.deliveryRate)
        .filter((value): value is number => value !== null),
    );
    const scheduledCount = campaigns.filter((campaign) => campaign.status === 'Scheduled').length;
    const failedDeliveries = campaigns.reduce((sum, campaign) => sum + campaign.failedCount, 0);

    return {
      metrics: {
        totalMessages,
        averageDeliveryRate,
        scheduledCount,
        failedDeliveries,
      },
      items: campaigns.map((campaign) => this.serializeCampaignRow(campaign)),
    };
  }

  async findOne(id: string, page = 1, limit = 5) {
    await this.ensureSeedData();

    const take = Math.min(Math.max(limit, 1), 50);
    const currentPage = Math.max(page, 1);

    const campaign = await this.prisma.campaign.findUnique({
      where: { id },
    });

    if (!campaign) {
      throw new NotFoundException('Campaign not found');
    }

    const recipientWhere: Prisma.CampaignRecipientWhereInput = {
      campaignId: campaign.id,
    };

    const [recipients, totalRecipients, deviceBreakdown] = await Promise.all([
      this.prisma.campaignRecipient.findMany({
        where: recipientWhere,
        orderBy: [{ sentAt: 'asc' }, { createdAt: 'asc' }],
        skip: (currentPage - 1) * take,
        take,
      }),
      this.prisma.campaignRecipient.count({ where: recipientWhere }),
      this.prisma.campaignRecipient.groupBy({
        by: ['deviceOs'],
        where: recipientWhere,
        _count: { _all: true },
      }),
    ]);

    return {
      campaign: this.serializeCampaignDetail(campaign),
      timeline: this.buildTimeline(campaign, recipients),
      recipients: {
        items: recipients.map((recipient) => this.serializeRecipient(recipient)),
        total: totalRecipients,
        page: currentPage,
        pageSize: take,
        totalPages: Math.max(1, Math.ceil(totalRecipients / take)),
      },
      deviceBreakdown: this.buildDeviceBreakdown(deviceBreakdown, totalRecipients),
    };
  }

  async create(dto: CreateCampaignDto, user: AuthenticatedUser, ipAddress?: string) {
    await this.ensureSeedData();
    await this.templatesService.assertTemplateExistsByName(dto.templateName);
    const actor = await this.findActor(user.sub, user.email);
    const name = normalizeText(dto.name) || 'Untitled Campaign';
    const totalRecipients = Math.max(0, dto.totalRecipients);
    const status = normalizeText(dto.status) || 'Draft';
    const campaign = await this.prisma.campaign.create({
      data: {
        id: randomUUID(),
        code: await this.generateCampaignCode(),
        name,
        audienceLabel: normalizeText(dto.audienceLabel) || 'Untitled Segment',
        audienceGroup: normalizeText(dto.audienceGroup) || 'General audience',
        status,
        totalRecipients,
        deliveredCount: 0,
        readCount: 0,
        failedCount: 0,
        deliveryRate: status === 'Draft' ? 0 : null,
        readRate: 0,
        scheduledAt: dto.scheduledAt ? new Date(dto.scheduledAt) : null,
        templateName: normalizeText(dto.templateName) || 'new_campaign_template',
        language: normalizeText(dto.language) || 'English (US)',
        messageTitle: normalizeText(dto.messageTitle) || 'New campaign draft',
        messageBody: normalizeText(dto.messageBody) || 'Add your WhatsApp message content here.',
        primaryButton: normalizeText(dto.primaryButton) || 'Primary CTA',
        secondaryButton: normalizeText(dto.secondaryButton) || 'Secondary CTA',
        bannerImageUrl:
          normalizeText(dto.bannerImageUrl) ||
          'https://images.unsplash.com/photo-1520607162513-77705c0f0d4a?auto=format&fit=crop&w=800&q=80',
      },
    });

    await this.prisma.auditLog.create({
      data: {
        actorUserId: actor.id,
        actorName: actor.name,
        actorEmail: actor.email,
        actionType: 'Campaign Created',
        module: 'Campaigns',
        ipAddress: ipAddress || null,
        severity: 'default',
        details: `Created campaign ${campaign.name} (${campaign.code}).`,
      },
    });

    return this.serializeCampaignRow(campaign);
  }

  async duplicate(id: string, user: AuthenticatedUser, ipAddress?: string) {
    await this.ensureSeedData();
    const actor = await this.findActor(user.sub, user.email);
    const source = await this.prisma.campaign.findUnique({
      where: { id },
      include: {
        recipients: true,
      },
    });

    if (!source) {
      throw new NotFoundException('Campaign not found');
    }

    const duplicate = await this.prisma.campaign.create({
      data: {
        id: randomUUID(),
        code: await this.generateCampaignCode(),
        name: `${source.name} Copy`,
        audienceLabel: source.audienceLabel,
        audienceGroup: source.audienceGroup,
        status: 'Draft',
        totalRecipients: source.totalRecipients,
        deliveredCount: 0,
        readCount: 0,
        failedCount: 0,
        deliveryRate: 0,
        readRate: 0,
        scheduledAt: null,
        templateName: source.templateName,
        language: source.language,
        messageTitle: source.messageTitle,
        messageBody: source.messageBody,
        primaryButton: source.primaryButton,
        secondaryButton: source.secondaryButton,
        bannerImageUrl: source.bannerImageUrl,
      },
    });

    await this.prisma.auditLog.create({
      data: {
        actorUserId: actor.id,
        actorName: actor.name,
        actorEmail: actor.email,
        actionType: 'Campaign Duplicated',
        module: 'Campaigns',
        ipAddress: ipAddress || null,
        severity: 'default',
        details: `Duplicated campaign ${source.name} into ${duplicate.name}.`,
      },
    });

    return {
      id: duplicate.id,
      code: duplicate.code,
      name: duplicate.name,
      status: duplicate.status,
    };
  }

  async update(id: string, dto: UpdateCampaignDto, user: AuthenticatedUser, ipAddress?: string) {
    await this.ensureSeedData();
    await this.templatesService.assertTemplateExistsByName(dto.templateName);
    const actor = await this.findActor(user.sub, user.email);
    const campaign = await this.prisma.campaign.update({
      where: { id },
      data: {
        ...(dto.name !== undefined ? { name: normalizeText(dto.name) || 'Untitled Campaign' } : {}),
        ...(dto.audienceLabel !== undefined ? { audienceLabel: normalizeText(dto.audienceLabel) || 'Untitled Segment' } : {}),
        ...(dto.audienceGroup !== undefined ? { audienceGroup: normalizeText(dto.audienceGroup) || 'General audience' } : {}),
        ...(dto.status !== undefined ? { status: normalizeText(dto.status) || 'Draft' } : {}),
        ...(dto.totalRecipients !== undefined ? { totalRecipients: Math.max(0, dto.totalRecipients) } : {}),
        ...(dto.scheduledAt !== undefined ? { scheduledAt: dto.scheduledAt ? new Date(dto.scheduledAt) : null } : {}),
        ...(dto.templateName !== undefined ? { templateName: normalizeText(dto.templateName) || 'new_campaign_template' } : {}),
        ...(dto.language !== undefined ? { language: normalizeText(dto.language) || 'English (US)' } : {}),
        ...(dto.messageTitle !== undefined ? { messageTitle: normalizeText(dto.messageTitle) || 'New campaign draft' } : {}),
        ...(dto.messageBody !== undefined ? { messageBody: normalizeText(dto.messageBody) || 'Add your WhatsApp message content here.' } : {}),
        ...(dto.primaryButton !== undefined ? { primaryButton: normalizeText(dto.primaryButton) || 'Primary CTA' } : {}),
        ...(dto.secondaryButton !== undefined ? { secondaryButton: normalizeText(dto.secondaryButton) || 'Secondary CTA' } : {}),
        ...(dto.bannerImageUrl !== undefined
          ? {
              bannerImageUrl:
                normalizeText(dto.bannerImageUrl) ||
                'https://images.unsplash.com/photo-1520607162513-77705c0f0d4a?auto=format&fit=crop&w=800&q=80',
            }
          : {}),
      },
    });

    await this.prisma.auditLog.create({
      data: {
        actorUserId: actor.id,
        actorName: actor.name,
        actorEmail: actor.email,
        actionType: 'Campaign Updated',
        module: 'Campaigns',
        ipAddress: ipAddress || null,
        severity: 'default',
        details: `Updated campaign ${campaign.name} (${campaign.code}).`,
      },
    });

    return this.serializeCampaignRow(campaign);
  }

  async remove(id: string, user: AuthenticatedUser, ipAddress?: string) {
    await this.ensureSeedData();
    const actor = await this.findActor(user.sub, user.email);
    const campaign = await this.prisma.campaign.delete({
      where: { id },
    });

    await this.prisma.auditLog.create({
      data: {
        actorUserId: actor.id,
        actorName: actor.name,
        actorEmail: actor.email,
        actionType: 'Campaign Deleted',
        module: 'Campaigns',
        ipAddress: ipAddress || null,
        severity: 'default',
        details: `Deleted campaign ${campaign.name} (${campaign.code}).`,
      },
    });

    return { id: campaign.id, deleted: true };
  }

  async send(id: string, dto: SendCampaignDto, user: AuthenticatedUser, ipAddress?: string) {
    await this.ensureSeedData();
    const actor = await this.findActor(user.sub, user.email);
    const campaign = await this.prisma.campaign.findUnique({ where: { id } });

    if (!campaign) {
      throw new NotFoundException('Campaign not found');
    }

    const requestedMode = dto.mode === 'scheduled' ? 'scheduled' : 'now';
    const requestedScheduleAt = dto.scheduledAt
      ? new Date(dto.scheduledAt)
      : campaign.scheduledAt;
    const shouldSchedule =
      requestedMode === 'scheduled' || Boolean(requestedScheduleAt && requestedScheduleAt.getTime() > Date.now());
    const availableAt =
      shouldSchedule && requestedScheduleAt && requestedScheduleAt.getTime() > Date.now()
        ? requestedScheduleAt
        : new Date();

    await this.prisma.campaign.update({
      where: { id },
      data: {
        status: shouldSchedule ? 'Scheduled' : 'Sending',
        scheduledAt: shouldSchedule ? availableAt : null,
        sentAt: shouldSchedule ? null : new Date(),
      },
    });

    const job = await this.jobsService.enqueue({
      queueName: 'campaigns',
      jobType: 'campaign.dispatch',
      payload: { campaignId: id },
      maxAttempts: 3,
      availableAt,
    });

    await this.prisma.auditLog.create({
      data: {
        actorUserId: actor.id,
        actorName: actor.name,
        actorEmail: actor.email,
        actionType: shouldSchedule ? 'Campaign Scheduled' : 'Campaign Send Queued',
        module: 'Campaigns',
        ipAddress: ipAddress || null,
        severity: 'default',
        details: shouldSchedule
          ? `Scheduled campaign ${campaign.name} for ${availableAt.toISOString()}.`
          : `Queued campaign ${campaign.name} for immediate dispatch.`,
        metadataJson: {
          campaignId: id,
          jobId: job.id,
          availableAt: availableAt.toISOString(),
        } as Prisma.InputJsonValue,
      },
    });

    return {
      id: campaign.id,
      jobId: job.id,
      status: shouldSchedule ? 'Scheduled' : 'Queued',
      availableAt: availableAt.toISOString(),
    };
  }

  async exportReport(
    id: string,
    format: 'csv' | 'xlsx',
    user: AuthenticatedUser,
    ipAddress?: string,
  ) {
    await this.ensureSeedData();
    const actor = await this.findActor(user.sub, user.email);
    const campaign = await this.prisma.campaign.findUnique({
      where: { id },
      include: {
        recipients: {
          orderBy: [{ sentAt: 'asc' }, { createdAt: 'asc' }],
        },
      },
    });

    if (!campaign) {
      throw new NotFoundException('Campaign not found');
    }

    const summaryRows = [
      ['Campaign Name', campaign.name],
      ['Campaign Code', campaign.code],
      ['Status', campaign.status],
      ['Total Recipients', campaign.totalRecipients],
      ['Delivered Count', campaign.deliveredCount],
      ['Delivered Rate', campaign.deliveryRate ?? 0],
      ['Read Count', campaign.readCount],
      ['Read Rate', campaign.readRate ?? 0],
      ['Failed Count', campaign.failedCount],
      ['Template Name', campaign.templateName || ''],
      ['Language', campaign.language || ''],
    ];

    const recipientRows = campaign.recipients.map((recipient) => ({
      phoneNumber: recipient.phoneNumber,
      status: recipient.status,
      timestamp: recipient.sentAt?.toISOString() || '',
      errorReason: recipient.errorReason || '',
      deviceOs: recipient.deviceOs || '',
    }));

    const baseName = this.slugifyFileName(`${campaign.name}-${campaign.code}`);

    await this.prisma.auditLog.create({
      data: {
        actorUserId: actor.id,
        actorName: actor.name,
        actorEmail: actor.email,
        actionType: 'Campaign Report Exported',
        module: 'Campaigns',
        ipAddress: ipAddress || null,
        severity: 'default',
        details: `Exported ${format.toUpperCase()} report for campaign ${campaign.name}.`,
      },
    });

    if (format === 'csv') {
      const lines = [
        ['Section', 'Key', 'Value'].join(','),
        ...summaryRows.map(([key, value]) => ['Summary', key, value].map((cell) => `"${String(cell).replaceAll('"', '""')}"`).join(',')),
        '',
        ['Phone Number', 'Status', 'Timestamp', 'Error Reason', 'Device OS'].join(','),
        ...recipientRows.map((row) =>
          [row.phoneNumber, row.status, row.timestamp, row.errorReason, row.deviceOs]
            .map((cell) => `"${String(cell).replaceAll('"', '""')}"`)
            .join(','),
        ),
      ].join('\n');

      return {
        fileName: `${baseName}.csv`,
        contentType: 'text/csv; charset=utf-8',
        buffer: Buffer.from(lines, 'utf8'),
      };
    }

    const workbook = XLSX.utils.book_new();
    const summarySheet = XLSX.utils.aoa_to_sheet([['Metric', 'Value'], ...summaryRows]);
    const recipientsSheet = XLSX.utils.json_to_sheet(recipientRows);
    XLSX.utils.book_append_sheet(workbook, summarySheet, 'Summary');
    XLSX.utils.book_append_sheet(workbook, recipientsSheet, 'Recipients');

    return {
      fileName: `${baseName}.xlsx`,
      contentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
      buffer: XLSX.write(workbook, { type: 'buffer', bookType: 'xlsx' }),
    };
  }

  async processJob(jobId: string) {
    const job = await this.jobsService.findById(jobId);
    if (!job) {
      return;
    }

    try {
      const payload = job.payloadJson as { campaignId?: string };
      const campaignId = payload?.campaignId;
      if (!campaignId) {
        throw new Error('Campaign job payload is missing campaignId');
      }

      await this.dispatchCampaign(campaignId);
      await this.jobsService.complete(jobId);
    } catch (error) {
      const message = error instanceof Error ? error.message : 'Unknown campaign processing error';
      if (job.attempts < job.maxAttempts) {
        await this.jobsService.retry(jobId, message, 2000 * job.attempts);
      } else {
        await this.jobsService.fail(jobId, message);
      }
    }
  }

  private async ensureSeedData() {
    const existingCodes = new Set(
      (
        await this.prisma.campaign.findMany({
          select: { code: true },
        })
      ).map((campaign) => campaign.code),
    );

    for (const seed of seededCampaigns) {
      if (existingCodes.has(seed.code)) {
        continue;
      }

      await this.prisma.campaign.create({
        data: {
          id: randomUUID(),
          code: seed.code,
          name: seed.name,
          audienceLabel: seed.audienceLabel,
          audienceGroup: seed.audienceGroup,
          status: seed.status,
          totalRecipients: seed.totalRecipients,
          deliveredCount: seed.deliveredCount,
          readCount: seed.readCount,
          failedCount: seed.failedCount,
          deliveryRate: seed.deliveryRate,
          readRate: seed.readRate,
          sentAt: seed.sentAt,
          scheduledAt: seed.scheduledAt,
          templateName: seed.templateName,
          language: seed.language,
          messageTitle: seed.messageTitle,
          messageBody: seed.messageBody,
          primaryButton: seed.primaryButton,
          secondaryButton: seed.secondaryButton,
          bannerImageUrl: seed.bannerImageUrl,
          recipients: {
            create: seed.recipients.map((recipient) => ({
              id: randomUUID(),
              phoneNumber: recipient.phoneNumber,
              status: recipient.status,
              sentAt: recipient.sentAt,
              errorReason: recipient.errorReason || null,
              deviceOs: recipient.deviceOs || null,
            })),
          },
        },
      });
    }
  }

  private serializeCampaignRow(campaign: Campaign) {
    return {
      id: campaign.id,
      code: campaign.code,
      name: campaign.name,
      audience: campaign.audienceLabel,
      audienceGroup: campaign.audienceGroup,
      sent: campaign.totalRecipients.toLocaleString('en-US'),
      opened: campaign.readCount.toLocaleString('en-US'),
      status: campaign.status,
      deliveryRate: campaign.deliveryRate,
      dateLabel: this.getDateLabel(campaign),
      timeLabel: this.getTimeLabel(campaign),
      templateName: campaign.templateName,
    };
  }

  private serializeCampaignDetail(campaign: Campaign) {
    return {
      id: campaign.id,
      code: campaign.code,
      name: campaign.name,
      status: campaign.status,
      initiatedAt: campaign.sentAt?.toISOString() || campaign.scheduledAt?.toISOString() || campaign.createdAt.toISOString(),
      totalRecipients: campaign.totalRecipients,
      deliveredCount: campaign.deliveredCount,
      deliveredRate: campaign.deliveryRate || 0,
      readCount: campaign.readCount,
      readRate: campaign.readRate || 0,
      failedCount: campaign.failedCount,
      failedRate:
        campaign.totalRecipients > 0 ? Number(((campaign.failedCount / campaign.totalRecipients) * 100).toFixed(1)) : 0,
      templateName: campaign.templateName || '-',
      language: campaign.language || '-',
      messageTitle: campaign.messageTitle || '',
      messageBody: campaign.messageBody || '',
      primaryButton: campaign.primaryButton || '',
      secondaryButton: campaign.secondaryButton || '',
      bannerImageUrl: campaign.bannerImageUrl || '',
    };
  }

  private serializeRecipient(recipient: CampaignRecipient) {
    return {
      id: recipient.id,
      phoneNumber: recipient.phoneNumber,
      status: recipient.status,
      sentAt: recipient.sentAt?.toISOString() || null,
      errorReason: recipient.errorReason || null,
      deviceOs: recipient.deviceOs || null,
    };
  }

  private buildTimeline(campaign: Campaign, recipients: CampaignRecipient[]) {
    const latestActivity = recipients.reduce<Date>(
      (latest, recipient) =>
        recipient.sentAt && recipient.sentAt.getTime() > latest.getTime() ? recipient.sentAt : latest,
      campaign.sentAt || campaign.createdAt,
    );
    const timelineEnd = new Date(latestActivity);
    timelineEnd.setUTCMinutes(0, 0, 0);
    timelineEnd.setUTCHours(timelineEnd.getUTCHours() + 1);

    const buckets = Array.from({ length: 12 }, (_, index) => {
      const start = new Date(timelineEnd);
      start.setUTCHours(timelineEnd.getUTCHours() - (11 - index) * 2);
      const bucketEnd = new Date(start);
      bucketEnd.setUTCHours(start.getUTCHours() + 2);
      const count = recipients.filter((recipient) => {
        if (!recipient.sentAt) {
          return false;
        }

        return recipient.sentAt >= start && recipient.sentAt < bucketEnd;
      }).length;

      return {
        label: start.toLocaleTimeString('en-US', {
          hour: '2-digit',
          minute: '2-digit',
          hour12: false,
          timeZone: 'UTC',
        }),
        count,
      };
    });

    const highest = Math.max(...buckets.map((bucket) => bucket.count), 1);
    return buckets.map((bucket) => ({
      label: bucket.label,
      count: bucket.count,
      height: Math.max(10, Math.round((bucket.count / highest) * 100)),
    }));
  }

  private buildDeviceBreakdown(
    breakdown: Array<{ deviceOs: string | null; _count: { _all: number } }>,
    total: number,
  ) {
    const totalSafe = total || 1;
    const normalized = ['Android', 'iOS', 'Web/Desktop'].map((label) => {
      const match = breakdown.find((entry) => entry.deviceOs === label);
      const count = match?._count._all || 0;
      return {
        label,
        count,
        percentage: Math.round((count / totalSafe) * 100),
      };
    });

    return normalized;
  }

  private getDateLabel(campaign: Campaign) {
    if (campaign.status === 'Scheduled' && campaign.scheduledAt) {
      return 'In 2 hours';
    }

    if (!campaign.sentAt) {
      return 'Not set';
    }

    return campaign.sentAt.toLocaleDateString('en-US', {
      month: 'long',
      day: 'numeric',
      year: 'numeric',
      timeZone: 'UTC',
    });
  }

  private getTimeLabel(campaign: Campaign) {
    const value = campaign.status === 'Scheduled' ? campaign.scheduledAt : campaign.sentAt;
    if (!value) {
      return '';
    }

    return value.toLocaleTimeString('en-US', {
      hour: '2-digit',
      minute: '2-digit',
      hour12: true,
      timeZone: 'UTC',
    });
  }

  private average(values: number[]) {
    if (values.length === 0) {
      return 0;
    }

    return Number((values.reduce((sum, value) => sum + value, 0) / values.length).toFixed(1));
  }

  private getSortScore(campaign: Campaign) {
    const statusWeight =
      campaign.status === 'Sent'
        ? 4000000000000
        : campaign.status === 'Scheduled'
          ? 3000000000000
          : campaign.status === 'Draft'
            ? 2000000000000
            : 1000000000000;

    const dateWeight = (campaign.sentAt || campaign.scheduledAt || campaign.createdAt).getTime();
    return statusWeight + dateWeight;
  }

  private async findActor(userId: string, email: string) {
    const actor = await this.prisma.user.findUnique({
      where: { id: userId },
      select: { id: true, name: true, email: true },
    });

    return {
      id: actor?.id || userId,
      name: actor?.name || email,
      email: actor?.email || email,
    };
  }

  private async generateCampaignCode() {
    const latest = await this.prisma.campaign.findMany({
      select: { code: true },
      orderBy: { createdAt: 'desc' },
      take: 20,
    });

    const numbers = latest
      .map((campaign) => Number(campaign.code.replace(/[^0-9]/g, '')))
      .filter((value) => Number.isFinite(value));
    const next = (numbers.length ? Math.max(...numbers) : 98200) + 1;
    return `CAM-${next}`;
  }

  private slugifyFileName(value: string) {
    return value
      .toLowerCase()
      .replace(/[^a-z0-9]+/g, '-')
      .replace(/(^-|-$)/g, '');
  }

  private async dispatchCampaign(campaignId: string) {
    const campaign = await this.prisma.campaign.findUnique({
      where: { id: campaignId },
      include: {
        recipients: true,
      },
    });

    if (!campaign) {
      throw new Error('Campaign not found');
    }

    let recipients = campaign.recipients;
    if (recipients.length === 0) {
      recipients = await this.generateRecipientsForCampaign(campaign);
    }

    const processedRecipients = recipients.map((recipient, index) => {
      const mod = index % 11;
      const status = mod === 0 ? 'Failed' : mod % 3 === 0 ? 'Read' : 'Delivered';
      const sentAt = new Date(Date.now() + index * 60 * 1000);
      return {
        id: recipient.id,
        status,
        sentAt,
        errorReason: status === 'Failed' ? 'Simulated Provider Rejection' : null,
        deviceOs: status === 'Failed'
          ? 'Android'
          : index % 4 === 0
            ? 'iOS'
            : index % 5 === 0
              ? 'Web/Desktop'
              : 'Android',
      };
    });

    const deliveredCount = processedRecipients.filter((recipient) => recipient.status !== 'Failed').length;
    const readCount = processedRecipients.filter((recipient) => recipient.status === 'Read').length;
    const failedCount = processedRecipients.filter((recipient) => recipient.status === 'Failed').length;
    const totalRecipients = processedRecipients.length;
    const deliveryRate = totalRecipients > 0 ? Number(((deliveredCount / totalRecipients) * 100).toFixed(1)) : 0;
    const readRate = totalRecipients > 0 ? Number(((readCount / totalRecipients) * 100).toFixed(1)) : 0;

    await this.prisma.$transaction(async (tx) => {
      for (const recipient of processedRecipients) {
        await tx.campaignRecipient.update({
          where: { id: recipient.id },
          data: {
            status: recipient.status,
            sentAt: recipient.sentAt,
            errorReason: recipient.errorReason,
            deviceOs: recipient.deviceOs,
          },
        });
      }

      await tx.campaign.update({
        where: { id: campaign.id },
        data: {
          status: 'Sent',
          totalRecipients,
          deliveredCount,
          readCount,
          failedCount,
          deliveryRate,
          readRate,
          sentAt: new Date(),
          scheduledAt: null,
        },
      });

      await tx.auditLog.create({
        data: {
          actorUserId: null,
          actorName: 'Campaign Worker',
          actorEmail: null,
          actionType: 'Campaign Delivered',
          module: 'Campaigns',
          severity: 'default',
          details: `Processed campaign ${campaign.name} with ${totalRecipients} recipients.`,
        },
      });
    });
  }

  private async generateRecipientsForCampaign(campaign: Campaign) {
    const contacts = await this.prisma.contact.findMany({
      orderBy: { createdAt: 'asc' },
      take: Math.min(Math.max(campaign.totalRecipients, 1), 250),
    });

    const desiredCount = Math.min(Math.max(campaign.totalRecipients, 1), 250);
    const rows = Array.from({ length: desiredCount }, (_, index) => {
      const contact = contacts[index];
      const phoneNumber = contact?.phoneNumber || this.syntheticPhoneNumber(index);

      return {
        id: randomUUID(),
        campaignId: campaign.id,
        phoneNumber,
        status: 'Queued',
        sentAt: null,
        errorReason: null,
        deviceOs: null,
      };
    });

    await this.prisma.campaignRecipient.createMany({
      data: rows,
    });

    return this.prisma.campaignRecipient.findMany({
      where: { campaignId: campaign.id },
      orderBy: { createdAt: 'asc' },
    });
  }

  private syntheticPhoneNumber(index: number) {
    return `+1 (555) ${String(100 + Math.floor(index / 100)).padStart(3, '0')}-${String(1000 + (index % 1000)).padStart(4, '0')}`;
  }
}
