import { BadRequestException, ConflictException, Injectable, NotFoundException } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { AuditService } from 'src/audit/audit.service';
import { PrismaService } from 'src/prisma/prisma.service';
import { decimalesDe, redondear } from '../common/inm-utils';
import { CreatePartidaDto, LotePartidasDto, UpdatePartidaDto } from './dto/obra.dto';
import { prorratearPorSuperficie, situacionPartida, superficieDe } from './obra-calculos';

type Cliente = PrismaService | Prisma.TransactionClient;

/** Proyecto con su moneda; todos los montos de obra van en esa moneda. */
export async function proyectoObra(prisma: Cliente, empresa_id: string, proyecto_id: string) {
  const p = await prisma.inm_proyectos.findFirst({
    where: { id: proyecto_id, empresa_id },
    select: { id: true, codigo: true, nombre: true, moneda: { select: { codigo: true } } },
  });
  if (!p) throw new NotFoundException('Proyecto no encontrado');
  const moneda_codigo = p.moneda?.codigo ?? 'PYG';
  return { id: p.id, codigo: p.codigo, nombre: p.nombre, moneda_codigo, dec: decimalesDe(moneda_codigo) };
}

/**
 * Presupuesto de obra por partidas (M120) y sus reportes: presupuesto contra
 * ejecutado y costo por unidad prorrateado por superficie.
 *
 * El ejecutado sale de `inm_imputaciones` (compras, gastos, certificados aprobados
 * y cargas manuales). Compras y gastos se leen; no se modifican.
 */
@Injectable()
export class PresupuestoObraInmService {
  constructor(
    private readonly prisma: PrismaService,
    private readonly audit: AuditService,
  ) {}

  // ----------------------------------------------------------------- partidas

  async crearPartida(proyecto_id: string, dto: CreatePartidaDto, empresa_id: string, user_id?: string) {
    const [p] = await this.crearPartidas(proyecto_id, [dto], empresa_id, user_id);
    return p;
  }

  async crearLote(proyecto_id: string, dto: LotePartidasDto, empresa_id: string, user_id?: string) {
    const creadas = await this.crearPartidas(proyecto_id, dto.partidas, empresa_id, user_id);
    return { creadas: creadas.length };
  }

  private async crearPartidas(proyecto_id: string, partidas: CreatePartidaDto[], empresa_id: string, user_id?: string) {
    await proyectoObra(this.prisma, empresa_id, proyecto_id);
    const codigos = partidas.map((p) => p.codigo.trim());
    const repetidos = codigos.filter((c, i) => codigos.indexOf(c) !== i);
    if (repetidos.length)
      throw new BadRequestException(`Códigos repetidos en la carga: ${[...new Set(repetidos)].join(', ')}`);
    const existentes = await this.prisma.inm_partidas.findMany({
      where: { proyecto_id, empresa_id, codigo: { in: codigos } },
      select: { codigo: true },
    });
    if (existentes.length)
      throw new ConflictException(`Ya existen en el proyecto: ${existentes.map((e) => e.codigo).join(', ')}`);
    await this.validarEtapas(
      empresa_id,
      proyecto_id,
      partidas.map((p) => p.etapa_id),
    );
    const ultimo = await this.prisma.inm_partidas.aggregate({
      where: { proyecto_id, empresa_id },
      _max: { orden: true },
    });
    let orden = ultimo._max.orden ?? 0;
    const creadas = await this.prisma.$transaction(
      partidas.map((p) =>
        this.prisma.inm_partidas.create({
          data: {
            empresa_id,
            proyecto_id,
            etapa_id: p.etapa_id ?? null,
            codigo: p.codigo.trim(),
            rubro: p.rubro.trim(),
            nombre: p.nombre.trim(),
            monto_presupuestado: p.monto_presupuestado,
            orden: p.orden ?? (orden += 10),
            created_by: user_id ?? null,
          },
        }),
      ),
    );
    await this.audit.log({
      empresa_id,
      user_id,
      action: 'CREAR',
      entity_type: 'inm_partidas',
      entity_id: creadas[0].id,
      new_value: { proyecto_id, codigos },
    });
    return creadas;
  }

