import { Repository, FindOptionsWhere, ILike, In } from 'typeorm';
import { Role, Permission, User } from '../entities/index.js';

export interface RoleSearchParams {
  tenantId: string;
  search?: string;
  isSystem?: boolean;
  limit?: number;
  offset?: number;
}

export interface CreateRoleDto {
  name: string;
  code: string;
  description?: string;
  isSystem?: boolean;
  color?: string;
  permissionIds?: string[];
}

export interface UpdateRoleDto extends Partial<CreateRoleDto> {}

export interface AssignPermissionsDto {
  permissionIds: string[];
}

export class RolesService {
  constructor(
    private readonly roleRepository: Repository<Role>,
    private readonly permissionRepository: Repository<Permission>,
  ) {}

  async findAll(params: RoleSearchParams): Promise<{ data: Role[]; total: number }> {
    const {
      tenantId,
      search,
      isSystem,
      limit = 50,
      offset = 0,
    } = params;

    const where: FindOptionsWhere<Role>[] = [];
    const baseWhere: FindOptionsWhere<Role> = { tenantId };

    if (isSystem !== undefined) {
      baseWhere.isSystem = isSystem;
    }

    if (search) {
      where.push(
        { ...baseWhere, name: ILike(`%${search}%`) },
        { ...baseWhere, code: ILike(`%${search}%`) },
        { ...baseWhere, description: ILike(`%${search}%`) }
      );
    } else {
      where.push(baseWhere);
    }

    const [data, total] = await this.roleRepository.findAndCount({
      where,
      relations: ['permissions'],
      take: limit,
      skip: offset,
      order: { isSystem: 'DESC', name: 'ASC' },
    });

    return { data, total };
  }

  async findOne(id: string, tenantId: string): Promise<Role | null> {
    return this.roleRepository.findOne({
      where: { id, tenantId },
      relations: ['permissions', 'users'],
    });
  }

  async findByCode(code: string, tenantId: string): Promise<Role | null> {
    return this.roleRepository.findOne({
      where: { code, tenantId },
      relations: ['permissions'],
    });
  }

  async create(tenantId: string, dto: CreateRoleDto, createdBy: string): Promise<Role> {
    // Check if code already exists
    const existingCode = await this.findByCode(dto.code, tenantId);
    if (existingCode) {
      throw new Error(`Ya existe un rol con el codigo "${dto.code}"`);
    }

    const role = this.roleRepository.create({
      tenantId,
      name: dto.name,
      code: dto.code,
      description: dto.description ?? null,
      isSystem: dto.isSystem ?? false,
      color: dto.color ?? null,
      createdBy,
    });

    // If permissions were provided, assign them
    if (dto.permissionIds && dto.permissionIds.length > 0) {
      const permissions = await this.permissionRepository.findBy({
        id: In(dto.permissionIds),
      });
      role.permissions = permissions;
    }

    return this.roleRepository.save(role);
  }

  async update(
    id: string,
    tenantId: string,
    dto: UpdateRoleDto,
    updatedBy: string
  ): Promise<Role | null> {
    const role = await this.findOne(id, tenantId);
    if (!role) return null;

    // Cannot modify system roles
    if (role.isSystem && !dto.description && !dto.color) {
      throw new Error('Los roles del sistema solo permiten modificar descripcion y color');
    }

    // Check for code uniqueness if changing code
    if (dto.code && dto.code !== role.code) {
      const existingCode = await this.findByCode(dto.code, tenantId);
      if (existingCode && existingCode.id !== id) {
        throw new Error(`Ya existe un rol con el codigo "${dto.code}"`);
      }
    }

    // Update basic fields
    if (dto.name !== undefined && !role.isSystem) role.name = dto.name;
    if (dto.code !== undefined && !role.isSystem) role.code = dto.code;
    if (dto.description !== undefined) role.description = dto.description ?? null;
    if (dto.color !== undefined) role.color = dto.color ?? null;
    role.updatedBy = updatedBy;

    // Update permissions if provided
    if (dto.permissionIds !== undefined) {
      const permissions = await this.permissionRepository.findBy({
        id: In(dto.permissionIds),
      });
      role.permissions = permissions;
    }

    return this.roleRepository.save(role);
  }

