import { BadRequestException, ConflictException, Injectable, NotFoundException } from '@nestjs/common';
import { inm_certificado_estado } from '@prisma/client';
import { AuditService } from 'src/audit/audit.service';
import { PrismaService } from 'src/prisma/prisma.service';
import { aFecha, hoyPy, isoDia } from '../common/inm-utils';
import { NumeracionInmService } from '../common/numeracion-inm.service';
import { ParametrosInmService } from '../config/parametros.service';
import {
  AnularCertificadoDto,
  CreateCertificadoDto,
  DevolverFondoDto,
  UpdateCertificadoDto,
  VincularFacturaDto,
} from './dto/obra.dto';
import { calcularCertificado } from './obra-calculos';
import { proyectoObra } from './presupuesto-obra.service';

/**
 * Certificados de avance de contratistas (M120).
 *
 * BORRADOR → APROBADO: al aprobar se imputa el monto bruto a la partida (el fondo
 * de reparo también es costo, sólo que se paga después) y el avance físico de la
 * partida sube al del certificado. Anular un aprobado quita esa imputación.
 *
 * El pago al contratista no sale de acá: la factura del contratista se carga en
 * Compras y se vincula al certificado, y se paga desde Tesorería como cualquier
 * compra. Esa compra no se imputa aparte (ya está en la obra por el certificado).
 */
@Injectable()
export class CertificadosObraInmService {
  constructor(
    private readonly prisma: PrismaService,
    private readonly audit: AuditService,
    private readonly numeracion: NumeracionInmService,
    private readonly parametros: ParametrosInmService,
  ) {}

  async listar(
    empresa_id: string,
    f: { proyecto_id?: string; partida_id?: string; proveedor_id?: string; estado?: inm_certificado_estado },
  ) {
    const filas = await this.prisma.inm_certificados.findMany({
      where: {
        empresa_id,
        ...(f.proyecto_id && { proyecto_id: f.proyecto_id }),
        ...(f.partida_id && { partida_id: f.partida_id }),
        ...(f.proveedor_id && { proveedor_id: f.proveedor_id }),
        ...(f.estado && { estado: f.estado }),
      },
      include: {
        inm_partidas: { select: { id: true, codigo: true, nombre: true, rubro: true } },
        inm_proyectos: { select: { id: true, codigo: true, nombre: true, moneda: { select: { codigo: true } } } },
      },
      orderBy: [{ fecha: 'desc' }, { numero: 'desc' }],
      take: 500,
    });
    const [proveedores, compras] = await Promise.all([
      this.nombresProveedores(
        empresa_id,
        filas.map((c) => c.proveedor_id),
      ),
      this.datosCompras(
        empresa_id,
        filas.map((c) => c.compra_cab_id),
      ),
    ]);
    return filas.map((c) => this.armar(c, proveedores, compras));
  }

  async detalle(id: string, empresa_id: string) {
    const c = await this.prisma.inm_certificados.findFirst({
      where: { id, empresa_id },
      include: {
        inm_partidas: { select: { id: true, codigo: true, nombre: true, rubro: true, pct_avance: true } },
        inm_proyectos: { select: { id: true, codigo: true, nombre: true, moneda: { select: { codigo: true } } } },
      },
    });
    if (!c) throw new NotFoundException('Certificado no encontrado');
    const [proveedores, compras] = await Promise.all([
      this.nombresProveedores(empresa_id, [c.proveedor_id]),
      this.datosCompras(empresa_id, [c.compra_cab_id]),
    ]);
    return this.armar(c, proveedores, compras);
  }

  async crear(dto: CreateCertificadoDto, empresa_id: string, user_id?: string) {
    const partida = await this.prisma.inm_partidas.findFirst({
      where: { id: dto.partida_id, empresa_id, activo: true },
    });
    if (!partida) throw new BadRequestException('La partida no existe o está de baja');
    await this.validarProveedor(empresa_id, dto.proveedor_id);
    const proyecto = await proyectoObra(this.prisma, empresa_id, partida.proyecto_id);
    const params = await this.parametros.obtener(empresa_id);
    const pct_fondo_reparo = dto.pct_fondo_reparo ?? Number(params.pct_fondo_reparo);
    const pct_retencion = dto.pct_retencion ?? 0;
    const montos = calcularCertificado({ monto_bruto: dto.monto_bruto, pct_fondo_reparo, pct_retencion }, proyecto.dec);

    const c = await this.prisma.$transaction(async (tx) => {
      const numero = await this.numeracion.siguiente(empresa_id, 'CER', tx);
      return tx.inm_certificados.create({
        data: {
          empresa_id,
          numero,
          proyecto_id: partida.proyecto_id,
          partida_id: partida.id,
          proveedor_id: dto.proveedor_id,
          fecha: aFecha(dto.fecha),
          descripcion: dto.descripcion?.trim() || null,
          pct_avance: dto.pct_avance ?? null,
          monto_bruto: dto.monto_bruto,
          pct_fondo_reparo,
          pct_retencion,
          ...montos,
          created_by: user_id ?? null,
        },
      });
    });
    await this.audit.log({
      empresa_id,
      user_id,
      action: 'CREAR',
      entity_type: 'inm_certificados',
      entity_id: c.id,
      new_value: { numero: c.numero, partida: partida.codigo, monto_bruto: dto.monto_bruto },
    });
    return this.detalle(c.id, empresa_id);
  }

