import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Announcement, AnnouncementType, TargetType } from '../../domain/entities/announcement.entity';
import { AnnouncementRead } from '../../domain/entities/announcement-read.entity';
import { AnnouncementTagTarget } from '../../domain/entities/announcement-tag-target.entity';
import { AnnouncementUserTarget } from '../../domain/entities/announcement-user-target.entity';
import {
  IAnnouncementRepository,
  AnnouncementWithReadStatus,
} from '../../domain/repositories/announcement.repository.interface';

@Injectable()
export class AnnouncementRepositoryImpl implements IAnnouncementRepository {
  constructor(
    @InjectRepository(Announcement)
    private readonly announcementRepo: Repository<Announcement>,
    @InjectRepository(AnnouncementRead)
    private readonly readRepo: Repository<AnnouncementRead>,
    @InjectRepository(AnnouncementTagTarget)
    private readonly tagTargetRepo: Repository<AnnouncementTagTarget>,
    @InjectRepository(AnnouncementUserTarget)
    private readonly userTargetRepo: Repository<AnnouncementUserTarget>,
  ) {}

  async save(announcement: Announcement): Promise<Announcement> {
    return this.announcementRepo.save(announcement);
  }

  async findById(id: string): Promise<Announcement | null> {
    return this.announcementRepo.findOne({ where: { id } });
  }

  async findAll(params?: {
    type?: AnnouncementType;
    isEnabled?: boolean;
    limit?: number;
    offset?: number;
  }): Promise<[Announcement[], number]> {
    const qb = this.announcementRepo.createQueryBuilder('a');

    if (params?.type) {
      qb.andWhere('a.type = :type', { type: params.type });
    }
    if (params?.isEnabled !== undefined) {
      qb.andWhere('a.is_enabled = :isEnabled', { isEnabled: params.isEnabled });
    }

    qb.orderBy('a.created_at', 'DESC');
    qb.take(params?.limit ?? 50);
    qb.skip(params?.offset ?? 0);

    return qb.getManyAndCount();
  }

  async findForUser(params: {
    userId: string;
    userTags: string[];
    type?: AnnouncementType;
    limit?: number;
    offset?: number;
  }): Promise<AnnouncementWithReadStatus[]> {
    const now = new Date();
    const limit = params.limit ?? 50;
    const offset = params.offset ?? 0;

    // Build subquery for visible announcements
    const qb = this.announcementRepo.createQueryBuilder('a');

    qb.andWhere('a.is_enabled = true');

    // Published: publishedAt is null (immediate) or publishedAt <= now
    qb.andWhere('(a.published_at IS NULL OR a.published_at <= :now)', { now });

    // Not expired: expiresAt is null or expiresAt > now
    qb.andWhere('(a.expires_at IS NULL OR a.expires_at > :now)', { now });

    if (params.type) {
      qb.andWhere('a.type = :type', { type: params.type });
    }

    // Target filtering: ALL, BY_TAG (user has matching tag), SPECIFIC (user in target list)
    const targetConditions = [`a.target_type = '${TargetType.ALL}'`];

    if (params.userTags.length > 0) {
      targetConditions.push(
        `(a.target_type = '${TargetType.BY_TAG}' AND EXISTS (
          SELECT 1 FROM announcement_tag_targets att
          WHERE att.announcement_id = a.id AND att.tag IN (:...userTags)
        ))`,
      );
    }

    targetConditions.push(
      `(a.target_type = '${TargetType.SPECIFIC}' AND EXISTS (
        SELECT 1 FROM announcement_user_targets aut
        WHERE aut.announcement_id = a.id AND aut.user_id = :userId
      ))`,
    );

    qb.andWhere(`(${targetConditions.join(' OR ')})`, {
      userId: params.userId,
      ...(params.userTags.length > 0 ? { userTags: params.userTags } : {}),
    });

    qb.orderBy('a.published_at', 'DESC', 'NULLS FIRST');
    qb.addOrderBy('a.created_at', 'DESC');
    qb.take(limit);
    qb.skip(offset);

    const announcements = await qb.getMany();

    if (announcements.length === 0) {
      return [];
    }

    // Fetch read status for these announcements
    const announcementIds = announcements.map((a) => a.id);
    const reads = await this.readRepo
      .createQueryBuilder('r')
      .where('r.announcement_id IN (:...ids)', { ids: announcementIds })
      .andWhere('r.user_id = :userId', { userId: params.userId })
      .getMany();

    const readMap = new Map<string, Date>();
    for (const r of reads) {
      readMap.set(r.announcementId, r.readAt);
    }

    return announcements.map((a) => ({
      announcement: a,
      isRead: readMap.has(a.id),
      readAt: readMap.get(a.id) ?? null,
    }));
  }

