import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, In, type DeepPartial } from 'typeorm';
import { ClsService } from 'nestjs-cls';
import { NotificationEntity, NotificationKind } from '../../entities/notification.entity';
import { UserEntity } from '../../entities/user.entity';
import {
  RolePermissionEntity,
  ModuleName,
  AccessLevel,
} from '../../entities/role-permission.entity';
import { CLS_TENANT_ID, CLS_USER_ID } from '../../common/cls/cls.constants';

export interface CreateNotificationInput {
  user_id: string;
  kind: NotificationKind;
  title: string;
  message: string;
  link?: string | null;
  related_entity_type?: string | null;
  related_entity_id?: string | null;
}

@Injectable()
export class NotificationService {
  constructor(
    @InjectRepository(NotificationEntity)
    private readonly repo: Repository<NotificationEntity>,
    @InjectRepository(UserEntity)
    private readonly userRepo: Repository<UserEntity>,
    @InjectRepository(RolePermissionEntity)
    private readonly rolePermRepo: Repository<RolePermissionEntity>,
    private readonly cls: ClsService,
  ) {}

  private tenantId(): string {
    return this.cls.get<string>(CLS_TENANT_ID);
  }

  /** Create a single notification. Used by other services (e.g.
   *  ChangeRequestService) to push entries into the bell feed. */
  async create(input: CreateNotificationInput): Promise<NotificationEntity> {
    const tenantId = this.tenantId();
    return this.repo.save({
      tenant_id: tenantId,
      user_id: input.user_id,
      kind: input.kind,
      title: input.title,
      message: input.message,
      link: input.link || null,
      related_entity_type: input.related_entity_type || null,
      related_entity_id: input.related_entity_id || null,
      is_read: false,
      created_by: this.cls.get<string>(CLS_USER_ID) || null,
      updated_by: this.cls.get<string>(CLS_USER_ID) || null,
    } as DeepPartial<NotificationEntity>);
  }

  /** Fan-out: notify every user in the tenant whose role has at least
   *  minLevel on `module`. Used for "notify all managers" style events. */
  async createForRoleWithAccess(
    module: ModuleName,
    minLevel: AccessLevel,
    template: Omit<CreateNotificationInput, 'user_id'>,
    excludeUserIds: string[] = [],
  ): Promise<void> {
    const tenantId = this.tenantId();
    const ACCESS_HIERARCHY: Record<AccessLevel, number> = {
      [AccessLevel.NONE]: 0,
      [AccessLevel.VIEW]: 1,
      [AccessLevel.CREATE]: 2,
      [AccessLevel.EDIT]: 3,
      [AccessLevel.DELETE]: 4,
    };
    const requiredLevel = ACCESS_HIERARCHY[minLevel];

    // Find all role_ids in this tenant that clear the bar for this module.
    const perms = await this.rolePermRepo.find({
      where: { tenant_id: tenantId, module, is_deleted: false },
      relations: {
        role: true,
      },
    });
    const qualifyingRoleIds = perms
      .filter((p) => ACCESS_HIERARCHY[p.access_level] >= requiredLevel)
      .map((p) => p.role?.id)
      .filter(Boolean);
    if (qualifyingRoleIds.length === 0) return;

    // All users in this tenant with one of those role_ids.
    const users = await this.userRepo.find({
      where: {
        tenant_id: tenantId,
        role_id: In(qualifyingRoleIds),
        is_active: true,
        is_deleted: false,
      },
      select: {
        id: true,
      },
    });
    const excludeSet = new Set(excludeUserIds);
    const targets = users.filter((u) => !excludeSet.has(u.id));
    if (targets.length === 0) return;

    await this.repo.save(
      targets.map((u) => ({
        tenant_id: tenantId,
        user_id: u.id,
        kind: template.kind,
        title: template.title,
        message: template.message,
        link: template.link || null,
        related_entity_type: template.related_entity_type || null,
        related_entity_id: template.related_entity_id || null,
        is_read: false,
      })) as DeepPartial<NotificationEntity>[],
    );
  }

  /** Recent notifications for the current user — feeds the bell dropdown. */
  async listForCurrentUser(limit = 20) {
    const userId = this.cls.get<string>(CLS_USER_ID);
    if (!userId) return [];
    return this.repo.find({
      where: { user_id: userId, tenant_id: this.tenantId(), is_deleted: false },
      order: { created_at: 'DESC' },
      take: limit,
    });
  }

  /** Unread count — cheap poll used by the bell badge. */
  async unreadCount(): Promise<number> {
    const userId = this.cls.get<string>(CLS_USER_ID);
    if (!userId) return 0;
    return this.repo.count({
      where: {
        user_id: userId,
        tenant_id: this.tenantId(),
        is_read: false,
        is_deleted: false,
      },
    });
  }

  async markRead(id: string): Promise<void> {
    const userId = this.cls.get<string>(CLS_USER_ID);
    const row = await this.repo.findOne({
      where: { id, user_id: userId, tenant_id: this.tenantId() },
    });
    if (!row) throw new NotFoundException('Notification not found');
    if (!row.is_read) {
      await this.repo.update(id, { is_read: true, read_at: new Date() });
    }
  }

  async markAllRead(): Promise<void> {
    const userId = this.cls.get<string>(CLS_USER_ID);
    if (!userId) return;
    await this.repo.update(
      { user_id: userId, tenant_id: this.tenantId(), is_read: false },
      { is_read: true, read_at: new Date() },
    );
  }
}
