import { Injectable, Logger } from '@nestjs/common';
import { formatDateTimeResponse } from 'src/utils/utilidades';
import { PrismaService } from '../prisma/prisma.service';

export type EmpresaType = string | null;

export interface AuditLogEntry {
  empresa_id?: string;
  user_id?: string;
  action: string;
  entity_type: string;
  entity_id?: string;
  old_value?: Record<string, unknown>;
  new_value?: Record<string, unknown>;
  ip_address?: string;
  user_agent?: string;
  descripcion?: string;
  caja_id?: string;
  sesion_caja_id?: string;
  terminal_id?: string;
}

@Injectable()
export class AuditService {
  private readonly logger = new Logger(AuditService.name);

  constructor(private readonly prisma: PrismaService) {}

  /** Recorta un valor al límite de la columna VarChar (la auditoría es best-effort). */
  private trunc(v: string | null | undefined, max: number): string | null {
    if (v == null) return null;
    return v.length > max ? v.slice(0, max) : v;
  }

  async log(entry: AuditLogEntry): Promise<void> {
    try {
      await this.prisma.audit_logs.create({
        data: {
          empresa_id: entry.empresa_id ?? null,
          user_id: entry.user_id ?? null,
          // Recortamos los String a su límite de columna: mensajes de error largos
          // (SIFEN/contabilidad) o un x-forwarded-for encadenado desbordaban el VarChar.
          action: this.trunc(entry.action, 30) ?? entry.action,
          entity_type: this.trunc(entry.entity_type, 60) ?? entry.entity_type,
          entity_id: entry.entity_id ?? null,
          old_value: entry.old_value ? JSON.parse(JSON.stringify(entry.old_value)) : undefined,
          new_value: entry.new_value ? JSON.parse(JSON.stringify(entry.new_value)) : undefined,
          ip_address: this.trunc(entry.ip_address, 45),
          user_agent: this.trunc(entry.user_agent, 500),
          descripcion: this.trunc(entry.descripcion, 255),
          caja_id: entry.caja_id ?? null,
          sesion_caja_id: entry.sesion_caja_id ?? null,
          terminal_id: this.trunc(entry.terminal_id, 100),
        },
      });
    } catch (err) {
      // No lanzamos error para no interrumpir el flujo principal
      this.logger.error(
        `Error al registrar auditoría: ${err instanceof Error ? err.message : 'unknown'}`,
        'AuditService',
      );
    }
  }

  getByEntity(entityType: string, entityId: string, limit = 50) {
    return this.prisma.audit_logs.findMany({
      where: { entity_type: entityType, entity_id: entityId },
      orderBy: { created_at: 'desc' },
      take: limit,
    });
  }

  getByEmpresa(empresaId: string, filtros?: { action?: string; entity_type?: string; limit?: number }) {
    const take = Math.min(filtros?.limit ?? 50, 200);
    return this.prisma.audit_logs.findMany({
      where: {
        empresa_id: empresaId,
        ...(filtros?.action && { action: filtros.action }),
        ...(filtros?.entity_type && { entity_type: filtros.entity_type }),
      },
      orderBy: { created_at: 'desc' },
      take,
    });
  }

  /**
   * Resuelve el conjunto de empresa_ids visibles según la jerarquía del tipo de empresa.
   * - holding/reseller: ven sus propios logs + todos los de sus subsidiarias (parent_id = empresaId)
   * - subsidiario/null: solo ven sus propios logs
   */
  private async resolverEmpresasVisibles(empresaId: string, empresaType: EmpresaType): Promise<string[]> {
    if (empresaType === 'holding' || empresaType === 'reseller') {
      const subsidiarias = await this.prisma.empresas.findMany({
        where: { parent_id: empresaId, active: true },
        select: { id: true },
      });
      const ids = [empresaId, ...subsidiarias.map((e) => e.id)];
      return ids;
    }
    return [empresaId];
  }

  /**
   * Convierte una fecha string YYYY-MM-DD al inicio del día en UTC-4 (Paraguay).
   * Para evitar el problema de timezone, se trata la fecha como local.
   */
  private parseFechaDesde(fecha: string): Date {
    const [y, m, d] = fecha.split('-').map(Number);
    // Inicio del día en UTC-4: sumar 4 horas => UTC
    return new Date(Date.UTC(y, m - 1, d, 4, 0, 0, 0));
  }

  private parseFechaHasta(fecha: string): Date {
    const [y, m, d] = fecha.split('-').map(Number);
    // Fin del día en UTC-4: día siguiente a las 04:00 UTC - 1ms
    return new Date(Date.UTC(y, m - 1, d + 1, 3, 59, 59, 999));
  }

