import { BadRequestException, ConflictException, Injectable, NotFoundException } from '@nestjs/common';
import { inm_proyecto_estado, inm_titular_tipo, Prisma } from '@prisma/client';
import { AuditService } from 'src/audit/audit.service';
import { PrismaService } from 'src/prisma/prisma.service';
import {
  CreateEtapaInmDto,
  CreateHitoDto,
  CumplirHitoDto,
  UpdateEtapaInmDto,
  UpdateHitoDto,
} from './dto/etapa-hito.dto';
import { CreateProyectoDto, UpdateProyectoDto } from './dto/proyecto.dto';

/** 'AAAA-MM-DD' (o ISO) → fecha sin hora; null borra, undefined no toca. */
function aFecha(valor: string | null | undefined): Date | null | undefined {
  if (valor === undefined) return undefined;
  if (valor === null || valor === '') return null;
  return new Date(`${valor.slice(0, 10)}T00:00:00.000Z`);
}

/** Código del centro de costo que nace con el proyecto. */
export function codigoCentroCosto(codigoProyecto: string): string {
  return `INM-${codigoProyecto.toUpperCase()}`.slice(0, 20);
}

/**
 * Proyectos, etapas e hitos de obra (M111).
 *
 * Nada se borra físicamente: las unidades y operaciones (F1) apuntan al proyecto
 * y un hito cumplido habilita cuotas. La baja es lógica, como en el resto del ERP.
 */
@Injectable()
export class ProyectosInmService {
  constructor(
    private readonly prisma: PrismaService,
    private readonly audit: AuditService,
  ) {}

  // ---------------------------------------------------------------- proyectos

  listar(empresa_id: string, filtros: { estado?: inm_proyecto_estado; q?: string; incluir_inactivos?: boolean } = {}) {
    const q = filtros.q?.trim();
    return this.prisma.inm_proyectos.findMany({
      where: {
        empresa_id,
        ...(filtros.incluir_inactivos ? {} : { activo: true }),
        ...(filtros.estado && { estado: filtros.estado }),
        ...(q && {
          OR: [
            { nombre: { contains: q, mode: 'insensitive' } },
            { codigo: { contains: q, mode: 'insensitive' } },
            { ciudad: { contains: q, mode: 'insensitive' } },
          ],
        }),
      },
      include: {
        moneda: { select: { id: true, codigo: true } },
        centro_costo: { select: { id: true, codigo: true, descripcion: true } },
        _count: { select: { inm_etapas: { where: { activo: true } }, inm_hitos: { where: { activo: true } } } },
      },
      orderBy: [{ activo: 'desc' }, { nombre: 'asc' }],
    });
  }

  async detalle(id: string, empresa_id: string) {
    const proyecto = await this.prisma.inm_proyectos.findFirst({
      where: { id, empresa_id },
      include: {
        moneda: { select: { id: true, codigo: true } },
        centro_costo: { select: { id: true, codigo: true, descripcion: true } },
        cuenta_bancaria: { select: { id: true, nombre: true, moneda: true } },
        inm_etapas: { where: { activo: true }, orderBy: [{ orden: 'asc' }, { nombre: 'asc' }] },
        inm_hitos: { where: { activo: true }, orderBy: [{ orden: 'asc' }, { fecha_estimada: 'asc' }] },
      },
    });
    if (!proyecto) throw new NotFoundException('Proyecto no encontrado');
    return proyecto;
  }

