import { BadRequestException, ConflictException, Injectable, NotFoundException } from '@nestjs/common';
import { inm_unidad_estado, Prisma } from '@prisma/client';
import { plainToInstance } from 'class-transformer';
import { validateSync } from 'class-validator';
import { AuditService } from 'src/audit/audit.service';
import { PrismaService } from 'src/prisma/prisma.service';
import { transicionarUnidad } from '../common/inm-utils';
import {
  BloquearUnidadDto,
  CambiarPrecioDto,
  CreateUnidadDto,
  FilaImportacionUnidadDto,
  ImportarUnidadesDto,
  UpdateUnidadDto,
} from './dto/unidad.dto';

const CAMPOS_FISICOS = [
  'tipo',
  'piso',
  'numero',
  'tipologia',
  'dormitorios',
  'banos',
  'm2_propios',
  'm2_comunes',
  'superficie_terreno',
  'manzana',
  'lote',
  'orientacion',
  'cta_catastral',
  'finca',
  'atributos',
] as const;

/** Estados en los que la unidad no está comprometida con ninguna venta. */
export const ESTADOS_LIBRES: inm_unidad_estado[] = ['BORRADOR', 'DISPONIBLE', 'BLOQUEADA'];

/**
 * Inventario de unidades (M112). Una unidad NO es un producto con stock: vive acá
 * y al facturar se usa el producto de servicio configurado en los parámetros.
 *
 * Toda transición de estado pasa por `transicionarUnidad` (bloqueo optimista).
 */
@Injectable()
export class UnidadesInmService {
  constructor(
    private readonly prisma: PrismaService,
    private readonly audit: AuditService,
  ) {}

  listar(
    empresa_id: string,
    f: { proyecto_id?: string; estado?: inm_unidad_estado; etapa_id?: string; q?: string; incluir_inactivas?: boolean },
  ) {
    const q = f.q?.trim();
    return this.prisma.inm_unidades.findMany({
      where: {
        empresa_id,
        ...(f.incluir_inactivas ? {} : { activo: true }),
        ...(f.proyecto_id && { proyecto_id: f.proyecto_id }),
        ...(f.etapa_id && { etapa_id: f.etapa_id }),
        ...(f.estado && { estado: f.estado }),
        ...(q && {
          OR: [
            { codigo: { contains: q, mode: 'insensitive' } },
            { tipologia: { contains: q, mode: 'insensitive' } },
            { manzana: { contains: q, mode: 'insensitive' } },
          ],
        }),
      },
      include: {
        moneda: { select: { id: true, codigo: true } },
        inm_etapas: { select: { id: true, nombre: true } },
        inm_proyectos: { select: { id: true, codigo: true, nombre: true } },
        unidad_padre: { select: { id: true, codigo: true } },
      },
      orderBy: [{ piso: { sort: 'desc', nulls: 'last' } }, { codigo: 'asc' }],
    });
  }

  async detalle(id: string, empresa_id: string) {
    const unidad = await this.prisma.inm_unidades.findFirst({
      where: { id, empresa_id },
      include: {
        moneda: { select: { id: true, codigo: true } },
        inm_etapas: { select: { id: true, nombre: true } },
        inm_proyectos: { select: { id: true, codigo: true, nombre: true } },
        unidad_padre: { select: { id: true, codigo: true } },
        vinculadas: { where: { activo: true }, select: { id: true, codigo: true, tipo: true, estado: true } },
        inm_unidad_precios: { orderBy: { created_at: 'desc' }, take: 30 },
        inm_operaciones: {
          where: { estado: { notIn: ['DESISTIDA', 'RESCINDIDA'] } },
          select: { id: true, numero: true, estado: true },
        },
      },
    });
    if (!unidad) throw new NotFoundException('Unidad no encontrada');
    return unidad;
  }

