import {
  BadRequestException,
  ForbiddenException,
  Injectable,
  NotFoundException,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import * as bcrypt from 'bcryptjs';
import { randomUUID } from 'node:crypto';
import { Brackets, Repository } from 'typeorm';
import { Role } from '../users/entities/role.entity';
import { User } from '../users/entities/user.entity';
import {
  AGENT_ROLE,
  ADMIN_ROLE,
  NORMAL_USER_ROLE,
  assertCanManageTarget,
  assertCanManageUsers,
  isAdmin,
  isAgent,
} from './admin-access';
import type {
  AssignUserRoleDto,
  CreateAdminUserDto,
  UpdateAdminUserDto,
  UpdateUserEnabledDto,
  UpdateUserPermissionsDto,
} from './dto/admin-user.dto';
import { normalizePermissions, permissionResources } from './permission-resources';

const MANAGEABLE_ROLE_CODES = [AGENT_ROLE, NORMAL_USER_ROLE];

@Injectable()
export class AdminUsersService {
  constructor(
    @InjectRepository(Role)
    private readonly rolesRepository: Repository<Role>,
    @InjectRepository(User)
    private readonly usersRepository: Repository<User>,
  ) {}

  async listUsers(actor: User, query: Record<string, unknown>) {
    assertCanManageUsers(actor);

    const page = Math.max(Number(query.page || 1), 1);
    const pageSize = Math.min(Math.max(Number(query.pageSize || 10), 1), 100);
    const keyword = String(query.keyword || '').trim();

    const builder = this.usersRepository
      .createQueryBuilder('user')
      .leftJoinAndSelect('user.roles', 'role')
      .orderBy('user.createdAt', 'DESC')
      .skip((page - 1) * pageSize)
      .take(pageSize);

    if (isAgent(actor)) {
      builder.andWhere(
        '(user.parentId = :actorId OR user.parentPath LIKE :actorPath)',
        {
          actorId: actor.id,
          actorPath: `%/${actor.id}/%`,
        },
      );
    }

    if (keyword) {
      builder.andWhere(
        new Brackets((qb) => {
          qb.where('user.name LIKE :keyword', { keyword: `%${keyword}%` }).orWhere(
            'user.email LIKE :keyword',
            { keyword: `%${keyword}%` },
          );
        }),
      );
    }

    const [users, total] = await builder.getManyAndCount();

    return {
      list: users.map((user) => this.toUserItem(user)),
      total,
      page,
      pageSize,
    };
  }

  async listRoles(actor: User) {
    assertCanManageUsers(actor);
    const roles = await this.rolesRepository.find({
      order: { code: 'ASC' },
    });

    const allowedCodes = isAdmin(actor)
      ? [ADMIN_ROLE, AGENT_ROLE, NORMAL_USER_ROLE]
      : MANAGEABLE_ROLE_CODES;

    return roles
      .filter((role) => allowedCodes.includes(role.code))
      .map((role) => ({
        code: role.code,
        name: role.name,
      }));
  }

  async createUser(actor: User, dto: CreateAdminUserDto) {
    assertCanManageUsers(actor);

    const name = String(dto.name || '').trim();
    const email = String(dto.email || '').trim();
    const password = String(dto.password || '');
    const roleCode = String(dto.roleCode || NORMAL_USER_ROLE);

    if (!name || !email || !password) {
      throw new BadRequestException('name, email and password are required');
    }

    this.assertAssignableRole(actor, roleCode);

    const existed = await this.usersRepository.findOne({ where: { email } });
    if (existed) {
      throw new BadRequestException('email already exists');
    }

    const parentId = isAdmin(actor) ? dto.parentId || null : actor.id;
    let parentPath: string | null = null;
    if (parentId) {
      const parent = await this.usersRepository.findOne({ where: { id: parentId } });
      if (!parent) {
        throw new BadRequestException('parent user does not exist');
      }
      if (!isAdmin(actor) && !this.isDescendantOf(parent, actor)) {
        throw new ForbiddenException('cannot create user under this parent');
      }
      parentPath = `${parent.parentPath || '/'}${parent.id}/`;
    }

    const role = await this.getRoleByCode(roleCode);
    const user = this.usersRepository.create({
      id: randomUUID(),
      name,
      email,
      passwordHash: await bcrypt.hash(password, 10),
      enabled: true,
      parentId,
      parentPath,
      tokenVersion: 0,
      mustChangePassword: dto.mustChangePassword ?? true,
      roles: [role],
    });

    await this.usersRepository.save(user);

    return this.toUserItem(user);
  }

  async updateEnabled(
    actor: User,
    userId: string,
    dto: UpdateUserEnabledDto,
  ) {
    const user = await this.getManageableUser(actor, userId);
    user.enabled = Boolean(dto.enabled);
    await this.usersRepository.save(user);

    return this.toUserItem(user);
  }

  async updateUser(
    actor: User,
    userId: string,
    dto: UpdateAdminUserDto,
  ) {
    const user = await this.getManageableUser(actor, userId);
    const name = String(dto.name ?? user.name).trim();
    const email = String(dto.email ?? user.email).trim();
    const roleCode = String(dto.roleCode || user.roles?.[0]?.code || '');

    if (!name || !email) {
      throw new BadRequestException('用户名和邮箱不能为空');
    }

    this.assertAssignableRole(actor, roleCode);

    const existed = await this.usersRepository.findOne({ where: { email } });
    if (existed && existed.id !== user.id) {
      throw new BadRequestException('邮箱已被其他用户使用');
    }

    const role = await this.getRoleByCode(roleCode);
    user.name = name;
    user.email = email;
    user.roles = [role];
    if (typeof dto.enabled === 'boolean') {
      user.enabled = dto.enabled;
    }
    if (typeof dto.mustChangePassword === 'boolean') {
      user.mustChangePassword = dto.mustChangePassword;
    }

    await this.usersRepository.save(user);

    return this.toUserItem(user);
  }

  async assignRole(
    actor: User,
    userId: string,
    dto: AssignUserRoleDto,
  ) {
    const roleCode = String(dto.roleCode || '');
    this.assertAssignableRole(actor, roleCode);

    const user = await this.getManageableUser(actor, userId);
    const role = await this.getRoleByCode(roleCode);
    user.roles = [role];

    await this.usersRepository.save(user);

    return this.toUserItem(user);
  }

  async getUserPermissions(actor: User, userId: string) {
    const user = await this.getManageableUser(actor, userId, { allowSelf: true });
    const rolePermissions = this.getRolePermissions(user.roles || []);
    const customPermissions = user.permissions ?? null;

    return {
      resources: permissionResources,
      rolePermissions,
      customPermissions,
      effectivePermissions: customPermissions ?? rolePermissions,
      isCustom: Boolean(customPermissions),
      canEdit: isAdmin(actor),
    };
  }

  async updateUserPermissions(
    actor: User,
    userId: string,
    dto: UpdateUserPermissionsDto,
  ) {
    if (!isAdmin(actor)) {
      throw new ForbiddenException('Only system administrators can set user permissions');
    }

    const user = await this.getManageableUser(actor, userId, { allowSelf: true });
    user.permissions = normalizePermissions(dto.permissions || {});
    await this.usersRepository.save(user);

    return this.getUserPermissions(actor, user.id);
  }

  async resetUserPermissions(actor: User, userId: string) {
    if (!isAdmin(actor)) {
      throw new ForbiddenException('Only system administrators can set user permissions');
    }

    const user = await this.getManageableUser(actor, userId, { allowSelf: true });
    user.permissions = null;
    await this.usersRepository.save(user);

    return this.getUserPermissions(actor, user.id);
  }

  private async getManageableUser(
    actor: User,
    userId: string,
    options: { allowSelf?: boolean } = {},
  ) {
    assertCanManageUsers(actor);

    const user = await this.usersRepository.findOne({ where: { id: userId } });
    if (!user) {
      throw new NotFoundException('user not found');
    }

    if (!options.allowSelf && user.id === actor.id) {
      throw new ForbiddenException('cannot manage yourself here');
    }

    assertCanManageTarget(actor, user);
    return user;
  }

  private assertAssignableRole(actor: User, roleCode: string) {
    if (isAdmin(actor)) {
      if (![ADMIN_ROLE, AGENT_ROLE, NORMAL_USER_ROLE].includes(roleCode)) {
        throw new BadRequestException('invalid role');
      }
      return;
    }

    if (isAgent(actor) && MANAGEABLE_ROLE_CODES.includes(roleCode)) {
      return;
    }

    throw new ForbiddenException('cannot assign this role');
  }

  private async getRoleByCode(code: string) {
    const role = await this.rolesRepository.findOne({ where: { code } });
    if (!role) {
      throw new BadRequestException('role does not exist');
    }

    return role;
  }

  private toUserItem(user: User) {
    return {
      id: user.id,
      name: user.name,
      email: user.email,
      enabled: user.enabled,
      parentId: user.parentId,
      parentPath: user.parentPath,
      mustChangePassword: user.mustChangePassword,
      roles: (user.roles || []).map((role) => ({
        code: role.code,
        name: role.name,
      })),
      roleCode: user.roles?.[0]?.code,
      roleName: user.roles?.[0]?.name,
      lastLoginAt: user.lastLoginAt,
      createdAt: user.createdAt,
      updatedAt: user.updatedAt,
    };
  }

  private getRolePermissions(roles: Role[]) {
    const permissions: Record<string, string[]> = {};

    for (const role of roles) {
      const rolePermissions = role.permissions;
      if (Array.isArray(rolePermissions)) {
        if (rolePermissions.includes('*')) {
          permissions['*'] = ['*'];
        }
        continue;
      }

      if (rolePermissions) {
        for (const [resource, actions] of Object.entries(rolePermissions)) {
          permissions[resource] = Array.from(
            new Set([...(permissions[resource] || []), ...actions]),
          );
        }
      }
    }

    return permissions;
  }

  private isDescendantOf(target: User, actor: User) {
    return target.id === actor.id || Boolean(target.parentPath?.includes(`/${actor.id}/`));
  }
}