  async crear(dto: CreateProyectoDto, empresa_id: string, user_id?: string) {
    const codigo = dto.codigo.trim().toUpperCase();
    this.validarTitular(dto.titular_tipo ?? inm_titular_tipo.PROPIO, dto.fiduciaria_nombre);
    await this.validarReferencias(empresa_id, dto);

    const existente = await this.prisma.inm_proyectos.findFirst({
      where: { empresa_id, codigo },
      select: { id: true },
    });
    if (existente) throw new ConflictException(`Ya existe un proyecto con el código ${codigo}`);

    const moneda_id =
      dto.moneda_id ??
      (await this.prisma.moneda.findFirst({ where: { codigo: 'PYG' }, select: { id: true } }))?.id ??
      null;

    const proyecto = await this.prisma.$transaction(async (tx) => {
      // Resultado por proyecto desde el día uno: si no eligieron centro de costo,
      // nace uno con el código del proyecto (o se reusa si ya existía).
      let centro_costo_id = dto.centro_costo_id ?? null;
      if (!centro_costo_id) {
        const cc = codigoCentroCosto(codigo);
        const previo = await tx.cont_centros_costo.findFirst({
          where: { empresa_id, codigo: cc },
          select: { id: true },
        });
        centro_costo_id =
          previo?.id ??
          (
            await tx.cont_centros_costo.create({
              data: { empresa_id, codigo: cc, descripcion: `Proyecto ${dto.nombre.trim()}`.slice(0, 200) },
              select: { id: true },
            })
          ).id;
      }

      return tx.inm_proyectos.create({
        data: {
          empresa_id,
          codigo,
          nombre: dto.nombre.trim(),
          tipo: dto.tipo,
          descripcion: dto.descripcion?.trim() || null,
          direccion: dto.direccion?.trim() || null,
          ciudad: dto.ciudad?.trim() || null,
          titular_tipo: dto.titular_tipo ?? inm_titular_tipo.PROPIO,
          fiduciaria_nombre: dto.fiduciaria_nombre?.trim() || null,
          fiduciaria_ruc: dto.fiduciaria_ruc?.trim() || null,
          centro_costo_id,
          cuenta_bancaria_id: dto.cuenta_bancaria_id ?? null,
          moneda_id,
          fecha_inicio: aFecha(dto.fecha_inicio) ?? null,
          fecha_entrega_estimada: aFecha(dto.fecha_entrega_estimada) ?? null,
          estado: dto.estado ?? inm_proyecto_estado.PLANIFICACION,
          created_by: user_id ?? null,
        },
      });
    });

    await this.audit.log({
      empresa_id,
      user_id,
      action: 'CREAR',
      entity_type: 'inm_proyectos',
      entity_id: proyecto.id,
      new_value: proyecto,
    });
    return this.detalle(proyecto.id, empresa_id);
  }

  async actualizar(id: string, dto: UpdateProyectoDto, empresa_id: string, user_id?: string) {
    const anterior = await this.deEmpresa(id, empresa_id);
    this.validarTitular(
      dto.titular_tipo ?? anterior.titular_tipo,
      dto.fiduciaria_nombre !== undefined ? dto.fiduciaria_nombre : anterior.fiduciaria_nombre,
    );
    await this.validarReferencias(empresa_id, dto);

    const data: Prisma.inm_proyectosUncheckedUpdateInput = { updated_at: new Date() };
    for (const campo of [
      'nombre',
      'descripcion',
      'direccion',
      'ciudad',
      'fiduciaria_nombre',
      'fiduciaria_ruc',
    ] as const) {
      if (dto[campo] !== undefined) data[campo] = dto[campo]?.trim() || (campo === 'nombre' ? anterior.nombre : null);
    }
    for (const campo of [
      'tipo',
      'titular_tipo',
      'estado',
      'activo',
      'centro_costo_id',
      'cuenta_bancaria_id',
      'moneda_id',
    ] as const) {
      if (dto[campo] !== undefined) (data as Record<string, unknown>)[campo] = dto[campo];
    }
    if (dto.fecha_inicio !== undefined) data.fecha_inicio = aFecha(dto.fecha_inicio);
    if (dto.fecha_entrega_estimada !== undefined) data.fecha_entrega_estimada = aFecha(dto.fecha_entrega_estimada);

    const proyecto = await this.prisma.inm_proyectos.update({ where: { id }, data });
    await this.audit.log({
      empresa_id,
      user_id,
      action: 'EDITAR',
      entity_type: 'inm_proyectos',
      entity_id: id,
      old_value: anterior,
      new_value: proyecto,
    });
    return this.detalle(id, empresa_id);
  }

  async darDeBaja(id: string, empresa_id: string, user_id?: string) {
    return this.actualizar(id, { activo: false }, empresa_id, user_id);
  }

  // ------------------------------------------------------------------- etapas

  async crearEtapa(proyecto_id: string, dto: CreateEtapaInmDto, empresa_id: string, user_id?: string) {
    await this.deEmpresa(proyecto_id, empresa_id);
    const etapa = await this.prisma.inm_etapas.create({
      data: {
        empresa_id,
        proyecto_id,
        nombre: dto.nombre.trim(),
        orden: dto.orden ?? 0,
        fecha_entrega_estimada: aFecha(dto.fecha_entrega_estimada) ?? null,
        fecha_entrega_real: aFecha(dto.fecha_entrega_real) ?? null,
        pct_avance: dto.pct_avance ?? 0,
        created_by: user_id ?? null,
      },
    });
    await this.audit.log({
      empresa_id,
      user_id,
      action: 'CREAR',
      entity_type: 'inm_etapas',
      entity_id: etapa.id,
      new_value: etapa,
    });
    return etapa;
  }

