import {
  ConflictException,
  Injectable,
  NotFoundException,
} from '@nestjs/common';
import { RolesRepository } from './roles.repository';
import { PermissionsRepository } from '@/modules/admin/permissions/permissions.repository';
import { CreateRoleDto } from './dto/create-role.dto';
import { UpdateRoleDto } from './dto/update-role.dto';
import { ListRolesDto } from './dto/list-roles.dto';
import { AssignPermissionsDto } from './dto/assign-permissions.dto';

// Default auth guard a role applies to when none is provided.
const DEFAULT_GUARD = 'web';
const FULL_ACCESS_PERMISSION_NAME = 'manage all';

const PERMISSION_MATRIX_ACTIONS = ['read', 'create', 'update', 'delete'];
const PERMISSION_MATRIX_SUBJECTS = [
  'User',
  'Role',
  'Permission',
  'Stakeholder',
];

const TOTAL_MATRIX_PERMISSIONS =
  PERMISSION_MATRIX_ACTIONS.length * PERMISSION_MATRIX_SUBJECTS.length;
const TOTAL_MATRIX_RESOURCES = PERMISSION_MATRIX_SUBJECTS.length;

@Injectable()
export class RolesService {
  constructor(
    private readonly roles: RolesRepository,
    private readonly permissions: PermissionsRepository,
  ) {}

  // GET many roles
  async list(query: ListRolesDto) {
    const roles = await this.roles.findMany(query);

    return roles.map((role) => {
      const { permissions, ...roleWithoutPermissions } = role;
      const permissionNames = permissions
        .filter((rolePermission) => !rolePermission.permission.deletedAt)
        .map((rolePermission) => rolePermission.permission.name);

      return {
        ...roleWithoutPermissions,
        _count: this.getEffectiveRoleCounts(permissionNames),
      };
    });
  }

  // GET a single role by its ID, including its assigned permissions.
  async getOne(id: number) {
    const role = await this.roles.findByIdWithPermissions(id);
    if (!role) throw new NotFoundException('Role not found');
    const { permissions, ...rest } = role;
    return { ...rest, permissions: permissions.map((p) => p.permission) };
  }

  // CREATE a new role.
  async create(dto: CreateRoleDto) {
    const name = dto.name.trim();
    const guardName = dto.guardName?.trim() || DEFAULT_GUARD;

    await this.ensureNameAvailable(name, guardName);

    const permissionIds = dto.permissionIds
      ? [...new Set(dto.permissionIds)]
      : [];
    if (permissionIds.length > 0) {
      await this.ensurePermissionsExist(permissionIds);
    }

    const role = await this.roles.create({ name, guardName });

    if (permissionIds.length > 0) {
      await this.roles.addPermissions(role.id, permissionIds);
    }

    return {
      message: 'Role created successfully.',
      role: await this.getOne(role.id),
    };
  }

  // UPDATE an existing role.
  async update(id: number, dto: UpdateRoleDto) {
    const current = await this.ensureExists(id);

    const name = dto.name?.trim() ?? current.name;
    const guardName = dto.guardName?.trim() ?? current.guardName;

    // Only re-check uniqueness if the identity (name + guard) actually changed.
    if (name !== current.name || guardName !== current.guardName) {
      await this.ensureNameAvailable(name, guardName, id);
    }

    await this.roles.update(id, {
      ...(dto.name !== undefined && { name }),
      ...(dto.guardName !== undefined && { guardName }),
    });

    if (dto.permissionIds !== undefined) {
      const permissionIds = [...new Set(dto.permissionIds)];
      if (permissionIds.length > 0) {
        await this.ensurePermissionsExist(permissionIds);
      }
      await this.roles.setPermissions(id, permissionIds);
    }

    return {
      message: 'Role updated successfully.',
      role: await this.getOne(id),
    };
  }

  // "Soft delete" a role.
  async remove(id: number) {
    await this.ensureExists(id);
    await this.roles.softDelete(id);
    return { message: 'Role deleted successfully.' };
  }

  // Assign one or more permissions to a role.
  async assignPermissions(roleId: number, dto: AssignPermissionsDto) {
    await this.ensureExists(roleId);

    const ids = [...new Set(dto.permissionIds)];
    const found = await this.permissions.findManyByIds(ids);
    if (found.length !== ids.length) {
      throw new NotFoundException('One or more permissions were not found');
    }

    await this.roles.addPermissions(roleId, ids);
    return {
      message: 'Permissions assigned to role.',
      role: await this.getOne(roleId),
    };
  }

  // Remove a single permission from a role.
  async removePermission(roleId: number, permissionId: number) {
    await this.ensureExists(roleId);

    const link = await this.roles.findRolePermission(roleId, permissionId);
    if (!link) {
      throw new NotFoundException('Permission is not assigned to this role');
    }

    await this.roles.removePermission(roleId, permissionId);
    return { message: 'Permission removed from role.' };
  }

  // role exists? avoid repeating the same lookup everywhere.
  private async ensureExists(id: number) {
    const role = await this.roles.findById(id);
    if (!role) throw new NotFoundException('Role not found');
    return role;
  }

  // Reject unless every permissionId resolves to a live (non-deleted) permission.
  private async ensurePermissionsExist(permissionIds: number[]) {
    const found = await this.permissions.findManyByIds(permissionIds);
    if (found.length !== permissionIds.length) {
      throw new NotFoundException('One or more permissions were not found');
    }
  }

  // Guard against duplicate name+guard pairs (no DB unique constraint).
  private async ensureNameAvailable(
    name: string,
    guardName: string,
    ignoreId?: number,
  ) {
    const existing = await this.roles.findByName(name, guardName);
    if (existing && existing.id !== ignoreId) {
      throw new ConflictException(
        `Role "${name}" already exists for guard "${guardName}"`,
      );
    }
  }

  private getEffectiveRoleCounts(permissionNames: string[]) {
    const hasFullAccess = permissionNames.some(
      (permissionName) =>
        permissionName.trim().toLowerCase() === FULL_ACCESS_PERMISSION_NAME,
    );

    if (hasFullAccess) {
      return {
        permissions: TOTAL_MATRIX_PERMISSIONS,
        resources: TOTAL_MATRIX_RESOURCES,
      };
    }

    const resourceNames = new Set<string>();

    for (const permissionName of permissionNames) {
      const [, ...subjectParts] = permissionName.trim().split(/\s+/);
      const subject = subjectParts.join(' ');

      if (subject && subject !== 'all') {
        resourceNames.add(subject);
      }
    }

    return {
      permissions: permissionNames.length,
      resources: resourceNames.size,
    };
  }
}