  async actualizar(id: string, dto: UpdateCertificadoDto, empresa_id: string, user_id?: string) {
    const c = await this.buscar(id, empresa_id);
    if (c.estado !== 'BORRADOR') throw new BadRequestException('Sólo se modifica un certificado en borrador');
    if (dto.proveedor_id && dto.proveedor_id !== c.proveedor_id) {
      if (c.compra_cab_id) throw new BadRequestException('Desvinculá la factura antes de cambiar el contratista');
      await this.validarProveedor(empresa_id, dto.proveedor_id);
    }
    const proyecto = await proyectoObra(this.prisma, empresa_id, c.proyecto_id);
    const monto_bruto = dto.monto_bruto ?? Number(c.monto_bruto);
    const pct_fondo_reparo = dto.pct_fondo_reparo ?? Number(c.pct_fondo_reparo);
    const pct_retencion = dto.pct_retencion ?? Number(c.pct_retencion);
    const montos = calcularCertificado({ monto_bruto, pct_fondo_reparo, pct_retencion }, proyecto.dec);
    const r = await this.prisma.inm_certificados.updateMany({
      where: { id, empresa_id, estado: 'BORRADOR' },
      data: {
        ...(dto.proveedor_id && { proveedor_id: dto.proveedor_id }),
        ...(dto.fecha && { fecha: aFecha(dto.fecha) }),
        ...(dto.descripcion !== undefined && { descripcion: dto.descripcion?.trim() || null }),
        ...(dto.pct_avance !== undefined && { pct_avance: dto.pct_avance }),
        monto_bruto,
        pct_fondo_reparo,
        pct_retencion,
        ...montos,
        updated_at: new Date(),
      },
    });
    if (!r.count) throw new ConflictException('El certificado cambió de estado mientras lo editabas. Recargá.');
    await this.audit.log({
      empresa_id,
      user_id,
      action: 'EDITAR',
      entity_type: 'inm_certificados',
      entity_id: id,
      old_value: { monto_bruto: Number(c.monto_bruto), pct_avance: c.pct_avance && Number(c.pct_avance) },
      new_value: dto as Record<string, unknown>,
    });
    return this.detalle(id, empresa_id);
  }

  async aprobar(id: string, empresa_id: string, user_id?: string) {
    const c = await this.buscar(id, empresa_id);
    if (c.estado !== 'BORRADOR') throw new BadRequestException('Sólo se aprueba un certificado en borrador');
    const partida = await this.prisma.inm_partidas.findFirst({ where: { id: c.partida_id, empresa_id } });
    if (!partida?.activo) throw new BadRequestException('La partida del certificado está de baja');
    if (c.pct_avance !== null) {
      // El avance de un certificado es acumulado: no puede ser menor al último
      // aprobado del mismo contratista en la misma partida.
      const previo = await this.prisma.inm_certificados.findFirst({
        where: { empresa_id, partida_id: c.partida_id, proveedor_id: c.proveedor_id, estado: 'APROBADO' },
        orderBy: [{ fecha: 'desc' }, { numero: 'desc' }],
        select: { numero: true, pct_avance: true },
      });
      if (previo?.pct_avance !== null && previo && Number(previo.pct_avance) > Number(c.pct_avance)) {
        throw new BadRequestException(
          `El avance (${Number(c.pct_avance)} %) es menor al del certificado ${previo.numero} (${Number(previo.pct_avance)} %). El avance de un certificado es acumulado.`,
        );
      }
    }
    await this.prisma.$transaction(async (tx) => {
      const r = await tx.inm_certificados.updateMany({
        where: { id, empresa_id, estado: 'BORRADOR' },
        data: { estado: 'APROBADO', aprobado_por: user_id ?? null, aprobado_at: new Date(), updated_at: new Date() },
      });
      if (!r.count) throw new ConflictException('El certificado ya cambió de estado. Recargá.');
      await tx.inm_imputaciones.create({
        data: {
          empresa_id,
          proyecto_id: c.proyecto_id,
          partida_id: c.partida_id,
          origen: 'CERTIFICADO',
          certificado_id: c.id,
          fecha: c.fecha,
          descripcion: `Certificado ${c.numero}`,
          monto: c.monto_bruto,
          created_by: user_id ?? null,
        },
      });
      if (c.pct_avance !== null && Number(c.pct_avance) > Number(partida.pct_avance)) {
        await tx.inm_partidas.updateMany({
          where: { id: c.partida_id, empresa_id },
          data: { pct_avance: c.pct_avance, updated_at: new Date() },
        });
      }
    });
    await this.audit.log({
      empresa_id,
      user_id,
      action: 'APROBAR',
      entity_type: 'inm_certificados',
      entity_id: id,
      new_value: { numero: c.numero, monto_bruto: Number(c.monto_bruto) },
    });
    return this.detalle(id, empresa_id);
  }

