import { ConflictException, Injectable, NotFoundException, Logger } from '@nestjs/common';
import { AuditService } from 'src/audit/audit.service';
import { PrismaService } from '../prisma/prisma.service';

export interface CreateNumeracionDto {
  empresa_id: string;
  punto_expedicion_id: string;
  tipo_documento_id: string;
  timbrado_id?: string;
  numero_inicial?: number;
  numero_final?: number;
}

export interface UpdateNumeracionDto {
  numero_actual?: number;
  numero_inicial?: number;
  numero_final?: number;
  timbrado_id?: string;
  active?: boolean;
}

@Injectable()
export class NumeracionesService {
  private readonly logger = new Logger(NumeracionesService.name);
  constructor(
    private readonly prisma: PrismaService,
    private readonly auditService: AuditService,
  ) {}

  async create(createDto: CreateNumeracionDto, user_id?: string) {
    const timbrado_id = createDto.timbrado_id || null;

    // Buscar si ya existe la misma combinación (activa o inactiva)
    const exists = await this.prisma.numeraciones_documento.findFirst({
      where: {
        punto_expedicion_id: createDto.punto_expedicion_id,
        tipo_documento_id: createDto.tipo_documento_id,
        timbrado_id,
      },
    });

    if (exists && exists.active) {
      throw new ConflictException(
        'Ya existe una numeración activa para este punto de expedición, tipo de documento y timbrado',
      );
    }

    // Si existe una inactiva con la misma combinación, reactivarla con los nuevos datos
    if (exists && !exists.active) {
      const reactivated = await this.prisma.numeraciones_documento.update({
        where: { id: exists.id },
        data: {
          active: true,
          numero_actual: createDto.numero_inicial || 1,
          numero_inicial: createDto.numero_inicial || 1,
          numero_final: createDto.numero_final || null,
        },
        include: {
          tipo_documento: true,
          timbrado: true,
          punto_expedicion: true,
        },
      });

      this.logger.log(
        `Numeración reactivada (${reactivated.id}) para empresa ${createDto.empresa_id}`,
        'NumeracionesService',
      );
      await this.auditService.log({
        empresa_id: createDto.empresa_id,
        user_id,
        action: 'UPDATE',
        entity_type: 'numeracion_documento',
        entity_id: reactivated.id,
        descripcion: `Numeración reactivada | Tipo doc: ${reactivated.tipo_documento?.descripcion ?? createDto.tipo_documento_id} | Inicial: ${reactivated.numero_inicial}`,
        old_value: { active: false, numero_actual: exists.numero_actual, numero_inicial: exists.numero_inicial },
        new_value: {
          active: true,
          numero_actual: reactivated.numero_actual,
          numero_inicial: reactivated.numero_inicial,
        },
      });
      return { message: 'Numeración reactivada exitosamente', data: reactivated };
    }

    const numeracion = await this.prisma.numeraciones_documento.create({
      data: {
        empresa_id: createDto.empresa_id,
        punto_expedicion_id: createDto.punto_expedicion_id,
        tipo_documento_id: createDto.tipo_documento_id,
        timbrado_id,
        numero_actual: createDto.numero_inicial || 1,
        numero_inicial: createDto.numero_inicial || 1,
        numero_final: createDto.numero_final || null,
      },
      include: {
        tipo_documento: true,
        timbrado: true,
        punto_expedicion: true,
      },
    });

    this.logger.log(`Numeración creada (${numeracion.id}) para empresa ${createDto.empresa_id}`, 'NumeracionesService');
    await this.auditService.log({
      empresa_id: createDto.empresa_id,
      user_id,
      action: 'CREATE',
      entity_type: 'numeracion_documento',
      entity_id: numeracion.id,
      descripcion: `Numeración creada | Tipo doc: ${numeracion.tipo_documento?.descripcion ?? createDto.tipo_documento_id} | Inicial: ${numeracion.numero_inicial}`,
      new_value: {
        empresa_id: createDto.empresa_id,
        punto_expedicion_id: createDto.punto_expedicion_id,
        tipo_documento_id: createDto.tipo_documento_id,
        numero_inicial: numeracion.numero_inicial,
        numero_final: numeracion.numero_final,
      },
    });
    return { message: 'Numeración creada exitosamente', data: numeracion };
  }

