import { BadRequestException, ConflictException, Injectable, NotFoundException } from '@nestjs/common';
import { inm_operacion_estado, Prisma } from '@prisma/client';
import { AuditService } from 'src/audit/audit.service';
import { PrismaService } from 'src/prisma/prisma.service';
import { ComisionesInmService } from '../comisiones/comisiones.service';
import { sumarMeses } from '../common/cronograma';
import { aFecha, decimalesDe, hoyPy, redondear } from '../common/inm-utils';
import { ParametrosInmService } from '../config/parametros.service';
import {
  EntregaDto,
  EscrituraDto,
  FirmarEscrituraDto,
  InscribirEscrituraDto,
  ObservacionDto,
  TareaDto,
  UpdateEntregaDto,
  UpdateObservacionDto,
  UpdateTareaDto,
} from './dto/postventa.dto';

/** Checklist por defecto de la escrituración (spec M117). Se edita por operación. */
export const TAREAS_ESCRITURA_DEFAULT = [
  'Elegir escribanía y fecha de firma',
  'Tasación (si hay hipoteca)',
  'Certificados de no adeudar (municipalidad, servicios)',
  'Impuesto inmobiliario al día',
  'Liquidación de impuestos y honorarios de la transferencia',
  'Firma de la escritura',
  'Inscripción en el Registro de Inmuebles',
];

const ESCRITURABLE: inm_operacion_estado[] = ['BOLETO_FIRMADO', 'CANCELADA_TOTAL', 'ENTREGADA'];
const ENTREGABLE: inm_operacion_estado[] = ['BOLETO_FIRMADO', 'CANCELADA_TOTAL', 'ESCRITURADA'];

/**
 * Escrituración (M117) y entrega con postventa (M118).
 *
 * Escritura y posesión pueden ir en cualquier orden (en pozo, la posesión suele ir
 * antes): la operación queda ENTREGADA si ya se entregó, ESCRITURADA si sólo se
 * escrituró. Escriturar con saldo exige hipoteca por el saldo.
 */
@Injectable()
export class PostventaInmService {
  constructor(
    private readonly prisma: PrismaService,
    private readonly audit: AuditService,
    private readonly parametros: ParametrosInmService,
    private readonly comisiones: ComisionesInmService,
  ) {}

  // ---------------------------------------------------------------- escritura

  async obtenerEscritura(operacion_id: string, empresa_id: string) {
    await this.operacion(operacion_id, empresa_id);
    return this.prisma.inm_escrituras.findFirst({
      where: { operacion_id, empresa_id },
      include: { inm_escritura_tareas: { orderBy: [{ orden: 'asc' }, { nombre: 'asc' }] } },
    });
  }

  /** Abre (o actualiza) el expediente de escritura con el checklist por defecto. */
  async guardarEscritura(operacion_id: string, dto: EscrituraDto, empresa_id: string, user_id?: string) {
    const op = await this.operacion(operacion_id, empresa_id);
    if (op.estado === 'DESISTIDA' || op.estado === 'RESCINDIDA' || op.estado === 'RESERVADA') {
      throw new BadRequestException('La escrituración empieza con el boleto firmado');
    }
    const data = {
      ...(dto.escribania !== undefined && { escribania: dto.escribania?.trim() || null }),
      ...(dto.escribano !== undefined && { escribano: dto.escribano?.trim() || null }),
      ...(dto.contacto !== undefined && { contacto: dto.contacto?.trim() || null }),
      ...(dto.fecha_prevista !== undefined && { fecha_prevista: aFecha(dto.fecha_prevista) }),
      ...(dto.hipoteca !== undefined && { hipoteca: dto.hipoteca }),
      ...(dto.hipoteca_monto !== undefined && { hipoteca_monto: dto.hipoteca_monto }),
      ...(dto.hipoteca_acreedor !== undefined && { hipoteca_acreedor: dto.hipoteca_acreedor?.trim() || null }),
      ...(dto.notas !== undefined && { notas: dto.notas?.trim() || null }),
    };
    const previa = await this.prisma.inm_escrituras.findFirst({ where: { operacion_id, empresa_id } });
    const escritura = previa
      ? await this.prisma.inm_escrituras.update({ where: { id: previa.id }, data: { ...data, updated_at: new Date() } })
      : await this.prisma.inm_escrituras.create({
          data: {
            empresa_id,
            operacion_id,
            ...data,
            created_by: user_id ?? null,
            inm_escritura_tareas: {
              create: TAREAS_ESCRITURA_DEFAULT.map((nombre, i) => ({ empresa_id, nombre, orden: i + 1 })),
            },
          },
        });
    await this.audit.log({
      empresa_id,
      user_id,
      action: previa ? 'EDITAR' : 'CREAR',
      entity_type: 'inm_escrituras',
      entity_id: escritura.id,
      old_value: previa ?? undefined,
      new_value: escritura,
    });
    return this.obtenerEscritura(operacion_id, empresa_id);
  }

