import { BadRequestException, Injectable } from '@nestjs/common';
import { AutorizacionesService } from 'src/cobranzas/autorizaciones.service';
import { CobrosService } from 'src/cobros/cobros.service';
import {
  CreateReciboCobroDto,
  PreviewDistribucionDto,
  ReciboCobroDetalleDto,
} from 'src/cobros/dto/create-cobro.dto';
import { PrismaService } from 'src/prisma/prisma.service';
import { VendedoresCobradoresService } from 'src/vendedores-cobradores/vendedores-cobradores.service';
import { CrearCobroMobileDto, FormaPagoMobile } from './dto/crear-cobro-mobile.dto';
import { CrearDesdeDistribucionMobileDto } from './dto/crear-desde-distribucion-mobile.dto';
import { InteresManualService } from 'src/cobranzas/interes-manual.service';

/** Código de `medio_pago` por forma de pago móvil (ver tabla medio_pago). */
const MEDIO_PAGO_CODIGO: Record<FormaPagoMobile, number> = {
  [FormaPagoMobile.EFECTIVO]: 1,
  [FormaPagoMobile.CHEQUE]: 2,
  [FormaPagoMobile.TRANSFERENCIA]: 5,
};

/**
 * Adapta el flujo de cobro móvil al `CobrosService` existente:
 * - traduce `forma_pago` (enum) a `medio_pago_id`,
 * - mapea `imputaciones` (cuota+monto) a `detalles` del recibo,
 * - persiste geolocalización y `origen='mobile'` (columnas nuevas),
 * - devuelve los datos de recibo listos para imprimir en ESC/POS.
 *
 * La integración contable se dispara dentro de `CobrosService.create()`.
 */
@Injectable()
export class MobileCobrosService {
  constructor(
    private readonly cobros: CobrosService,
    private readonly vendedores: VendedoresCobradoresService,
    private readonly prisma: PrismaService,
    private readonly autorizaciones: AutorizacionesService,
    private readonly interesManual: InteresManualService,
  ) {}

  /** Preview de distribución automática (sin persistir nada). */
  // Sin la empresa, el preview no puede calcular mora ni interés manual: los
  // deja en cero y el cobrador ve menos de lo que el cliente debe.
  previewDistribucion(empresaId: string, dto: PreviewDistribucionDto) {
    return this.cobros.previewDistribucion(dto, empresaId);
  }

  /** Crea el recibo a partir de la distribución calculada, marcando autorización si aplica. */
  async crearDesdeDistribucion(
    empresaId: string,
    usuarioId: string,
    dto: CrearDesdeDistribucionMobileDto,
  ) {
    if (!dto.cuota_ids?.length) {
      throw new BadRequestException('Debe seleccionar al menos una cuota');
    }

    const previewDto: PreviewDistribucionDto = {
      cuota_ids: dto.cuota_ids,
      monto_recibido: dto.monto_recibido,
      medio_pago_id: dto.medio_pago_id,
      descuento_porcentaje: dto.descuento_porcentaje,
      descuento_monto: dto.descuento_monto,
      motivo_descuento: dto.motivo_descuento,
      moneda_id: dto.moneda_id,
      cotizacion: dto.cotizacion,
      // Interés manual: ya no lo decide la app. El preview lo imputa después
      // del capital. Se sigue pasando solo para que el service loguee el WARN.
      interes_manual_monto: dto.interes_manual_monto,
    };

    const numeroCheque = dto.cheque?.numero;
    const bancoEmisor = dto.cheque?.banco_emisor;
    const observacion = this.componerObservacionDistribucion(dto);

    const recibo = (await this.cobros.createFromDistribucion(
      previewDto,
      dto.sucursal_id,
      dto.cliente_id,
      dto.sesion_caja_id ?? null,
      usuarioId,
      empresaId,
      observacion,
      undefined, // cobrador_id (se resuelve dentro del service base por sesión)
      undefined, // fecha_registro
      numeroCheque,
      bancoEmisor,
      undefined, // fecha_emision
      undefined, // banco_id
      undefined, // exoneraciones de interés (las resuelve el service base)
      // El cobrador externo no tiene caja propia: su cobro se rinde por el
      // módulo de rendiciones. Sin esto el recibo se imputaba a cualquier caja
      // abierta a la que el usuario tuviera acceso, típicamente la de otro cajero.
      { soloSesionPropia: true },
    )) as { id: string; numero_recibo: string };

    // Marcar autorización de descuento como usada (si vino una)
    if (dto.autorizacion_descuento_id) {
      try {
        await this.autorizaciones.marcarUsada(dto.autorizacion_descuento_id, recibo.id);
      } catch {
        // No bloquear el cobro si la autorización ya fue marcada por otro flujo
      }
    }

    // Persistir geo + origen mobile
    await this.prisma.recibos_cobro.update({
      where: { id: recibo.id },
      data: {
        origen: 'mobile',
        geo_lat: dto.geo?.lat ?? null,
        geo_lng: dto.geo?.lng ?? null,
        geo_accuracy: dto.geo?.accuracy ?? null,
      },
    });

    const recibo_data = await this.cobros.getReciboPrintData(recibo.id, empresaId, { paperWidth: 80 });

    return {
      cobro_id: recibo.id,
      numero_recibo: recibo.numero_recibo,
      recibo_data,
    };
  }

