import { ForbiddenException, Injectable, NotFoundException, NotImplementedException } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { PrismaService } from '../../prisma/prisma.service';
import type {
  CuentaDetalle,
  CuentaPendiente,
  CuentasFilter,
  Cuota,
  IFuenteCartera,
  ModoOperativo,
  OrigenCartera,
  Recibo,
  ReciboCobro,
  RegistrarCobroInput,
} from './i-fuente-cartera.interface';

/**
 * Implementación de IFuenteCartera que lee la cartera del propio ERP Novasis
 * (factura_cab + factura_cuotas). Es la fuente por defecto para tenants que
 * ya operan dentro del ERP completo.
 */
@Injectable()
export class ErpNovasisFuente implements IFuenteCartera {
  constructor(private readonly prisma: PrismaService) {}

  getOrigen(): OrigenCartera {
    return 'erp-novasis';
  }

  private modo: ModoOperativo = 'operacional';

  setModo(modo: ModoOperativo): void {
    this.modo = modo;
  }

  getModo(): ModoOperativo {
    return this.modo;
  }

  /**
   * Devuelve las facturas con saldo pendiente del tenant, una row por documento.
   * El saldo pendiente se calcula sumando las cuotas en estado != pagada con
   * saldo > 0. `dias_mora` es la cantidad de días desde el vencimiento más
   * antiguo entre las cuotas pendientes.
   */
  async listarCuentasPendientes(filtros: CuentasFilter): Promise<CuentaPendiente[]> {
    const page = filtros.page && filtros.page > 0 ? filtros.page : 1;
    const pageSize = filtros.page_size && filtros.page_size > 0 ? filtros.page_size : 50;
    const offset = (page - 1) * pageSize;

    const where: Prisma.Sql[] = [
      Prisma.sql`fc.empresa_id = ${filtros.empresa_id}::uuid`,
      Prisma.sql`(fc.estado IS NULL OR fc.estado <> 'Anulado')`,
      Prisma.sql`fc.saldo_pendiente IS NOT NULL AND fc.saldo_pendiente > 0`,
    ];
    if (filtros.cliente_id) {
      where.push(Prisma.sql`fc.cliente_id = ${filtros.cliente_id}::uuid`);
    }
    if (filtros.cobrador_id) {
      where.push(Prisma.sql`c.cobrador_id = ${filtros.cobrador_id}::uuid`);
    }
    if (filtros.vencido_desde) {
      where.push(Prisma.sql`venc.oldest_due <= ${filtros.vencido_desde.toISOString()}::timestamp`);
    }

    const whereSql = Prisma.sql`${Prisma.join(where, ' AND ')}`;

    const rows = await this.prisma.$queryRaw<
      Array<{
        id: string;
        cliente_id: string;
        cliente_nombre: string;
        documento_numero: string;
        monto_total: string;
        saldo_pendiente: string;
        fecha_emision: Date;
        fecha_vencimiento: Date | null;
        dias_mora: number;
        moneda_codigo: string;
      }>
    >(Prisma.sql`
      WITH venc AS (
        SELECT
          fcu.factura_cab_id,
          MIN(fcu.dvenccuo) AS oldest_due
        FROM factura_cuotas fcu
        WHERE fcu.saldo_pendiente > 0 AND fcu.estado <> 'pagada'
        GROUP BY fcu.factura_cab_id
      )
      SELECT
        fc.id::text                                                AS id,
        fc.cliente_id::text                                        AS cliente_id,
        COALESCE(c.nombre_fantasia, p.razon_social, p.ruc, '')     AS cliente_nombre,
        (fc.dest || '-' || fc.dpunexp || '-' || fc.dnumdoc)         AS documento_numero,
        COALESCE(fc.total_factura, 0)::text                        AS monto_total,
        COALESCE(fc.saldo_pendiente, 0)::text                      AS saldo_pendiente,
        fc.dfeemide                                                AS fecha_emision,
        venc.oldest_due                                            AS fecha_vencimiento,
        GREATEST(0, EXTRACT(DAY FROM (NOW() - venc.oldest_due))::int) AS dias_mora,
        COALESCE(m.codigo, 'PYG')                                  AS moneda_codigo
      FROM factura_cab fc
      JOIN clientes c ON c.id = fc.cliente_id
      LEFT JOIN personas p ON p.id = c.persona_id
      LEFT JOIN moneda m ON m.id = fc.moneda_id
      LEFT JOIN venc ON venc.factura_cab_id = fc.id
      WHERE ${whereSql}
      ORDER BY venc.oldest_due ASC NULLS LAST, fc.dfeemide ASC
      LIMIT ${pageSize} OFFSET ${offset}
    `);

    return rows.map((r) => ({
      id: r.id,
      cliente_id: r.cliente_id,
      cliente_nombre: r.cliente_nombre,
      documento_numero: r.documento_numero,
      monto_total: Number(r.monto_total),
      saldo_pendiente: Number(r.saldo_pendiente),
      fecha_emision: r.fecha_emision,
      fecha_vencimiento: r.fecha_vencimiento,
      dias_mora: Number(r.dias_mora),
      moneda_codigo: r.moneda_codigo,
    }));
  }

