import { BadRequestException, Injectable, Logger, NotFoundException, NotImplementedException } from '@nestjs/common';
import { FacturasService } from 'src/facturas/facturas.service';
import { PrismaService } from 'src/prisma/prisma.service';
import { PedidosService } from './pedidos.service';

export interface FacturarOrdenOpciones {
  /** Timbrado y punto de expedición que emite. Si se omite, se resuelve por sucursal. */
  numeracion_id?: string;
  /** Contado / crédito. Por defecto, la condición de la orden o contado. */
  condicion_operacion_id?: string;
  medio_pago_id?: string;
  /** Preparado para la facturación parcial (fase 4B). Todavía no implementado. */
  items?: Array<{ pedido_detalle_id: string; cantidad: number }>;
}

/** Estados de orden desde los que se puede emitir. */
const ESTADOS_FACTURABLES = ['aprobada', 'en_proceso', 'facturada_parcial'];

/**
 * Convierte una orden de venta en factura (plan-ordenes-venta.md, fase 2B).
 *
 * Vive en su propio servicio y no dentro de PedidosService porque ese archivo
 * ya pasa las 1.800 líneas, y porque esto cruza dos módulos: arma el payload y
 * se lo entrega a FacturasService, que es el único que emite.
 *
 * **Sobre la atomicidad**: `FacturasService.create()` abre su propia transacción
 * y al terminar la factura ya está persistida y encolada a SIFEN. No hay forma
 * de deshacerla. Por eso todo lo que puede fallar —estado de la orden, items,
 * numeración, maestros— se valida ANTES de llamarla, y si el marcado posterior
 * de la orden fallara, el error lo dice con el número de factura en la mano
 * para poder vincularla a mano en vez de dejar al usuario adivinando.
 */
@Injectable()
export class OrdenFacturacionService {
  private readonly logger = new Logger(OrdenFacturacionService.name);

  constructor(
    private readonly prisma: PrismaService,
    private readonly facturasService: FacturasService,
    private readonly pedidosService: PedidosService,
  ) {}

  /**
   * Elige el timbrado que emite. Por sucursal de la orden, en este orden:
   *   1. El que venga explícito en la llamada.
   *   2. El marcado por defecto para factura electrónica.
   *   3. Si hay exactamente uno activo, ese.
   * Con varios y ninguno por defecto se pide elegir: adivinar significaría
   * emitir con un timbrado que no es el que la sucursal usa.
   */
  private async resolverNumeracion(empresa_id: string, sucursal_id: string | null, pedida?: string) {
    if (pedida) {
      const n = await this.prisma.numeraciones_documento.findFirst({
        where: { id: pedida, empresa_id, active: true },
        select: { id: true },
      });
      if (!n) throw new BadRequestException('La numeración indicada no existe o está inactiva');
      return n.id;
    }

    const candidatas = await this.prisma.numeraciones_documento.findMany({
      where: {
        empresa_id,
        active: true,
        tipo_documento: { codigo: 1 },
        ...(sucursal_id ? { empresas_puntos_expedicion: { sucursal_id } } : {}),
      },
      select: { id: true, es_default: true },
    });

    if (!candidatas.length) {
      throw new BadRequestException(
        'La sucursal de la orden no tiene una numeración de factura electrónica activa',
      );
    }
    const porDefecto = candidatas.find((c) => c.es_default);
    if (porDefecto) return porDefecto.id;
    if (candidatas.length === 1) return candidatas[0].id;

    throw new BadRequestException(
      `Hay ${candidatas.length} numeraciones de factura en la sucursal y ninguna marcada por defecto: ` +
        'indicá numeracion_id o marcá una como predeterminada',
    );
  }