  async countUnreadForUser(userId: string, userTags: string[]): Promise<number> {
    const now = new Date();

    const qb = this.announcementRepo.createQueryBuilder('a');

    qb.andWhere('a.is_enabled = true');
    qb.andWhere('(a.published_at IS NULL OR a.published_at <= :now)', { now });
    qb.andWhere('(a.expires_at IS NULL OR a.expires_at > :now)', { now });

    // Exclude already-read
    qb.andWhere(
      `NOT EXISTS (
        SELECT 1 FROM announcement_reads ar
        WHERE ar.announcement_id = a.id AND ar.user_id = :userId
      )`,
      { userId },
    );

    // Target filtering
    const targetConditions = [`a.target_type = '${TargetType.ALL}'`];

    if (userTags.length > 0) {
      targetConditions.push(
        `(a.target_type = '${TargetType.BY_TAG}' AND EXISTS (
          SELECT 1 FROM announcement_tag_targets att
          WHERE att.announcement_id = a.id AND att.tag IN (:...userTags)
        ))`,
      );
    }

    targetConditions.push(
      `(a.target_type = '${TargetType.SPECIFIC}' AND EXISTS (
        SELECT 1 FROM announcement_user_targets aut
        WHERE aut.announcement_id = a.id AND aut.user_id = :userId2
      ))`,
    );

    qb.andWhere(`(${targetConditions.join(' OR ')})`, {
      userId2: userId,
      ...(userTags.length > 0 ? { userTags } : {}),
    });

    return qb.getCount();
  }

  async markAsRead(announcementId: string, userId: string): Promise<void> {
    // Upsert: insert if not exists, ignore if already exists
    await this.readRepo
      .createQueryBuilder()
      .insert()
      .into(AnnouncementRead)
      .values({ announcementId, userId })
      .orIgnore()
      .execute();
  }

  async markAllAsRead(userId: string, userTags: string[]): Promise<void> {
    const now = new Date();

    // Find all unread announcement IDs for this user
    const qb = this.announcementRepo.createQueryBuilder('a');
    qb.select('a.id');
    qb.andWhere('a.is_enabled = true');
    qb.andWhere('(a.published_at IS NULL OR a.published_at <= :now)', { now });
    qb.andWhere('(a.expires_at IS NULL OR a.expires_at > :now)', { now });
    qb.andWhere(
      `NOT EXISTS (
        SELECT 1 FROM announcement_reads ar
        WHERE ar.announcement_id = a.id AND ar.user_id = :userId
      )`,
      { userId },
    );

    const targetConditions = [`a.target_type = '${TargetType.ALL}'`];
    if (userTags.length > 0) {
      targetConditions.push(
        `(a.target_type = '${TargetType.BY_TAG}' AND EXISTS (
          SELECT 1 FROM announcement_tag_targets att
          WHERE att.announcement_id = a.id AND att.tag IN (:...userTags)
        ))`,
      );
    }
    targetConditions.push(
      `(a.target_type = '${TargetType.SPECIFIC}' AND EXISTS (
        SELECT 1 FROM announcement_user_targets aut
        WHERE aut.announcement_id = a.id AND aut.user_id = :userId2
      ))`,
    );
    qb.andWhere(`(${targetConditions.join(' OR ')})`, {
      userId2: userId,
      ...(userTags.length > 0 ? { userTags } : {}),
    });

    const unreadAnnouncements = await qb.getRawMany();

    if (unreadAnnouncements.length === 0) {
      return;
    }

    const values = unreadAnnouncements.map((a: { a_id: string }) => ({
      announcementId: a.a_id,
      userId,
    }));

    await this.readRepo
      .createQueryBuilder()
      .insert()
      .into(AnnouncementRead)
      .values(values)
      .orIgnore()
      .execute();
  }

  async delete(id: string): Promise<void> {
    await this.announcementRepo.delete(id);
  }

  async saveTagTargets(announcementId: string, tags: string[]): Promise<void> {
    if (tags.length === 0) return;

    const values = tags.map((tag) => ({ announcementId, tag }));
    await this.tagTargetRepo
      .createQueryBuilder()
      .insert()
      .into(AnnouncementTagTarget)
      .values(values)
      .execute();
  }

  async saveUserTargets(announcementId: string, userIds: string[]): Promise<void> {
    if (userIds.length === 0) return;

    const values = userIds.map((userId) => ({ announcementId, userId }));
    await this.userTargetRepo
      .createQueryBuilder()
      .insert()
      .into(AnnouncementUserTarget)
      .values(values)
      .execute();
  }

  async clearTargets(announcementId: string): Promise<void> {
    await this.tagTargetRepo.delete({ announcementId });
    await this.userTargetRepo.delete({ announcementId });
  }
}
