import { BadRequestException, ConflictException, Injectable, NotFoundException } from '@nestjs/common';
import { PrismaService } from 'src/prisma/prisma.service';
import { MonedasService } from 'src/monedas/monedas.service';
import { ViajeDocumentosService } from './viaje-documentos.service';
import { ViajesService } from './viajes.service';
import { TIPO_DOC_VIAJE } from './viajes.constants';

/** Ver la nota de MONEDA_BASE en viaje-calculos.service.ts: va como constante de módulo. */
const MONEDA_BASE = 'PYG';
const RENDICION_ANULADA = 'ANULADA';
const SIN_DETALLE = 'Sin detalle';
const ESTADOS_FACTURA_BAJA = ['Anulado', 'ANULADO', 'anulado', 'Rechazado', 'RECHAZADO', 'rechazado'];

const nombreEmpleado = (e: any) => [e?.nombres, e?.apellidos].filter(Boolean).join(' ').trim() || 'Sin nombre';

const sumarEn = (mapa: Map<string, number>, clave: string, monto: number) =>
  mapa.set(clave, (mapa.get(clave) ?? 0) + monto);

const ordenado = (mapa: Map<string, number>, campo: string) =>
  [...mapa.entries()]
    .filter(([, v]) => Math.abs(v) >= 1)
    .sort((a, b) => b[1] - a[1])
    .map(([k, v]) => ({ [campo]: k, monto_base: v }));

/**
 * Costo y rentabilidad del viaje (spec 2026-10-01, Fase B).
 *
 * - Costo = suma de lo rendido en las rendiciones de viáticos asociadas (una por
 *   persona: en DOBA viajan el vendedor y el que entrega, cada uno con la suya).
 * - Desglose por tipo de gasto desde los gastos de cada rendición; lo rendido sin
 *   gasto que lo respalde va como "Sin detalle".
 * - Margen bruto solo con las líneas de factura que tienen `costo_total_venta`: en
 *   DOBA está cargado en el 45 % de las líneas, así que se informa la cobertura y
 *   nunca se toma costo cero para las que no lo tienen.
 */
@Injectable()
export class ViajeCostosService {
  constructor(
    private readonly prisma: PrismaService,
    private readonly documentos: ViajeDocumentosService,
    private readonly viajes: ViajesService,
    private readonly monedas: MonedasService,
  ) {}

  private async aBase(
    monto: number,
    codigo: string | undefined,
    tasaDoc: unknown,
    empresaId: string,
    marca: { estimada: boolean },
    cache: Map<string, number>,
  ) {
    const c = codigo ?? MONEDA_BASE;
    if (!monto || c === MONEDA_BASE) return monto;
    const tasa = Number(tasaDoc);
    if (tasa > 0) return monto * tasa;
    marca.estimada = true;
    if (!cache.has(c)) {
      const v = await this.monedas.getCotizacionVigente(empresaId, c).catch(() => null);
      cache.set(c, Number(v?.tasa) > 0 ? Number(v?.tasa) : 0);
    }
    return monto * (cache.get(c) as number);
  }

