import { Injectable, Logger, NotFoundException, BadRequestException } from '@nestjs/common';
import { InjectQueue } from '@nestjs/bullmq';
import { Queue } from 'bullmq';
import { Prisma } from '@prisma/client';
import { PrismaService } from '../../../prisma/prisma.service';

export interface BroadcastFilters {
  clubs?: string[];
  source?: string[];
  isRegistered?: boolean;
  bonusGranted?: boolean;
  mailingConsent?: boolean;
  isSubscribed?: boolean;
  invitedCountMin?: number;
  invitedCountMax?: number;
  registeredAfter?: string;
  registeredBefore?: string;
  tags?: string[];
}

export interface BroadcastMessage {
  text: string;
  image?: string;
  buttons?: Array<{ text: string; url?: string }>;
}

@Injectable()
export class BroadcastService {
  private readonly logger = new Logger(BroadcastService.name);

  constructor(
    private readonly prisma: PrismaService,
    @InjectQueue('telegram-bot-broadcast')
    private readonly broadcastQueue: Queue,
  ) {}

  async create(
    clubId: string,
    botId: string,
    data: {
      name: string;
      message: BroadcastMessage;
      filters: BroadcastFilters;
      scheduledAt?: string;
    },
  ) {
    const totalCount = await this.countAudience(botId, data.filters);

    return this.prisma.tgBotBroadcast.create({
      data: {
        clubId,
        botId,
        name: data.name,
        message: data.message as unknown as Prisma.InputJsonValue,
        filters: data.filters as unknown as Prisma.InputJsonValue,
        totalCount,
        scheduledAt: data.scheduledAt ? new Date(data.scheduledAt) : null,
      },
    });
  }

  async findAll(clubId: string, botId?: string) {
    return this.prisma.tgBotBroadcast.findMany({
      where: { clubId, ...(botId ? { botId } : {}) },
      orderBy: { createdAt: 'desc' },
      select: {
        id: true,
        botId: true,
        name: true,
        status: true,
        totalCount: true,
        sentCount: true,
        failedCount: true,
        scheduledAt: true,
        startedAt: true,
        completedAt: true,
        createdAt: true,
      },
    });
  }

  async findOne(clubId: string, id: string) {
    const broadcast = await this.prisma.tgBotBroadcast.findFirst({
      where: { id, clubId },
    });
    if (!broadcast) throw new NotFoundException('Broadcast not found');
    return broadcast;
  }

  async update(
    clubId: string,
    id: string,
    data: {
      name?: string;
      message?: BroadcastMessage;
      filters?: BroadcastFilters;
      scheduledAt?: string | null;
    },
  ) {
    const broadcast = await this.prisma.tgBotBroadcast.findFirst({ where: { id, clubId } });
    if (!broadcast) throw new NotFoundException('Broadcast not found');
    if (broadcast.status !== 'DRAFT') {
      throw new BadRequestException('Can only update DRAFT broadcasts');
    }

    const updateData: Prisma.TgBotBroadcastUpdateInput = {};
    if (data.name) updateData.name = data.name;
    if (data.message) updateData.message = data.message as unknown as Prisma.InputJsonValue;
    if (data.filters) {
      updateData.filters = data.filters as unknown as Prisma.InputJsonValue;
      updateData.totalCount = await this.countAudience(broadcast.botId, data.filters);
    }
    if (data.scheduledAt !== undefined) {
      updateData.scheduledAt = data.scheduledAt ? new Date(data.scheduledAt) : null;
    }

    return this.prisma.tgBotBroadcast.update({ where: { id }, data: updateData });
  }