  /** Descuento autorizado vigente para una factura (si lo hay). */
  getDescuentoActivoFactura(facturaId: string, usuarioId: string, empresaId: string) {
    return this.autorizaciones.getActivaParaUsuario(facturaId, usuarioId, empresaId);
  }

  /** Medios de pago activos para la empresa (mismo shape que sync). */
  async mediosPago(empresaId: string) {
    const empresaMedios = await this.prisma.empresa_medio_pago.findMany({
      where: { empresa_id: empresaId, activo: true },
      include: { medio_pago: true },
      orderBy: { orden: 'asc' },
    });
    return empresaMedios.map((em) => ({
      id: em.medio_pago.id,
      codigo: em.medio_pago.codigo,
      descripcion: em.medio_pago.descripcion,
      orden: em.orden,
    }));
  }

  private componerObservacionDistribucion(
    dto: CrearDesdeDistribucionMobileDto,
  ): string | undefined {
    const partes: string[] = [];
    if (dto.observacion) partes.push(dto.observacion);
    if (dto.cheque) {
      const c = dto.cheque;
      const extra = [
        c.titular ? `Titular: ${c.titular}` : null,
        c.fecha_emision ? `Emisión: ${c.fecha_emision}` : null,
        c.fecha_vencimiento ? `Vencimiento: ${c.fecha_vencimiento}` : null,
        c.diferido ? 'Diferido' : null,
      ].filter(Boolean);
      if (extra.length) partes.push(`Cheque [${extra.join(', ')}]`);
    }
    if (dto.transferencia) {
      const t = dto.transferencia;
      partes.push(`Transferencia [Op: ${t.numero_operacion}${t.fecha ? `, ${t.fecha}` : ''}]`);
    }
    return partes.length ? partes.join(' · ') : undefined;
  }

  private async resolverMedioPagoId(forma: FormaPagoMobile): Promise<string> {
    const codigo = MEDIO_PAGO_CODIGO[forma];
    const medio = await this.prisma.medio_pago.findFirst({ where: { codigo } });
    if (!medio) {
      throw new BadRequestException(`Medio de pago no configurado para "${forma}"`);
    }
    return medio.id;
  }