  async costos(viajeId: string, empresaId: string) {
    await this.viajes.findOne(viajeId, empresaId);
    const marca = { estimada: false };
    const cache = new Map<string, number>();
    const base = (monto: number, codigo: string | undefined, tasa: unknown) =>
      this.aBase(monto, codigo, tasa, empresaId, marca, cache);

    const [filas, facturaIds] = await Promise.all([
      this.prisma.viaje_rendiciones.findMany({
        where: { viaje_id: viajeId },
        orderBy: { created_at: 'asc' },
        select: {
          id: true,
          rendicion: {
            select: {
              id: true, numero_rendicion: true, concepto: true, estado: true,
              fecha_inicio: true, fecha_fin: true,
              monto_rendido_total: true, monto_adelanto_total: true, cotizacion: true,
              moneda: { select: { codigo: true } },
              empleado: { select: { nombres: true, apellidos: true } },
              gastos: {
                where: { OR: [{ anulado: false }, { anulado: null }] },
                select: {
                  cotizacion: true,
                  moneda: { select: { codigo: true } },
                  gasto_det: { select: { total: true, tipo_gasto: { select: { descripcion: true } } } },
                },
              },
            },
          },
        },
      }),
      this.documentos.idsVigentes(viajeId, TIPO_DOC_VIAJE.FACTURA),
    ]);

    const porTipo = new Map<string, number>();
    const porPersona = new Map<string, number>();
    const tiposConNombre = new Set<string>();
    let costoBase = 0;
    let activas = 0;
    const rendiciones: any[] = [];

    for (const f of filas as any[]) {
      const r = f.rendicion;
      const codigo = r.moneda?.codigo ?? MONEDA_BASE;
      const anulada = r.estado === RENDICION_ANULADA;
      const rendidoBase = await base(Number(r.monto_rendido_total ?? 0), codigo, r.cotizacion);

      const tipos = new Map<string, number>();
      for (const g of r.gastos ?? []) {
        for (const d of g.gasto_det ?? []) {
          const tipo = d.tipo_gasto?.descripcion ?? SIN_DETALLE;
          sumarEn(tipos, tipo, await base(Number(d.total ?? 0), g.moneda?.codigo ?? codigo, g.cotizacion));
        }
      }
      const detallado = [...tipos.values()].reduce((a, b) => a + b, 0);
      if (rendidoBase - detallado >= 1) sumarEn(tipos, SIN_DETALLE, rendidoBase - detallado);

      if (!anulada) {
        activas += 1;
        costoBase += rendidoBase;
        sumarEn(porPersona, nombreEmpleado(r.empleado), rendidoBase);
        for (const [tipo, monto] of tipos) {
          sumarEn(porTipo, tipo, monto);
          if (tipo !== SIN_DETALLE && monto >= 1) tiposConNombre.add(tipo);
        }
      }

      rendiciones.push({
        fila_id: f.id,
        id: r.id,
        numero: r.numero_rendicion,
        concepto: r.concepto,
        estado: r.estado,
        anulada,
        empleado: nombreEmpleado(r.empleado),
        fecha_inicio: r.fecha_inicio ? new Date(r.fecha_inicio).toISOString().slice(0, 10) : null,
        fecha_fin: r.fecha_fin ? new Date(r.fecha_fin).toISOString().slice(0, 10) : null,
        rendido: Number(r.monto_rendido_total ?? 0),
        adelanto: Number(r.monto_adelanto_total ?? 0),
        moneda: codigo,
        rendido_base: rendidoBase,
        por_tipo: ordenado(tipos, 'tipo'),
      });
    }

    const margen = await this.margen(facturaIds, base);

    return {
      moneda_base: MONEDA_BASE,
      costo_base: costoBase,
      rendiciones,
      rendiciones_activas: activas,
      por_persona: ordenado(porPersona, 'empleado'),
      por_tipo: ordenado(porTipo, 'tipo'),
      // Con todo en un solo tipo ("VIÁTICOS A VENDEDORES") el desglose no dice en
      // qué se gastó: la UI pide que se tipifiquen combustible, hospedaje, etc.
      un_solo_tipo: costoBase > 0 && tiposConNombre.size <= 1,
      margen,
      cotizacion_estimada: marca.estimada,
    };
  }

  private async margen(
    facturaIds: string[],
    base: (monto: number, codigo: string | undefined, tasa: unknown) => Promise<number>,
  ) {
    const lineas = facturaIds.length
      ? await this.prisma.factura_det.findMany({
          where: { factura_cab_id: { in: facturaIds } },
          select: {
            dtotopeitem: true,
            costo_total_venta: true,
            factura_cab: { select: { dticam: true, estado: true, moneda: { select: { codigo: true } } } },
          },
        })
      : [];

    let importe = 0;
    let importeConCosto = 0;
    let costo = 0;
    for (const l of lineas as any[]) {
      if (ESTADOS_FACTURA_BAJA.includes(l.factura_cab?.estado)) continue;
      const codigo = l.factura_cab?.moneda?.codigo;
      const tasa = l.factura_cab?.dticam;
      const imp = await base(Number(l.dtotopeitem ?? 0), codigo, tasa);
      importe += imp;
      const c = Number(l.costo_total_venta ?? 0);
      if (c > 0) {
        importeConCosto += imp;
        costo += await base(c, codigo, tasa);
      }
    }

    const margenBruto = importeConCosto > 0 ? importeConCosto - costo : null;
    return {
      importe_base: importe,
      importe_con_costo_base: importeConCosto,
      costo_mercaderia_base: costo,
      margen_bruto_base: margenBruto,
      margen_pct: margenBruto != null ? margenBruto / importeConCosto : null,
      cobertura: importe > 0 ? importeConCosto / importe : 0,
    };
  }

