import {
  BadRequestException,
  ForbiddenException,
  Injectable,
  NotFoundException,
} from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { AuditService } from '../audit/audit.service';
import { UpsertPerfilDto } from './dto/upsert-perfil.dto';
import { SeguridadSeedOrchestrator } from './seeds/seed-orchestrator.service';

type AuditMeta = {
  userId?: string;
  ip?: string;
  userAgent?: string;
};

@Injectable()
export class SeguridadService {
  constructor(
    private readonly prisma: PrismaService,
    private readonly audit: AuditService,
    private readonly seedOrch: SeguridadSeedOrchestrator,
  ) {}

  /**
   * Devuelve el árbol Módulo → Submódulo → Privilegio filtrado por
   * la suscripción de la empresa:
   *  - Solo módulos contratados en `suscripcion_modulos` (activo=true).
   *  - Submódulos addon: solo si hay `suscripcion_submodulos` (active=true).
   *  - Submódulos no-addon: incluidos por default, excluidos si hay
   *    `suscripcion_submodulos` (active=false).
   *
   * Bypass: empresas tipo holding/reseller ven el árbol completo (no tienen
   * suscripción tradicional). SuperAdmin NO bypasea porque el perfil pertenece
   * a la empresa y debe reflejar lo que la empresa tiene contratado.
   */
  async getArbol(empresaId: string, _userId: string) {
    const empresa = await this.prisma.empresas.findUnique({
      where: { id: empresaId },
      select: { type: true },
    });

    const bypass = empresa?.type === 'holding' || empresa?.type === 'reseller';

    const modulos = await this.prisma.modulos.findMany({
      where: { active: true },
      orderBy: [{ orden: 'asc' }, { descripcion: 'asc' }],
      include: {
        submodulos: {
          where: { active: true },
          orderBy: [{ orden: 'asc' }, { descripcion: 'asc' }],
          include: {
            privilegios: {
              where: { active: true },
              orderBy: [{ accion: 'asc' }, { codigo: 'asc' }],
              select: {
                id: true,
                codigo: true,
                desc_privilegio: true,
                recurso: true,
                accion: true,
                is_sistema: true,
              },
            },
          },
        },
      },
    });

    if (bypass) return modulos;

    const suscripcion = await this.prisma.suscripciones.findFirst({
      where: {
        empresa_id: empresaId,
        estado: { in: ['Activo', 'EnGracia'] },
      },
      orderBy: { created_at: 'desc' },
      select: { id: true },
    });
    if (!suscripcion) return [];

    const [modulosActivos, submodulosSuscripcion] = await Promise.all([
      this.prisma.suscripcion_modulos.findMany({
        where: { suscripcion_id: suscripcion.id, activo: true },
        select: { modulo_id: true },
      }),
      this.prisma.suscripcion_submodulos.findMany({
        where: { suscripcion_id: suscripcion.id },
        select: { submodulo_id: true, active: true },
      }),
    ]);

    const modulosOk = new Set(modulosActivos.map((m) => m.modulo_id));
    const addonsActivos = new Set(
      submodulosSuscripcion.filter((s) => s.active).map((s) => s.submodulo_id),
    );
    const exclusiones = new Set(
      submodulosSuscripcion.filter((s) => !s.active).map((s) => s.submodulo_id),
    );

    return modulos
      .filter((m) => modulosOk.has(m.id))
      .map((m) => ({
        ...m,
        submodulos: m.submodulos.filter((s) =>
          s.es_addon ? addonsActivos.has(s.id) : !exclusiones.has(s.id),
        ),
      }))
      .filter((m) => m.submodulos.length > 0);
  }

  async listarPerfiles(empresaId: string) {
    return this.prisma.perfiles.findMany({
      where: { empresa_id: empresaId },
      orderBy: [{ is_sistema: 'desc' }, { perfil: 'asc' }],
      select: {
        id: true,
        perfil: true,
        desc_perfil: true,
        is_sistema: true,
        created_at: true,
        updated_at: true,
        _count: { select: { perfiles_privilegios: true, usuario_perfiles: true } },
      },
    });
  }

