import { Injectable, Inject, Logger, NotFoundException } from '@nestjs/common';
import {
  Announcement,
  AnnouncementType,
  TargetType,
  AnnouncementTargetConfig,
} from '../../domain/entities/announcement.entity';
import {
  ANNOUNCEMENT_REPOSITORY,
  IAnnouncementRepository,
  AnnouncementWithReadStatus,
} from '../../domain/repositories/announcement.repository.interface';
import {
  USER_TAG_REPOSITORY,
  IUserTagRepository,
} from '../../domain/repositories/user-tag.repository.interface';
import { PushDispatcherService } from './push-dispatcher.service';

export interface CreateAnnouncementParams {
  title: string;
  content: string;
  type: AnnouncementType;
  priority?: string;
  targetType?: TargetType;
  targetConfig?: { tags?: string[]; userIds?: string[] };
  imageUrl?: string;
  linkUrl?: string;
  publishedAt?: Date;
  expiresAt?: Date;
  createdBy?: string;
}

export interface UpdateAnnouncementParams {
  title?: string;
  content?: string;
  type?: AnnouncementType;
  priority?: string;
  targetType?: TargetType;
  targetConfig?: { tags?: string[]; userIds?: string[] };
  imageUrl?: string | null;
  linkUrl?: string | null;
  isEnabled?: boolean;
  publishedAt?: Date | null;
  expiresAt?: Date | null;
}

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

  constructor(
    @Inject(ANNOUNCEMENT_REPOSITORY)
    private readonly announcementRepo: IAnnouncementRepository,
    @Inject(USER_TAG_REPOSITORY)
    private readonly userTagRepo: IUserTagRepository,
    private readonly pushDispatcher: PushDispatcherService,
  ) {}

  async create(params: CreateAnnouncementParams): Promise<Announcement> {
    const targetType = params.targetType ?? TargetType.ALL;

    let targetConfig: AnnouncementTargetConfig | null = null;
    if (params.targetConfig || targetType !== TargetType.ALL) {
      targetConfig = {
        type: targetType,
        tags: params.targetConfig?.tags,
        userIds: params.targetConfig?.userIds,
      };
    }

    const announcement = this.announcementRepo.save(
      Object.assign(new Announcement(), {
        title: params.title,
        content: params.content,
        type: params.type,
        priority: params.priority ?? 'NORMAL',
        targetType,
        targetConfig,
        imageUrl: params.imageUrl ?? null,
        linkUrl: params.linkUrl ?? null,
        isEnabled: true,
        publishedAt: params.publishedAt ?? null,
        expiresAt: params.expiresAt ?? null,
        createdBy: params.createdBy ?? null,
      }),
    );

    const saved = await announcement;

    // Save targeting data to junction tables
    await this.announcementRepo.clearTargets(saved.id);

    if (targetType === TargetType.BY_TAG && params.targetConfig?.tags?.length) {
      await this.announcementRepo.saveTagTargets(saved.id, params.targetConfig.tags);
    }

    if (targetType === TargetType.SPECIFIC && params.targetConfig?.userIds?.length) {
      await this.announcementRepo.saveUserTargets(saved.id, params.targetConfig.userIds);
    }

    this.logger.log(`Announcement created: ${saved.id} (targetType=${targetType})`);

    // If published immediately (publishedAt is null or ≤ now), trigger push
    const shouldPush = !saved.publishedAt || saved.publishedAt <= new Date();
    if (shouldPush) {
      this.triggerPushForAnnouncement(saved).catch((err) =>
        this.logger.error(`Push trigger failed for announcement ${saved.id}: ${err.message}`),
      );
    }

    return saved;
  }

  async update(id: string, params: UpdateAnnouncementParams): Promise<Announcement> {
    const existing = await this.announcementRepo.findById(id);
    if (!existing) {
      throw new NotFoundException('Announcement not found');
    }

    // Apply partial update
    if (params.title !== undefined) existing.title = params.title;
    if (params.content !== undefined) existing.content = params.content;
    if (params.type !== undefined) existing.type = params.type;
    if (params.priority !== undefined) existing.priority = params.priority as any;
    if (params.isEnabled !== undefined) existing.isEnabled = params.isEnabled;
    if (params.imageUrl !== undefined) existing.imageUrl = params.imageUrl;
    if (params.linkUrl !== undefined) existing.linkUrl = params.linkUrl;
    if (params.publishedAt !== undefined) existing.publishedAt = params.publishedAt;
    if (params.expiresAt !== undefined) existing.expiresAt = params.expiresAt;

    // Update targeting
    if (params.targetType !== undefined || params.targetConfig !== undefined) {
      const targetType = params.targetType ?? existing.targetType;
      existing.targetType = targetType;

      if (params.targetConfig || targetType !== TargetType.ALL) {
        existing.targetConfig = {
          type: targetType,
          tags: params.targetConfig?.tags ?? existing.targetConfig?.tags,
          userIds: params.targetConfig?.userIds ?? existing.targetConfig?.userIds,
        };
      } else {
        existing.targetConfig = null;
      }

      // Refresh junction tables
      await this.announcementRepo.clearTargets(id);

      if (targetType === TargetType.BY_TAG && existing.targetConfig?.tags?.length) {
        await this.announcementRepo.saveTagTargets(id, existing.targetConfig.tags);
      }
      if (targetType === TargetType.SPECIFIC && existing.targetConfig?.userIds?.length) {
        await this.announcementRepo.saveUserTargets(id, existing.targetConfig.userIds);
      }
    }

    const saved = await this.announcementRepo.save(existing);
    this.logger.log(`Announcement updated: ${id}`);
    return saved;
  }

  async delete(id: string): Promise<void> {
    await this.announcementRepo.delete(id);
    this.logger.log(`Announcement deleted: ${id}`);
  }

  async findAll(params?: {
    type?: AnnouncementType;
    isEnabled?: boolean;
    page?: number;
    limit?: number;
  }): Promise<{ items: Announcement[]; total: number; page: number; limit: number }> {
    const page = params?.page ?? 1;
    const limit = params?.limit ?? 20;
    const offset = (page - 1) * limit;

    const [items, total] = await this.announcementRepo.findAll({
      type: params?.type,
      isEnabled: params?.isEnabled,
      limit,
      offset,
    });

    return { items, total, page, limit };
  }

  async findById(id: string): Promise<Announcement> {
    const announcement = await this.announcementRepo.findById(id);
    if (!announcement) {
      throw new NotFoundException('Announcement not found');
    }
    return announcement;
  }

  async getForUser(
    userId: string,
    params?: { type?: AnnouncementType; page?: number; limit?: number },
  ): Promise<{
    items: AnnouncementWithReadStatus[];
    unreadCount: number;
    page: number;
    limit: number;
  }> {
    const page = params?.page ?? 1;
    const limit = params?.limit ?? 20;
    const offset = (page - 1) * limit;

    const userTags = await this.userTagRepo.findTagsByUserId(userId);

    const [items, unreadCount] = await Promise.all([
      this.announcementRepo.findForUser({
        userId,
        userTags,
        type: params?.type,
        limit,
        offset,
      }),
      this.announcementRepo.countUnreadForUser(userId, userTags),
    ]);

    return { items, unreadCount, page, limit };
  }

  async countUnreadForUser(userId: string): Promise<number> {
    const userTags = await this.userTagRepo.findTagsByUserId(userId);
    return this.announcementRepo.countUnreadForUser(userId, userTags);
  }

  async markAsRead(announcementId: string, userId: string): Promise<void> {
    await this.announcementRepo.markAsRead(announcementId, userId);
  }

  async markAllAsRead(userId: string): Promise<void> {
    const userTags = await this.userTagRepo.findTagsByUserId(userId);
    await this.announcementRepo.markAllAsRead(userId, userTags);
  }

  // ── Push Integration ──

  /**
   * Trigger device push for an announcement based on its targeting:
   * - ALL → sendToUsers with empty filter (all registered tokens)
   * - BY_TAG → resolve userIds from tags, then push
   * - SPECIFIC → push to specific userIds
   */
  private async triggerPushForAnnouncement(announcement: Announcement): Promise<void> {
    const data: Record<string, string> = {
      announcementId: announcement.id,
      type: announcement.type,
    };
    if (announcement.linkUrl) {
      data.linkUrl = announcement.linkUrl;
    }

    if (announcement.targetType === TargetType.ALL) {
      // For ALL targeting, pass empty array — PushDispatcher handles gracefully
      // In production, this would page through all users
      this.logger.log(`Push for ALL-targeted announcement ${announcement.id} (broadcast)`);
      // Broadcast push requires fetching all user IDs or using topic-based push
      // For now we log; real implementation would use FCM topics or iterate users
      return;
    }

    if (announcement.targetType === TargetType.BY_TAG && announcement.targetConfig?.tags?.length) {
      const tags = announcement.targetConfig.tags;
      const userIds: string[] = [];
      for (const tag of tags) {
        const ids = await this.userTagRepo.findUserIdsByTag(tag);
        userIds.push(...ids);
      }
      // Deduplicate
      const uniqueUserIds = [...new Set(userIds)];
      if (uniqueUserIds.length > 0) {
        this.logger.log(
          `Push for BY_TAG announcement ${announcement.id}: ${uniqueUserIds.length} users`,
        );
        await this.pushDispatcher.sendToUsers(
          uniqueUserIds,
          announcement.title,
          announcement.content.slice(0, 200),
          data,
        );
      }
      return;
    }

    if (announcement.targetType === TargetType.SPECIFIC && announcement.targetConfig?.userIds?.length) {
      const userIds = announcement.targetConfig.userIds;
      this.logger.log(
        `Push for SPECIFIC announcement ${announcement.id}: ${userIds.length} users`,
      );
      await this.pushDispatcher.sendToUsers(
        userIds,
        announcement.title,
        announcement.content.slice(0, 200),
        data,
      );
    }
  }
}