  /**
   * Consulta paginada de audit logs con filtros avanzados.
   */
  async getPaginated(
    empresaId: string,
    empresaType: EmpresaType,
    filtros?: {
      action?: string;
      entity_type?: string;
      user_id?: string;
      search?: string;
      fecha_desde?: string;
      fecha_hasta?: string;
      empresa_filtro_id?: string;
      caja_id?: string;
      sesion_caja_id?: string;
      terminal_id?: string;
      page?: number;
      limit?: number;
    },
  ) {
    const page = Math.max(filtros?.page ?? 1, 1);
    const take = Math.min(Math.max(filtros?.limit ?? 25, 1), 100);
    const skip = (page - 1) * take;

    // Resolver qué empresas puede ver este usuario
    const empresasVisibles = await this.resolverEmpresasVisibles(empresaId, empresaType);

    // Si el usuario aplicó un filtro por empresa específica, validar que tenga acceso
    const empresaFiltroId = filtros?.empresa_filtro_id;
    let empresaWhereIds: string[];
    if (empresaFiltroId && empresasVisibles.includes(empresaFiltroId)) {
      empresaWhereIds = [empresaFiltroId];
    } else {
      empresaWhereIds = empresasVisibles;
    }

    const where: any = { empresa_id: { in: empresaWhereIds } };
    if (filtros?.action) where.action = filtros.action;
    if (filtros?.entity_type) where.entity_type = filtros.entity_type;
    if (filtros?.user_id) where.user_id = filtros.user_id;
    if (filtros?.caja_id) where.caja_id = filtros.caja_id;
    if (filtros?.sesion_caja_id) where.sesion_caja_id = filtros.sesion_caja_id;
    if (filtros?.terminal_id) where.terminal_id = { contains: filtros.terminal_id, mode: 'insensitive' };
    if (filtros?.search) {
      // Búsqueda OR en descripción y entity_type para mayor cobertura
      where.OR = [
        { descripcion: { contains: filtros.search, mode: 'insensitive' } },
        { entity_type: { contains: filtros.search, mode: 'insensitive' } },
      ];
    }
    if (filtros?.fecha_desde || filtros?.fecha_hasta) {
      where.created_at = {};
      if (filtros.fecha_desde) where.created_at.gte = this.parseFechaDesde(filtros.fecha_desde);
      if (filtros.fecha_hasta) where.created_at.lte = this.parseFechaHasta(filtros.fecha_hasta);
    }

    const [data, total] = await Promise.all([
      this.prisma.audit_logs.findMany({
        where,
        orderBy: { created_at: 'desc' },
        skip,
        take,
        // Solo los campos necesarios para la lista (evitar traer JSON pesados)
        select: {
          id: true,
          empresa_id: true,
          user_id: true,
          action: true,
          entity_type: true,
          entity_id: true,
          descripcion: true,
          old_value: true,
          new_value: true,
          ip_address: true,
          caja_id: true,
          sesion_caja_id: true,
          terminal_id: true,
          created_at: true,
        },
      }),
      this.prisma.audit_logs.count({ where }),
    ]);

    // Resolver nombres de usuarios y empresas en batch
    const userIds = [...new Set(data.map((d) => d.user_id).filter(Boolean))] as string[];
    const empresaIds = [...new Set(data.map((d) => d.empresa_id).filter(Boolean))] as string[];

    const [usuarios, empresas] = await Promise.all([
      userIds.length > 0
        ? this.prisma.usuario.findMany({
            where: { id: { in: userIds } },
            select: { id: true, nombres: true, apellidos: true, email: true },
          })
        : Promise.resolve(
            [] as { id: string; nombres: string | null; apellidos: string | null; email: string | null }[],
          ),
      empresaIds.length > 0
        ? this.prisma.empresas.findMany({
            where: { id: { in: empresaIds } },
            select: { id: true, razon_social: true, ruc: true },
          })
        : Promise.resolve([] as { id: string; razon_social: string | null; ruc: string }[]),
    ]);

    const userMap = new Map(usuarios.map((u) => [u.id, u]));
    const empresaMap = new Map(empresas.map((e) => [e.id, e]));

    return {
      data: data.map((item) => {
        const user = item.user_id ? userMap.get(item.user_id) : null;
        const empresa = item.empresa_id ? empresaMap.get(item.empresa_id) : null;
        return {
          ...item,
          created_at: formatDateTimeResponse(item.created_at),
          user_nombre: user ? `${user.nombres ?? ''} ${user.apellidos ?? ''}`.trim() || user.email : null,
          empresa_nombre: empresa?.razon_social ?? null,
          empresa_ruc: empresa?.ruc ?? null,
        };
      }),
      pagination: {
        page,
        limit: take,
        total,
        totalPages: Math.ceil(total / take),
      },
    };
  }

  /**
   * Obtener valores únicos de action y entity_type para filtros del frontend.
   * Usa groupBy que es más eficiente que findMany+distinct en tablas grandes.
   */
  async getFilterOptions(empresaId: string, empresaType: EmpresaType) {
    const empresasVisibles = await this.resolverEmpresasVisibles(empresaId, empresaType);
    const whereEmpresa = { empresa_id: { in: empresasVisibles } };

    const [actions, entityTypes, subsidiarias] = await Promise.all([
      this.prisma.audit_logs.groupBy({
        by: ['action'],
        where: whereEmpresa,
        orderBy: { action: 'asc' },
      }),
      this.prisma.audit_logs.groupBy({
        by: ['entity_type'],
        where: whereEmpresa,
        orderBy: { entity_type: 'asc' },
      }),
      empresaType === 'holding' || empresaType === 'reseller'
        ? this.prisma.empresas.findMany({
            where: { parent_id: empresaId, active: true },
            select: { id: true, razon_social: true },
            orderBy: { razon_social: 'asc' },
          })
        : Promise.resolve([]),
    ]);

    return {
      actions: actions.map((a) => a.action),
      entity_types: entityTypes.map((e) => e.entity_type),
      empresas: subsidiarias.length > 0 ? [{ id: empresaId, razon_social: 'Mi empresa' }, ...subsidiarias] : [],
    };
  }
}