  async obtenerPerfil(empresaId: string, perfilId: string) {
    const perfil = await this.prisma.perfiles.findFirst({
      where: { id: perfilId, empresa_id: empresaId },
      include: {
        perfiles_privilegios: {
          where: { active: true },
          select: { privilegio_id: true },
        },
      },
    });
    if (!perfil) throw new NotFoundException('Perfil no encontrado');
    return {
      id: perfil.id,
      perfil: perfil.perfil,
      desc_perfil: perfil.desc_perfil,
      is_sistema: perfil.is_sistema,
      privilegio_ids: perfil.perfiles_privilegios.map((pp) => pp.privilegio_id),
    };
  }

  async crearPerfil(empresaId: string, dto: UpsertPerfilDto, meta: AuditMeta) {
    const privIds = await this.validarPrivilegios(dto.privilegio_ids);
    const privModMap = await this.mapearPrivilegiosAModulos(privIds);

    const perfil = await this.prisma.perfiles.create({
      data: {
        empresa_id: empresaId,
        perfil: dto.perfil,
        desc_perfil: dto.desc_perfil ?? null,
        is_sistema: false,
        perfiles_privilegios: {
          create: privIds.map((pid) => ({
            privilegio_id: pid,
            modulo_id: privModMap.get(pid) ?? null,
            active: true,
          })),
        },
      },
      select: { id: true, perfil: true, desc_perfil: true, is_sistema: true },
    });

    await this.audit.log({
      empresa_id: empresaId,
      user_id: meta.userId,
      action: 'CREATE',
      entity_type: 'perfil',
      entity_id: perfil.id,
      descripcion: `Creó perfil "${perfil.perfil}" con ${privIds.length} privilegios`,
      new_value: { perfil: perfil.perfil, desc_perfil: perfil.desc_perfil, privilegios: privIds.length },
      ip_address: meta.ip,
      user_agent: meta.userAgent,
    });

    return perfil;
  }

  async actualizarPerfil(
    empresaId: string,
    perfilId: string,
    dto: UpsertPerfilDto,
    meta: AuditMeta,
  ) {
    const actual = await this.prisma.perfiles.findFirst({
      where: { id: perfilId, empresa_id: empresaId },
      include: { perfiles_privilegios: { select: { privilegio_id: true } } },
    });
    if (!actual) throw new NotFoundException('Perfil no encontrado');
    if (actual.is_sistema) {
      throw new ForbiddenException(
        'No se puede editar un perfil del sistema. Cloná el perfil y editá la copia.',
      );
    }

    const privIds = await this.validarPrivilegios(dto.privilegio_ids);
    const privModMap = await this.mapearPrivilegiosAModulos(privIds);

    await this.prisma.$transaction([
      this.prisma.perfiles_privilegios.deleteMany({ where: { perfil_id: perfilId } }),
      this.prisma.perfiles.update({
        where: { id: perfilId },
        data: {
          perfil: dto.perfil,
          desc_perfil: dto.desc_perfil ?? null,
          perfiles_privilegios: {
            create: privIds.map((pid) => ({
              privilegio_id: pid,
              modulo_id: privModMap.get(pid) ?? null,
              active: true,
            })),
          },
        },
      }),
    ]);

    await this.audit.log({
      empresa_id: empresaId,
      user_id: meta.userId,
      action: 'UPDATE',
      entity_type: 'perfil',
      entity_id: perfilId,
      descripcion: `Actualizó perfil "${dto.perfil}" (${privIds.length} privilegios)`,
      old_value: {
        perfil: actual.perfil,
        desc_perfil: actual.desc_perfil,
        privilegios: actual.perfiles_privilegios.length,
      },
      new_value: {
        perfil: dto.perfil,
        desc_perfil: dto.desc_perfil,
        privilegios: privIds.length,
      },
      ip_address: meta.ip,
      user_agent: meta.userAgent,
    });

    return this.obtenerPerfil(empresaId, perfilId);
  }

