import { BadRequestException, Injectable, NotFoundException, UnauthorizedException, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { JwtService } from '@nestjs/jwt';
import * as bcrypt from 'bcrypt';
import { randomUUID } from 'crypto';
import { AuditService } from 'src/audit/audit.service';
import { RegistroEmpresaService } from 'src/empresas/registro-empresa.service';
import { PrismaService } from 'src/prisma/prisma.service';
import { QueuesService } from 'src/queues/queues.service';
import { RedisService } from 'src/redis/redis.service';
import { EstadoSuscripcion } from 'src/suscripciones/dto/enum-sucripcion';
import { SuscripcionesService } from 'src/suscripciones/suscripciones.service';
import { RubrosService } from 'src/rubros/rubros.service';
import { EstadoUser } from 'src/users/dto/enum-user';
import { LoginDto } from './dto/login.dto';
import { RegistroDto } from './dto/registro.dto';

@Injectable()
export class AuthService {
  private readonly logger = new Logger(AuthService.name);
  constructor(
    private readonly prisma: PrismaService,
    private readonly jwtService: JwtService,
    private readonly redis: RedisService,
    private readonly config: ConfigService,
    private readonly auditService: AuditService,
    private queuesService: QueuesService,
    private readonly registroEmpresaService: RegistroEmpresaService,
    private readonly suscripcionesService: SuscripcionesService,
    private readonly rubrosService: RubrosService,
  ) {}

  async register(dto: RegistroDto) {
    try {
      const result = await this.registroEmpresaService.registrar({
        ruc: dto.ruc,
        dv: dto.dv,
        razon_social: dto.razon_social,
        nombre_fantasia: dto.nombre_fantasia,
        tipo_contribuyente: dto.tipo_contribuyente,
        celular: dto.celular,
        email: dto.email,
        usuario: dto.usuario,
        password: dto.password,
      });

      // Sincronizar con middleware (Alta)
      await this.queuesService.enqueueMiddlewareEmpresa(result.empresa.id, 'A', 'empresas');

      this.logger.log(`Empresa registrada: ${dto.razon_social} (${result.empresa.id})`, 'AuthService');
      await this.auditService.log({
        empresa_id: result.empresa.id,
        user_id: result.usuario.id,
        action: 'CREATE',
        entity_type: 'empresa',
        entity_id: result.empresa.id,
        descripcion: `Registro de empresa: ${dto.razon_social} | RUC: ${dto.ruc}`,
        new_value: { razon_social: dto.razon_social, ruc: dto.ruc, usuario: dto.usuario },
      });

      return {
        message: 'Empresa y usuario creado exitosamente',
        data: {
          empresa_id: result.empresa.id,
          usuario_id: result.usuario.id,
          razon_social: dto.razon_social,
          ruc: dto.ruc,
        },
      };
    } catch (error) {
      if (error instanceof NotFoundException) throw error;
      if (error.status) throw error; // ConflictException, etc.
      console.error('Error en registro:', error);
      const message = error instanceof Error ? error.message : 'Error desconocido';
      throw new BadRequestException(`Error al registrar: ${message}`);
    }
  }

  // ── GET /auth/me — Contexto completo del usuario autenticado ──
  async getMe(userId: string, empresaId: string) {
    // 1. Usuario + empresa + perfiles
    const user = await this.prisma.usuario.findUnique({
      where: { id: userId },
      select: {
        id: true,
        username: true,
        nombres: true,
        apellidos: true,
        email: true,
        is_superadmin: true,
        empresa_id: true,
        empresa_id_actual: true,
        empresa_rel: {
          select: {
            id: true,
            razon_social: true,
            nombre_fantasia: true,
            ruc: true,
            dv: true,
            type: true,
            sifen_activo: true,
          },
        },
        usuario_perfiles: {
          select: {
            perfiles: {
              select: {
                id: true,
                perfil: true,
                perfiles_privilegios: {
                  where: { active: true },
                  select: {
                    modulos: { select: { id: true, codigo: true } },
                    privilegios: {
                      select: {
                        id: true,
                        codigo: true,
                        submodulos: {
                          select: {
                            id: true,
                            codigo: true,
                            modulos: { select: { id: true, codigo: true } },
                          },
                        },
                      },
                    },
                  },
                },
              },
            },
          },
        },
      },
    });

    if (!user) throw new NotFoundException('Usuario no encontrado');

    // 2. Suscripción activa de la empresa actual (incluye EnGracia)
    const suscripcion = await this.prisma.suscripciones.findFirst({
      where: {
        empresa_id: empresaId,
        estado: { in: [EstadoSuscripcion.Activo, EstadoSuscripcion.EnGracia] },
      },
      select: {
        id: true,
        estado: true,
        fecha_inicio: true,
        fecha_fin: true,
        costo_mensual: true,
        planes: {
          select: {
            id: true,
            codigo: true,
            descripcion: true,
            max_user: true,
            max_producto: true,
            max_documentos: true,
          },
        },
        suscripcion_modulos: {
          where: { activo: true },
          select: {
            modulos: { select: { id: true, codigo: true, descripcion: true } },
            tipo: true,
          },
        },
      },
      orderBy: { created_at: 'desc' },
    });

    // 3. Construir mapa de módulos activos de la suscripción
    const modulosActivos: string[] = [];
    const modulosDetalle: Array<{
      codigo: string;
      descripcion: string;
      tipo: string | null;
    }> = [];

    if (suscripcion) {
      for (const sm of suscripcion.suscripcion_modulos) {
        if (sm.modulos?.codigo) {
          modulosActivos.push(sm.modulos.codigo);
          modulosDetalle.push({
            codigo: sm.modulos.codigo,
            descripcion: sm.modulos.descripcion ?? '',
            tipo: sm.tipo,
          });
        }
      }
    }

    // 4. Empresa activa (puede ser diferente a empresa_id original si hizo switch)
    let empresaActiva = user.empresa_rel;
    if (empresaId !== user.empresa_id) {
      const emp = await this.prisma.empresas.findUnique({
        where: { id: empresaId },
        select: {
          id: true,
          razon_social: true,
          nombre_fantasia: true,
          ruc: true,
          dv: true,
          type: true,
          sifen_activo: true,
        },
      });
      if (emp) empresaActiva = emp;
    }

    // 5. Construir mapa de permisos del usuario: { MODULO_CODIGO: ['LEER','CREAR',...] }
    // Holdings/resellers no tienen suscripción propia, así que no se filtran por modulosActivos
    const isHoldingOrReseller = empresaActiva?.type === 'holding' || empresaActiva?.type === 'reseller';

    // Set de submódulos activos (incluidos del plan + addons contratados - exclusiones).
    // Holdings/resellers bypasean este filtro al igual que el de módulos.
    const submodulosActivosIds = new Set<string>();
    if (!isHoldingOrReseller) {
      const submodulosActivos = await this.suscripcionesService.getSubmodulosActivos(empresaId);
      for (const s of submodulosActivos) submodulosActivosIds.add(s.id);
    }

    const permisos: Record<string, string[]> = {};
    for (const up of user.usuario_perfiles) {
      if (!up.perfiles) continue;
      for (const pp of up.perfiles.perfiles_privilegios) {
        // Derivar módulo desde la cadena privilegio→submódulo→módulo (nueva jerarquía).
        // Fallback al modulo_id legacy de la fila para compatibilidad con datos pre-refactor.
        const modCodigo =
          pp.privilegios?.submodulos?.modulos?.codigo ?? pp.modulos?.codigo;
        const submoduloId = pp.privilegios?.submodulos?.id;
        const privCodigo = pp.privilegios?.codigo;
        if (!modCodigo || !privCodigo) continue;
        // Solo incluir permisos de módulos activos en la suscripción (holdings/resellers bypasean)
        if (!isHoldingOrReseller && !modulosActivos.includes(modCodigo)) continue;
        // Solo incluir permisos de submódulos activos (función contratada/incluida en plan).
        // Holdings/resellers bypasean. Si el privilegio no cuelga de un submódulo (datos legacy),
        // se conserva el comportamiento previo y se acepta.
        if (!isHoldingOrReseller && submoduloId && !submodulosActivosIds.has(submoduloId)) continue;
        if (!permisos[modCodigo]) permisos[modCodigo] = [];
        if (!permisos[modCodigo].includes(privCodigo)) {
          permisos[modCodigo].push(privCodigo);
        }
      }
    }

    // Privilegios BLOQUEADOS por el plan (Capa 1 a nivel submódulo):
    // privilegios cuyo módulo está activo pero cuyo submódulo fue excluido del plan
    // (o no contratado como addon). El frontend los usa para gatear también a superAdmin,
    // que bypasea la Capa 2 (perfil) pero NO el plan. Holdings/resellers no aplican (bypasean).
    let privilegiosBloqueados: string[] = [];
    if (!isHoldingOrReseller && submodulosActivosIds.size > 0 && modulosActivos.length > 0) {
      const bloqueados = await this.prisma.privilegios.findMany({
        where: {
          submodulos: {
            modulos: { codigo: { in: modulosActivos } },
            id: { notIn: Array.from(submodulosActivosIds) },
          },
        },
        select: { codigo: true },
      });
      privilegiosBloqueados = bloqueados
        .map((p) => p.codigo)
        .filter((c): c is string => !!c);
    }

    // Estado de bloqueo de la suscripción (fuente de verdad para el frontend).
    // Holdings/resellers nunca quedan bloqueados (no tienen suscripción propia).
    const estadoSuscripcion = isHoldingOrReseller
      ? { activa: true, bloqueada: false, razon: null as string | null }
      : await this.suscripcionesService.verificarEstadoSuscripcion(empresaId);

    // Contexto de rubro/sucursal: default del selector de rubro del frontend, y
    // la sucursal real del usuario (antes se leía un `user.sucursal_id` que no
    // existe en el modelo). Ver docs/plan-rubros-multi-sucursal.md.
    const contextoRubros = await this.rubrosService.contextoDeUsuario(user.id, empresaId);

    return {
      user: {
        id: user.id,
        username: user.username,
        nombres: user.nombres,
        apellidos: user.apellidos,
        email: user.email,
        is_superadmin: user.is_superadmin,
        // El frontend viene leyendo `user.sucursal_id` en 14 lugares (defaults de
        // sucursal en Compras, POS, Inventario, Presupuestos...) pero el campo
        // nunca existió en el modelo: siempre caía al fallback `sucursales[0]`.
        // Se completa acá desde las asignaciones del usuario. `null` cuando no
        // tiene ninguna, que mantiene el fallback anterior.
        sucursal_id: contextoRubros.sucursal_default_id,
      },
      empresa: empresaActiva,
      suscripcion: suscripcion
        ? {
            id: suscripcion.id,
            estado: suscripcion.estado,
            fecha_inicio: suscripcion.fecha_inicio,
            fecha_fin: suscripcion.fecha_fin,
            costo_mensual: suscripcion.costo_mensual,
            plan: suscripcion.planes,
            bloqueada: estadoSuscripcion.bloqueada,
            razon_bloqueo: estadoSuscripcion.razon,
          }
        : {
            id: null,
            estado: null,
            fecha_inicio: null,
            fecha_fin: null,
            costo_mensual: null,
            plan: null,
            bloqueada: estadoSuscripcion.bloqueada,
            razon_bloqueo: estadoSuscripcion.razon,
          },
      modulos: modulosActivos,
      modulos_detalle: modulosDetalle,
      permisos,
      privilegios_bloqueados: privilegiosBloqueados,
      ...contextoRubros,
    };
  }

  async login(dto: LoginDto, options?: { refreshTtlSec?: number }) {
    const user = await this.prisma.usuario.findFirst({
      where: { username: dto.usuario, estado: EstadoUser.Activo },
      include: {
        empresa_rel: {
          select: { id: true, razon_social: true, ruc: true, type: true, sifen_activo: true },
        },
      },
    });
    if (!user || !user.password) {
      throw new UnauthorizedException('Credenciales inválidas');
    }
    const isValid = await bcrypt.compare(dto.password, user.password);
    if (!isValid) {
      throw new UnauthorizedException('Credenciales inválidas');
    }
    // Resetear empresa_id_actual para que el usuario inicie con su empresa original
    if (user.empresa_id_actual && user.empresa_id_actual !== user.empresa_id) {
      await this.prisma.usuario.update({
        where: { id: user.id },
        data: { empresa_id_actual: null },
      });
    }
    const { access_token, refresh_token, expires_in, refresh_expires_in } = await this.signTokens(
      user.id,
      user.username,
      user.empresa_id,
      options?.refreshTtlSec,
    );
    this.logger.log(`Login: usuario ${user.username} (${user.id}) empresa ${user.empresa_id}`, 'AuthService');
    await this.auditService.log({
      empresa_id: user.empresa_id,
      user_id: user.id,
      action: 'LOGIN',
      entity_type: 'usuario',
      entity_id: user.id,
      descripcion: `Inicio de sesión: ${user.username}`,
    });
    return {
      access_token,
      refresh_token,
      expires_in,
      refresh_expires_in,
      user: {
        id: user.id,
        username: user.username,
        nombres: user.nombres,
        apellidos: user.apellidos,
        empresa_id: user.empresa_id,
        empresa: user.empresa_rel,
      },
    };
  }

  private getRefreshConfig() {
    const secret = this.config.get<string>('JWT_REFRESH_SECRET') ?? 'dev-refresh-secret';
    const expiresSecEnv = Number(this.config.get('JWT_REFRESH_EXPIRES_IN'));
    const expiresInSec = Number.isFinite(expiresSecEnv) && expiresSecEnv > 0 ? expiresSecEnv : 60 * 60 * 24 * 7; // 7d
    return { secret, expiresInSec };
  }

  private async createRefreshToken(userId: string, ttlSecOverride?: number) {
    const { secret, expiresInSec: defaultExpiresInSec } = this.getRefreshConfig();
    const expiresInSec =
      Number.isFinite(ttlSecOverride) && (ttlSecOverride ?? 0) > 0
        ? (ttlSecOverride as number)
        : defaultExpiresInSec;
    const jti = randomUUID();
    const payload = { sub: userId, jti } as const;
    const token = await this.jwtService.signAsync(payload, {
      secret,
      expiresIn: expiresInSec,
    });
    // Guardar jti en Redis con TTL (ms)
    await this.redis.set(`refresh:${jti}`, userId, expiresInSec * 1000);
    return { token, jti, expiresInSec };
  }

  private async signTokens(
    userId: string,
    username: string | null,
    empresa_id: string,
    refreshTtlOverride?: number,
  ) {
    const payload = { sub: userId, username, empresa_id };
    // access token usa la config del JwtModule
    const access_token = await this.jwtService.signAsync(payload);
    // best-effort: leer expires del módulo no es directo; opcionalmente exponerlo via env también
    const expires_in = Number(this.config.get('JWT_EXPIRES_IN')) || 60 * 60 * 24;

    const { token: refresh_token, expiresInSec: refresh_expires_in } = await this.createRefreshToken(
      userId,
      refreshTtlOverride,
    );
    return { access_token, refresh_token, expires_in, refresh_expires_in };
  }

  async refresh(refresh_token: string, refreshTtlOverride?: number) {
    if (!refresh_token) throw new BadRequestException('Refresh token requerido');
    const { secret } = this.getRefreshConfig();
    let payload: { sub?: string; jti?: string };
    try {
      payload = await this.jwtService.verifyAsync(refresh_token, { secret });
    } catch {
      throw new UnauthorizedException('Refresh token inválido');
    }
    const jti: string | undefined = payload?.jti;
    const sub: string | undefined = payload?.sub;
    if (!jti || !sub) throw new UnauthorizedException('Refresh token inválido');

    const stored = await this.redis.get<string>(`refresh:${jti}`);
    if (!stored || stored !== sub) {
      throw new UnauthorizedException('Refresh token revocado o expirado');
    }
    // Rotación: invalidar jti anterior
    await this.redis.del(`refresh:${jti}`);

    // Emitir nuevos tokens (mantener empresa_id_actual si el usuario cambió de ambiente)
    const user = await this.prisma.usuario.findUnique({ where: { id: sub } });
    if (!user) throw new UnauthorizedException('Usuario no encontrado');
    const empresaId = user.empresa_id_actual || user.empresa_id;
    return this.signTokens(user.id, user.username, empresaId, refreshTtlOverride);
  }

  async logout(refresh_token: string) {
    if (!refresh_token) return { message: 'Logout exitoso' };
    const { secret } = this.getRefreshConfig();
    try {
      const payload: { sub?: string; jti?: string } = await this.jwtService.verifyAsync(refresh_token, { secret });
      const jti: string | undefined = payload?.jti;
      const sub: string | undefined = payload?.sub;
      if (jti) await this.redis.del(`refresh:${jti}`);
      if (sub) {
        const user = await this.prisma.usuario.findUnique({
          where: { id: sub },
          select: { id: true, username: true, empresa_id: true },
        });
        if (user) {
          this.logger.log(`Logout: usuario ${user.username} (${user.id})`, 'AuthService');
          await this.auditService.log({
            empresa_id: user.empresa_id,
            user_id: user.id,
            action: 'LOGOUT',
            entity_type: 'usuario',
            entity_id: user.id,
            descripcion: `Cierre de sesión: ${user.username}`,
          });
        }
      }
    } catch {
      // ignore
    }
    return { message: 'Logout exitoso' };
  }

  async switchEmpresa(userId: string, empresaDestinoId: string, empresaActualId: string) {
    // Verificar que la empresa destino existe
    const empresaDestino = await this.prisma.empresas.findUnique({
      where: { id: empresaDestinoId },
      select: { id: true, razon_social: true, active: true, parent_id: true },
    });
    if (!empresaDestino) throw new NotFoundException('Empresa no encontrada');
    if (!empresaDestino.active) throw new BadRequestException('La empresa destino no está activa');

    // Verificar que el usuario tiene acceso a la empresa destino
    const tieneAcceso = await this.verificarAccesoEmpresa(userId, empresaDestinoId, empresaActualId);
    if (!tieneAcceso) {
      throw new UnauthorizedException('No tienes permiso para acceder a esta empresa');
    }

    // Obtener datos del usuario
    const user = await this.prisma.usuario.findUnique({
      where: { id: userId },
    });
    if (!user) throw new NotFoundException('Usuario no encontrado');

    // Si vuelve a su empresa original, limpiar empresa_id_actual (null = empresa original)
    const nuevoValor = empresaDestinoId === user.empresa_id ? null : empresaDestinoId;
    await this.prisma.usuario.update({
      where: { id: userId },
      data: { empresa_id_actual: nuevoValor },
    });

    // Generar nuevos tokens con la empresa destino
    const tokens = await this.signTokens(user.id, user.username, empresaDestinoId);

    this.logger.log(`Switch empresa: usuario ${userId} -> empresa ${empresaDestinoId}`, 'AuthService');
    await this.auditService.log({
      empresa_id: empresaDestinoId,
      user_id: userId,
      action: 'SWITCH_EMPRESA',
      entity_type: 'empresa',
      entity_id: empresaDestinoId,
      descripcion: `Cambio de contexto de empresa a: ${empresaDestino.razon_social}`,
      old_value: { empresa_id: empresaActualId },
      new_value: { empresa_id: empresaDestinoId },
    });
    return {
      message: 'Cambio de empresa exitoso',
      empresa: {
        id: empresaDestino.id,
        razon_social: empresaDestino.razon_social,
      },
      ...tokens,
    };
  }

  private async verificarAccesoEmpresa(
    userId: string,
    empresaDestinoId: string,
    empresaActualId: string,
  ): Promise<boolean> {
    // 1. Si es la misma empresa, permitir
    if (empresaDestinoId === empresaActualId) return true;

    // 2. Obtener usuario con su empresa principal
    const usuario = await this.prisma.usuario.findUnique({
      where: { id: userId },
      select: {
        empresa_id: true,
        is_superadmin: true,
      },
    });
    if (!usuario) return false;

    // 3. Verificar si el usuario tiene acceso explícito en usuario_empresas
    const accesoExplicito = await this.prisma.usuario_empresas.findFirst({
      where: {
        usuario_id: userId,
        empresa_id: empresaDestinoId,
        active: true,
      },
    });
    if (accesoExplicito) return true;

    // 4. Solo superAdmin puede acceder automaticamente a empresas hijas de SU empresa
    if (usuario.is_superadmin) {
      const empresaDestino = await this.prisma.empresas.findUnique({
        where: { id: empresaDestinoId },
        select: { parent_id: true },
      });
      // Solo si la empresa destino es hija de la empresa PRINCIPAL del usuario
      if (empresaDestino?.parent_id === usuario.empresa_id) return true;
    }

    return false;
  }

  async getEmpresasAccesibles(userId: string, empresaActualId: string) {
    // Campos completos para mostrar en la UI
    const empresaSelect = {
      id: true,
      razon_social: true,
      nombre_fantasia: true,
      ruc: true,
      dv: true,
      // Identidad visual de la empresa: la usa el home para mostrar su logo
      // junto a la razón social y el RUC.
      logo: true,
      type: true,
      email: true,
      celular: true,
      parent_id: true,
      active: true,
      sifen_activo: true,
      estado_empresa: true,
      tipo_contribuyente_empresas: {
        select: {
          codigo: true,
          descripcion: true,
        },
      },
      empresas: {
        select: {
          id: true,
          razon_social: true,
        },
      },
    };

    // Obtener usuario con su empresa
    const usuario = await this.prisma.usuario.findUnique({
      where: { id: userId },
      select: {
        empresa_id: true,
        is_superadmin: true,
        empresa_rel: { select: empresaSelect },
      },
    });
    if (!usuario) throw new NotFoundException('Usuario no encontrado');
    // Verificar si es superAdmin
    const esSuperAdmin = usuario.is_superadmin === true;

    const empresasMap = new Map();

    // Si es superAdmin: ve su empresa original + todos sus descendientes
    // usuario.empresa_id es SIEMPRE la empresa original (no cambia al hacer switchEmpresa)
    if (esSuperAdmin) {
      empresasMap.set(usuario.empresa_id, {
        ...usuario.empresa_rel,
        tipo_acceso: 'principal',
        rol: 'superadmin',
      });

      // BFS descendente desde la empresa original del usuario
      const idsABuscar = [usuario.empresa_id];
      const visitados = new Set<string>([usuario.empresa_id]);

      while (idsABuscar.length > 0) {
        const padreId = idsABuscar.shift()!;
        const hijas = await this.prisma.empresas.findMany({
          where: { parent_id: padreId, active: true },
          select: empresaSelect,
        });
        for (const e of hijas) {
          if (!visitados.has(e.id)) {
            visitados.add(e.id);
            empresasMap.set(e.id, { ...e, tipo_acceso: 'hija', rol: 'admin' });
            idsABuscar.push(e.id);
          }
        }
      }
    }

    // Obtener empresas con acceso explícito (para TODOS los usuarios, incluyendo superAdmin)
    const accesosExplicitos = await this.prisma.usuario_empresas.findMany({
      where: {
        usuario_id: userId,
        active: true,
      },
      select: {
        rol: true,
        empresa: { select: empresaSelect },
      },
    });

    // Si NO es superAdmin y tiene accesos explícitos, SOLO ve esas empresas
    // Si NO es superAdmin y NO tiene accesos explícitos, solo ve su empresa principal
    if (!esSuperAdmin) {
      if (accesosExplicitos.length > 0) {
        // Solo ve las empresas asignadas explícitamente
        accesosExplicitos.forEach((a) => {
          if (a.empresa.active) {
            empresasMap.set(a.empresa.id, {
              ...a.empresa,
              tipo_acceso: 'asignada',
              rol: a.rol,
            });
          }
        });
      } else {
        // Sin accesos explícitos, solo ve su empresa principal
        empresasMap.set(usuario.empresa_id, {
          ...usuario.empresa_rel,
          tipo_acceso: 'principal',
          rol: 'admin',
        });
      }
    } else {
      // SuperAdmin también puede tener accesos explícitos adicionales
      accesosExplicitos.forEach((a) => {
        if (a.empresa.active && !empresasMap.has(a.empresa.id)) {
          empresasMap.set(a.empresa.id, {
            ...a.empresa,
            tipo_acceso: 'asignada',
            rol: a.rol,
          });
        }
      });
    }

    return {
      empresa_actual: empresaActualId,
      es_superadmin: esSuperAdmin,
      empresas: Array.from(empresasMap.values()),
    };
  }
}