  async findAllByPuntoExpedicion(punto_expedicion_id: string) {
    const numeraciones = await this.prisma.numeraciones_documento.findMany({
      where: { punto_expedicion_id },
      include: {
        tipo_documento: true,
        timbrado: true,
      },
      orderBy: { created_at: 'desc' },
    });

    return { message: 'Numeraciones obtenidas', data: numeraciones };
  }

  /**
   * Numeraciones de una sucursal.
   *
   * `soloActivos` lo usa el lookup operativo (emisión de documentos): ahí no
   * deben aparecer puntos de expedición ni numeraciones dados de baja, porque
   * el POS los ofrecía igual y se podía facturar contra un punto inactivo.
   * La pantalla de configuración llama sin el flag, porque necesita listar los
   * inactivos para poder reactivarlos.
   */
  async findAllBySucursal(sucursal_id: string, tipo_documento_id?: string, soloActivos = false) {
    // Validar que sucursal_id sea un UUID válido
    const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
    if (!sucursal_id || !uuidRegex.test(sucursal_id)) {
      return [];
    }

    // Obtener puntos de expedición de la sucursal
    const puntosExpedicion = await this.prisma.empresas_puntos_expedicion.findMany({
      where: { sucursal_id, ...(soloActivos ? { active: true } : {}) },
      select: { id: true, cajas: true },
    });

    if (puntosExpedicion.length === 0) {
      return [];
    }

    const puntoIds = puntosExpedicion.map((p) => p.id);

    const whereClause: Record<string, unknown> = {
      punto_expedicion_id: { in: puntoIds },
      ...(soloActivos ? { active: true } : {}),
    };

    // Filtrar por tipo_documento_id si se especifica (UUID del tipo de documento)
    if (tipo_documento_id && uuidRegex.test(tipo_documento_id)) {
      whereClause.tipo_documento_id = tipo_documento_id;
    }

    const numeraciones = await this.prisma.numeraciones_documento.findMany({
      where: whereClause,
      include: {
        tipo_documento: true,
        timbrado: true,
        punto_expedicion: {
          include: {
            cajas: true,
            empresas_sucursales: true,
          },
        },
      },
      // Default primero — el frontend preselecciona el primero del array.
      orderBy: [{ es_default: 'desc' }, { created_at: 'desc' }],
    });

    return numeraciones;
  }

  async setDefault(id: string, user_id?: string) {
    const num = await this.prisma.numeraciones_documento.findUnique({ where: { id } });
    if (!num) throw new NotFoundException('Numeración no encontrada');
    if (!num.active) throw new ConflictException('No se puede marcar como predeterminada una numeración inactiva');

    await this.prisma.$transaction([
      this.prisma.numeraciones_documento.updateMany({
        where: {
          punto_expedicion_id: num.punto_expedicion_id,
          tipo_documento_id: num.tipo_documento_id,
          es_default: true,
          NOT: { id },
        },
        data: { es_default: false },
      }),
      this.prisma.numeraciones_documento.update({
        where: { id },
        data: { es_default: true },
      }),
    ]);

    await this.auditService.log({
      empresa_id: num.empresa_id,
      user_id,
      action: 'UPDATE',
      entity_type: 'numeracion',
      entity_id: id,
      descripcion: `Numeración marcada como predeterminada: ${id}`,
      old_value: { es_default: num.es_default },
      new_value: { es_default: true },
    });

    return { message: 'Numeración marcada como predeterminada', data: { id, es_default: true } };
  }