  async convertirAFactura(
    id: string,
    empresa_id: string,
    user_id: string,
    opciones: FacturarOrdenOpciones = {},
  ) {
    if (opciones.items?.length) {
      throw new NotImplementedException(
        'La facturación parcial por ítem es la fase 4B y todavía no está implementada',
      );
    }

    const orden = await this.prisma.pedidos.findFirst({
      where: { id, empresa_id },
      include: { detalle: { orderBy: { created_at: "asc" } } },
    });
    if (!orden) throw new NotFoundException('Orden no encontrada');
    if (orden.tipo_pedido !== 'orden_venta') throw new BadRequestException('No es una orden de venta');

    const estado = String(orden.estado_orden ?? '');
    if (!ESTADOS_FACTURABLES.includes(estado)) {
      throw new BadRequestException(`No se puede facturar una orden en estado "${estado}"`);
    }
    if (!orden.cliente_id) throw new BadRequestException('La orden no tiene cliente');

    // Sólo lo que queda pendiente: una orden facturada parcialmente no vuelve a
    // facturar lo que ya salió.
    const pendientes = orden.detalle.filter((d) => {
      const pend = d.cant_pendiente != null ? Number(d.cant_pendiente) : Number(d.cantidad);
      return pend > 0;
    });
    if (!pendientes.length) throw new BadRequestException('La orden no tiene ítems pendientes de facturar');

    const numeracion_id = await this.resolverNumeracion(empresa_id, orden.sucursal_id, opciones.numeracion_id);

    const [moneda, indicador, tipoImpuesto, tipoTransaccion, condicion, medioPago] = await Promise.all([
      orden.moneda_id
        ? this.prisma.moneda.findUnique({ where: { id: orden.moneda_id } })
        : this.prisma.moneda.findFirst({ where: { codigo: 'PYG' } }),
      this.prisma.indicador_presencia.findFirst({ where: { codigo: 1 } }),
      this.prisma.tipo_impuesto.findFirst({ where: { codigo: 1 } }),
      this.prisma.tipo_transaccion.findFirst({ where: { codigo: 1 } }),
      opciones.condicion_operacion_id
        ? this.prisma.condicion_operacion.findUnique({ where: { id: opciones.condicion_operacion_id } })
        : orden.condicion_operacion_id
          ? this.prisma.condicion_operacion.findUnique({ where: { id: orden.condicion_operacion_id } })
          : this.prisma.condicion_operacion.findFirst({ where: { codigo: 1 } }),
      opciones.medio_pago_id
        ? this.prisma.medio_pago.findUnique({ where: { id: opciones.medio_pago_id } })
        : this.prisma.medio_pago.findFirst({ where: { codigo: 1 } }),
    ]);
    if (!moneda || !indicador || !tipoImpuesto || !tipoTransaccion || !condicion || !medioPago) {
      throw new BadRequestException(
        'Faltan parámetros maestros para emitir (moneda, indicador de presencia, tipos, condición o medio de pago)',
      );
    }

    // Los importes salen de la orden tal como el cliente los aprobó. Recalcularlos
    // acá haría que la factura pueda no coincidir con la orden firmada si entre
    // medio cambió una lista de precios.
    const lineas = pendientes.map((d) => {
      const cantidad = d.cant_pendiente != null ? Number(d.cant_pendiente) : Number(d.cantidad);
      const total = Number(d.subtotal ?? 0);
      const iva = Number(d.monto_iva ?? 0);
      const pct = Number(d.porcentaje_iva ?? 10);
      const exento = pct === 0;
      return {
        producto_id: d.producto_id,
        unidad_medida: d.unidad_medida || 'UNI',
        descripcion: (d.descripcion || '').slice(0, 2000),
        cantidad,
        precio_unitario: cantidad > 0 ? Number((total / cantidad).toFixed(4)) : 0,
        total_bruto: total,
        descuento: Number(d.descuento_monto ?? 0),
        porcentaje_descuento: Number(d.descuento_porcentaje ?? 0),
        descuento_global: Number(d.descuento_global_item ?? 0),
        total_operacion: total,
        total_gs: total,
        // 1 = Gravado, 3 = Exento. Exonerado (2) sólo con decreto específico.
        afectacion_iva: exento ? 3 : 1,
        porcentaje_iva: pct,
        proporcion_iva: Number(d.proporcion_iva ?? 100),
        base_gravada: exento ? 0 : total - iva,
        liquidacion_iva: iva,
        base_gravada_exenta: exento ? total : 0,
        pct,
        iva,
        total,
      };
    });

    const acum = (f: (l: (typeof lineas)[number]) => number) => lineas.reduce((a, l) => a + f(l), 0);
    const totalFactura = acum((l) => l.total);
    const totalIva = acum((l) => l.iva);
    const base10 = acum((l) => (l.pct === 10 ? l.total - l.iva : 0));
    const base5 = acum((l) => (l.pct === 5 ? l.total - l.iva : 0));
    const iva10 = acum((l) => (l.pct === 10 ? l.iva : 0));
    const iva5 = acum((l) => (l.pct === 5 ? l.iva : 0));
    const exento = acum((l) => (l.pct === 0 ? l.total : 0));

    const dto: any = {
      cabecera: {
        empresa_id,
        sucursal_id: orden.sucursal_id ?? undefined,
        cliente_id: orden.cliente_id,
        tipo_transaccion_id: tipoTransaccion.id,
        tipo_impuesto_id: tipoImpuesto.id,
        moneda_id: moneda.id,
        indicador_presencia_id: indicador.id,
        condicion_operacion_id: condicion.id,
        numeracion_id,
        dfeemide: new Date().toISOString().slice(0, 19).replace('T', ' '),
        dinfadic: `Factura de la orden de venta${orden.numero_pedido ? ` ${orden.numero_pedido}` : ''}`.slice(0, 5000),
      },
      items: lineas.map(({ pct, iva, total, ...item }) => item),
      pagos: [{ medio_pago_id: medioPago.id, moneda_id: moneda.id, monto_pago: totalFactura }],
      subtotal: {
        subtotal_exento: exento,
        subtotal_exonerado: 0,
        subtotal_5: base5 + iva5,
        subtotal_10: base10 + iva10,
        liquidacion_iva_5: iva5,
        liquidacion_iva_10: iva10,
        liquidacion_redondeo_5: 0,
        liquidacion_redondeo_10: 0,
        liquidacion_iva_comision: 0,
        total_iva: totalIva,
        total_operacion: totalFactura,
        total_descuento: acum((l) => l.descuento),
        total_descuento_global_item: 0,
        total_descuento_particular_item: 0,
        total_bruto_operacion: totalFactura,
        total_anticipo_item: 0,
        total_anticipo_global_item: 0,
        porcentaje_descuento_total: 0,
        total_anticipo_operacion: 0,
        total_redondeo: 0,
        total_comision: 0,
        total_general_operacion: totalFactura,
        total_base_gravada_5: base5,
        total_base_gravada_10: base10,
        total_base_gravada: base5 + base10,
        total_gs: totalFactura,
      },
    };

    // A partir de acá la factura existe y no se puede deshacer.
    const resultado = await this.facturasService.create(dto, empresa_id, user_id);
    const factura_id: string | undefined = (resultado as any)?.data?.id;
    const factura_numero: string | undefined = (resultado as any)?.data?.numero;
    if (!factura_id) throw new BadRequestException('La facturación no devolvió el id de la factura');

    try {
      await this.pedidosService.facturarOrden(id, factura_id, empresa_id, user_id);
    } catch (e) {
      // La factura YA se emitió: decirlo es lo único útil acá.
      this.logger.error(
        `Factura ${factura_id} emitida pero la orden ${id} no pudo marcarse: ${(e as Error)?.message}`,
      );
      throw new BadRequestException(
        `La factura${factura_numero ? ` ${factura_numero}` : ''} se emitió correctamente pero la orden${orden.numero_pedido ? ` ${orden.numero_pedido}` : ''} no pudo marcarse como facturada. ` +
          'Vinculalas a mano y revisá el estado de la orden.',
      );
    }

    this.logger.log(`Orden ${orden.numero_pedido ?? id} facturada → factura ${factura_id}`);
    return {
      message: 'Orden facturada correctamente',
      data: { factura_id, orden: await this.pedidosService.findOneOrden(id, empresa_id) },
    };
  }
}