  async firmarEscritura(operacion_id: string, dto: FirmarEscrituraDto, empresa_id: string, user_id?: string) {
    const op = await this.operacion(operacion_id, empresa_id);
    if (!ESCRITURABLE.includes(op.estado))
      throw new BadRequestException(
        `No se puede escriturar una operación ${op.estado.toLowerCase().replace('_', ' ')}`,
      );
    const esc = await this.prisma.inm_escrituras.findFirst({ where: { operacion_id, empresa_id } });
    if (!esc) throw new BadRequestException('Cargá primero los datos de la escritura (escribanía)');
    if (esc.fecha_firma) throw new ConflictException('La escritura ya está firmada');
    const fecha = dto.fecha_firma.slice(0, 10);
    if (fecha > hoyPy()) throw new BadRequestException('La fecha de firma no puede ser futura');

    const saldo = await this.saldo(op.id, Number(op.precio_final), op.moneda_id);
    if (saldo > 0 && !esc.hipoteca) {
      throw new BadRequestException(
        `La venta tiene saldo (${saldo}): para escriturar antes de cancelar, marcá la hipoteca por el saldo`,
      );
    }

    await this.prisma.$transaction(async (tx) => {
      await tx.inm_escrituras.update({
        where: { id: esc.id },
        data: {
          fecha_firma: aFecha(fecha),
          numero_escritura: dto.numero_escritura?.trim() || esc.numero_escritura,
          ...(saldo > 0 && !esc.hipoteca_monto && { hipoteca_monto: saldo }),
          updated_at: new Date(),
        },
      });
      await tx.inm_escritura_tareas.updateMany({
        where: { escritura_id: esc.id, empresa_id, nombre: 'Firma de la escritura', estado: 'PENDIENTE' },
        data: { estado: 'HECHA', fecha: aFecha(fecha), updated_at: new Date() },
      });
      await tx.inm_operaciones.update({
        where: { id: op.id },
        data: {
          fecha_escritura: aFecha(fecha),
          ...(op.estado !== 'ENTREGADA' && { estado: 'ESCRITURADA' }),
          updated_at: new Date(),
        },
      });
      if (op.estado !== 'ENTREGADA') {
        await tx.inm_unidades.updateMany({
          where: { id: op.unidad_id, empresa_id, estado: 'VENDIDA' },
          data: { estado: 'ESCRITURADA', version: { increment: 1 }, updated_at: new Date() },
        });
      }
    });
    await this.comisiones.devengar(op.id, empresa_id);
    await this.audit.log({
      empresa_id,
      user_id,
      action: 'CONFIRMAR',
      entity_type: 'inm_escrituras',
      entity_id: esc.id,
      new_value: { fecha_firma: fecha, saldo_hipoteca: saldo },
    });
    return this.obtenerEscritura(operacion_id, empresa_id);
  }