  async crear(empresaId: string, usuarioId: string, dto: CrearCobroMobileDto) {
    if (!dto.imputaciones?.length) {
      throw new BadRequestException('Debe imputar al menos una cuota');
    }
    const medioPagoId = await this.resolverMedioPagoId(dto.forma_pago);

    const esCheque = dto.forma_pago === FormaPagoMobile.CHEQUE;
    if (esCheque && !dto.cheque) {
      throw new BadRequestException('Faltan los datos del cheque');
    }
    if (dto.forma_pago === FormaPagoMobile.TRANSFERENCIA && !dto.transferencia) {
      throw new BadRequestException('Faltan los datos de la transferencia');
    }

    const detalles: ReciboCobroDetalleDto[] = dto.imputaciones.map((imp) => {
      const detalle: ReciboCobroDetalleDto = {
        factura_cuota_id: imp.factura_cuota_id,
        medio_pago_id: medioPagoId,
        monto_pagado: imp.monto,
        moneda_id: dto.moneda_id,
        cotizacion: dto.cotizacion,
      };
      if (esCheque && dto.cheque) {
        detalle.numero_cheque = dto.cheque.numero;
        detalle.banco_emisor = dto.cheque.banco_emisor;
        detalle.banco_id = dto.cheque.banco_id;
      }
      return detalle;
    });

    // Datos que el recibo_cobro_detalle no persiste (titular/fechas de cheque,
    // operación de transferencia) se conservan en la observación del recibo.
    const observacion = this.componerObservacion(dto);

    const createDto: CreateReciboCobroDto = {
      cliente_id: dto.cliente_id,
      sucursal_id: dto.sucursal_id,
      sesion_caja_id: dto.sesion_caja_id,
      moneda_id: dto.moneda_id,
      cotizacion: dto.cotizacion,
      observacion,
      detalles,
    };

    const recibo = (await this.cobros.create(createDto, usuarioId, empresaId, {
      // Ver crearDesdeDistribucion: los cobros de la app nunca se imputan a la
      // caja de otro usuario.
      soloSesionPropia: true,
    })) as {
      id: string;
      numero_recibo: string;
    };

    // Geolocalización + marca de origen (columnas nuevas, no tocan el service base).
    await this.prisma.recibos_cobro.update({
      where: { id: recibo.id },
      data: {
        origen: 'mobile',
        geo_lat: dto.geo?.lat ?? null,
        geo_lng: dto.geo?.lng ?? null,
        geo_accuracy: dto.geo?.accuracy ?? null,
      },
    });

    const recibo_data = await this.cobros.getReciboPrintData(recibo.id, empresaId, { paperWidth: 80 });

    return {
      cobro_id: recibo.id,
      numero_recibo: recibo.numero_recibo,
      recibo_data,
    };
  }

  private componerObservacion(dto: CrearCobroMobileDto): string | undefined {
    const partes: string[] = [];
    if (dto.observacion) partes.push(dto.observacion);
    if (dto.forma_pago === FormaPagoMobile.CHEQUE && dto.cheque) {
      const c = dto.cheque;
      const extra = [
        c.titular ? `Titular: ${c.titular}` : null,
        c.fecha_emision ? `Emisión: ${c.fecha_emision}` : null,
        c.fecha_vencimiento ? `Vencimiento: ${c.fecha_vencimiento}` : null,
        c.diferido ? 'Diferido' : null,
      ].filter(Boolean);
      if (extra.length) partes.push(`Cheque [${extra.join(', ')}]`);
    }
    if (dto.forma_pago === FormaPagoMobile.TRANSFERENCIA && dto.transferencia) {
      const t = dto.transferencia;
      partes.push(`Transferencia [Op: ${t.numero_operacion}${t.fecha ? `, ${t.fecha}` : ''}]`);
    }
    return partes.length ? partes.join(' · ') : undefined;
  }

  /** Cobros del cobrador logueado en un rango de fechas. */
  async mios(empresaId: string, usuarioId: string, desde?: string, hasta?: string) {
    const cobrador = await this.vendedores.findCobradorVinculado(empresaId, usuarioId);
    return this.cobros.findAll(empresaId, {
      cobradorId: cobrador?.id,
      fechaDesde: desde ? new Date(desde) : undefined,
      fechaHasta: hasta ? new Date(hasta) : undefined,
    });
  }

  /** Datos del recibo para reimpresión (mismo shape que el cobro original). */
  reciboData(id: string, empresaId: string) {
    return this.cobros.getReciboPrintData(id, empresaId, { paperWidth: 80 });
  }

