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, UserAccessContext } from './access.service';
import { NotificationsService } from './notifications.service';
import { DEFAULT_USER_ROLE_SLUG, BOT_ROLE_SLUG } from './rbac.constants';

export interface OAuthClientListItem {
  id: string;
  name: string;
  clientId: string;
  redirectUris: string[];
  scopes: string[];
  isActive: boolean;
  type: OAuthClientType;
  createdById?: string | null;
  createdByDisplayName?: string | null;
  canManage: boolean;
}

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

  async createRole(actorUserId: string, data: { slug: string; name: string; description?: string; permissionSlugs?: string[] }) {
    await this.access.assertRbacManage(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 updateRole(
    actorUserId: string,
    roleSlug: string,
    data: { name?: string; description?: string; permissionSlugs?: string[] }
  ) {
    await this.access.assertRbacManage(actorUserId);
    const role = await this.prisma.role.findUnique({ where: { slug: roleSlug } });
    if (!role) {
      throw new NotFoundException('Роль не найдена');
    }

    if (data.permissionSlugs !== undefined) {
      await this.prisma.rolePermission.deleteMany({ where: { roleId: role.id } });
      for (const slug of data.permissionSlugs) {
        const permission = await this.prisma.permission.findUnique({ where: { slug } });
        if (!permission) continue;
        await this.prisma.rolePermission.create({
          data: { roleId: role.id, permissionId: permission.id }
        });
      }
    }

    return this.prisma.role.update({
      where: { id: role.id },
      data: {
        name: data.name,
        description: data.description
      },
      include: { permissions: { include: { permission: true } } }
    });
  }

  async deleteRole(actorUserId: string, roleSlug: string) {
    await this.access.assertRbacManage(actorUserId);
    const role = await this.prisma.role.findUnique({ where: { slug: roleSlug } });
    if (!role) {
      throw new NotFoundException('Роль не найдена');
    }
    if (role.isSystem) {
      throw new BadRequestException('Системную роль нельзя удалить');
    }

    await this.prisma.userRole.deleteMany({ where: { roleId: role.id } });
    await this.prisma.rolePermission.deleteMany({ where: { roleId: role.id } });
    await this.prisma.role.delete({ where: { id: role.id } });
    return { slug: roleSlug };
  }

  async assignRole(actorUserId: string, userId: string, roleSlug: string) {
    await this.access.assertRbacManage(actorUserId);
    const role = await this.prisma.role.findUnique({ where: { slug: roleSlug } });
    if (!role) {
      throw new NotFoundException('Роль не найдена');
    }
    await this.ensureUserExists(userId);

    const existingLink = await this.prisma.userRole.findUnique({
      where: { userId_roleId: { userId, roleId: role.id } }
    });
    if (existingLink) {
      return this.getUserRoleSlugs(userId);
    }

    await this.prisma.userRole.create({
      data: { userId, roleId: role.id }
    });

    await this.notifications.create(userId, 'role_assigned', 'Роли', `Вам назначена роль: ${role.name}`, {
      roleSlug: role.slug,
      roleName: role.name
    });

    return this.getUserRoleSlugs(userId);
  }

  async removeRole(actorUserId: string, userId: string, roleSlug: string) {
    await this.access.assertRbacManage(actorUserId);
    const role = await this.prisma.role.findUnique({ where: { slug: roleSlug } });
    if (!role) {
      throw new NotFoundException('Роль не найдена');
    }
    if (role.isSystem || role.isDefault || role.slug === DEFAULT_USER_ROLE_SLUG) {
      throw new BadRequestException('Стандартную роль «Пользователь» нельзя снять');
    }

    const existingLink = await this.prisma.userRole.findUnique({
      where: { userId_roleId: { userId, roleId: role.id } }
    });
    if (!existingLink) {
      return this.getUserRoleSlugs(userId);
    }

    await this.prisma.userRole.deleteMany({ where: { userId, roleId: role.id } });

    await this.notifications.create(userId, 'role_removed', 'Роли', `С вас снята роль: ${role.name}`, {
      roleSlug: role.slug,
      roleName: role.name
    });

    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.assertCanCreateOAuth(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,
        createdById: actorUserId,
        scopes: {
          create: data.scopes.map((slug) => ({
            scope: { connect: { slug } }
          }))
        }
      },
      include: { scopes: { include: { scope: true } }, createdBy: true }
    });

    return { client, clientSecret };
  }

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

    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 } }, createdBy: true }
    });

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

  async deleteOAuthClient(actorUserId: string, clientId: string) {
    const existing = await this.prisma.oAuthClient.findUnique({ where: { clientId } });
    if (!existing) {
      throw new NotFoundException('OAuth-приложение не найдено');
    }
    await this.assertCanManageOAuthClient(actorUserId, existing);
    await this.prisma.oAuthClient.delete({ where: { id: existing.id } });
    return { clientId };
  }

  async ensureDefaultUserRole(userId: string) {
    const user = await this.prisma.user.findUnique({
      where: { id: userId },
      select: { linkedBotId: true, isSystemAccount: true }
    });
    if (user?.linkedBotId || user?.isSystemAccount) {
      await this.ensureBotUserRole(userId);
      return;
    }

    const role = await this.prisma.role.findUnique({ where: { slug: DEFAULT_USER_ROLE_SLUG } });
    if (!role) return;
    await this.prisma.userRole.upsert({
      where: { userId_roleId: { userId, roleId: role.id } },
      create: { userId, roleId: role.id },
      update: {}
    });
  }

  async ensureBotUserRole(userId: string) {
    const [botRole, userRole] = await Promise.all([
      this.prisma.role.findUnique({ where: { slug: BOT_ROLE_SLUG } }),
      this.prisma.role.findUnique({ where: { slug: DEFAULT_USER_ROLE_SLUG } })
    ]);
    if (!botRole) return;

    if (userRole) {
      await this.prisma.userRole.deleteMany({
        where: { userId, roleId: userRole.id }
      });
    }

    await this.prisma.userRole.upsert({
      where: { userId_roleId: { userId, roleId: botRole.id } },
      create: { userId, roleId: botRole.id },
      update: {}
    });
  }

  async assignUserPermission(actorUserId: string, userId: string, permissionSlug: string) {
    await this.access.assertRbacManage(actorUserId);
    await this.ensureUserExists(userId);
    const permission = await this.prisma.permission.findUnique({ where: { slug: permissionSlug } });
    if (!permission) {
      throw new NotFoundException('Право не найдено');
    }

    await this.prisma.userPermission.upsert({
      where: { userId_permissionId: { userId, permissionId: permission.id } },
      create: { userId, permissionId: permission.id },
      update: {}
    });

    return this.getUserPermissionSlugs(userId);
  }

  async removeUserPermission(actorUserId: string, userId: string, permissionSlug: string) {
    await this.access.assertRbacManage(actorUserId);
    const permission = await this.prisma.permission.findUnique({ where: { slug: permissionSlug } });
    if (!permission) {
      throw new NotFoundException('Право не найдено');
    }

    await this.prisma.userPermission.deleteMany({ where: { userId, permissionId: permission.id } });
    return this.getUserPermissionSlugs(userId);
  }

  async rotateOAuthSecret(actorUserId: string, clientId: string) {
    const existing = await this.prisma.oAuthClient.findUnique({ where: { clientId } });
    if (!existing) {
      throw new NotFoundException('OAuth-приложение не найдено');
    }
    await this.assertCanManageOAuthClient(actorUserId, existing);
    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(actorUserId: string): Promise<OAuthClientListItem[]> {
    const { access } = await this.getActorAccess(actorUserId);
    if (!access.canViewOAuth) {
      throw new ForbiddenException('Недостаточно прав для просмотра OAuth-приложений');
    }

    const clients = await this.prisma.oAuthClient.findMany({
      orderBy: { createdAt: 'desc' },
      include: { scopes: { include: { scope: true } }, createdBy: true }
    });

    return clients.map((client) => this.toOAuthClientListItem(client, actorUserId, access));
  }

  private toOAuthClientListItem(
    client: {
      id: string;
      name: string;
      clientId: string;
      redirectUris: string[];
      isActive: boolean;
      type: OAuthClientType;
      createdById: string | null;
      createdBy?: { displayName: string } | null;
      scopes: Array<{ scope: { slug: string } }>;
    },
    actorUserId: string,
    access: UserAccessContext
  ): OAuthClientListItem {
    return {
      id: client.id,
      name: client.name,
      clientId: client.clientId,
      redirectUris: client.redirectUris,
      scopes: client.scopes.map((link) => link.scope.slug),
      isActive: client.isActive,
      type: client.type,
      createdById: client.createdById,
      createdByDisplayName: client.createdBy?.displayName ?? null,
      canManage: this.access.canManageOAuthClient(access, actorUserId, client)
    };
  }

  private async getActorAccess(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);
    return { actor, access };
  }

  private async assertCanCreateOAuth(actorUserId: string) {
    const { access } = await this.getActorAccess(actorUserId);
    if (!access.canManageOAuth) {
      throw new ForbiddenException('Недостаточно прав для управления OAuth-приложениями');
    }
  }

  private async assertCanManageOAuthClient(actorUserId: string, client: { createdById: string | null }) {
    const { access } = await this.getActorAccess(actorUserId);
    if (!this.access.canManageOAuthClient(access, actorUserId, client)) {
      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 getUserPermissionSlugs(userId: string) {
    const links = await this.prisma.userPermission.findMany({
      where: { userId },
      include: { permission: true }
    });
    return links.map((link) => link.permission.slug);
  }

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