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 { RoleEntity } from '../../entities/role.entity';
import {
  RolePermissionEntity,
  ModuleName,
  AccessLevel,
  MASTER_MODULES_VIEW_DEFAULT,
} from '../../entities/role-permission.entity';
import { UserEntity } from '../../entities/user.entity';
import { RedisService } from '../../redis/redis.service';
import { CreateRoleDto, UpdateRoleDto, AssignPermissionsDto } from './dto';
import { RoleHierarchyService } from './role-hierarchy.service';
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,
    private readonly hierarchyService: RoleHierarchyService,
  ) {}

  /**
   * 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: true,
      },
    });

    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 findAllForLookup() {
    return this.roleRepo.find({
      select: {
        id: true,
        name: true,
        is_system: true,
      },
      order: { name: 'ASC' },
    });
  }

  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,
      view_all_bookings_queries: dto.view_all_bookings_queries ?? false,
    });

    await this.hierarchyService.ensureRoot(role.id);
    if (dto.parent_role_id) {
      await this.hierarchyService.setParent(role.id, dto.parent_role_id);
    }

    // Masters moved out of Settings default to VIEW for every new role
    // (client decision 2026-10-15); everything else starts at none.
    await this.rolePermRepo.save(
      [...MASTER_MODULES_VIEW_DEFAULT].map((module) => ({
        role: { id: role.id },
        module,
        access_level: AccessLevel.VIEW,
        tenant_id: this.getTenantId(),
      })),
    );

    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: Partial<RoleEntity> = {};
    if (dto.name !== undefined) updateData.name = dto.name;
    if (dto.description !== undefined) updateData.description = dto.description;
    if (dto.view_all_bookings_queries !== undefined) {
      updateData.view_all_bookings_queries = dto.view_all_bookings_queries;
    }

    // updated_by auto-set via CLS
    await this.roleRepo.update(id, updateData);
    if (dto.parent_role_id !== undefined) {
      await this.hierarchyService.setParent(id, dto.parent_role_id ?? null);
    }
    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.hierarchyService.detach(id);
    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: RoleEntity) {
    return {
      id: role.id,
      name: role.name,
      description: role.description,
      is_system: role.is_system,
      view_all_bookings_queries: role.view_all_bookings_queries,
      created_at: role.created_at,
      updated_at: role.updated_at,
      permissions:
        role.permissions?.map((p) => ({
          module: p.module,
          access_level: p.access_level,
        })) || [],
    };
  }
}