  async actualizarPartida(id: string, dto: UpdatePartidaDto, empresa_id: string, user_id?: string) {
    const p = await this.prisma.inm_partidas.findFirst({ where: { id, empresa_id, activo: true } });
    if (!p) throw new NotFoundException('Partida no encontrada');
    if (dto.codigo && dto.codigo.trim() !== p.codigo) {
      const otra = await this.prisma.inm_partidas.findFirst({
        where: { proyecto_id: p.proyecto_id, empresa_id, codigo: dto.codigo.trim(), id: { not: id } },
        select: { id: true },
      });
      if (otra) throw new ConflictException(`Ya existe la partida ${dto.codigo.trim()} en el proyecto`);
    }
    if (dto.etapa_id) await this.validarEtapas(empresa_id, p.proyecto_id, [dto.etapa_id]);
    const r = await this.prisma.inm_partidas.update({
      where: { id },
      data: {
        ...(dto.codigo !== undefined && { codigo: dto.codigo.trim() }),
        ...(dto.rubro !== undefined && { rubro: dto.rubro.trim() }),
        ...(dto.nombre !== undefined && { nombre: dto.nombre.trim() }),
        ...(dto.monto_presupuestado !== undefined && { monto_presupuestado: dto.monto_presupuestado }),
        ...(dto.pct_avance !== undefined && { pct_avance: dto.pct_avance }),
        ...(dto.orden !== undefined && { orden: dto.orden }),
        ...(dto.etapa_id !== undefined && { etapa_id: dto.etapa_id }),
        updated_at: new Date(),
      },
    });
    await this.audit.log({
      empresa_id,
      user_id,
      action: 'EDITAR',
      entity_type: 'inm_partidas',
      entity_id: id,
      old_value: {
        monto_presupuestado: Number(p.monto_presupuestado),
        pct_avance: Number(p.pct_avance),
        nombre: p.nombre,
      },
      new_value: dto as Record<string, unknown>,
    });
    return r;
  }

  async bajaPartida(id: string, empresa_id: string, user_id?: string) {
    const p = await this.prisma.inm_partidas.findFirst({ where: { id, empresa_id, activo: true } });
    if (!p) throw new NotFoundException('Partida no encontrada');
    const [imputaciones, certificados] = await Promise.all([
      this.prisma.inm_imputaciones.count({ where: { partida_id: id, empresa_id } }),
      this.prisma.inm_certificados.count({ where: { partida_id: id, empresa_id, estado: { not: 'ANULADO' } } }),
    ]);
    if (imputaciones || certificados)
      throw new BadRequestException(
        'La partida tiene costos imputados o certificados: quitá esas imputaciones o anulá los certificados antes de darla de baja',
      );
    await this.prisma.inm_partidas.update({ where: { id }, data: { activo: false, updated_at: new Date() } });
    await this.audit.log({
      empresa_id,
      user_id,
      action: 'ELIMINAR',
      entity_type: 'inm_partidas',
      entity_id: id,
      old_value: { codigo: p.codigo, nombre: p.nombre },
    });
    return { ok: true };
  }

  /** Listado liviano para los selectores de certificados y costos. */
  async listarPartidas(proyecto_id: string, empresa_id: string) {
    await proyectoObra(this.prisma, empresa_id, proyecto_id);
    return this.prisma.inm_partidas.findMany({
      where: { proyecto_id, empresa_id, activo: true },
      select: { id: true, codigo: true, rubro: true, nombre: true, pct_avance: true, etapa_id: true },
      orderBy: [{ orden: 'asc' }, { codigo: 'asc' }],
    });
  }

  // ----------------------------------------------------------------- reportes

