import { Repository, IsNull, In } from 'typeorm';
import { Role, Permission, UserRole } from '../entities';

export interface ServiceContext {
  tenantId: string;
  userId?: string;
}

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

export interface UpdateRoleDto {
  code?: string;
  name?: string;
  description?: string;
  permissionIds?: string[];
  isActive?: boolean;
}

export interface RoleFilters {
  code?: string;
  isActive?: boolean;
  search?: string;
}

export interface PaginatedResult<T> {
  data: T[];
  total: number;
  page: number;
  limit: number;
  totalPages: number;
}

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

  async create(ctx: ServiceContext, data: CreateRoleDto): Promise<Role> {
    const existing = await this.roleRepository.findOne({
      where: { tenantId: ctx.tenantId, code: data.code, deletedAt: IsNull() },
    });

    if (existing) {
      throw new Error(`Role with code '${data.code}' already exists`);
    }

    const role = this.roleRepository.create({
      tenantId: ctx.tenantId,
      code: data.code,
      name: data.name,
      description: data.description,
      isActive: data.isActive ?? true,
      createdBy: ctx.userId ?? null,
    });

    const savedRole = await this.roleRepository.save(role);

    if (data.permissionIds?.length) {
      await this.assignPermissions(ctx, savedRole.id, data.permissionIds);
    }

    return this.findById(ctx, savedRole.id) as Promise<Role>;
  }

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

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

  async findAll(ctx: ServiceContext): Promise<Role[]> {
    return this.roleRepository.find({
      where: { tenantId: ctx.tenantId, deletedAt: IsNull() },
      relations: ['permissions'],
      order: { name: 'ASC' },
    });
  }

  async findWithFilters(
    ctx: ServiceContext,
    filters: RoleFilters,
    page = 1,
    limit = 50,
  ): Promise<PaginatedResult<Role>> {
    const qb = this.roleRepository
      .createQueryBuilder('role')
      .leftJoinAndSelect('role.permissions', 'permissions')
      .where('role.tenant_id = :tenantId', { tenantId: ctx.tenantId })
      .andWhere('role.deleted_at IS NULL');

    if (filters.code) {
      qb.andWhere('role.code = :code', { code: filters.code });
    }

    if (filters.isActive !== undefined) {
      qb.andWhere('role.is_active = :isActive', { isActive: filters.isActive });
    }

    if (filters.search) {
      qb.andWhere('(role.name ILIKE :search OR role.code ILIKE :search)', {
        search: `%${filters.search}%`,
      });
    }

    const [data, total] = await qb
      .orderBy('role.name', 'ASC')
      .skip((page - 1) * limit)
      .take(limit)
      .getManyAndCount();

    return {
      data,
      total,
      page,
      limit,
      totalPages: Math.ceil(total / limit),
    };
  }

  async update(ctx: ServiceContext, id: string, data: UpdateRoleDto): Promise<Role | null> {
    const role = await this.findById(ctx, id);
    if (!role) return null;

    if (data.code && data.code !== role.code) {
      const existing = await this.roleRepository.findOne({
        where: { tenantId: ctx.tenantId, code: data.code, deletedAt: IsNull() },
      });
      if (existing) {
        throw new Error(`Role with code '${data.code}' already exists`);
      }
    }

    Object.assign(role, {
      ...data,
      updatedBy: ctx.userId ?? null,
    });

    await this.roleRepository.save(role);

    if (data.permissionIds !== undefined) {
      await this.syncPermissions(ctx, id, data.permissionIds);
    }

    return this.findById(ctx, id);
  }

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

    role.deletedAt = new Date();
    role.deletedBy = ctx.userId ?? null;
    await this.roleRepository.save(role);

    return true;
  }

  async assignPermissions(ctx: ServiceContext, roleId: string, permissionIds: string[]): Promise<void> {
    const role = await this.findById(ctx, roleId);
    if (!role) throw new Error('Role not found');

    const permissions = await this.permissionRepository.find({
      where: { id: In(permissionIds), tenantId: ctx.tenantId, deletedAt: IsNull() },
    });

    role.permissions = [...(role.permissions || []), ...permissions];
    await this.roleRepository.save(role);
  }

  async removePermissions(ctx: ServiceContext, roleId: string, permissionIds: string[]): Promise<void> {
    const role = await this.findById(ctx, roleId);
    if (!role) throw new Error('Role not found');

    role.permissions = (role.permissions || []).filter(p => !permissionIds.includes(p.id));
    await this.roleRepository.save(role);
  }

  async syncPermissions(ctx: ServiceContext, roleId: string, permissionIds: string[]): Promise<void> {
    const role = await this.findById(ctx, roleId);
    if (!role) throw new Error('Role not found');

    const permissions = await this.permissionRepository.find({
      where: { id: In(permissionIds), tenantId: ctx.tenantId, deletedAt: IsNull() },
    });

    role.permissions = permissions;
    await this.roleRepository.save(role);
  }

  async getUsersWithRole(ctx: ServiceContext, roleId: string): Promise<UserRole[]> {
    return this.userRoleRepository.find({
      where: { roleId, tenantId: ctx.tenantId },
      relations: ['user'],
    });
  }

  async assignRoleToUser(ctx: ServiceContext, userId: string, roleId: string): Promise<UserRole> {
    const existing = await this.userRoleRepository.findOne({
      where: { userId, roleId, tenantId: ctx.tenantId },
    });

    if (existing) return existing;

    const userRole = this.userRoleRepository.create({
      tenantId: ctx.tenantId,
      userId,
      roleId,
      createdBy: ctx.userId ?? null,
    });

    return this.userRoleRepository.save(userRole);
  }

  async removeRoleFromUser(ctx: ServiceContext, userId: string, roleId: string): Promise<boolean> {
    const result = await this.userRoleRepository.delete({
      userId,
      roleId,
      tenantId: ctx.tenantId,
    });

    return (result.affected ?? 0) > 0;
  }
}