  async clonarPerfil(empresaId: string, perfilId: string, meta: AuditMeta) {
    const origen = await this.prisma.perfiles.findFirst({
      where: { id: perfilId, empresa_id: empresaId },
      include: {
        perfiles_privilegios: {
          where: { active: true },
          select: { privilegio_id: true },
        },
      },
    });
    if (!origen) throw new NotFoundException('Perfil no encontrado');

    const privIds = origen.perfiles_privilegios.map((pp) => pp.privilegio_id);
    const privModMap = await this.mapearPrivilegiosAModulos(privIds);

    const nuevo = await this.prisma.perfiles.create({
      data: {
        empresa_id: empresaId,
        perfil: this.proximoNombreCopia(origen.perfil ?? 'Perfil'),
        desc_perfil: origen.desc_perfil,
        is_sistema: false,
        perfiles_privilegios: {
          create: privIds.map((pid) => ({
            privilegio_id: pid,
            modulo_id: privModMap.get(pid) ?? null,
            active: true,
          })),
        },
      },
      select: { id: true, perfil: true },
    });

    await this.audit.log({
      empresa_id: empresaId,
      user_id: meta.userId,
      action: 'CREATE',
      entity_type: 'perfil',
      entity_id: nuevo.id,
      descripcion: `Clonó perfil "${origen.perfil}" → "${nuevo.perfil}"`,
      new_value: { origen_id: origen.id, nuevo_id: nuevo.id },
      ip_address: meta.ip,
      user_agent: meta.userAgent,
    });

    return nuevo;
  }

  async eliminarPerfil(empresaId: string, perfilId: string, meta: AuditMeta) {
    const perfil = await this.prisma.perfiles.findFirst({
      where: { id: perfilId, empresa_id: empresaId },
      include: { _count: { select: { usuario_perfiles: true } } },
    });
    if (!perfil) throw new NotFoundException('Perfil no encontrado');
    if (perfil.is_sistema) {
      throw new ForbiddenException('No se puede eliminar un perfil del sistema.');
    }
    if (perfil._count.usuario_perfiles > 0) {
      throw new BadRequestException(
        `El perfil tiene ${perfil._count.usuario_perfiles} usuario(s) asignado(s). Desasignalos antes de eliminar.`,
      );
    }

    await this.prisma.$transaction([
      this.prisma.perfiles_privilegios.deleteMany({ where: { perfil_id: perfilId } }),
      this.prisma.perfiles.delete({ where: { id: perfilId } }),
    ]);

    await this.audit.log({
      empresa_id: empresaId,
      user_id: meta.userId,
      action: 'DELETE',
      entity_type: 'perfil',
      entity_id: perfilId,
      descripcion: `Eliminó perfil "${perfil.perfil}"`,
      old_value: { perfil: perfil.perfil, desc_perfil: perfil.desc_perfil },
      ip_address: meta.ip,
      user_agent: meta.userAgent,
    });

    return { ok: true };
  }

  async cargarPerfilesSugeridos(empresaId: string, meta: AuditMeta) {
    const r = await this.seedOrch.runPerfilesSistemaParaEmpresa(empresaId);
    await this.audit.log({
      empresa_id: empresaId,
      user_id: meta.userId,
      action: 'CREATE',
      entity_type: 'perfil',
      descripcion: `Cargó perfiles sugeridos del sistema (creados=${r.perfiles_creados}, omitidos=${r.perfiles_omitidos})`,
      new_value: r,
      ip_address: meta.ip,
      user_agent: meta.userAgent,
    });
    return r;
  }

  // ============ Internos ============

  private async validarPrivilegios(ids: string[]): Promise<string[]> {
    if (!ids || ids.length === 0) return [];
    const unicos = Array.from(new Set(ids));
    const existentes = await this.prisma.privilegios.findMany({
      where: { id: { in: unicos }, active: true },
      select: { id: true },
    });
    if (existentes.length !== unicos.length) {
      throw new BadRequestException(
        `Hay privilegios inválidos o inactivos en la selección (${unicos.length - existentes.length}).`,
      );
    }
    return existentes.map((p) => p.id);
  }

  private async mapearPrivilegiosAModulos(
    privIds: string[],
  ): Promise<Map<string, string>> {
    if (!privIds.length) return new Map();
    const rows = await this.prisma.privilegios.findMany({
      where: { id: { in: privIds } },
      select: { id: true, submodulos: { select: { modulo_id: true } } },
    });
    return new Map(
      rows
        .filter((r) => r.submodulos?.modulo_id)
        .map((r) => [r.id, r.submodulos!.modulo_id]),
    );
  }

  private proximoNombreCopia(base: string): string {
    // Asegura que el nombre nuevo no choque; el constraint actual no es UNIQUE
    // pero igualmente evitamos duplicados textuales.
    return `Copia de ${base}`.slice(0, 50);
  }
}