  async anular(id: string, dto: AnularCertificadoDto, empresa_id: string, user_id?: string) {
    const c = await this.buscar(id, empresa_id);
    if (c.estado === 'ANULADO') throw new BadRequestException('El certificado ya está anulado');
    if (c.fondo_reparo_devuelto_at)
      throw new BadRequestException('El fondo de reparo ya se devolvió: el certificado no se puede anular');
    await this.prisma.$transaction(async (tx) => {
      const r = await tx.inm_certificados.updateMany({
        where: { id, empresa_id, estado: c.estado },
        data: {
          estado: 'ANULADO',
          motivo_anulacion: dto.motivo.trim(),
          compra_cab_id: null,
          updated_at: new Date(),
        },
      });
      if (!r.count) throw new ConflictException('El certificado ya cambió de estado. Recargá.');
      await tx.inm_imputaciones.deleteMany({ where: { certificado_id: id, empresa_id } });
    });
    await this.audit.log({
      empresa_id,
      user_id,
      action: 'ANULAR',
      entity_type: 'inm_certificados',
      entity_id: id,
      old_value: { estado: c.estado, compra_cab_id: c.compra_cab_id },
      new_value: { motivo: dto.motivo },
    });
    // El avance físico de la partida no se baja solo: puede venir de otro
    // certificado o de una carga manual. Se avisa para que lo revisen.
    return {
      ...(await this.detalle(id, empresa_id)),
      aviso:
        c.estado === 'APROBADO' && c.pct_avance !== null
          ? 'Revisá el avance físico de la partida: no se modifica al anular el certificado.'
          : null,
    };
  }

  /** Vincula (o quita) la factura del contratista cargada en Compras. */
  async vincularFactura(id: string, dto: VincularFacturaDto, empresa_id: string, user_id?: string) {
    const c = await this.buscar(id, empresa_id);
    if (c.estado === 'ANULADO') throw new BadRequestException('El certificado está anulado');
    if (dto.compra_cab_id) {
      const compra = await this.prisma.compra_cab.findFirst({
        where: { id: dto.compra_cab_id, empresa_id },
        select: { id: true, proveedor_id: true, anulado: true, estado: true, numero_factura: true },
      });
      if (!compra) throw new BadRequestException('La compra no existe en esta empresa');
      if (compra.anulado || compra.estado === 'anulada') throw new BadRequestException('La compra está anulada');
      if (compra.proveedor_id !== c.proveedor_id)
        throw new BadRequestException('La factura es de otro proveedor que el contratista del certificado');
      const [otro, imputada] = await Promise.all([
        this.prisma.inm_certificados.findFirst({
          where: { empresa_id, compra_cab_id: compra.id, estado: { not: 'ANULADO' }, id: { not: id } },
          select: { numero: true },
        }),
        this.prisma.inm_imputaciones.count({ where: { empresa_id, compra_cab_id: compra.id } }),
      ]);
      if (otro) throw new ConflictException(`La factura ya está vinculada al certificado ${otro.numero}`);
      if (imputada)
        throw new ConflictException(
          'La factura ya está imputada a una partida. Quitá esa imputación: el costo entra a la obra por el certificado.',
        );
    }
    await this.prisma.inm_certificados.updateMany({
      where: { id, empresa_id },
      data: { compra_cab_id: dto.compra_cab_id ?? null, updated_at: new Date() },
    });
    await this.audit.log({
      empresa_id,
      user_id,
      action: 'EDITAR',
      entity_type: 'inm_certificados',
      entity_id: id,
      old_value: { compra_cab_id: c.compra_cab_id },
      new_value: { compra_cab_id: dto.compra_cab_id ?? null },
    });
    return this.detalle(id, empresa_id);
  }