  /** Presupuesto contra ejecutado, por partida, por rubro y del proyecto. */
  async resumen(proyecto_id: string, empresa_id: string) {
    const proyecto = await proyectoObra(this.prisma, empresa_id, proyecto_id);
    const dec = proyecto.dec;
    const [partidas, ejecutado, certificado, fondo] = await Promise.all([
      this.prisma.inm_partidas.findMany({
        where: { proyecto_id, empresa_id, activo: true },
        include: { inm_etapas: { select: { id: true, nombre: true } } },
        orderBy: [{ orden: 'asc' }, { codigo: 'asc' }],
      }),
      this.prisma.inm_imputaciones.groupBy({
        by: ['partida_id'],
        where: { proyecto_id, empresa_id },
        _sum: { monto: true },
      }),
      this.prisma.inm_certificados.groupBy({
        by: ['partida_id'],
        where: { proyecto_id, empresa_id, estado: 'APROBADO' },
        _sum: { monto_bruto: true },
      }),
      this.prisma.inm_certificados.groupBy({
        by: ['partida_id'],
        where: { proyecto_id, empresa_id, estado: 'APROBADO', fondo_reparo_devuelto_at: null },
        _sum: { fondo_reparo: true },
      }),
    ]);
    const porPartida = <T>(filas: T[], campo: (f: T) => unknown, id: (f: T) => string) =>
      new Map(filas.map((f) => [id(f), Number(campo(f) ?? 0)]));
    const ej = porPartida(
      ejecutado,
      (f) => f._sum.monto,
      (f) => f.partida_id,
    );
    const cert = porPartida(
      certificado,
      (f) => f._sum.monto_bruto,
      (f) => f.partida_id,
    );
    const fr = porPartida(
      fondo,
      (f) => f._sum.fondo_reparo,
      (f) => f.partida_id,
    );

    const filas = partidas.map((p) => {
      const presupuestado = Number(p.monto_presupuestado);
      const pct_avance = Number(p.pct_avance);
      const ejecutadoP = ej.get(p.id) ?? 0;
      return {
        id: p.id,
        codigo: p.codigo,
        rubro: p.rubro,
        nombre: p.nombre,
        orden: p.orden,
        etapa: p.inm_etapas,
        presupuestado,
        pct_avance,
        ejecutado: ejecutadoP,
        certificado: cert.get(p.id) ?? 0,
        fondo_reparo_retenido: fr.get(p.id) ?? 0,
        ...situacionPartida({ presupuestado, ejecutado: ejecutadoP, pct_avance }, dec),
      };
    });

    const totalizar = (xs: typeof filas) => {
      const presupuestado = redondear(
        xs.reduce((a, x) => a + x.presupuestado, 0),
        dec,
      );
      const ejecutadoT = redondear(
        xs.reduce((a, x) => a + x.ejecutado, 0),
        dec,
      );
      // Avance físico ponderado por presupuesto: una partida chica al 100 % no
      // mueve el avance de la obra como una grande.
      const pct_avance =
        presupuestado > 0
          ? redondear(xs.reduce((a, x) => a + x.presupuestado * x.pct_avance, 0) / presupuestado, 2)
          : 0;
      return {
        presupuestado,
        ejecutado: ejecutadoT,
        certificado: redondear(
          xs.reduce((a, x) => a + x.certificado, 0),
          dec,
        ),
        fondo_reparo_retenido: redondear(
          xs.reduce((a, x) => a + x.fondo_reparo_retenido, 0),
          dec,
        ),
        pct_avance,
        ...situacionPartida({ presupuestado, ejecutado: ejecutadoT, pct_avance }, dec),
      };
    };
    const rubros = [...new Set(filas.map((f) => f.rubro))].map((rubro) => ({
      rubro,
      ...totalizar(filas.filter((f) => f.rubro === rubro)),
    }));
    const sinPartida = await this.prisma.inm_imputaciones.count({
      where: { proyecto_id, empresa_id, inm_partidas: { activo: false } },
    });
    return {
      proyecto: { id: proyecto.id, codigo: proyecto.codigo, nombre: proyecto.nombre },
      moneda: { codigo: proyecto.moneda_codigo },
      partidas: filas,
      rubros,
      totales: totalizar(filas),
      imputaciones_en_partidas_de_baja: sinPartida,
    };
  }

