import { ConflictException, Injectable, NotFoundException, Logger } from '@nestjs/common';
import { AuditService } from 'src/audit/audit.service';
import { EmpresasService } from '../empresas/empresas.service';
import { PrismaService } from '../prisma/prisma.service';
import { SucursalesService } from '../sucursales/sucursales.service';
import { CreatePuntoExpedicionDto } from './dto/create-punto-expedicione.dto';
import { UpdatePuntoExpedicioneDto } from './dto/update-punto-expedicione.dto';

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

  async create(createPuntoExpedicioneDto: CreatePuntoExpedicionDto, user_id?: string) {
    const { empresa_id, sucursal_id, caja_id, punto_expedicion, num_inicial, descripcion, eslogan, usuario } =
      createPuntoExpedicioneDto;

    const empresa = await this.empresasService.findOne(empresa_id);
    const sucursal = await this.sucursalesService.findOne(sucursal_id);

    if (!empresa) throw new NotFoundException('Empresa no encontrada');
    if (!sucursal) throw new NotFoundException('Sucursal no encontrada');

    // La unicidad es por sucursal y NO mira `active` a propósito: el punto forma
    // parte del número de comprobante (establecimiento-punto-correlativo), así
    // que reutilizar el código de uno desactivado haría que el nuevo repita
    // números que el viejo ya emitió. Para liberar el código hay que borrarlo,
    // lo que sólo se permite si nunca se usó (ver `remove`).
    const puntoExpedicionExiste = await this.prisma.empresas_puntos_expedicion.findFirst({
      where: {
        empresa_id,
        sucursal_id,
        punto_expedicion,
      },
      include: { cajas: { select: { descripcion: true } } },
    });

    if (puntoExpedicionExiste) {
      const caja = puntoExpedicionExiste.cajas?.descripcion;
      const estado = puntoExpedicionExiste.active ? '' : ' (inactivo)';
      throw new ConflictException(
        `El punto ${punto_expedicion} ya existe en esta sucursal${caja ? `, en la caja "${caja}"` : ''}${estado}. ` +
          'Eliminalo si no tiene movimientos, o usá otro código.',
      );
    }

    const nuevoPuntoExpedicion = await this.prisma.empresas_puntos_expedicion.create({
      data: {
        empresa_id,
        sucursal_id,
        caja_id,
        punto_expedicion,
        num_inicial,
        descripcion,
        eslogan,
        usuario,
      },
      include: {
        empresas_sucursales: true,
        cajas: true,
      },
    });

    this.logger.log(
      `Punto de expedición creado: ${punto_expedicion} (${nuevoPuntoExpedicion.id})`,
      'PuntoExpedicionesService',
    );
    await this.auditService.log({
      empresa_id,
      user_id,
      action: 'CREATE',
      entity_type: 'punto_expedicion',
      entity_id: nuevoPuntoExpedicion.id,
      descripcion: `Punto de expedición creado: ${punto_expedicion} | Empresa: ${empresa_id} | Sucursal: ${sucursal_id}`,
      new_value: { empresa_id, sucursal_id, caja_id, punto_expedicion, num_inicial },
    });
    return { message: 'Punto de expedicion creado exitosamente', data: nuevoPuntoExpedicion };
  }

  async findAll(page: number, limit: number, empresa_id?: string, sucursal_id?: string, caja_id?: string) {
    const take = Math.max(1, Math.min(100, Number(limit) || 10));
    const currentPage = Math.max(1, Number(page) || 1);
    const skip = (currentPage - 1) * take;

    const where = {
      ...(empresa_id && { empresa_id }),
      ...(sucursal_id && { sucursal_id }),
      ...(caja_id && { caja_id }),
    };

    const puntoExpediciones = await this.prisma.empresas_puntos_expedicion.findMany({
      where,
      skip,
      take,
      include: {
        empresas_sucursales: true,
        cajas: true,
        numeraciones_documento: {
          include: { tipo_documento: true },
        },
      },
      orderBy: [{ es_default: 'desc' }, { created_at: 'desc' }],
    });

    const total = await this.prisma.empresas_puntos_expedicion.count({ where });

    return {
      items: puntoExpediciones,
      total,
      page: currentPage,
      limit: take,
      lastPage: Math.max(1, Math.ceil(total / take) || 1),
    };
  }

  async findByCaja(caja_id: string) {
    const puntoExpediciones = await this.prisma.empresas_puntos_expedicion.findMany({
      where: { caja_id },
      include: {
        empresas_sucursales: true,
        cajas: true,
        numeraciones_documento: {
          include: { tipo_documento: true },
        },
      },
      orderBy: [{ es_default: 'desc' }, { punto_expedicion: 'asc' }],
    });

    return { message: 'Puntos de expedición obtenidos', data: puntoExpediciones };
  }

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

    if (!puntoExpedicion) throw new NotFoundException('Punto de expedicion no encontrado');

    return { message: 'Punto de expedicion encontrado exitosamente', data: puntoExpedicion };
  }

  async update(id: string, updatePuntoExpedicioneDto: UpdatePuntoExpedicioneDto, user_id?: string) {
    const puntoExpedicionFind = await this.prisma.empresas_puntos_expedicion.findUnique({ where: { id } });
    if (!puntoExpedicionFind) throw new NotFoundException('Punto de expedicion no encontrado');

    if (updatePuntoExpedicioneDto.punto_expedicion) {
      const puntoExpedicionExists = await this.prisma.empresas_puntos_expedicion.findFirst({
        where: {
          empresa_id: updatePuntoExpedicioneDto.empresa_id || puntoExpedicionFind.empresa_id,
          sucursal_id: updatePuntoExpedicioneDto.sucursal_id || puntoExpedicionFind.sucursal_id,
          punto_expedicion: updatePuntoExpedicioneDto.punto_expedicion,
          id: { not: id },
        },
      });

      if (puntoExpedicionExists) throw new ConflictException('Punto de expedicion ya existe');
    }

    const puntoExpedicion = await this.prisma.empresas_puntos_expedicion.update({
      where: { id },
      data: {
        empresa_id: updatePuntoExpedicioneDto.empresa_id,
        sucursal_id: updatePuntoExpedicioneDto.sucursal_id,
        caja_id: updatePuntoExpedicioneDto.caja_id,
        punto_expedicion: updatePuntoExpedicioneDto.punto_expedicion,
        num_inicial: updatePuntoExpedicioneDto.num_inicial,
        descripcion: updatePuntoExpedicioneDto.descripcion,
        eslogan: updatePuntoExpedicioneDto.eslogan,
        usuario: updatePuntoExpedicioneDto.usuario,
      },
      include: {
        empresas_sucursales: true,
        cajas: true,
      },
    });

    this.logger.log(
      `Punto de expedición actualizado: ${puntoExpedicion.punto_expedicion} (${id})`,
      'PuntoExpedicionesService',
    );
    await this.auditService.log({
      empresa_id: puntoExpedicionFind.empresa_id,
      user_id,
      action: 'UPDATE',
      entity_type: 'punto_expedicion',
      entity_id: id,
      descripcion: `Punto de expedición actualizado: ${puntoExpedicion.punto_expedicion}`,
      old_value: {
        punto_expedicion: puntoExpedicionFind.punto_expedicion,
        descripcion: puntoExpedicionFind.descripcion,
        active: puntoExpedicionFind.active,
      },
      new_value: updatePuntoExpedicioneDto as Record<string, unknown>,
    });
    return { message: 'Punto de expedicion actualizado exitosamente', data: puntoExpedicion };
  }

  async activarDesactivar(id: string, user_id?: string) {
    const puntoExpedicionFind = await this.prisma.empresas_puntos_expedicion.findUnique({ where: { id } });
    if (!puntoExpedicionFind) throw new NotFoundException('Punto de expedicion no encontrado');

    const puntoExpedicion = await this.prisma.empresas_puntos_expedicion.update({
      where: { id },
      data: { active: !puntoExpedicionFind.active },
    });
    const action = puntoExpedicion.active ? 'ACTIVATE' : 'DEACTIVATE';
    this.logger.log(
      `Punto de expedición (${id}) ${puntoExpedicion.active ? 'activado' : 'desactivado'}`,
      'PuntoExpedicionesService',
    );
    await this.auditService.log({
      empresa_id: puntoExpedicionFind.empresa_id,
      user_id,
      action,
      entity_type: 'punto_expedicion',
      entity_id: id,
      descripcion: `Punto de expedición ${puntoExpedicion.active ? 'activado' : 'desactivado'}: ${puntoExpedicionFind.punto_expedicion}`,
      old_value: { active: puntoExpedicionFind.active },
      new_value: { active: puntoExpedicion.active },
    });
    return { message: 'Estado actualizado exitosamente', data: puntoExpedicion };
  }

  async setDefault(id: string, user_id?: string) {
    const pe = await this.prisma.empresas_puntos_expedicion.findUnique({ where: { id } });
    if (!pe) throw new NotFoundException('Punto de expedición no encontrado');
    if (!pe.sucursal_id) throw new ConflictException('Punto de expedición sin sucursal_id');
    if (!pe.active) throw new ConflictException('No se puede marcar como predeterminado un PE inactivo');

    const sucursal_id = pe.sucursal_id;
    await this.prisma.$transaction([
      this.prisma.empresas_puntos_expedicion.updateMany({
        where: { sucursal_id, es_default: true, NOT: { id } },
        data: { es_default: false },
      }),
      this.prisma.empresas_puntos_expedicion.update({
        where: { id },
        data: { es_default: true },
      }),
    ]);

    await this.auditService.log({
      empresa_id: pe.empresa_id,
      user_id,
      action: 'UPDATE',
      entity_type: 'punto_expedicion',
      entity_id: id,
      descripcion: `Punto de expedición marcado como predeterminado: ${pe.punto_expedicion}`,
      old_value: { es_default: pe.es_default },
      new_value: { es_default: true },
    });

    return { message: 'Punto de expedición marcado como predeterminado', data: { id, es_default: true } };
  }

  async unsetDefault(id: string, user_id?: string) {
    const pe = await this.prisma.empresas_puntos_expedicion.findUnique({ where: { id } });
    if (!pe) throw new NotFoundException('Punto de expedición no encontrado');
    if (!pe.es_default) {
      return { message: 'No estaba marcado como predeterminado', data: { id, es_default: false } };
    }

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

    await this.auditService.log({
      empresa_id: pe.empresa_id,
      user_id,
      action: 'UPDATE',
      entity_type: 'punto_expedicion',
      entity_id: id,
      descripcion: `Punto de expedición desmarcado como predeterminado: ${pe.punto_expedicion}`,
      old_value: { es_default: true },
      new_value: { es_default: false },
    });

    return { message: 'Punto de expedición desmarcado como predeterminado', data: { id, es_default: false } };
  }

  /**
   * Motivos por los que un punto no se puede borrar, en lenguaje del usuario.
   * Array vacío = nunca se usó y se puede eliminar.
   *
   * Se chequea antes de borrar porque `numeraciones_documento` tiene la FK en
   * `onDelete: Cascade`: sin esta verificación, borrar un punto se llevaría
   * puestas sus numeraciones en silencio, y con ellas el rastro de los números
   * ya emitidos.
   */
  private async motivosQueImpidenBorrar(punto: {
    id: string;
    empresa_id: string | null;
    sucursal_id: string | null;
    punto_expedicion: string | null;
  }): Promise<string[]> {
    const numeraciones = await this.prisma.numeraciones_documento.findMany({
      where: { punto_expedicion_id: punto.id },
      select: { id: true, numero_actual: true, numero_inicial: true },
    });
    const numeracionIds = numeraciones.map((n) => n.id);

    // La sucursal aporta el establecimiento: la factura no tiene FK al punto,
    // guarda el número fiscal desarmado en `dest`/`dpunexp`.
    const sucursal = punto.sucursal_id
      ? await this.prisma.empresas_sucursales.findUnique({
          where: { id: punto.sucursal_id },
          select: { punto_establecimiento: true },
        })
      : null;

    const [pagos, docsSesion, remisiones, facturas] = await Promise.all([
      // Se navega por la relación y no por la columna a propósito: en el schema,
      // `pagos.empresas_puntos_expedicion` está declarada sobre `caja_id` (un
      // nombre que quedó del modelado viejo y no describe lo que guarda). Usar
      // la relación evita depender de ese nombre.
      this.prisma.pagos.count({ where: { empresas_puntos_expedicion: { id: punto.id } } }),
      numeracionIds.length
        ? this.prisma.sesion_caja_documentos.count({ where: { numeracion_id: { in: numeracionIds } } })
        : Promise.resolve(0),
      numeracionIds.length
        ? this.prisma.nota_remision_cab.count({ where: { numeracion_id: { in: numeracionIds } } })
        : Promise.resolve(0),
      // `dest`/`dpunexp` son strings sueltos, sin FK al punto: el filtro por
      // empresa es obligatorio, si no se cuentan las facturas de otras empresas
      // que casualmente usan el mismo establecimiento y punto.
      sucursal?.punto_establecimiento && punto.punto_expedicion && punto.empresa_id
        ? this.prisma.factura_cab.count({
            where: {
              empresa_id: punto.empresa_id,
              dest: sucursal.punto_establecimiento,
              dpunexp: punto.punto_expedicion,
            },
          })
        : Promise.resolve(0),
    ]);

    const plural = (n: number, sing: string, plu: string) => `${n} ${n === 1 ? sing : plu}`;
    const motivos: string[] = [];
    if (facturas) motivos.push(plural(facturas, 'factura emitida', 'facturas emitidas'));
    if (pagos) motivos.push(plural(pagos, 'pago', 'pagos'));
    if (docsSesion) motivos.push(plural(docsSesion, 'documento de caja', 'documentos de caja'));
    if (remisiones) motivos.push(plural(remisiones, 'nota de remisión', 'notas de remisión'));

    // `numero_actual` es el PRÓXIMO número a emitir y arranca en
    // `numero_inicial`, así que una numeración sin estrenar los tiene iguales.
    // Compararlo contra 1 marcaba como usada a cualquier numeración que no
    // empezara en 1 —lo normal cuando se continúa un talonario— y bloqueaba el
    // borrado de puntos que nunca emitieron nada.
    const usadas = numeraciones.filter(
      (n) => (n.numero_actual ?? 1) > (n.numero_inicial ?? 1),
    ).length;
    if (usadas) motivos.push(plural(usadas, 'numeración con comprobantes emitidos', 'numeraciones con comprobantes emitidos'));

    return motivos;
  }

  async remove(id: string, user_id?: string) {
    const puntoExpedicion = await this.prisma.empresas_puntos_expedicion.findUnique({ where: { id } });
    if (!puntoExpedicion) throw new NotFoundException('Punto de expedicion no encontrado');

    const motivos = await this.motivosQueImpidenBorrar(puntoExpedicion);
    if (motivos.length) {
      throw new ConflictException(
        `No se puede eliminar el punto ${puntoExpedicion.punto_expedicion}: tiene ${motivos.join(', ')}. ` +
          'Desactivalo en vez de eliminarlo.',
      );
    }

    await this.prisma.empresas_puntos_expedicion.delete({ where: { id } });
    this.logger.log(
      `Punto de expedición eliminado: ${puntoExpedicion.punto_expedicion} (${id})`,
      'PuntoExpedicionesService',
    );
    await this.auditService.log({
      empresa_id: puntoExpedicion.empresa_id,
      user_id,
      action: 'DELETE',
      entity_type: 'punto_expedicion',
      entity_id: id,
      descripcion: `Punto de expedición eliminado: ${puntoExpedicion.punto_expedicion} | Sucursal: ${puntoExpedicion.sucursal_id}`,
      old_value: {
        punto_expedicion: puntoExpedicion.punto_expedicion,
        sucursal_id: puntoExpedicion.sucursal_id,
        caja_id: puntoExpedicion.caja_id,
      },
    });
    return { message: 'Punto de expedicion eliminado exitosamente' };
  }
}