  async send(clubId: string, id: string) {
    const broadcast = await this.prisma.tgBotBroadcast.findFirst({ where: { id, clubId } });
    if (!broadcast) throw new NotFoundException('Broadcast not found');
    if (broadcast.status !== 'DRAFT' && broadcast.status !== 'SCHEDULED') {
      throw new BadRequestException(`Cannot send broadcast in status ${broadcast.status}`);
    }

    // Update status
    await this.prisma.tgBotBroadcast.update({
      where: { id },
      data: { status: 'SENDING', startedAt: new Date() },
    });

    // Get audience
    const subscribers = await this.getAudience(
      broadcast.botId,
      broadcast.filters as unknown as BroadcastFilters,
    );

    // Update total count with actual
    await this.prisma.tgBotBroadcast.update({
      where: { id },
      data: { totalCount: subscribers.length },
    });

    // Enqueue batch jobs (50 per batch to respect TG rate limits)
    const batchSize = 50;
    for (let i = 0; i < subscribers.length; i += batchSize) {
      const batch = subscribers.slice(i, i + batchSize);
      await this.broadcastQueue.add(
        'send-batch',
        {
          broadcastId: id,
          botId: broadcast.botId,
          message: broadcast.message,
          subscribers: batch.map((s) => ({ id: s.id, chatId: s.chatId })),
          batchIndex: Math.floor(i / batchSize),
        },
        {
          delay: Math.floor(i / batchSize) * 2000, // 2s between batches (~25 msg/sec)
          attempts: 1,
          removeOnComplete: true,
        },
      );
    }

    return { status: 'SENDING', totalCount: subscribers.length };
  }

  async cancel(clubId: string, id: string) {
    const broadcast = await this.prisma.tgBotBroadcast.findFirst({ where: { id, clubId } });
    if (!broadcast) throw new NotFoundException('Broadcast not found');
    if (broadcast.status !== 'SENDING' && broadcast.status !== 'SCHEDULED') {
      throw new BadRequestException(`Cannot cancel broadcast in status ${broadcast.status}`);
    }

    // Remove pending jobs
    const jobs = await this.broadcastQueue.getJobs(['delayed', 'waiting']);
    for (const job of jobs) {
      const jobData = job.data as Record<string, unknown> | undefined;
      if (jobData?.broadcastId === id) {
        await job.remove();
      }
    }

    return this.prisma.tgBotBroadcast.update({
      where: { id },
      data: { status: 'CANCELLED', completedAt: new Date() },
    });
  }

  async previewCount(botId: string, filters: BroadcastFilters): Promise<{ count: number }> {
    const count = await this.countAudience(botId, filters);
    return { count };
  }

  // ─── Audience filtering ────────────────────────────────────────

  private async countAudience(botId: string, filters: BroadcastFilters): Promise<number> {
    const where = this.buildWhere(botId, filters);
    return this.prisma.tgBotSubscriber.count({ where });
  }

  private async getAudience(botId: string, filters: BroadcastFilters) {
    const where = this.buildWhere(botId, filters);
    return this.prisma.tgBotSubscriber.findMany({
      where,
      select: { id: true, chatId: true },
    });
  }

  private buildWhere(botId: string, filters: BroadcastFilters): Prisma.TgBotSubscriberWhereInput {
    const where: Prisma.TgBotSubscriberWhereInput = {
      botId,
      isSubscribed: true, // only active subscribers by default
    };

    if (filters.clubs?.length) {
      where.clubId = { in: filters.clubs };
    }
    if (filters.source?.length) {
      where.source = { in: filters.source };
    }
    if (filters.isRegistered !== undefined) {
      where.isRegistered = filters.isRegistered;
    }
    if (filters.bonusGranted !== undefined) {
      where.bonusGranted = filters.bonusGranted;
    }
    if (filters.mailingConsent !== undefined) {
      where.mailingConsent = filters.mailingConsent;
    }
    if (filters.isSubscribed !== undefined) {
      where.isSubscribed = filters.isSubscribed;
    }
    if (filters.invitedCountMin !== undefined || filters.invitedCountMax !== undefined) {
      where.invitedCount = {};
      if (filters.invitedCountMin !== undefined) {
        where.invitedCount.gte = filters.invitedCountMin;
      }
      if (filters.invitedCountMax !== undefined) {
        where.invitedCount.lte = filters.invitedCountMax;
      }
    }
    if (filters.registeredAfter || filters.registeredBefore) {
      where.registeredAt = {};
      if (filters.registeredAfter) {
        where.registeredAt.gte = new Date(filters.registeredAfter);
      }
      if (filters.registeredBefore) {
        where.registeredAt.lte = new Date(filters.registeredBefore);
      }
    }
    if (filters.tags?.length) {
      where.tags = { hasSome: filters.tags };
    }

    return where;
  }
}