  async actualizarEtapa(id: string, dto: UpdateEtapaInmDto, empresa_id: string, user_id?: string) {
    const anterior = await this.etapaDeEmpresa(id, empresa_id);
    const etapa = await this.prisma.inm_etapas.update({
      where: { id },
      data: {
        ...(dto.nombre !== undefined && { nombre: dto.nombre.trim() }),
        ...(dto.orden !== undefined && { orden: dto.orden }),
        ...(dto.pct_avance !== undefined && { pct_avance: dto.pct_avance }),
        ...(dto.fecha_entrega_estimada !== undefined && { fecha_entrega_estimada: aFecha(dto.fecha_entrega_estimada) }),
        ...(dto.fecha_entrega_real !== undefined && { fecha_entrega_real: aFecha(dto.fecha_entrega_real) }),
        updated_at: new Date(),
      },
    });
    await this.audit.log({
      empresa_id,
      user_id,
      action: 'EDITAR',
      entity_type: 'inm_etapas',
      entity_id: id,
      old_value: anterior,
      new_value: etapa,
    });
    return etapa;
  }

  async darDeBajaEtapa(id: string, empresa_id: string, user_id?: string) {
    const anterior = await this.etapaDeEmpresa(id, empresa_id);
    const hitosVivos = await this.prisma.inm_hitos.count({ where: { etapa_id: id, empresa_id, activo: true } });
    if (hitosVivos > 0) {
      throw new BadRequestException(
        `La etapa tiene ${hitosVivos} hito(s). Pasalos a otra etapa o dalos de baja primero.`,
      );
    }
    const etapa = await this.prisma.inm_etapas.update({
      where: { id },
      data: { activo: false, updated_at: new Date() },
    });
    await this.audit.log({
      empresa_id,
      user_id,
      action: 'ELIMINAR',
      entity_type: 'inm_etapas',
      entity_id: id,
      old_value: anterior,
    });
    return etapa;
  }

  // -------------------------------------------------------------------- hitos

  async crearHito(proyecto_id: string, dto: CreateHitoDto, empresa_id: string, user_id?: string) {
    await this.deEmpresa(proyecto_id, empresa_id);
    if (dto.etapa_id) await this.validarEtapaDelProyecto(dto.etapa_id, proyecto_id, empresa_id);
    const hito = await this.prisma.inm_hitos.create({
      data: {
        empresa_id,
        proyecto_id,
        etapa_id: dto.etapa_id ?? null,
        nombre: dto.nombre.trim(),
        orden: dto.orden ?? 0,
        fecha_estimada: aFecha(dto.fecha_estimada) ?? null,
        created_by: user_id ?? null,
      },
    });
    await this.audit.log({
      empresa_id,
      user_id,
      action: 'CREAR',
      entity_type: 'inm_hitos',
      entity_id: hito.id,
      new_value: hito,
    });
    return hito;
  }

  async actualizarHito(id: string, dto: UpdateHitoDto, empresa_id: string, user_id?: string) {
    const anterior = await this.hitoDeEmpresa(id, empresa_id);
    if (dto.etapa_id) await this.validarEtapaDelProyecto(dto.etapa_id, anterior.proyecto_id, empresa_id);
    const hito = await this.prisma.inm_hitos.update({
      where: { id },
      data: {
        ...(dto.nombre !== undefined && { nombre: dto.nombre.trim() }),
        ...(dto.orden !== undefined && { orden: dto.orden }),
        ...(dto.etapa_id !== undefined && { etapa_id: dto.etapa_id || null }),
        ...(dto.fecha_estimada !== undefined && { fecha_estimada: aFecha(dto.fecha_estimada) }),
        updated_at: new Date(),
      },
    });
    await this.audit.log({
      empresa_id,
      user_id,
      action: 'EDITAR',
      entity_type: 'inm_hitos',
      entity_id: id,
      old_value: anterior,
      new_value: hito,
    });
    return hito;
  }