  async assignPermissions(
    id: string,
    tenantId: string,
    dto: AssignPermissionsDto,
    updatedBy: string
  ): Promise<Role | null> {
    const role = await this.findOne(id, tenantId);
    if (!role) return null;

    const permissions = await this.permissionRepository.findBy({
      id: In(dto.permissionIds),
    });

    role.permissions = permissions;
    role.updatedBy = updatedBy;

    return this.roleRepository.save(role);
  }

  async addPermission(
    id: string,
    tenantId: string,
    permissionId: string,
    updatedBy: string
  ): Promise<Role | null> {
    const role = await this.findOne(id, tenantId);
    if (!role) return null;

    const permission = await this.permissionRepository.findOne({
      where: { id: permissionId },
    });
    if (!permission) {
      throw new Error('Permiso no encontrado');
    }

    // Check if permission already assigned
    const hasPermission = role.permissions.some(p => p.id === permissionId);
    if (!hasPermission) {
      role.permissions.push(permission);
      role.updatedBy = updatedBy;
      return this.roleRepository.save(role);
    }

    return role;
  }

  async removePermission(
    id: string,
    tenantId: string,
    permissionId: string,
    updatedBy: string
  ): Promise<Role | null> {
    const role = await this.findOne(id, tenantId);
    if (!role) return null;

    role.permissions = role.permissions.filter(p => p.id !== permissionId);
    role.updatedBy = updatedBy;

    return this.roleRepository.save(role);
  }

  async getPermissions(id: string, tenantId: string): Promise<Permission[]> {
    const role = await this.findOne(id, tenantId);
    if (!role) return [];

    return role.permissions;
  }

  async getUsers(id: string, tenantId: string): Promise<User[]> {
    const role = await this.findOne(id, tenantId);
    if (!role) return [];

    return role.users;
  }

  async getUserCount(id: string, tenantId: string): Promise<number> {
    const role = await this.roleRepository.findOne({
      where: { id, tenantId },
      relations: ['users'],
    });

    return role?.users?.length ?? 0;
  }

  // Get system roles (admin, user, etc.)
  async getSystemRoles(tenantId: string): Promise<Role[]> {
    return this.roleRepository.find({
      where: { tenantId, isSystem: true },
      relations: ['permissions'],
      order: { name: 'ASC' },
    });
  }

  // Get custom roles (non-system)
  async getCustomRoles(tenantId: string): Promise<Role[]> {
    return this.roleRepository.find({
      where: { tenantId, isSystem: false },
      relations: ['permissions'],
      order: { name: 'ASC' },
    });
  }

  // Clone a role
  async cloneRole(
    id: string,
    tenantId: string,
    newCode: string,
    newName: string,
    createdBy: string
  ): Promise<Role | null> {
    const sourceRole = await this.findOne(id, tenantId);
    if (!sourceRole) return null;

    // Check if new code exists
    const existingCode = await this.findByCode(newCode, tenantId);
    if (existingCode) {
      throw new Error(`Ya existe un rol con el codigo "${newCode}"`);
    }

    const newRole = this.roleRepository.create({
      tenantId,
      name: newName,
      code: newCode,
      description: sourceRole.description,
      isSystem: false, // Cloned roles are never system roles
      color: sourceRole.color,
      permissions: sourceRole.permissions,
      createdBy,
    });

    return this.roleRepository.save(newRole);
  }

  async delete(id: string, tenantId: string): Promise<boolean> {
    const role = await this.findOne(id, tenantId);
    if (!role) return false;

    // Cannot delete system roles
    if (role.isSystem) {
      throw new Error('No se pueden eliminar roles del sistema');
    }

    // Check if role has users
    if (role.users && role.users.length > 0) {
      throw new Error('No se puede eliminar un rol que tiene usuarios asignados');
    }

    const result = await this.roleRepository.softDelete(id);
    return (result.affected ?? 0) > 0;
  }
}
