import {
  Injectable,
  NotFoundException,
  ConflictException,
  BadRequestException,
  ForbiddenException,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { ClsService } from 'nestjs-cls';
import { RoleRepository } from './repositories/role.repository';
import { RolePermissionEntity, ModuleName } from '../../entities/role-permission.entity';
import { UserEntity } from '../../entities/user.entity';
import { RedisService } from '../../redis/redis.service';
import { CreateRoleDto, UpdateRoleDto, AssignPermissionsDto } from './dto';
import { CLS_TENANT_ID, CLS_USER_ID } from '../../common/cls/cls.constants';

/**
 * RoleService — CRUD for roles + permission assignment with Redis cache invalidation.
 *
 * Per CLAUDE.md:
 * - Roles fully dynamic, managed per tenant by Admin — never static role enums
 * - On permission update → always invalidate Redis cache
 * - No setTenantId() calls — CLS handles it
 */
@Injectable()
export class RoleService {
  constructor(
    private readonly roleRepo: RoleRepository,
    @InjectRepository(RolePermissionEntity)
    private readonly rolePermRepo: Repository<RolePermissionEntity>,
    @InjectRepository(UserEntity)
    private readonly userRepo: Repository<UserEntity>,
    private readonly redisService: RedisService,
    private readonly cls: ClsService,
  ) {}

  /**
   * Prevent admin from modifying their own role's permissions or deleting it.
   */
  private async preventSelfRoleAction(targetRoleId: string, action: string): Promise<void> {
    const currentUserId = this.cls.get<string>(CLS_USER_ID);
    if (!currentUserId) return;

    const currentUser = await this.userRepo.findOne({
      where: { id: currentUserId },
      select: ['role_id'],
    });

    if (currentUser?.role_id === targetRoleId) {
      throw new ForbiddenException(`You cannot ${action} your own role`);
    }
  }

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

  private getUserId(): string | null {
    return this.cls.get<string>(CLS_USER_ID) || null;
  }

  async findAll(query?: { page?: number; limit?: number; search?: string }) {
    const result = await this.roleRepo.findPaginated({
      page: query?.page,
      limit: query?.limit,
      search: query?.search,
      searchColumns: ['name', 'description'],
      relations: ['permissions'],
      order: { name: 'ASC' },
    });
    return {
      items: result.items.map((r) => this.toResponse(r)),
      meta: result.meta,
    };
  }

  async findById(id: string) {
    const role = await this.roleRepo.findByIdWithPermissions(id);
    if (!role) {
      throw new NotFoundException('Role not found');
    }
    return this.toResponse(role);
  }

  async create(dto: CreateRoleDto) {
    const existing = await this.roleRepo.findByName(dto.name);
    if (existing) {
      throw new ConflictException('Role name already exists in this tenant');
    }

    // tenant_id, created_by, updated_by auto-injected via CLS
    const role = await this.roleRepo.save({
      name: dto.name,
      description: dto.description || null,
      is_system: false,
    });

    return { id: role.id, message: 'Role created successfully' };
  }

  async update(id: string, dto: UpdateRoleDto) {
    const role = await this.roleRepo.findByIdWithPermissions(id);
    if (!role) {
      throw new NotFoundException('Role not found');
    }

    if (dto.name && dto.name !== role.name) {
      const existing = await this.roleRepo.findByName(dto.name);
      if (existing) {
        throw new ConflictException('Role name already exists in this tenant');
      }
    }

    const updateData: any = {};
    if (dto.name !== undefined) updateData.name = dto.name;
    if (dto.description !== undefined) updateData.description = dto.description;

    // updated_by auto-set via CLS
    await this.roleRepo.update(id, updateData);
    return { id, message: 'Role updated successfully' };
  }

  async remove(id: string) {
    await this.preventSelfRoleAction(id, 'delete');

    const role = await this.roleRepo.findByIdWithPermissions(id);
    if (!role) {
      throw new NotFoundException('Role not found');
    }

    if (role.is_system) {
      throw new BadRequestException('System roles cannot be deleted');
    }

    await this.roleRepo.softDelete(id);
    await this.invalidatePermissionCache(id);

    return { message: 'Role deleted successfully' };
  }

  /**
   * Assign permissions to a role — bulk replace.
   * Per CLAUDE.md: On permission update → always invalidate Redis cache
   */
  async assignPermissions(roleId: string, dto: AssignPermissionsDto) {
    await this.preventSelfRoleAction(roleId, 'modify permissions of');

    const role = await this.roleRepo.findByIdWithPermissions(roleId);
    if (!role) {
      throw new NotFoundException('Role not found');
    }

    const validModules = Object.values(ModuleName);
    for (const perm of dto.permissions) {
      if (!validModules.includes(perm.module as ModuleName)) {
        throw new BadRequestException(`Invalid module: ${perm.module}`);
      }
    }

    const tenantId = this.getTenantId();
    const userId = this.getUserId();

    // Delete existing permissions for this role within tenant
    await this.rolePermRepo
      .createQueryBuilder()
      .delete()
      .where('role_id = :roleId AND tenant_id = :tenantId', {
        roleId,
        tenantId,
      })
      .execute();

    // Insert new permissions
    const permEntities = dto.permissions.map((p) => {
      const entity = new RolePermissionEntity();
      entity.role = role;
      entity.module = p.module;
      entity.access_level = p.access_level;
      entity.tenant_id = tenantId;
      entity.created_by = userId;
      entity.updated_by = userId;
      return entity;
    });

    await this.rolePermRepo.save(permEntities);

    // ⚠️ CRITICAL: Invalidate Redis cache on permission change
    await this.invalidatePermissionCache(roleId);

    return { message: 'Permissions assigned successfully' };
  }

  async getPermissionsMatrix(roleId: string) {
    const role = await this.roleRepo.findByIdWithPermissions(roleId);
    if (!role) {
      throw new NotFoundException('Role not found');
    }

    const allModules = Object.values(ModuleName);
    return allModules.map((mod) => {
      const perm = role.permissions?.find((p) => p.module === mod);
      return {
        module: mod,
        access_level: perm ? perm.access_level : 'none',
      };
    });
  }

  private async invalidatePermissionCache(roleId: string): Promise<void> {
    await this.redisService.invalidateByPattern(
      `perm:role:${this.getTenantId()}:${roleId}`,
    );
  }

  async invalidateAllTenantPermissions(): Promise<void> {
    await this.redisService.invalidateByPattern(
      `perm:role:${this.getTenantId()}:*`,
    );
  }

  private toResponse(role: any) {
    return {
      id: role.id,
      name: role.name,
      description: role.description,
      is_system: role.is_system,
      created_at: role.created_at,
      updated_at: role.updated_at,
      permissions: role.permissions?.map((p: any) => ({
        module: p.module,
        access_level: p.access_level,
      })) || [],
    };
  }
}