  /** Registra que el fondo de reparo se devolvió al contratista (el pago va por Tesorería). */
  async devolverFondo(id: string, dto: DevolverFondoDto, empresa_id: string, user_id?: string) {
    const c = await this.buscar(id, empresa_id);
    if (c.estado !== 'APROBADO') throw new BadRequestException('Sólo se devuelve el fondo de un certificado aprobado');
    if (!(Number(c.fondo_reparo) > 0)) throw new BadRequestException('El certificado no retuvo fondo de reparo');
    if (c.fondo_reparo_devuelto_at) throw new BadRequestException('El fondo de reparo ya se devolvió');
    if (dto.fecha.slice(0, 10) < (isoDia(c.fecha) ?? '')) {
      throw new BadRequestException('La devolución no puede ser anterior al certificado');
    }
    if (dto.fecha.slice(0, 10) > hoyPy()) throw new BadRequestException('La fecha de devolución no puede ser futura');
    await this.prisma.inm_certificados.updateMany({
      where: { id, empresa_id },
      data: { fondo_reparo_devuelto_at: aFecha(dto.fecha), updated_at: new Date() },
    });
    await this.audit.log({
      empresa_id,
      user_id,
      action: 'EDITAR',
      entity_type: 'inm_certificados',
      entity_id: id,
      new_value: { fondo_reparo_devuelto_at: dto.fecha, fondo_reparo: Number(c.fondo_reparo) },
    });
    return this.detalle(id, empresa_id);
  }

  // ---------------------------------------------------------------- auxiliares

  private async buscar(id: string, empresa_id: string) {
    const c = await this.prisma.inm_certificados.findFirst({ where: { id, empresa_id } });
    if (!c) throw new NotFoundException('Certificado no encontrado');
    return c;
  }

  private async validarProveedor(empresa_id: string, proveedor_id: string) {
    const p = await this.prisma.proveedores.findFirst({
      where: { id: proveedor_id, empresa_id },
      select: { id: true, activo: true },
    });
    if (!p) throw new BadRequestException('El contratista no existe como proveedor de esta empresa');
    if (p.activo === false) throw new BadRequestException('El proveedor está inactivo');
  }

  private async nombresProveedores(empresa_id: string, ids: string[]) {
    const unicos = [...new Set(ids)];
    if (!unicos.length) return new Map<string, { id: string; nombre: string; ruc: string | null }>();
    const ps = await this.prisma.proveedores.findMany({
      where: { id: { in: unicos }, empresa_id },
      select: { id: true, personas: { select: { razon_social: true, ruc: true, dv: true } } },
    });
    return new Map(
      ps.map((p) => [
        p.id,
        {
          id: p.id,
          nombre: p.personas?.razon_social ?? 'Proveedor',
          ruc: p.personas?.ruc ? `${p.personas.ruc}${p.personas.dv ? `-${p.personas.dv}` : ''}` : null,
        },
      ]),
    );
  }

  private async datosCompras(empresa_id: string, ids: Array<string | null>) {
    const unicos = [...new Set(ids.filter((x): x is string => Boolean(x)))];
    if (!unicos.length) return new Map<string, { id: string; numero_factura: string | null; total: number }>();
    const cs = await this.prisma.compra_cab.findMany({
      where: { id: { in: unicos }, empresa_id },
      select: {
        id: true,
        establecimiento: true,
        punto_expedicion: true,
        numero_factura: true,
        fecha_emision: true,
        total: true,
        estado: true,
        moneda: { select: { codigo: true } },
      },
    });
    return new Map(
      cs.map((c) => [
        c.id,
        {
          id: c.id,
          numero_factura: numeroComprobante(c),
          fecha_emision: isoDia(c.fecha_emision),
          total: Number(c.total ?? 0),
          estado: c.estado,
          moneda: c.moneda?.codigo ?? 'PYG',
        },
      ]),
    );
  }

  private armar<
    T extends {
      proveedor_id: string;
      compra_cab_id: string | null;
      fecha: Date;
      fondo_reparo_devuelto_at: Date | null;
      inm_proyectos: { moneda: { codigo: string | null } | null };
    },
  >(c: T, proveedores: Map<string, unknown>, compras: Map<string, unknown>) {
    return {
      ...c,
      fecha: isoDia(c.fecha),
      fondo_reparo_devuelto_at: isoDia(c.fondo_reparo_devuelto_at),
      moneda: { codigo: c.inm_proyectos.moneda?.codigo ?? 'PYG' },
      proveedor: proveedores.get(c.proveedor_id) ? { ...(proveedores.get(c.proveedor_id) as object) } : null,
      compra: c.compra_cab_id && compras.get(c.compra_cab_id) ? { ...(compras.get(c.compra_cab_id) as object) } : null,
    };
  }
}

/** 001-001-0000123 cuando están las partes; si no, el número tal cual se cargó. */
export function numeroComprobante(c: {
  establecimiento?: string | null;
  punto_expedicion?: string | null;
  numero_factura?: string | null;
}) {
  if (!c.numero_factura) return null;
  if (c.establecimiento && c.punto_expedicion && !c.numero_factura.includes('-'))
    return `${c.establecimiento}-${c.punto_expedicion}-${c.numero_factura}`;
  return c.numero_factura;
}
