import { Injectable, Inject, Logger } from '@nestjs/common';
import { PushChannel } from '../../domain/entities/device-token.entity';
import {
  DEVICE_TOKEN_REPOSITORY,
  IDeviceTokenRepository,
} from '../../domain/repositories/device-token.repository.interface';
import {
  IPushChannelProvider,
  PushPayload,
  FCM_PUSH_PROVIDER,
  APNS_PUSH_PROVIDER,
  HMS_PUSH_PROVIDER,
  XIAOMI_PUSH_PROVIDER,
  OPPO_PUSH_PROVIDER,
  VIVO_PUSH_PROVIDER,
} from '../../domain/ports/push-channel-provider.interface';

@Injectable()
export class PushDispatcherService {
  private readonly logger = new Logger(PushDispatcherService.name);
  private readonly providerMap: Map<PushChannel, IPushChannelProvider>;

  constructor(
    @Inject(DEVICE_TOKEN_REPOSITORY)
    private readonly deviceTokenRepo: IDeviceTokenRepository,
    @Inject(FCM_PUSH_PROVIDER) fcm: IPushChannelProvider,
    @Inject(APNS_PUSH_PROVIDER) apns: IPushChannelProvider,
    @Inject(HMS_PUSH_PROVIDER) hms: IPushChannelProvider,
    @Inject(XIAOMI_PUSH_PROVIDER) xiaomi: IPushChannelProvider,
    @Inject(OPPO_PUSH_PROVIDER) oppo: IPushChannelProvider,
    @Inject(VIVO_PUSH_PROVIDER) vivo: IPushChannelProvider,
  ) {
    this.providerMap = new Map<PushChannel, IPushChannelProvider>([
      [PushChannel.FCM, fcm],
      [PushChannel.APNS, apns],
      [PushChannel.HMS, hms],
      [PushChannel.XIAOMI, xiaomi],
      [PushChannel.OPPO, oppo],
      [PushChannel.VIVO, vivo],
    ]);
  }

  /**
   * Send push to a single user across all their registered devices.
   */
  async sendToUser(
    userId: string,
    title: string,
    body: string,
    data?: Record<string, string>,
  ): Promise<void> {
    const tokens = await this.deviceTokenRepo.findActiveByUserId(userId);
    if (!tokens.length) {
      this.logger.debug(`No active device tokens for user ${userId}`);
      return;
    }

    // Group tokens by channel
    const grouped = new Map<PushChannel, PushPayload[]>();
    for (const t of tokens) {
      const payloads = grouped.get(t.channel) ?? [];
      payloads.push({ token: t.token, title, body, data });
      grouped.set(t.channel, payloads);
    }

    await this.dispatchByChannel(grouped);
  }

  /**
   * Send push to multiple users (batch).
   * Fetches tokens in batches of 500, then dispatches by channel.
   */
  async sendToUsers(
    userIds: string[],
    title: string,
    body: string,
    data?: Record<string, string>,
  ): Promise<void> {
    if (!userIds.length) return;

    const tokens = await this.deviceTokenRepo.findActiveByUserIds(userIds);
    if (!tokens.length) {
      this.logger.debug(`No active device tokens for ${userIds.length} users`);
      return;
    }

    // Group tokens by channel
    const grouped = new Map<PushChannel, PushPayload[]>();
    for (const t of tokens) {
      const payloads = grouped.get(t.channel) ?? [];
      payloads.push({ token: t.token, title, body, data });
      grouped.set(t.channel, payloads);
    }

    await this.dispatchByChannel(grouped);
  }

  private async dispatchByChannel(
    grouped: Map<PushChannel, PushPayload[]>,
  ): Promise<void> {
    const promises: Promise<void>[] = [];

    for (const [channel, payloads] of grouped.entries()) {
      const provider = this.providerMap.get(channel);
      if (!provider) {
        this.logger.warn(`No provider registered for channel ${channel}`);
        continue;
      }

      promises.push(
        provider
          .sendBatch(payloads)
          .then(async (results) => {
            let successCount = 0;
            for (let i = 0; i < results.length; i++) {
              if (results[i].success) {
                successCount++;
              } else if (results[i].shouldDeactivateToken) {
                // Token is invalid, deactivate it
                await this.deviceTokenRepo.deactivateToken(payloads[i].token);
                this.logger.warn(
                  `Deactivated invalid token: ${payloads[i].token.slice(0, 12)}… (${channel})`,
                );
              }
            }
            this.logger.log(
              `[${channel}] Sent ${successCount}/${payloads.length} push notifications`,
            );
          })
          .catch((err) => {
            this.logger.error(`[${channel}] Batch send failed: ${err.message}`);
          }),
      );
    }

    await Promise.allSettled(promises);
  }
}