  async inscribirEscritura(operacion_id: string, dto: InscribirEscrituraDto, empresa_id: string, user_id?: string) {
    const esc = await this.prisma.inm_escrituras.findFirst({ where: { operacion_id, empresa_id } });
    if (!esc?.fecha_firma) throw new BadRequestException('Primero se firma la escritura');
    if (dto.fecha_inscripcion.slice(0, 10) > hoyPy())
      throw new BadRequestException('La fecha de inscripción no puede ser futura');
    await this.prisma.$transaction([
      this.prisma.inm_escrituras.update({
        where: { id: esc.id },
        data: { fecha_inscripcion: aFecha(dto.fecha_inscripcion), updated_at: new Date() },
      }),
      this.prisma.inm_escritura_tareas.updateMany({
        where: { escritura_id: esc.id, empresa_id, nombre: { startsWith: 'Inscripción' }, estado: 'PENDIENTE' },
        data: { estado: 'HECHA', fecha: aFecha(dto.fecha_inscripcion), updated_at: new Date() },
      }),
    ]);
    await this.audit.log({
      empresa_id,
      user_id,
      action: 'EDITAR',
      entity_type: 'inm_escrituras',
      entity_id: esc.id,
      new_value: { ...dto },
    });
    return this.obtenerEscritura(operacion_id, empresa_id);
  }

  async agregarTarea(operacion_id: string, dto: TareaDto, empresa_id: string) {
    const esc = await this.prisma.inm_escrituras.findFirst({ where: { operacion_id, empresa_id } });
    if (!esc) throw new BadRequestException('Cargá primero los datos de la escritura');
    await this.prisma.inm_escritura_tareas.create({
      data: {
        empresa_id,
        escritura_id: esc.id,
        nombre: dto.nombre.trim(),
        orden: dto.orden ?? 99,
        estado: dto.estado ?? 'PENDIENTE',
        fecha: aFecha(dto.fecha) ?? null,
        responsable: dto.responsable?.trim() || null,
        notas: dto.notas?.trim() || null,
      },
    });
    return this.obtenerEscritura(operacion_id, empresa_id);
  }

  async actualizarTarea(tarea_id: string, dto: UpdateTareaDto, empresa_id: string) {
    const t = await this.prisma.inm_escritura_tareas.findFirst({
      where: { id: tarea_id, empresa_id },
      include: { inm_escrituras: { select: { operacion_id: true } } },
    });
    if (!t) throw new NotFoundException('Tarea no encontrada');
    await this.prisma.inm_escritura_tareas.update({
      where: { id: tarea_id },
      data: {
        ...(dto.nombre !== undefined && { nombre: dto.nombre.trim() }),
        ...(dto.orden !== undefined && { orden: dto.orden }),
        ...(dto.estado !== undefined && { estado: dto.estado }),
        ...(dto.fecha !== undefined && { fecha: aFecha(dto.fecha) }),
        ...(dto.estado === 'HECHA' && dto.fecha === undefined && !t.fecha && { fecha: aFecha(hoyPy()) }),
        ...(dto.responsable !== undefined && { responsable: dto.responsable?.trim() || null }),
        ...(dto.notas !== undefined && { notas: dto.notas?.trim() || null }),
        updated_at: new Date(),
      },
    });
    return this.obtenerEscritura(t.inm_escrituras.operacion_id, empresa_id);
  }

  async eliminarTarea(tarea_id: string, empresa_id: string) {
    const t = await this.prisma.inm_escritura_tareas.findFirst({
      where: { id: tarea_id, empresa_id },
      include: { inm_escrituras: { select: { operacion_id: true } } },
    });
    if (!t) throw new NotFoundException('Tarea no encontrada');
    await this.prisma.inm_escritura_tareas.delete({ where: { id: tarea_id } });
    return this.obtenerEscritura(t.inm_escrituras.operacion_id, empresa_id);
  }

  // ------------------------------------------------------------------ entrega

