import { BadRequestException, ForbiddenException, Injectable, NotFoundException } from '@nestjs/common';
import { randomBytes } from 'node:crypto';
import { OAuthClientType } from '../generated/prisma/client';
import { PrismaService } from '../infra/prisma.service';
import { AccessService } from './access.service';

@Injectable()
export class RbacService {
  constructor(
    private readonly prisma: PrismaService,
    private readonly access: AccessService
  ) {}

  async createRole(actorUserId: string, data: { slug: string; name: string; description?: string; permissionSlugs?: string[] }) {
    await this.access.assertSuperAdmin(actorUserId);
    return this.prisma.role.create({
      data: {
        slug: data.slug,
        name: data.name,
        description: data.description,
        permissions: data.permissionSlugs?.length
          ? {
              create: data.permissionSlugs.map((slug) => ({
                permission: { connect: { slug } }
              }))
            }
          : undefined
      },
      include: { permissions: { include: { permission: true } } }
    });
  }

  async upsertPermission(data: { slug: string; name: string; description?: string }) {
    return this.prisma.permission.upsert({
      where: { slug: data.slug },
      create: data,
      update: { name: data.name, description: data.description }
    });
  }

  async assignRole(actorUserId: string, userId: string, roleSlug: string) {
    await this.access.assertSuperAdmin(actorUserId);
    const role = await this.prisma.role.findUnique({ where: { slug: roleSlug } });
    if (!role) {
      throw new NotFoundException('Роль не найдена');
    }
    await this.ensureUserExists(userId);
    await this.prisma.userRole.upsert({
      where: { userId_roleId: { userId, roleId: role.id } },
      create: { userId, roleId: role.id },
      update: {}
    });
    return this.getUserRoleSlugs(userId);
  }

  async removeRole(actorUserId: string, userId: string, roleSlug: string) {
    await this.access.assertSuperAdmin(actorUserId);
    const role = await this.prisma.role.findUnique({ where: { slug: roleSlug } });
    if (!role) {
      throw new NotFoundException('Роль не найдена');
    }
    await this.prisma.userRole.deleteMany({ where: { userId, roleId: role.id } });
    return this.getUserRoleSlugs(userId);
  }

  async listRoles() {
    return this.prisma.role.findMany({
      orderBy: { name: 'asc' },
      include: { permissions: { include: { permission: true } } }
    });
  }

  async listPermissions() {
    return this.prisma.permission.findMany({ orderBy: { slug: 'asc' } });
  }

  async listOAuthScopes() {
    return this.prisma.oAuthScope.findMany({ orderBy: { slug: 'asc' } });
  }

  async createOAuthClient(actorUserId: string, data: { name: string; redirectUris: string[]; scopes: string[]; type?: OAuthClientType }) {
    await this.assertOAuthPermission(actorUserId);
    if (!data.redirectUris.length) {
      throw new BadRequestException('Укажите хотя бы один redirect URI');
    }
    if (!data.scopes.length) {
      throw new BadRequestException('Выберите хотя бы один scope');
    }

    const clientSecret = data.type === OAuthClientType.PUBLIC ? null : randomBytes(32).toString('base64url');
    const client = await this.prisma.oAuthClient.create({
      data: {
        name: data.name,
        clientId: `lendry_${randomBytes(16).toString('hex')}`,
        clientSecret,
        redirectUris: data.redirectUris,
        type: data.type ?? OAuthClientType.CONFIDENTIAL,
        scopes: {
          create: data.scopes.map((slug) => ({
            scope: { connect: { slug } }
          }))
        }
      },
      include: { scopes: { include: { scope: true } } }
    });

    return { client, clientSecret };
  }

  async updateOAuthClient(
    actorUserId: string,
    clientId: string,
    data: { name?: string; redirectUris?: string[]; scopes?: string[]; isActive?: boolean }
  ) {
    await this.assertOAuthPermission(actorUserId);
    const existing = await this.prisma.oAuthClient.findUnique({
      where: { clientId },
      include: { scopes: true }
    });
    if (!existing) {
      throw new NotFoundException('OAuth-приложение не найдено');
    }

    if (data.redirectUris && !data.redirectUris.length) {
      throw new BadRequestException('Укажите хотя бы один redirect URI');
    }

    if (data.scopes) {
      await this.prisma.oAuthClientScope.deleteMany({ where: { clientId: existing.id } });
      for (const slug of data.scopes) {
        const scope = await this.prisma.oAuthScope.findUnique({ where: { slug } });
        if (!scope) continue;
        await this.prisma.oAuthClientScope.create({
          data: { clientId: existing.id, scopeId: scope.id }
        });
      }
    }

    const updated = await this.prisma.oAuthClient.update({
      where: { id: existing.id },
      data: {
        name: data.name,
        redirectUris: data.redirectUris,
        isActive: data.isActive
      },
      include: { scopes: { include: { scope: true } } }
    });

    return { client: updated, clientSecret: undefined as string | undefined };
  }

  async rotateOAuthSecret(actorUserId: string, clientId: string) {
    await this.assertOAuthPermission(actorUserId);
    const existing = await this.prisma.oAuthClient.findUnique({ where: { clientId } });
    if (!existing) {
      throw new NotFoundException('OAuth-приложение не найдено');
    }
    if (existing.type === OAuthClientType.PUBLIC) {
      throw new BadRequestException('У публичного клиента нет client secret');
    }

    const clientSecret = randomBytes(32).toString('base64url');
    await this.prisma.oAuthClient.update({
      where: { id: existing.id },
      data: { clientSecret }
    });

    return { clientId, clientSecret };
  }

  async listOAuthClients() {
    return this.prisma.oAuthClient.findMany({
      orderBy: { createdAt: 'desc' },
      include: { scopes: { include: { scope: true } } }
    });
  }

  private async assertOAuthPermission(actorUserId: string) {
    const actor = await this.prisma.user.findUnique({ where: { id: actorUserId } });
    if (!actor) {
      throw new NotFoundException('Пользователь не найден');
    }
    const access = await this.access.getUserAccess(actor.id, actor.isSuperAdmin);
    if (!access.canManageOAuth) {
      throw new ForbiddenException('Недостаточно прав для управления OAuth-приложениями');
    }
  }

  private async getUserRoleSlugs(userId: string) {
    const links = await this.prisma.userRole.findMany({
      where: { userId },
      include: { role: true }
    });
    return links.map((link) => link.role.slug);
  }

  private async ensureUserExists(userId: string) {
    const user = await this.prisma.user.findUnique({ where: { id: userId } });
    if (!user) {
      throw new NotFoundException('Пользователь не найден');
    }
  }
}