  async unsetDefault(id: string, user_id?: string) {
    const num = await this.prisma.numeraciones_documento.findUnique({ where: { id } });
    if (!num) throw new NotFoundException('Numeración no encontrada');
    if (!num.es_default) {
      return { message: 'No estaba marcada como predeterminada', data: { id, es_default: false } };
    }

    await this.prisma.numeraciones_documento.update({
      where: { id },
      data: { es_default: false },
    });

    await this.auditService.log({
      empresa_id: num.empresa_id,
      user_id,
      action: 'UPDATE',
      entity_type: 'numeracion',
      entity_id: id,
      descripcion: `Numeración desmarcada como predeterminada: ${id}`,
      old_value: { es_default: true },
      new_value: { es_default: false },
    });

    return { message: 'Numeración desmarcada como predeterminada', data: { id, es_default: false } };
  }

  async findOne(id: string) {
    const numeracion = await this.prisma.numeraciones_documento.findUnique({
      where: { id },
      include: {
        tipo_documento: true,
        timbrado: true,
        punto_expedicion: {
          include: {
            empresas_sucursales: true,
            cajas: true,
          },
        },
      },
    });

    if (!numeracion) throw new NotFoundException('Numeración no encontrada');

    return { message: 'Numeración encontrada', data: numeracion };
  }

  async update(id: string, updateDto: UpdateNumeracionDto, user_id?: string) {
    const exists = await this.prisma.numeraciones_documento.findUnique({ where: { id } });
    if (!exists) throw new NotFoundException('Numeración no encontrada');

    const numeracion = await this.prisma.numeraciones_documento.update({
      where: { id },
      data: updateDto,
      include: {
        tipo_documento: true,
        timbrado: true,
      },
    });

    this.logger.log(`Numeración actualizada (${id})`, 'NumeracionesService');
    await this.auditService.log({
      empresa_id: exists.empresa_id,
      user_id,
      action: 'UPDATE',
      entity_type: 'numeracion_documento',
      entity_id: id,
      descripcion: `Numeración actualizada`,
      old_value: { numero_actual: exists.numero_actual, numero_final: exists.numero_final, active: exists.active },
      new_value: updateDto as Record<string, unknown>,
    });
    return { message: 'Numeración actualizada', data: numeracion };
  }

  async getNextNumber(id: string) {
    const numeracion = await this.prisma.numeraciones_documento.findUnique({
      where: { id },
      include: {
        tipo_documento: true,
        punto_expedicion: {
          include: {
            empresas_sucursales: true,
          },
        },
      },
    });

    if (!numeracion) throw new NotFoundException('Numeración no encontrada');
    if (!numeracion.active) throw new ConflictException('La numeración está inactiva');

    // Verificar si se alcanzó el límite
    if (numeracion.numero_final && numeracion.numero_actual >= numeracion.numero_final) {
      throw new ConflictException('Se alcanzó el número final de la numeración');
    }

    return {
      message: 'Próximo número obtenido',
      data: {
        numero_actual: numeracion.numero_actual,
        numero_formateado: String(numeracion.numero_actual).padStart(7, '0'),
        punto_establecimiento: numeracion.punto_expedicion?.empresas_sucursales?.punto_establecimiento,
        punto_expedicion: numeracion.punto_expedicion?.punto_expedicion,
        tipo_documento: numeracion.tipo_documento,
      },
    };
  }

  async incrementNumber(id: string) {
    const numeracion = await this.prisma.numeraciones_documento.findUnique({ where: { id } });

    if (!numeracion) throw new NotFoundException('Numeración no encontrada');
    if (!numeracion.active) throw new ConflictException('La numeración está inactiva');

    if (numeracion.numero_final && numeracion.numero_actual >= numeracion.numero_final) {
      throw new ConflictException('Se alcanzó el número final de la numeración');
    }

    const updated = await this.prisma.numeraciones_documento.update({
      where: { id },
      data: { numero_actual: numeracion.numero_actual + 1 },
    });

    return { message: 'Número incrementado', data: updated };
  }