  async obtenerEntrega(operacion_id: string, empresa_id: string) {
    await this.operacion(operacion_id, empresa_id);
    return this.prisma.inm_entregas.findFirst({
      where: { operacion_id, empresa_id },
      include: { inm_entrega_observaciones: { orderBy: [{ estado: 'asc' }, { created_at: 'asc' }] } },
    });
  }

  /**
   * Entrega de la unidad con su acta. Las cuotas "contra posesión" sin fecha pasan a
   * vencer el día de la entrega, y empieza a correr la garantía.
   */
  async registrarEntrega(operacion_id: string, dto: EntregaDto, empresa_id: string, user_id?: string) {
    const op = await this.operacion(operacion_id, empresa_id);
    if (!ENTREGABLE.includes(op.estado))
      throw new BadRequestException(`No se puede entregar una operación ${op.estado.toLowerCase().replace('_', ' ')}`);
    const fecha = dto.fecha.slice(0, 10);
    if (fecha > hoyPy()) throw new BadRequestException('La fecha de entrega no puede ser futura');
    const params = await this.parametros.obtener(empresa_id);
    try {
      const entrega = await this.prisma.$transaction(async (tx) => {
        const e = await tx.inm_entregas.create({
          data: {
            empresa_id,
            operacion_id,
            fecha: aFecha(fecha),
            recibido_por: dto.recibido_por.trim(),
            acta_firmada: dto.acta_firmada ?? false,
            garantia_hasta: aFecha(sumarMeses(fecha, params.meses_garantia)),
            notas: dto.notas?.trim() || null,
            created_by: user_id ?? null,
            inm_entrega_observaciones: {
              create: (dto.observaciones ?? []).map((o) => ({
                empresa_id,
                descripcion: o.descripcion.trim(),
                rubro: o.rubro?.trim() || null,
                responsable: o.responsable?.trim() || null,
                notas: o.notas?.trim() || null,
              })),
            },
          },
        });
        await tx.inm_operacion_cuotas.updateMany({
          where: { operacion_id, tipo: 'POSESION', fecha_vencimiento: null, estado: { in: ['PENDIENTE', 'PARCIAL'] } },
          data: { fecha_vencimiento: aFecha(fecha), updated_at: new Date() },
        });
        await tx.inm_operaciones.update({
          where: { id: op.id },
          data: { estado: 'ENTREGADA', fecha_entrega: aFecha(fecha), updated_at: new Date() },
        });
        await tx.inm_unidades.updateMany({
          where: { id: op.unidad_id, empresa_id, estado: { in: ['VENDIDA', 'ESCRITURADA'] } },
          data: { estado: 'ENTREGADA', version: { increment: 1 }, updated_at: new Date() },
        });
        return e;
      });
      await this.comisiones.devengar(op.id, empresa_id);
      await this.audit.log({
        empresa_id,
        user_id,
        action: 'CONFIRMAR',
        entity_type: 'inm_entregas',
        entity_id: entrega.id,
        new_value: { fecha, observaciones: dto.observaciones?.length ?? 0 },
      });
      return this.obtenerEntrega(operacion_id, empresa_id);
    } catch (e) {
      if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === 'P2002')
        throw new ConflictException('La unidad ya se entregó');
      throw e;
    }
  }

  async actualizarEntrega(operacion_id: string, dto: UpdateEntregaDto, empresa_id: string, user_id?: string) {
    const e = await this.prisma.inm_entregas.findFirst({ where: { operacion_id, empresa_id } });
    if (!e) throw new NotFoundException('La unidad todavía no se entregó');
    await this.prisma.inm_entregas.update({
      where: { id: e.id },
      data: {
        ...(dto.acta_firmada !== undefined && { acta_firmada: dto.acta_firmada }),
        ...(dto.recibido_por !== undefined && { recibido_por: dto.recibido_por.trim() }),
        ...(dto.notas !== undefined && { notas: dto.notas?.trim() || null }),
        updated_at: new Date(),
      },
    });
    await this.audit.log({
      empresa_id,
      user_id,
      action: 'EDITAR',
      entity_type: 'inm_entregas',
      entity_id: e.id,
      old_value: e,
      new_value: { ...dto },
    });
    return this.obtenerEntrega(operacion_id, empresa_id);
  }

  async agregarObservacion(operacion_id: string, dto: ObservacionDto, empresa_id: string) {
    const e = await this.prisma.inm_entregas.findFirst({ where: { operacion_id, empresa_id } });
    if (!e) throw new BadRequestException('La unidad todavía no se entregó');
    await this.prisma.inm_entrega_observaciones.create({
      data: {
        empresa_id,
        entrega_id: e.id,
        descripcion: dto.descripcion.trim(),
        rubro: dto.rubro?.trim() || null,
        responsable: dto.responsable?.trim() || null,
        notas: dto.notas?.trim() || null,
      },
    });
    return this.obtenerEntrega(operacion_id, empresa_id);
  }

  async actualizarObservacion(obs_id: string, dto: UpdateObservacionDto, empresa_id: string, user_id?: string) {
    const o = await this.prisma.inm_entrega_observaciones.findFirst({
      where: { id: obs_id, empresa_id },
      include: { inm_entregas: { select: { operacion_id: true } } },
    });
    if (!o) throw new NotFoundException('Observación no encontrada');
    const resuelve = dto.estado === 'RESUELTA' && o.estado !== 'RESUELTA';
    await this.prisma.inm_entrega_observaciones.update({
      where: { id: obs_id },
      data: {
        ...(dto.descripcion !== undefined && { descripcion: dto.descripcion.trim() }),
        ...(dto.rubro !== undefined && { rubro: dto.rubro?.trim() || null }),
        ...(dto.responsable !== undefined && { responsable: dto.responsable?.trim() || null }),
        ...(dto.notas !== undefined && { notas: dto.notas?.trim() || null }),
        ...(dto.estado !== undefined && { estado: dto.estado }),
        ...(resuelve && { fecha_resuelta: aFecha(dto.fecha_resuelta ?? hoyPy()) }),
        ...(dto.estado === 'PENDIENTE' && { fecha_resuelta: null }),
        updated_at: new Date(),
      },
    });
    await this.audit.log({
      empresa_id,
      user_id,
      action: 'EDITAR',
      entity_type: 'inm_entrega_observaciones',
      entity_id: obs_id,
      old_value: o,
      new_value: { ...dto },
    });
    return this.obtenerEntrega(o.inm_entregas.operacion_id, empresa_id);
  }

  /** Para la tarea diaria: garantías que vencen en los próximos días. */
  garantiasPorVencer(empresa_id: string, dias = 30) {
    const hasta = new Date(`${hoyPy()}T00:00:00Z`);
    hasta.setUTCDate(hasta.getUTCDate() + dias);
    return this.prisma.inm_entregas.findMany({
      where: { empresa_id, garantia_hasta: { gte: aFecha(hoyPy()), lte: hasta } },
      include: { inm_operaciones: { select: { id: true, numero: true, inm_unidades: { select: { codigo: true } } } } },
    });
  }

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

  private async operacion(id: string, empresa_id: string) {
    const op = await this.prisma.inm_operaciones.findFirst({ where: { id, empresa_id } });
    if (!op) throw new NotFoundException('Operación no encontrada');
    return op;
  }

  private async saldo(operacion_id: string, precio: number, moneda_id: string) {
    const [cuotas, moneda] = await Promise.all([
      this.prisma.inm_operacion_cuotas.findMany({
        where: { operacion_id, estado: { not: 'ANULADA' } },
        select: { monto_cobrado: true },
      }),
      this.prisma.moneda.findUnique({ where: { id: moneda_id }, select: { codigo: true } }),
    ]);
    return redondear(precio - cuotas.reduce((a, c) => a + Number(c.monto_cobrado), 0), decimalesDe(moneda?.codigo));
  }
}