  /**
   * Costo por unidad prorrateado por superficie. Las partidas de una etapa se
   * reparten sólo entre las unidades de esa etapa; las generales, entre todas.
   */
  async costoPorUnidad(proyecto_id: string, empresa_id: string) {
    const proyecto = await proyectoObra(this.prisma, empresa_id, proyecto_id);
    const dec = proyecto.dec;
    const [unidades, resumen] = await Promise.all([
      this.prisma.inm_unidades.findMany({
        where: { proyecto_id, empresa_id, activo: true },
        select: {
          id: true,
          codigo: true,
          tipo: true,
          estado: true,
          etapa_id: true,
          m2_propios: true,
          m2_comunes: true,
          superficie_terreno: true,
          precio_lista: true,
          moneda: { select: { codigo: true } },
        },
        orderBy: { codigo: 'asc' },
      }),
      this.resumen(proyecto_id, empresa_id),
    ]);
    const base = unidades.map((u) => ({ id: u.id, etapa_id: u.etapa_id, m2: superficieDe(u) }));
    const costos = new Map(unidades.map((u) => [u.id, { presupuestado: 0, ejecutado: 0 }]));

    const grupos = new Map<string | null, { presupuestado: number; ejecutado: number }>();
    for (const p of resumen.partidas) {
      const k = p.etapa?.id ?? null;
      const g = grupos.get(k) ?? { presupuestado: 0, ejecutado: 0 };
      g.presupuestado += p.presupuestado;
      g.ejecutado += p.ejecutado;
      grupos.set(k, g);
    }
    for (const [etapa_id, g] of grupos) {
      const delGrupo = etapa_id ? base.filter((u) => u.etapa_id === etapa_id && u.m2 > 0) : [];
      // Etapa sin unidades con superficie: se reparte entre todas, para no perder costo.
      const destino = delGrupo.length ? delGrupo : base;
      for (const campo of ['presupuestado', 'ejecutado'] as const) {
        for (const r of prorratearPorSuperficie(redondear(g[campo], dec), destino, dec)) {
          const c = costos.get(r.id);
          if (c) c[campo] = redondear(c[campo] + r.costo, dec);
        }
      }
    }

    const superficie_total = base.reduce((a, u) => a + u.m2, 0);
    return {
      proyecto: { id: proyecto.id, codigo: proyecto.codigo, nombre: proyecto.nombre },
      moneda: { codigo: proyecto.moneda_codigo },
      superficie_total: redondear(superficie_total, 2),
      sin_superficie: base.filter((u) => !(u.m2 > 0)).map((u) => unidades.find((x) => x.id === u.id)?.codigo),
      totales: { presupuestado: resumen.totales.presupuestado, ejecutado: resumen.totales.ejecutado },
      unidades: unidades.map((u) => {
        const m2 = superficieDe(u);
        const c = costos.get(u.id) ?? { presupuestado: 0, ejecutado: 0 };
        const conMargen = (u.moneda?.codigo ?? 'PYG') === proyecto.moneda_codigo && m2 > 0;
        const precio = Number(u.precio_lista);
        return {
          id: u.id,
          codigo: u.codigo,
          tipo: u.tipo,
          estado: u.estado,
          m2,
          participacion: superficie_total > 0 ? redondear((m2 / superficie_total) * 100, 4) : 0,
          costo_presupuestado: c.presupuestado,
          costo_ejecutado: c.ejecutado,
          costo_m2: m2 > 0 ? redondear(c.presupuestado / m2, dec) : null,
          precio_lista: precio,
          moneda_precio: u.moneda?.codigo ?? 'PYG',
          // Margen sólo con precio y costo en la misma moneda (convertir con la cotización
          // de hoy un costo de años engaña) y con superficie (sin ella no recibe costo).
          margen: conMargen && precio > 0 ? redondear(precio - c.presupuestado, dec) : null,
          pct_margen: conMargen && precio > 0 ? redondear(((precio - c.presupuestado) / precio) * 100, 2) : null,
        };
      }),
    };
  }

  private async validarEtapas(empresa_id: string, proyecto_id: string, etapas: Array<string | null | undefined>) {
    const ids = [...new Set(etapas.filter((e): e is string => Boolean(e)))];
    if (!ids.length) return;
    const n = await this.prisma.inm_etapas.count({ where: { id: { in: ids }, proyecto_id, empresa_id } });
    if (n !== ids.length) throw new BadRequestException('La etapa no pertenece al proyecto');
  }
}