  /**
   * Rendiciones que se pueden asociar: de la empresa, no anuladas y que no estén
   * en ningún viaje. Primero las que se cruzan con las fechas del viaje.
   */
  async sugeridas(viajeId: string, empresaId: string) {
    const viaje = await this.viajes.findOne(viajeId, empresaId);
    const rendiciones = await this.prisma.rendicion_viatico.findMany({
      where: { empresa_id: empresaId, estado: { not: RENDICION_ANULADA }, viajes: { none: {} } },
      select: {
        id: true, numero_rendicion: true, concepto: true, estado: true, fecha_inicio: true, fecha_fin: true,
        monto_rendido_total: true, moneda: { select: { codigo: true } },
        empleado: { select: { nombres: true, apellidos: true } },
      },
      orderBy: { fecha_inicio: 'desc' },
      take: 200,
    });

    const ymd = (d: any) => (d ? new Date(d).toISOString().slice(0, 10) : null);
    const salida = ymd(viaje.fecha_salida) as string;
    const retorno = ymd(viaje.fecha_retorno) ?? salida;
    return rendiciones
      .map((r: any) => {
        const ini = ymd(r.fecha_inicio);
        const fin = ymd(r.fecha_fin) ?? ini;
        return {
          id: r.id,
          numero: r.numero_rendicion,
          concepto: r.concepto,
          estado: r.estado,
          empleado: nombreEmpleado(r.empleado),
          fecha_inicio: ini,
          fecha_fin: fin,
          rendido: Number(r.monto_rendido_total ?? 0),
          moneda: r.moneda?.codigo ?? MONEDA_BASE,
          // Se cruza con el viaje si sus períodos se superponen.
          en_rango: !!ini && !!fin && ini <= retorno && fin >= salida,
        };
      })
      .sort((a, b) => Number(b.en_rango) - Number(a.en_rango));
  }

  async asociar(viajeId: string, empresaId: string, rendicionIds: string[], userId?: string) {
    await this.viajes.exigirEditable(viajeId, empresaId);
    const nuevas: string[] = [];
    for (const id of [...new Set(rendicionIds)]) {
      const r = await this.prisma.rendicion_viatico.findFirst({
        where: { id, empresa_id: empresaId },
        select: { id: true, estado: true, numero_rendicion: true },
      });
      if (!r) throw new NotFoundException('Rendición de viáticos no encontrada');
      if (r.estado === RENDICION_ANULADA) {
        throw new BadRequestException(`La rendición ${r.numero_rendicion} está anulada`);
      }
      const existente = await this.prisma.viaje_rendiciones.findFirst({
        where: { rendicion_viatico_id: id },
        select: { viaje_id: true, viaje: { select: { numero: true } } },
      });
      if (existente?.viaje_id === viajeId) continue;
      if (existente) {
        throw new ConflictException(
          `La rendición ${r.numero_rendicion} ya está en el viaje ${existente.viaje?.numero}: quitala de ahí primero`,
        );
      }
      nuevas.push(id);
    }
    if (nuevas.length) {
      await this.prisma.viaje_rendiciones.createMany({
        data: nuevas.map((id) => ({ viaje_id: viajeId, rendicion_viatico_id: id, created_by: userId ?? null })),
      });
    }
    return { id: viajeId, agregadas: nuevas.length };
  }

  async quitar(viajeId: string, empresaId: string, filaId: string) {
    await this.viajes.exigirEditable(viajeId, empresaId);
    const fila = await this.prisma.viaje_rendiciones.findFirst({
      where: { id: filaId, viaje_id: viajeId },
      select: { id: true },
    });
    if (!fila) throw new NotFoundException('La rendición no está en este viaje');
    await this.prisma.viaje_rendiciones.delete({ where: { id: filaId } });
    return { id: viajeId };
  }
}