  /**
   * Reserva el siguiente número de forma atómica para evitar duplicados.
   * Usa una transacción con bloqueo pesimista para garantizar unicidad.
   * Retorna el número reservado ya incrementado en la base de datos.
   */
  async reserveNextNumber(id: string) {
    return await this.prisma.$transaction(async (tx) => {
      // Bloqueo pesimista: SELECT FOR UPDATE
      const numeraciones = await tx.$queryRaw<
        Array<{
          id: string;
          numero_actual: number;
          numero_final: number | null;
          active: boolean;
          punto_expedicion_id: string;
        }>
      >`SELECT id, numero_actual, numero_final, active, punto_expedicion_id 
        FROM numeraciones_documento 
        WHERE id = ${id}::uuid 
        FOR UPDATE`;

      if (numeraciones.length === 0) {
        throw new NotFoundException('Numeración no encontrada');
      }

      const numeracion = numeraciones[0];

      if (!numeracion.active) {
        throw new ConflictException('La numeración está inactiva');
      }

      if (numeracion.numero_final && numeracion.numero_actual >= numeracion.numero_final) {
        throw new ConflictException('Se alcanzó el número final de la numeración');
      }

      // El número a usar es el actual
      const numeroReservado = numeracion.numero_actual;

      // Incrementar atómicamente
      await tx.numeraciones_documento.update({
        where: { id },
        data: { numero_actual: numeroReservado + 1 },
      });

      // Obtener datos adicionales para la respuesta
      const numeracionCompleta = await tx.numeraciones_documento.findUnique({
        where: { id },
        include: {
          tipo_documento: true,
          punto_expedicion: {
            include: {
              empresas_sucursales: true,
            },
          },
        },
      });

      return {
        message: 'Número reservado exitosamente',
        data: {
          numero_reservado: numeroReservado,
          numero_formateado: String(numeroReservado).padStart(7, '0'),
          punto_establecimiento: numeracionCompleta?.punto_expedicion?.empresas_sucursales?.punto_establecimiento,
          punto_expedicion: numeracionCompleta?.punto_expedicion?.punto_expedicion,
          tipo_documento: numeracionCompleta?.tipo_documento,
          siguiente_disponible: numeroReservado + 1,
        },
      };
    });
  }

  async toggleStatus(id: string, user_id?: string) {
    const numeracion = await this.prisma.numeraciones_documento.findUnique({ where: { id } });
    if (!numeracion) throw new NotFoundException('Numeración no encontrada');

    const updated = await this.prisma.numeraciones_documento.update({
      where: { id },
      data: { active: !numeracion.active },
    });

    const action = updated.active ? 'ACTIVATE' : 'DEACTIVATE';
    this.logger.log(`Numeración (${id}) ${updated.active ? 'activada' : 'desactivada'}`, 'NumeracionesService');
    await this.auditService.log({
      empresa_id: numeracion.empresa_id,
      user_id,
      action,
      entity_type: 'numeracion_documento',
      entity_id: id,
      descripcion: `Numeración ${updated.active ? 'activada' : 'desactivada'}`,
      old_value: { active: numeracion.active },
      new_value: { active: updated.active },
    });
    return { message: 'Estado actualizado', data: updated };
  }

  async remove(id: string, user_id?: string) {
    const numeracion = await this.prisma.numeraciones_documento.findUnique({ where: { id } });
    if (!numeracion) throw new NotFoundException('Numeración no encontrada');

    await this.prisma.numeraciones_documento.delete({ where: { id } });
    this.logger.log(`Numeración eliminada (${id})`, 'NumeracionesService');
    await this.auditService.log({
      empresa_id: numeracion.empresa_id,
      user_id,
      action: 'DELETE',
      entity_type: 'numeracion_documento',
      entity_id: id,
      descripcion: `Numeración eliminada | Punto expedición: ${numeracion.punto_expedicion_id}`,
      old_value: {
        punto_expedicion_id: numeracion.punto_expedicion_id,
        tipo_documento_id: numeracion.tipo_documento_id,
        numero_actual: numeracion.numero_actual,
      },
    });
    return { message: 'Numeración eliminada' };
  }
}