  /**
   * Cuotas pendientes de un cliente — enriquecidas con:
   *   - `mora_estimada` por cuota (si la empresa tiene config_mora activa)
   *   - `interes_manual` por cuota e `interes_manual_cuenta` por factura: el
   *     interés que el administrativo cargó desde Cuentas por Cobrar
   *   - `descuento_autorizado` resumen por factura (si hay autorización vigente)
   * Así el cobrador ve los mismos campos que el flujo web (CobrosTemplateV2).
   */
  async cuotasPendientes(clienteId: string, empresaId: string) {
    const cuotas = await this.cobros.getCuotasPendientes(clienteId, empresaId);

    const facturaIdsArr = Array.from(
      new Set((cuotas as any[]).map((c) => c.factura_cab?.id).filter(Boolean) as string[]),
    );

    const interesManual = await this.interesManual.pendienteParaCobro(
      empresaId,
      (cuotas as any[]).map((c) => c.id),
      facturaIdsArr,
    );

    const [configMora, autorizaciones, ultimosPagos] = await Promise.all([
      this.prisma.config_mora.findFirst({ where: { empresa_id: empresaId } }),
      this.prisma.autorizaciones_descuento.findMany({
        where: {
          empresa_id: empresaId,
          cliente_id: clienteId,
          estado: { in: ['aprobado', 'aprobada'] },
          expires_at: { gt: new Date() },
        },
        select: {
          id: true,
          factura_cab_id: true,
          descuento_porcentaje: true,
          descuento_monto: true,
          motivo: true,
        },
      }),
      facturaIdsArr.length === 0
        ? Promise.resolve([] as any[])
        : this.prisma.recibo_cobro_detalle.findMany({
            where: {
              factura_cuotas: { factura_cab_id: { in: facturaIdsArr } },
              recibos_cobro: { estado: { notIn: ['anulado', 'Anulado'] } },
            },
            select: {
              monto_pagado: true,
              recibo_cobro_id: true,
              factura_cuotas: { select: { factura_cab_id: true } },
              recibos_cobro: {
                select: {
                  fecha_emision: true,
                  numero_recibo: true,
                  cobrador: { select: { nombre: true, apellido: true } },
                  usuario: { select: { nombres: true, apellidos: true, username: true } },
                },
              },
            },
          }),
    ]);

    const descPorFactura = new Map<string, any>();
    for (const a of autorizaciones) {
      // descuento sobre toda la factura (factura_cab_id null = sobre cualquiera del cliente)
      const key = a.factura_cab_id ?? '__cliente__';
      descPorFactura.set(key, a);
    }

    // Último pago por factura: agrupa detalles por (factura, recibo), suma montos del mismo recibo,
    // y se queda con el recibo más reciente. Sirve para responder "¿cuándo me pagó la última vez?".
    type Pago = { fecha: Date; monto: number; numero_recibo: string; cobrador: string | null };
    const nombreCobrador = (rc: any): string | null => {
      const c = rc?.cobrador;
      if (c) return [c.nombre, c.apellido].filter(Boolean).join(' ').trim() || null;
      const u = rc?.usuario;
      if (u) {
        const n = [u.nombres, u.apellidos].filter(Boolean).join(' ').trim();
        return n || u.username || null;
      }
      return null;
    };
    const porFacturaRecibo = new Map<string, Map<string, Pago>>();
    for (const d of ultimosPagos as any[]) {
      const fid = d.factura_cuotas?.factura_cab_id;
      if (!fid) continue;
      let m = porFacturaRecibo.get(fid);
      if (!m) {
        m = new Map();
        porFacturaRecibo.set(fid, m);
      }
      const ex = m.get(d.recibo_cobro_id);
      if (ex) {
        ex.monto += Number(d.monto_pagado);
      } else {
        m.set(d.recibo_cobro_id, {
          fecha: d.recibos_cobro.fecha_emision,
          monto: Number(d.monto_pagado),
          numero_recibo: d.recibos_cobro.numero_recibo,
          cobrador: nombreCobrador(d.recibos_cobro),
        });
      }
    }
    const ultimoPagoPorFactura = new Map<string, Pago>();
    for (const [fid, m] of porFacturaRecibo) {
      let latest: Pago | null = null;
      for (const p of m.values()) {
        if (!latest || p.fecha > latest.fecha) latest = p;
      }
      if (latest) ultimoPagoPorFactura.set(fid, latest);
    }

    return (cuotas as any[]).map((c) => {
      const montoCuota = Number(c.dmoncuota ?? c.saldo_pendiente ?? 0);
      const diasVencido = Number(c.dias_vencido ?? 0);
      let mora_estimada = 0;
      if (configMora?.activo && diasVencido > 0) {
        const diasEfectivos = Math.max(0, diasVencido - (configMora.periodo_gracia ?? 0));
        if (diasEfectivos > 0) {
          const tasa = Number(configMora.tasa || 0);
          let mora = 0;
          if (configMora.tipo_calculo === 'diario') mora = montoCuota * (tasa / 100) * diasEfectivos;
          else if (configMora.tipo_calculo === 'mensual') mora = montoCuota * (tasa / 100) * (diasEfectivos / 30);
          else if (configMora.tipo_calculo === 'fijo_por_cuota') mora = tasa;
          const montoMin = Number((configMora as any).monto_minimo_mora || 0);
          const montoMax = Number((configMora as any).monto_maximo_mora || 0);
          if (montoMin && mora < montoMin) mora = montoMin;
          if (montoMax && mora > montoMax) mora = montoMax;
          mora_estimada = Math.round(mora);
        }
      }
      const facturaId = c.factura_cab?.id;
      const desc = (facturaId && descPorFactura.get(facturaId)) || descPorFactura.get('__cliente__');
      const ultimoPago = facturaId ? ultimoPagoPorFactura.get(facturaId) : null;

      // Política de cobranza acordada (viene desde cuentas_cobrar vía relation).
      // La aplanamos en el shape de la cuota para que el cobrador móvil la vea inline.
      const cc = (c as any).cuentas_cobrar;
      const politica_cobranza =
        cc && (
          cc.intervalo_dias != null ||
          cc.dia_fijo_pago_1 != null ||
          cc.dia_fijo_pago_2 != null ||
          cc.dia_cobro_semana != null ||
          cc.dias_gracia_override != null
        )
          ? {
              intervalo_dias: cc.intervalo_dias,
              dia_fijo_pago_1: cc.dia_fijo_pago_1,
              dia_fijo_pago_2: cc.dia_fijo_pago_2,
              dia_cobro_semana: cc.dia_cobro_semana,
              dias_gracia_override: cc.dias_gracia_override,
            }
          : null;

      // Descartamos la relación cruda; ya está aplanada en politica_cobranza.
      const { cuentas_cobrar: _cc, ...cSinCC } = c as any;

      return {
        ...cSinCC,
        factura_cab: cSinCC.factura_cab
          ? {
              ...cSinCC.factura_cab,
              politica_cobranza,
              ultimo_pago: ultimoPago
                ? {
                    fecha: ultimoPago.fecha.toISOString(),
                    monto: ultimoPago.monto,
                    numero_recibo: ultimoPago.numero_recibo,
                    cobrador: ultimoPago.cobrador,
                  }
                : null,
            }
          : cSinCC.factura_cab,
        mora_estimada,
        // Interés cargado a mano: el de esta cuota y el de su cuenta. El cobrador
        // los ve y decide cuánto cobrar; no los puede cargar ni modificar.
        interes_manual: interesManual.porCuota.get(c.id) ?? 0,
        interes_manual_cuenta: facturaId ? (interesManual.porCuenta.get(facturaId) ?? 0) : 0,
        descuento_autorizado: desc
          ? {
              id: desc.id,
              porcentaje: Number(desc.descuento_porcentaje ?? 0) || null,
              monto: Number(desc.descuento_monto ?? 0) || null,
              motivo: desc.motivo,
            }
          : null,
      };
    });
  }

  /** Bancos de la empresa (para datos de cheque / transferencia). */
  async bancos(empresaId: string) {
    return this.prisma.bancos.findMany({
      where: { empresa_id: empresaId, activo: true },
      select: { id: true, codigo: true, descripcion: true },
      orderBy: { descripcion: 'asc' },
    });
  }
}