  async obtenerCuenta(id: string): Promise<CuentaDetalle> {
    const cuotas = await this.listarCuotas(id);
    const factura = await this.prisma.factura_cab.findUnique({
      where: { id },
      include: {
        clientes: { include: { personas: true } },
        moneda: { select: { codigo: true } },
      },
    });
    if (!factura) {
      throw new NotFoundException(`Factura ${id} no encontrada`);
    }
    const fechaVencimientoMin = cuotas
      .filter((cu) => cu.saldo > 0)
      .map((cu) => cu.fecha_vencimiento)
      .sort((a, b) => a.getTime() - b.getTime())[0] ?? null;
    const diasMora = fechaVencimientoMin
      ? Math.max(0, Math.floor((Date.now() - fechaVencimientoMin.getTime()) / 86_400_000))
      : 0;
    return {
      id: factura.id,
      cliente_id: factura.cliente_id,
      cliente_nombre:
        factura.clientes.nombre_fantasia ??
        factura.clientes.personas?.razon_social ??
        factura.clientes.personas?.ruc ??
        '',
      documento_numero: `${factura.dest}-${factura.dpunexp}-${factura.dnumdoc}`,
      monto_total: Number(factura.total_factura ?? 0),
      saldo_pendiente: Number(factura.saldo_pendiente ?? 0),
      fecha_emision: factura.dfeemide,
      fecha_vencimiento: fechaVencimientoMin,
      dias_mora: diasMora,
      moneda_codigo: factura.moneda?.codigo ?? 'PYG',
      cuotas,
    };
  }

  async listarCuotas(facturaId: string): Promise<Cuota[]> {
    const rows = await this.prisma.factura_cuotas.findMany({
      where: { factura_cab_id: facturaId },
      orderBy: { nro_cuota: 'asc' },
    });
    return rows.map((c) => ({
      id: c.id,
      numero: c.nro_cuota ?? 0,
      monto: Number(c.dmoncuota),
      saldo: Number(c.saldo_pendiente ?? 0),
      fecha_vencimiento: c.dvenccuo,
      estado: (c.estado as Cuota['estado']) ?? 'pendiente',
    }));
  }

  async obtenerSaldoVencido(_clienteId: string): Promise<number> {
    throw new NotImplementedException('Sprint 1 — usar query agregada de mesa-gestion');
  }

  async obtenerHistorialCobros(_clienteId: string): Promise<Recibo[]> {
    throw new NotImplementedException('Sprint 1 — delegar a RecibosService');
  }

  async registrarCobro(_input: RegistrarCobroInput): Promise<ReciboCobro> {
    if (this.modo === 'co-pilot') {
      throw new ForbiddenException(
        'Modo co-pilot: el cobro se registra en el sistema del cliente, no acá.',
      );
    }
    throw new NotImplementedException('Sprint 1 — delegar a CobrosService existente');
  }
}