  async crear(dto: CreateUnidadDto, empresa_id: string, user_id?: string) {
    const proyecto = await this.proyectoDeEmpresa(dto.proyecto_id, empresa_id);
    await this.validarReferencias(empresa_id, dto.proyecto_id, dto);
    const codigo = dto.codigo.trim().toUpperCase();
    const precio = dto.precio_lista ?? 0;
    try {
      const unidad = await this.prisma.$transaction(async (tx) => {
        const u = await tx.inm_unidades.create({
          data: {
            empresa_id,
            proyecto_id: dto.proyecto_id,
            codigo,
            etapa_id: dto.etapa_id ?? null,
            unidad_padre_id: dto.unidad_padre_id ?? null,
            ...this.camposFisicos(dto),
            precio_lista: precio,
            moneda_id: dto.moneda_id ?? proyecto.moneda_id,
            estado: dto.publicar !== false && precio > 0 ? 'DISPONIBLE' : 'BORRADOR',
            created_by: user_id ?? null,
          },
        });
        if (precio > 0) {
          await tx.inm_unidad_precios.create({
            data: {
              empresa_id,
              unidad_id: u.id,
              precio_anterior: null,
              precio,
              motivo: 'Precio inicial',
              usuario_id: user_id ?? null,
            },
          });
        }
        return u;
      });
      await this.audit.log({
        empresa_id,
        user_id,
        action: 'CREAR',
        entity_type: 'inm_unidades',
        entity_id: unidad.id,
        new_value: unidad,
      });
      return unidad;
    } catch (e) {
      if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === 'P2002') {
        throw new ConflictException(`Ya existe la unidad ${codigo} en este proyecto`);
      }
      throw e;
    }
  }

  async actualizar(id: string, dto: UpdateUnidadDto, empresa_id: string, user_id?: string) {
    const anterior = await this.deEmpresa(id, empresa_id);
    await this.validarReferencias(empresa_id, anterior.proyecto_id, dto);
    try {
      const unidad = await this.prisma.inm_unidades.update({
        where: { id },
        data: {
          ...(dto.codigo !== undefined && { codigo: dto.codigo.trim().toUpperCase() }),
          ...(dto.etapa_id !== undefined && { etapa_id: dto.etapa_id || null }),
          ...(dto.unidad_padre_id !== undefined && { unidad_padre_id: dto.unidad_padre_id || null }),
          ...(dto.moneda_id !== undefined && ESTADOS_LIBRES.includes(anterior.estado) && { moneda_id: dto.moneda_id }),
          ...this.camposFisicos(dto),
          updated_at: new Date(),
        },
      });
      await this.audit.log({
        empresa_id,
        user_id,
        action: 'EDITAR',
        entity_type: 'inm_unidades',
        entity_id: id,
        old_value: anterior,
        new_value: unidad,
      });
      return unidad;
    } catch (e) {
      if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === 'P2002') {
        throw new ConflictException('Ya existe otra unidad con ese código en el proyecto');
      }
      throw e;
    }
  }

  /** Cambio de precio de lista con historial. No toca ventas ya reservadas: esas tienen su precio. */
  async cambiarPrecio(id: string, dto: CambiarPrecioDto, empresa_id: string, user_id?: string) {
    const anterior = await this.deEmpresa(id, empresa_id);
    if (Number(anterior.precio_lista) === dto.precio) throw new BadRequestException('El precio es el mismo');
    const unidad = await this.prisma.$transaction(async (tx) => {
      await tx.inm_unidad_precios.create({
        data: {
          empresa_id,
          unidad_id: id,
          precio_anterior: anterior.precio_lista,
          precio: dto.precio,
          motivo: dto.motivo.trim(),
          usuario_id: user_id ?? null,
        },
      });
      return tx.inm_unidades.update({ where: { id }, data: { precio_lista: dto.precio, updated_at: new Date() } });
    });
    await this.audit.log({
      empresa_id,
      user_id,
      action: 'EDITAR',
      entity_type: 'inm_unidades',
      entity_id: id,
      old_value: { precio_lista: anterior.precio_lista },
      new_value: { precio_lista: dto.precio, motivo: dto.motivo },
    });
    return unidad;
  }

  async publicar(id: string, version: number | undefined, empresa_id: string, user_id?: string) {
    const u = await this.deEmpresa(id, empresa_id);
    if (!(Number(u.precio_lista) > 0))
      throw new BadRequestException('Cargá el precio de lista antes de publicar la unidad');
    await this.prisma.$transaction((tx) =>
      transicionarUnidad(tx, { id, empresa_id, desde: ['BORRADOR'], hacia: 'DISPONIBLE', version }),
    );
    await this.audit.log({
      empresa_id,
      user_id,
      action: 'CONFIRMAR',
      entity_type: 'inm_unidades',
      entity_id: id,
      new_value: { estado: 'DISPONIBLE' },
    });
    return this.deEmpresa(id, empresa_id);
  }

  async bloquear(id: string, dto: BloquearUnidadDto, empresa_id: string, user_id?: string) {
    await this.deEmpresa(id, empresa_id);
    await this.prisma.$transaction((tx) =>
      transicionarUnidad(tx, {
        id,
        empresa_id,
        desde: ['DISPONIBLE', 'BORRADOR'],
        hacia: 'BLOQUEADA',
        version: dto.version,
        extra: { bloqueo_motivo: dto.motivo.trim() },
      }),
    );
    await this.audit.log({
      empresa_id,
      user_id,
      action: 'EDITAR',
      entity_type: 'inm_unidades',
      entity_id: id,
      new_value: { estado: 'BLOQUEADA', motivo: dto.motivo },
    });
    return this.deEmpresa(id, empresa_id);
  }

  async desbloquear(id: string, version: number | undefined, empresa_id: string, user_id?: string) {
    await this.deEmpresa(id, empresa_id);
    await this.prisma.$transaction((tx) =>
      transicionarUnidad(tx, {
        id,
        empresa_id,
        desde: ['BLOQUEADA'],
        hacia: 'DISPONIBLE',
        version,
        extra: { bloqueo_motivo: null },
      }),
    );
    await this.audit.log({
      empresa_id,
      user_id,
      action: 'EDITAR',
      entity_type: 'inm_unidades',
      entity_id: id,
      new_value: { estado: 'DISPONIBLE' },
    });
    return this.deEmpresa(id, empresa_id);
  }

  async darDeBaja(id: string, empresa_id: string, user_id?: string) {
    const u = await this.deEmpresa(id, empresa_id);
    if (!ESTADOS_LIBRES.includes(u.estado)) {
      throw new BadRequestException('La unidad está en una venta: no se puede dar de baja');
    }
    const r = await this.prisma.inm_unidades.update({ where: { id }, data: { activo: false, updated_at: new Date() } });
    await this.audit.log({
      empresa_id,
      user_id,
      action: 'ELIMINAR',
      entity_type: 'inm_unidades',
      entity_id: id,
      old_value: u,
    });
    return r;
  }

  /**
   * Carga masiva desde Excel (el frontend parsea y manda las filas). Por código:
   * si la unidad existe se actualizan sus datos y su precio (con historial), si no
   * se crea. Cada fila se valida sola: una mala no cancela las demás.
   */
  async importar(dto: ImportarUnidadesDto, empresa_id: string, user_id?: string) {
    const proyecto = await this.proyectoDeEmpresa(dto.proyecto_id, empresa_id);
    const etapas = await this.prisma.inm_etapas.findMany({
      where: { proyecto_id: proyecto.id, empresa_id, activo: true },
      select: { id: true, nombre: true },
    });
    const etapaPorNombre = new Map(etapas.map((e) => [e.nombre.trim().toLowerCase(), e.id]));
    const existentes = await this.prisma.inm_unidades.findMany({
      where: { proyecto_id: proyecto.id, empresa_id },
      select: { id: true, codigo: true, precio_lista: true, estado: true },
    });
    const porCodigo = new Map(existentes.map((u) => [u.codigo, u]));

    const resultado = {
      creadas: 0,
      actualizadas: 0,
      errores: [] as Array<{ fila: number; codigo?: string; mensaje: string }>,
    };

    for (const [i, cruda] of dto.filas.entries()) {
      const fila = i + 2; // fila 1 = encabezados en el Excel
      const obj = plainToInstance(FilaImportacionUnidadDto, cruda);
      const errs = validateSync(obj, { whitelist: true });
      if (errs.length) {
        resultado.errores.push({
          fila,
          codigo: typeof cruda?.codigo === 'string' || typeof cruda?.codigo === 'number' ? String(cruda.codigo) : '',
          mensaje: errs.flatMap((e) => Object.values(e.constraints ?? {})).join(' · '),
        });
        continue;
      }
      const codigo = obj.codigo.trim().toUpperCase();
      let etapa_id: string | null = null;
      if (obj.etapa?.trim()) {
        etapa_id = etapaPorNombre.get(obj.etapa.trim().toLowerCase()) ?? null;
        if (!etapa_id) {
          resultado.errores.push({ fila, codigo, mensaje: `La etapa "${obj.etapa}" no existe en el proyecto` });
          continue;
        }
      }
      try {
        const previa = porCodigo.get(codigo);
        if (previa) {
          await this.prisma.$transaction(async (tx) => {
            await tx.inm_unidades.update({
              where: { id: previa.id },
              data: {
                ...this.camposFisicos(obj),
                ...(obj.etapa !== undefined && { etapa_id }),
                updated_at: new Date(),
              },
            });
            const precio = obj.precio_lista;
            if (precio !== undefined && precio !== Number(previa.precio_lista)) {
              await tx.inm_unidad_precios.create({
                data: {
                  empresa_id,
                  unidad_id: previa.id,
                  precio_anterior: previa.precio_lista,
                  precio,
                  motivo: 'Importación desde Excel',
                  usuario_id: user_id ?? null,
                },
              });
              await tx.inm_unidades.update({ where: { id: previa.id }, data: { precio_lista: precio } });
            }
          });
          resultado.actualizadas++;
        } else {
          const u = await this.crear(
            { ...obj, codigo, proyecto_id: proyecto.id, etapa_id } as CreateUnidadDto,
            empresa_id,
            user_id,
          );
          porCodigo.set(codigo, { id: u.id, codigo, precio_lista: u.precio_lista, estado: u.estado });
          resultado.creadas++;
        }
      } catch (e) {
        resultado.errores.push({ fila, codigo, mensaje: e instanceof Error ? e.message : String(e) });
      }
    }

    await this.audit.log({
      empresa_id,
      user_id,
      action: 'IMPORTAR',
      entity_type: 'inm_unidades',
      entity_id: proyecto.id,
      new_value: {
        creadas: resultado.creadas,
        actualizadas: resultado.actualizadas,
        errores: resultado.errores.length,
      },
    });
    return resultado;
  }

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

  private camposFisicos(dto: Partial<Record<(typeof CAMPOS_FISICOS)[number], unknown>>) {
    const data: Record<string, unknown> = {};
    for (const c of CAMPOS_FISICOS) {
      if (dto[c] === undefined) continue;
      const v = dto[c];
      data[c] = typeof v === 'string' ? v.trim() || null : v;
    }
    return data as Partial<Prisma.inm_unidadesUncheckedCreateInput>;
  }

  private async proyectoDeEmpresa(id: string, empresa_id: string) {
    const p = await this.prisma.inm_proyectos.findFirst({
      where: { id, empresa_id, activo: true },
      select: { id: true, moneda_id: true },
    });
    if (!p) throw new NotFoundException('Proyecto no encontrado');
    return p;
  }

  private async validarReferencias(
    empresa_id: string,
    proyecto_id: string,
    dto: { etapa_id?: string | null; unidad_padre_id?: string | null; moneda_id?: string | null },
  ) {
    if (dto.etapa_id) {
      const e = await this.prisma.inm_etapas.findFirst({
        where: { id: dto.etapa_id, proyecto_id, empresa_id },
        select: { id: true },
      });
      if (!e) throw new BadRequestException('La etapa no pertenece a este proyecto');
    }
    if (dto.unidad_padre_id) {
      const u = await this.prisma.inm_unidades.findFirst({
        where: { id: dto.unidad_padre_id, proyecto_id, empresa_id },
        select: { id: true },
      });
      if (!u) throw new BadRequestException('La unidad vinculada no pertenece a este proyecto');
    }
    if (dto.moneda_id) {
      const m = await this.prisma.moneda.findUnique({ where: { id: dto.moneda_id }, select: { id: true } });
      if (!m) throw new BadRequestException('La moneda no existe');
    }
  }

  private async deEmpresa(id: string, empresa_id: string) {
    const u = await this.prisma.inm_unidades.findFirst({ where: { id, empresa_id } });
    if (!u) throw new NotFoundException('Unidad no encontrada');
    return u;
  }
}