  /** Dar un hito por cumplido. En F1 esto habilita el cobro de las cuotas atadas a él. */
  async cumplirHito(id: string, dto: CumplirHitoDto, empresa_id: string, user_id?: string) {
    const anterior = await this.hitoDeEmpresa(id, empresa_id);
    if (anterior.fecha_cumplido) throw new ConflictException('El hito ya está cumplido');
    const hito = await this.prisma.inm_hitos.update({
      where: { id },
      data: {
        fecha_cumplido: aFecha(dto.fecha_cumplido) ?? aFecha(new Date().toISOString()),
        cumplido_por: user_id ?? null,
        updated_at: new Date(),
      },
    });
    await this.audit.log({
      empresa_id,
      user_id,
      action: 'CONFIRMAR',
      entity_type: 'inm_hitos',
      entity_id: id,
      old_value: anterior,
      new_value: hito,
    });
    return hito;
  }

  async revertirHito(id: string, empresa_id: string, user_id?: string) {
    const anterior = await this.hitoDeEmpresa(id, empresa_id);
    if (!anterior.fecha_cumplido) throw new ConflictException('El hito no está cumplido');
    const hito = await this.prisma.inm_hitos.update({
      where: { id },
      data: { fecha_cumplido: null, cumplido_por: null, updated_at: new Date() },
    });
    await this.audit.log({
      empresa_id,
      user_id,
      action: 'REVERTIR',
      entity_type: 'inm_hitos',
      entity_id: id,
      old_value: anterior,
      new_value: hito,
    });
    return hito;
  }

  async darDeBajaHito(id: string, empresa_id: string, user_id?: string) {
    const anterior = await this.hitoDeEmpresa(id, empresa_id);
    const hito = await this.prisma.inm_hitos.update({ where: { id }, data: { activo: false, updated_at: new Date() } });
    await this.audit.log({
      empresa_id,
      user_id,
      action: 'ELIMINAR',
      entity_type: 'inm_hitos',
      entity_id: id,
      old_value: anterior,
    });
    return hito;
  }

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

  private validarTitular(titular: inm_titular_tipo, fiduciaria?: string | null) {
    if (titular === inm_titular_tipo.FIDEICOMISO && !fiduciaria?.trim()) {
      throw new BadRequestException('Un proyecto en fideicomiso necesita el nombre de la fiduciaria');
    }
  }

  private async validarReferencias(
    empresa_id: string,
    dto: { centro_costo_id?: string | null; cuenta_bancaria_id?: string | null; moneda_id?: string | null },
  ) {
    if (dto.centro_costo_id) {
      const cc = await this.prisma.cont_centros_costo.findFirst({
        where: { id: dto.centro_costo_id, empresa_id },
        select: { id: true },
      });
      if (!cc) throw new BadRequestException('El centro de costo no existe en esta empresa');
    }
    if (dto.cuenta_bancaria_id) {
      const cuenta = await this.prisma.tes_cuentas.findFirst({
        where: { id: dto.cuenta_bancaria_id, empresa_id },
        select: { id: true },
      });
      if (!cuenta) throw new BadRequestException('La cuenta de Tesorería no existe en esta empresa');
    }
    if (dto.moneda_id) {
      const moneda = await this.prisma.moneda.findUnique({ where: { id: dto.moneda_id }, select: { id: true } });
      if (!moneda) throw new BadRequestException('La moneda no existe');
    }
  }

  private async deEmpresa(id: string, empresa_id: string) {
    const proyecto = await this.prisma.inm_proyectos.findFirst({ where: { id, empresa_id } });
    if (!proyecto) throw new NotFoundException('Proyecto no encontrado');
    return proyecto;
  }

  private async etapaDeEmpresa(id: string, empresa_id: string) {
    const etapa = await this.prisma.inm_etapas.findFirst({ where: { id, empresa_id } });
    if (!etapa) throw new NotFoundException('Etapa no encontrada');
    return etapa;
  }

  private async hitoDeEmpresa(id: string, empresa_id: string) {
    const hito = await this.prisma.inm_hitos.findFirst({ where: { id, empresa_id } });
    if (!hito) throw new NotFoundException('Hito no encontrado');
    return hito;
  }

  private async validarEtapaDelProyecto(etapa_id: string, proyecto_id: string, empresa_id: string) {
    const etapa = await this.prisma.inm_etapas.findFirst({
      where: { id: etapa_id, proyecto_id, empresa_id, activo: true },
      select: { id: true },
    });
    if (!etapa) throw new BadRequestException('La etapa no pertenece a este proyecto');
  }
}
