import { BadRequestException, Injectable, Logger, NotFoundException } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { ContabilidadIntegracionService } from 'src/contabilidad/services/integracion.service';
import { TipoCambioService } from 'src/contabilidad/services/tipo-cambio.service';
import { PrismaService } from 'src/prisma/prisma.service';
import {
  CreateComponenteCostoDto,
  CreateConceptoCostoDto,
  CreateDespachoDto,
  CreateEmbarqueDto,
  CreateItemEmbarqueDto,
  LiberarEmbarqueDto,
  UpdateComponenteCostoDto,
  UpdateConceptoCostoDto,
  UpdateDespachoDto,
  UpdateEmbarqueDto,
  UpdateEstadoEmbarqueDto,
  UpdateImportacionesConfigDto,
  UpdateItemEmbarqueDto,
} from '../dto/importaciones.dto';
import { buildBitacoraDiff, ImportacionesBitacoraPresenter } from './importaciones-bitacora.presenter';
import { ImportacionesCosteoService } from './importaciones-costeo.service';

type Tx = Prisma.TransactionClient;

const ESTADOS_EDITABLES = new Set([
  'BORRADOR',
  'EN_TRANSITO',
  'ARRIBADO',
  'EN_DESPACHO',
  'DESPACHADO',
  'LIBERADO',
]);

const ESTADOS_PERMITIDOS_CAMBIO_PERFIL = new Set(['CERRADO', 'SELLADO']);

// Máquina de estados según backend/docs/plan-importaciones.md.
const TRANSICIONES_ESTADO: Record<string, string[]> = {
  BORRADOR: ['EN_TRANSITO'],
  EN_TRANSITO: ['ARRIBADO', 'BORRADOR'],
  ARRIBADO: ['EN_DESPACHO', 'EN_TRANSITO'],
  EN_DESPACHO: ['DESPACHADO', 'ARRIBADO'],
  DESPACHADO: ['LIBERADO', 'EN_DESPACHO'],
  LIBERADO: ['CERRADO', 'DESPACHADO'],
  CERRADO: ['SELLADO', 'LIBERADO'],
  SELLADO: [],
};

// Mapping campo de imp_despachos → código de imp_conceptos_costo (sistema)
type DespachoTributoField =
  | 'arancel_gs'
  | 'iva_importacion_gs'
  | 'isc_gs'
  | 'inc_gs'
  | 'anticipo_ire_gs'
  | 'tasa_ana_gs'
  | 'otros_gs';

const CONCEPTO_TRIBUTO_BY_FIELD: Array<{ field: DespachoTributoField; codigo: string }> = [
  { field: 'arancel_gs', codigo: 'IMP_ARANCEL' },
  { field: 'iva_importacion_gs', codigo: 'IMP_IVA_IMPORTACION' },
  { field: 'isc_gs', codigo: 'IMP_ISC' },
  { field: 'inc_gs', codigo: 'IMP_INC' },
  { field: 'anticipo_ire_gs', codigo: 'IMP_ANTICIPO_IRE' },
  { field: 'tasa_ana_gs', codigo: 'IMP_TASA_ANA' },
  { field: 'otros_gs', codigo: 'IMP_OTROS_TRIBUTOS' },
];

@Injectable()
export class ImportacionesService {
  private readonly logger = new Logger(ImportacionesService.name);

  constructor(
    private readonly prisma: PrismaService,
    private readonly costeo: ImportacionesCosteoService,
    private readonly tipoCambio: TipoCambioService,
    private readonly contabilidadIntegracion: ContabilidadIntegracionService,
    private readonly bitacoraPresenter: ImportacionesBitacoraPresenter,
  ) {}

  async findAll(
    empresaId: string,
    query?: { page?: string; limit?: string; perfil?: string; estado?: string; proveedor_id?: string; search?: string },
  ) {
    const take = Math.max(1, Math.min(100, Number(query?.limit) || 20));
    const page = Math.max(1, Number(query?.page) || 1);
    const skip = (page - 1) * take;
    const where: Prisma.imp_embarquesWhereInput = { empresa_id: empresaId, activo: true };

    if (query?.perfil) where.perfil = query.perfil;
    if (query?.estado) where.estado = query.estado;
    if (query?.proveedor_id) where.proveedor_id = query.proveedor_id;
    if (query?.search?.trim()) {
      const search = query.search.trim();
      where.OR = [
        { numero: { contains: search, mode: 'insensitive' } },
        { bill_of_lading: { contains: search, mode: 'insensitive' } },
        { booking: { contains: search, mode: 'insensitive' } },
        { origen_puerto: { contains: search, mode: 'insensitive' } },
        { destino_puerto: { contains: search, mode: 'insensitive' } },
        { proveedores: { personas: { razon_social: { contains: search, mode: 'insensitive' } } } },
      ];
    }

    const [items, total] = await this.prisma.$transaction([
      this.prisma.imp_embarques.findMany({
        where,
        include: {
          proveedores: { include: { personas: { select: { razon_social: true, ruc: true, dv: true } } } },
          moneda: { select: { id: true, codigo: true, simbolo: true, descripcion: true } },
          _count: { select: { items: true, componentes_costo: true } },
        },
        orderBy: [{ created_at: 'desc' }],
        skip,
        take,
      }),
      this.prisma.imp_embarques.count({ where }),
    ]);

    return { items, total, page, limit: take, lastPage: Math.max(1, Math.ceil(total / take)) };
  }

  async findOne(id: string, empresaId: string, tx?: Tx) {
    const db = tx ?? this.prisma;
    const embarque = await db.imp_embarques.findFirst({
      where: { id, empresa_id: empresaId, activo: true },
      include: {
        proveedores: {
          include: { personas: { select: { razon_social: true, ruc: true, dv: true, email: true, telefono: true } } },
        },
        moneda: true,
        items: {
          where: { activo: true },
          include: {
            producto: { select: { id: true, cod_producto: true, descripcion: true } },
            sku_interno: { select: { id: true, cod_producto: true, descripcion: true } },
          },
          orderBy: [{ orden: 'asc' }, { created_at: 'asc' }],
        },
        componentes_costo: {
          where: { activo: true },
          include: {
            concepto: true,
            moneda: { select: { id: true, codigo: true, simbolo: true, descripcion: true } },
            item: { select: { id: true, chasis: true, sku_proveedor: true, descripcion_origen: true } },
            proveedor: {
              include: { personas: { select: { razon_social: true, ruc: true, dv: true } } },
            },
            cuenta_pagar: { select: { id: true, numero_cuota: true, total_cuotas: true, saldo_pendiente: true } },
          },
          orderBy: { created_at: 'asc' },
        },
      },
    });
    if (!embarque) throw new NotFoundException('Embarque no encontrado');
    return embarque;
  }

  async searchProductos(empresaId: string, q?: string, limit?: string) {
    const search = q?.trim();
    if (!search || search.length < 2) return { message: 'Búsqueda exitosa', data: [] };

    const take = Math.max(1, Math.min(50, Number(limit) || 20));
    const data = await this.prisma.productos.findMany({
      where: {
        deleted: false,
        active: true,
        OR: [{ empresa_id: empresaId }, { empresa_id: null }],
        AND: [
          {
            OR: [
              { descripcion: { contains: search, mode: 'insensitive' } },
              { cod_producto: { contains: search, mode: 'insensitive' } },
              { codigo_barra: { contains: search, mode: 'insensitive' } },
              {
                producto_presentaciones: {
                  some: { codigo_barra: { equals: search, mode: 'insensitive' }, active: true },
                },
              },
            ],
          },
        ],
      },
      select: {
        id: true,
        descripcion: true,
        cod_producto: true,
        codigo_barra: true,
        precio: true,
        porcentaje_iva: true,
        maneja_inventario: true,
        categoria: { select: { id: true, descripcion: true } },
        unidades_de_medida: { select: { id: true, codigo: true, descripcion: true } },
      },
      orderBy: [{ descripcion: 'asc' }],
      take,
    });

    return { message: 'Búsqueda exitosa', data };
  }

  async getConfig(empresaId: string) {
    const { config, hasModuloSuscripcion } = await this.prisma.$transaction(async (tx) => {
      const current = await this.getOrCreateConfig(tx, empresaId);
      const hasModule = await this.hasImportacionesModuleInSubscription(tx, empresaId);
      return { config: current, hasModuloSuscripcion: hasModule };
    });

    return this.buildConfigResponse(config, hasModuloSuscripcion);
  }

  async updateConfig(empresaId: string, dto: UpdateImportacionesConfigDto, userId?: string) {
    const { config, hasModuloSuscripcion } = await this.prisma.$transaction(async (tx) => {
      const current = await this.getOrCreateConfig(tx, empresaId);
      const nextPerfil = dto.perfil_activo ?? current.perfil_activo;

      if (nextPerfil !== current.perfil_activo) {
        const embarquesActivosPerfilActual = await tx.imp_embarques.count({
          where: {
            empresa_id: empresaId,
            perfil: current.perfil_activo,
            activo: true,
            estado: { notIn: Array.from(ESTADOS_PERMITIDOS_CAMBIO_PERFIL) },
          },
        });
        if (embarquesActivosPerfilActual > 0) {
          throw new BadRequestException(
            `No se puede cambiar de perfil mientras existan embarques activos en perfil ${current.perfil_activo}`,
          );
        }
      }

      const config = await tx.imp_config_empresa.update({
        where: { empresa_id: empresaId },
        data: {
          ...(dto.modulo_operativo_activo !== undefined && { modulo_operativo_activo: dto.modulo_operativo_activo }),
          ...(dto.perfil_activo !== undefined && { perfil_activo: dto.perfil_activo }),
          ...(userId && { usuario_id: userId }),
          updated_at: new Date(),
        },
      });
      const hasModule = await this.hasImportacionesModuleInSubscription(tx, empresaId);
      return { config, hasModuloSuscripcion: hasModule };
    });

    return this.buildConfigResponse(config, hasModuloSuscripcion);
  }

  async create(dto: CreateEmbarqueDto, empresaId: string, userId?: string) {
    const id = await this.prisma.$transaction(async (tx) => {
      const config = await this.getOrCreateConfig(tx, empresaId);
      this.assertModuloOperativoActivo(config.modulo_operativo_activo);
      const perfil = dto.perfil ?? config.perfil_activo;
      this.assertPerfilConfigMatches(perfil, config.perfil_activo);
      await this.validateEmbarqueRefs(tx, empresaId, dto.proveedor_id, dto.moneda_id);
      const numero = await this.generarNumero(tx, empresaId);
      const embarque = await tx.imp_embarques.create({
        data: {
          empresa_id: empresaId,
          numero,
          proveedor_id: dto.proveedor_id,
          moneda_id: dto.moneda_id || null,
          perfil,
          estado: 'BORRADOR',
          origen_pais: dto.origen_pais || null,
          origen_puerto: dto.origen_puerto || null,
          destino_puerto: dto.destino_puerto || null,
          modalidad: dto.modalidad || null,
          booking: dto.booking || null,
          bill_of_lading: dto.bill_of_lading || null,
          fecha_embarque: dto.fecha_embarque ? new Date(dto.fecha_embarque) : null,
          fecha_eta: dto.fecha_eta ? new Date(dto.fecha_eta) : null,
          fecha_arribo_real: dto.fecha_arribo_real ? new Date(dto.fecha_arribo_real) : null,
          cotizacion_cierre: dto.cotizacion_cierre ?? null,
          notas: dto.notas?.trim() || null,
          usuario_id: userId || null,
        },
      });
      await this.registerBitacora(tx, {
        embarqueId: embarque.id,
        empresaId,
        evento: 'EMBARQUE_CREADO',
        estadoNuevo: embarque.estado,
        detalle: {
          numero: embarque.numero,
          perfil: embarque.perfil,
          proveedor_id: embarque.proveedor_id,
        },
        usuarioId: userId,
      });
      return embarque.id;
    });
    return this.findOne(id, empresaId);
  }

  async update(id: string, dto: UpdateEmbarqueDto, empresaId: string, userId?: string) {
    const embarqueId = await this.prisma.$transaction(async (tx) => {
      const config = await this.getOrCreateConfig(tx, empresaId);
      this.assertModuloOperativoActivo(config.modulo_operativo_activo);
      const existing = await this.getEditableEmbarque(tx, id, empresaId);
      if (existing.perfil !== config.perfil_activo) {
        throw new BadRequestException(
          `Solo se pueden editar embarques del perfil activo (${config.perfil_activo}) para esta empresa`,
        );
      }
      if (dto.perfil !== undefined && dto.perfil !== existing.perfil) {
        throw new BadRequestException(
          'El perfil de un embarque no se puede cambiar; está definido al crearlo según la configuración de la empresa',
        );
      }
      await this.validateEmbarqueRefs(tx, empresaId, dto.proveedor_id || existing.proveedor_id, dto.moneda_id);
      const diff = this.buildEmbarqueDiff(existing, dto);
      await tx.imp_embarques.update({
        where: { id },
        data: {
          ...(dto.proveedor_id !== undefined && { proveedor_id: dto.proveedor_id }),
          ...(dto.moneda_id !== undefined && { moneda_id: dto.moneda_id || null }),
          ...(dto.origen_pais !== undefined && { origen_pais: dto.origen_pais || null }),
          ...(dto.origen_puerto !== undefined && { origen_puerto: dto.origen_puerto || null }),
          ...(dto.destino_puerto !== undefined && { destino_puerto: dto.destino_puerto || null }),
          ...(dto.modalidad !== undefined && { modalidad: dto.modalidad || null }),
          ...(dto.booking !== undefined && { booking: dto.booking || null }),
          ...(dto.bill_of_lading !== undefined && { bill_of_lading: dto.bill_of_lading || null }),
          ...(dto.fecha_embarque !== undefined && {
            fecha_embarque: dto.fecha_embarque ? new Date(dto.fecha_embarque) : null,
          }),
          ...(dto.fecha_eta !== undefined && { fecha_eta: dto.fecha_eta ? new Date(dto.fecha_eta) : null }),
          ...(dto.fecha_arribo_real !== undefined && {
            fecha_arribo_real: dto.fecha_arribo_real ? new Date(dto.fecha_arribo_real) : null,
          }),
          ...(dto.cotizacion_cierre !== undefined && { cotizacion_cierre: dto.cotizacion_cierre ?? null }),
          ...(dto.notas !== undefined && { notas: dto.notas?.trim() || null }),
          updated_at: new Date(),
        },
      });
      if (diff) {
        await this.registerBitacora(tx, {
          embarqueId: id,
          empresaId,
          evento: 'EMBARQUE_EDITADO',
          detalle: { cambios: diff },
          usuarioId: userId,
        });
      }
      return id;
    });
    return this.findOne(embarqueId, empresaId);
  }

  async updateEstado(id: string, dto: UpdateEstadoEmbarqueDto, empresaId: string, userId?: string) {
    await this.prisma.$transaction(async (tx) => {
      const embarque = await this.ensureEmbarqueExists(tx, id, empresaId);
      if (embarque.estado === dto.estado) return;
      const allowed = TRANSICIONES_ESTADO[embarque.estado] ?? [];
      if (!allowed.includes(dto.estado)) {
        throw new BadRequestException(
          `Transición no permitida: ${embarque.estado} → ${dto.estado}. ` +
            `Próximos estados válidos: ${allowed.length ? allowed.join(', ') : 'ninguno'}.`,
        );
      }

      // EN_DESPACHO → DESPACHADO es exclusiva de `confirmarDespacho`: es ahí donde los
      // tributos entran al costeo y se dispara el asiento contable. Permitirla acá dejaba
      // el despacho en BORRADOR sin poder confirmarse nunca (confirmarDespacho exige
      // EN_DESPACHO), y el embarque avanzaba sin sus tributos prorrateados.
      if (embarque.estado === 'EN_DESPACHO' && dto.estado === 'DESPACHADO') {
        const borrador = await tx.imp_despachos.findFirst({
          where: { embarque_id: id, empresa_id: empresaId, estado: 'BORRADOR' },
          select: { numero_despacho: true },
        });
        if (borrador) {
          throw new BadRequestException(
            `El despacho ${borrador.numero_despacho ?? ''} está en BORRADOR. ` +
              'Confirmalo desde la pestaña Despacho: al confirmarlo el embarque pasa a DESPACHADO automáticamente.',
          );
        }
      }

      await tx.imp_embarques.update({
        where: { id },
        data: { estado: dto.estado, updated_at: new Date() },
      });
      await this.registerBitacora(tx, {
        embarqueId: id,
        empresaId,
        evento: 'ESTADO_CAMBIO',
        estadoAnterior: embarque.estado,
        estadoNuevo: dto.estado,
        usuarioId: userId,
      });
    });

    // Al cerrar, la importación se descarga de la cuenta puente contra
    // mercaderías. Fuera de la transacción y fail-silent, igual que el asiento
    // del despacho: un problema contable no puede impedir cerrar el embarque.
    // Sólo hace algo si la empresa usa el modelo de cuenta puente.
    if (dto.estado === 'CERRADO') {
      this.contabilidadIntegracion.integrarCierreImportacion?.(id).catch((err) => {
        this.logger.error(
          `Embarque ${id} cerrado pero no se pudo generar el asiento: ${err instanceof Error ? err.message : err}`,
        );
      });
    }

    return this.findOne(id, empresaId);
  }

  async remove(id: string, empresaId: string, userId?: string) {
    await this.prisma.$transaction(async (tx) => {
      const embarque = await this.ensureEmbarqueExists(tx, id, empresaId);
      if (embarque.estado === 'SELLADO') {
        throw new BadRequestException('No se puede eliminar un embarque sellado');
      }
      await tx.imp_embarques.update({ where: { id }, data: { activo: false, updated_at: new Date() } });
      await this.registerBitacora(tx, {
        embarqueId: id,
        empresaId,
        evento: 'EMBARQUE_ELIMINADO',
        estadoAnterior: embarque.estado,
        usuarioId: userId,
      });
    });
    return { message: 'Embarque eliminado' };
  }

  async addItem(embarqueId: string, dto: CreateItemEmbarqueDto, empresaId: string, userId?: string) {
    await this.prisma.$transaction(async (tx) => {
      const embarque = await this.getEditableEmbarque(tx, embarqueId, empresaId);
      await this.validateItemPayload(tx, embarque, dto);
      const normalized = await this.normalizeItem(tx, embarque, dto, embarqueId);
      const created = await tx.imp_embarque_items.create({
        data: {
          empresa_id: empresaId,
          embarque_id: embarqueId,
          perfil: embarque.perfil,
          ...normalized,
          usuario_id: userId || null,
        },
      });
      await this.costeo.recalcular(tx, empresaId, embarqueId);
      await this.registerBitacora(tx, {
        embarqueId,
        empresaId,
        evento: 'ITEM_AGREGADO',
        detalle: this.summarizeItem(created),
        usuarioId: userId,
      });
    });
    return this.findOne(embarqueId, empresaId);
  }

  async updateItem(
    embarqueId: string,
    itemId: string,
    dto: UpdateItemEmbarqueDto,
    empresaId: string,
    userId?: string,
  ) {
    await this.prisma.$transaction(async (tx) => {
      const embarque = await this.getEditableEmbarque(tx, embarqueId, empresaId);
      const existing = await tx.imp_embarque_items.findFirst({
        where: { id: itemId, embarque_id: embarqueId, empresa_id: empresaId, activo: true },
      });
      if (!existing) throw new NotFoundException('Ítem no encontrado');
      const merged = { ...existing, ...dto } as any;
      await this.validateItemPayload(tx, embarque, merged, itemId);
      const normalized = await this.normalizeItem(tx, embarque, merged, embarqueId, existing);
      const diff = buildBitacoraDiff(existing, normalized, Object.keys(dto));
      await tx.imp_embarque_items.update({
        where: { id: itemId },
        data: { ...normalized, updated_at: new Date() },
      });
      await this.costeo.recalcular(tx, empresaId, embarqueId);
      await this.registerBitacora(tx, {
        embarqueId,
        empresaId,
        evento: 'ITEM_EDITADO',
        detalle: {
          item_id: itemId,
          identificador: this.itemIdentifier(existing),
          cambios: diff,
        },
        usuarioId: userId,
      });
    });
    return this.findOne(embarqueId, empresaId);
  }

  async removeItem(embarqueId: string, itemId: string, empresaId: string, userId?: string) {
    await this.prisma.$transaction(async (tx) => {
      await this.getEditableEmbarque(tx, embarqueId, empresaId);
      const existing = await tx.imp_embarque_items.findFirst({
        where: { id: itemId, embarque_id: embarqueId, empresa_id: empresaId, activo: true },
      });
      if (!existing) throw new NotFoundException('Ítem no encontrado');
      await tx.imp_embarque_items.update({
        where: { id: itemId },
        data: { activo: false, updated_at: new Date() },
      });
      await tx.imp_componentes_costo.updateMany({
        where: { item_id: itemId, empresa_id: empresaId },
        data: { activo: false, updated_at: new Date() },
      });
      await this.costeo.recalcular(tx, empresaId, embarqueId);
      await this.registerBitacora(tx, {
        embarqueId,
        empresaId,
        evento: 'ITEM_ELIMINADO',
        detalle: this.summarizeItem(existing),
        usuarioId: userId,
      });
    });
    return this.findOne(embarqueId, empresaId);
  }

  async getConceptos(empresaId: string, perfil?: string) {
    const where: Prisma.imp_conceptos_costoWhereInput = {
      activo: true,
      OR: [{ empresa_id: empresaId }, { empresa_id: null, es_sistema: true }],
    };
    if (perfil) {
      where.AND = [{ OR: [{ aplica_perfil: null }, { aplica_perfil: perfil }] }];
    }
    return this.prisma.imp_conceptos_costo.findMany({
      where,
      orderBy: [{ orden: 'asc' }, { grupo: 'asc' }, { nombre: 'asc' }],
    });
  }

  async crearConcepto(dto: CreateConceptoCostoDto, empresaId: string) {
    const codigo = dto.codigo.trim().toUpperCase();
    const existente = await this.prisma.imp_conceptos_costo.findFirst({
      where: { empresa_id: empresaId, codigo },
    });
    if (existente) {
      throw new BadRequestException(`Ya existe un concepto con código "${codigo}" en esta empresa`);
    }

    return this.prisma.imp_conceptos_costo.create({
      data: {
        empresa_id: empresaId,
        codigo,
        nombre: dto.nombre.trim(),
        descripcion: dto.nombre.trim(),
        grupo: dto.grupo ?? null,
        imputacion_default: dto.imputacion_default ?? 'COMPARTIDO',
        criterio_prorrateo_default: dto.criterio_prorrateo_default ?? null,
        moneda_default: dto.moneda_default ?? 'USD',
        aplica_perfil: dto.aplica_perfil ?? null,
        es_sistema: false,
        activo: true,
      },
    });
  }

  async actualizarConcepto(id: string, dto: UpdateConceptoCostoDto, empresaId: string) {
    const current = await this.prisma.imp_conceptos_costo.findUnique({ where: { id } });
    if (!current || current.activo === false) {
      throw new NotFoundException(`Concepto de costo ${id} no encontrado`);
    }
    if (current.es_sistema || current.empresa_id !== empresaId) {
      throw new BadRequestException('Solo se pueden editar conceptos de costo propios de la empresa');
    }

    const data: Prisma.imp_conceptos_costoUpdateInput = { updated_at: new Date() };

    if (dto.codigo !== undefined) {
      const codigo = dto.codigo.trim().toUpperCase();
      if (codigo !== current.codigo) {
        const duplicado = await this.prisma.imp_conceptos_costo.findFirst({
          where: { empresa_id: empresaId, codigo, id: { not: id } },
        });
        if (duplicado) {
          throw new BadRequestException(`Ya existe un concepto con código "${codigo}" en esta empresa`);
        }
      }
      data.codigo = codigo;
    }
    if (dto.nombre !== undefined) {
      data.nombre = dto.nombre.trim();
      data.descripcion = dto.nombre.trim();
    }
    if (dto.grupo !== undefined) data.grupo = dto.grupo || null;
    if (dto.imputacion_default !== undefined) data.imputacion_default = dto.imputacion_default;
    if (dto.criterio_prorrateo_default !== undefined) {
      data.criterio_prorrateo_default = dto.criterio_prorrateo_default || null;
    }
    if (dto.moneda_default !== undefined) data.moneda_default = dto.moneda_default;
    if (dto.aplica_perfil !== undefined) data.aplica_perfil = dto.aplica_perfil || null;

    return this.prisma.imp_conceptos_costo.update({ where: { id }, data });
  }

  async eliminarConcepto(id: string, empresaId: string) {
    const current = await this.prisma.imp_conceptos_costo.findUnique({ where: { id } });
    if (!current || current.activo === false) {
      throw new NotFoundException(`Concepto de costo ${id} no encontrado`);
    }
    if (current.es_sistema || current.empresa_id !== empresaId) {
      throw new BadRequestException('Solo se pueden desactivar conceptos de costo propios de la empresa');
    }

    const updated = await this.prisma.imp_conceptos_costo.update({
      where: { id },
      data: { activo: false, updated_at: new Date() },
    });

    return { message: `Concepto de costo "${updated.nombre}" desactivado`, id: updated.id };
  }

  async addCosto(embarqueId: string, dto: CreateComponenteCostoDto, empresaId: string, userId?: string) {
    await this.prisma.$transaction(async (tx) => {
      const embarque = await this.getEditableEmbarque(tx, embarqueId, empresaId);
      const data = await this.normalizeCosto(tx, empresaId, embarqueId, embarque.estado, dto);
      const created = await tx.imp_componentes_costo.create({ data: { ...data, usuario_id: userId || null } });
      await this.costeo.recalcular(tx, empresaId, embarqueId);
      await this.registerBitacora(tx, {
        embarqueId,
        empresaId,
        evento: 'COSTO_AGREGADO',
        detalle: await this.summarizeCosto(tx, created),
        usuarioId: userId,
      });
    });
    return this.findOne(embarqueId, empresaId);
  }

  async updateCosto(
    embarqueId: string,
    costoId: string,
    dto: UpdateComponenteCostoDto,
    empresaId: string,
    userId?: string,
  ) {
    await this.prisma.$transaction(async (tx) => {
      const embarque = await this.getEditableEmbarque(tx, embarqueId, empresaId);
      const existing = await tx.imp_componentes_costo.findFirst({
        where: { id: costoId, embarque_id: embarqueId, empresa_id: empresaId, activo: true },
      });
      if (!existing) throw new NotFoundException('Componente de costo no encontrado');
      const merged = { ...existing, ...dto } as any;
      const data = await this.normalizeCosto(tx, empresaId, embarqueId, embarque.estado, merged);
      const diff = buildBitacoraDiff(existing, data, Object.keys(dto));
      await tx.imp_componentes_costo.update({ where: { id: costoId }, data: { ...data, updated_at: new Date() } });
      await this.costeo.recalcular(tx, empresaId, embarqueId);
      const concepto = await tx.imp_conceptos_costo.findUnique({
        where: { id: existing.concepto_id },
        select: { nombre: true },
      });
      await this.registerBitacora(tx, {
        embarqueId,
        empresaId,
        evento: 'COSTO_EDITADO',
        detalle: {
          componente_id: costoId,
          concepto: concepto?.nombre ?? null,
          cambios: diff,
        },
        usuarioId: userId,
      });
    });
    return this.findOne(embarqueId, empresaId);
  }

  async removeCosto(embarqueId: string, costoId: string, empresaId: string, userId?: string) {
    await this.prisma.$transaction(async (tx) => {
      await this.getEditableEmbarque(tx, embarqueId, empresaId);
      const existing = await tx.imp_componentes_costo.findFirst({
        where: { id: costoId, embarque_id: embarqueId, empresa_id: empresaId, activo: true },
      });
      if (!existing) throw new NotFoundException('Componente de costo no encontrado');
      await tx.imp_componentes_costo.update({
        where: { id: costoId },
        data: { activo: false, updated_at: new Date() },
      });
      await this.costeo.recalcular(tx, empresaId, embarqueId);
      await this.registerBitacora(tx, {
        embarqueId,
        empresaId,
        evento: 'COSTO_ELIMINADO',
        detalle: await this.summarizeCosto(tx, existing),
        usuarioId: userId,
      });
    });
    return this.findOne(embarqueId, empresaId);
  }

  async getBitacora(embarqueId: string, empresaId: string) {
    await this.ensureEmbarqueExists(this.prisma, embarqueId, empresaId);
    const events = await this.prisma.imp_embarques_bitacora.findMany({
      where: { embarque_id: embarqueId, empresa_id: empresaId },
      select: {
        evento: true,
        estado_anterior: true,
        estado_nuevo: true,
        detalle: true,
        observacion: true,
        usuario_id: true,
        created_at: true,
      },
      orderBy: { created_at: 'desc' },
      take: 200,
    });
    return this.bitacoraPresenter.present(events, empresaId, embarqueId);
  }

  // ── Fase 2: Despacho ────────────────────────────────────────────

  async getDespacho(embarqueId: string, empresaId: string) {
    await this.ensureEmbarqueExists(this.prisma, embarqueId, empresaId);
    return this.prisma.imp_despachos.findUnique({
      where: { embarque_id: embarqueId },
      include: { despachante: { include: { personas: { select: { razon_social: true, ruc: true, dv: true } } } } },
    });
  }

  async createDespacho(embarqueId: string, dto: CreateDespachoDto, empresaId: string, userId?: string) {
    return this.prisma.$transaction(async (tx) => {
      const embarque = await this.ensureEmbarqueExists(tx, embarqueId, empresaId);
      if (!['EN_DESPACHO', 'ARRIBADO'].includes(embarque.estado)) {
        throw new BadRequestException(
          `Solo se puede crear despacho cuando el embarque está en ARRIBADO o EN_DESPACHO (actual: ${embarque.estado})`,
        );
      }
      const existente = await tx.imp_despachos.findUnique({ where: { embarque_id: embarqueId } });
      if (existente) throw new BadRequestException('El embarque ya tiene un despacho cargado');
      if (dto.despachante_id) {
        const despachante = await tx.proveedores.findFirst({
          where: { id: dto.despachante_id, empresa_id: empresaId, activo: true },
        });
        if (!despachante) throw new BadRequestException('Despachante inválido para la empresa');
      }
      const tributos = this.normalizeTributos(dto);
      const total = this.sumTributos(tributos);
      const despacho = await tx.imp_despachos.create({
        data: {
          empresa_id: empresaId,
          embarque_id: embarqueId,
          numero_despacho: dto.numero_despacho.trim(),
          fecha_despacho: new Date(dto.fecha_despacho),
          despachante_id: dto.despachante_id || null,
          ...tributos,
          total_tributos_gs: total,
          notas: dto.notas?.trim() || null,
          usuario_id: userId || null,
          estado: 'BORRADOR',
        },
      });
      // Si el embarque estaba en ARRIBADO, lo movemos a EN_DESPACHO automáticamente.
      if (embarque.estado === 'ARRIBADO') {
        await tx.imp_embarques.update({
          where: { id: embarqueId },
          data: { estado: 'EN_DESPACHO', updated_at: new Date() },
        });
        await this.registerBitacora(tx, {
          embarqueId,
          empresaId,
          evento: 'ESTADO_CAMBIO',
          estadoAnterior: 'ARRIBADO',
          estadoNuevo: 'EN_DESPACHO',
          usuarioId: userId,
        });
      }
      await this.registerBitacora(tx, {
        embarqueId,
        empresaId,
        evento: 'DESPACHO_CREADO',
        detalle: {
          despacho_id: despacho.id,
          numero_despacho: despacho.numero_despacho,
          total_tributos_gs: total,
        },
        usuarioId: userId,
      });
      return despacho;
    });
  }

  async updateDespacho(
    embarqueId: string,
    despachoId: string,
    dto: UpdateDespachoDto,
    empresaId: string,
    userId?: string,
  ) {
    return this.prisma.$transaction(async (tx) => {
      const despacho = await this.ensureDespachoEditable(tx, embarqueId, despachoId, empresaId);
      if (dto.despachante_id !== undefined && dto.despachante_id) {
        const despachante = await tx.proveedores.findFirst({
          where: { id: dto.despachante_id, empresa_id: empresaId, activo: true },
        });
        if (!despachante) throw new BadRequestException('Despachante inválido para la empresa');
      }
      const merged = { ...despacho, ...dto } as any;
      const tributos = this.normalizeTributos(merged);
      const total = this.sumTributos(tributos);
      const updated = await tx.imp_despachos.update({
        where: { id: despachoId },
        data: {
          ...(dto.numero_despacho !== undefined && { numero_despacho: dto.numero_despacho.trim() }),
          ...(dto.fecha_despacho !== undefined && { fecha_despacho: new Date(dto.fecha_despacho) }),
          ...(dto.despachante_id !== undefined && { despachante_id: dto.despachante_id || null }),
          ...tributos,
          total_tributos_gs: total,
          ...(dto.notas !== undefined && { notas: dto.notas?.trim() || null }),
          updated_at: new Date(),
        },
      });
      const diffKeys = [...Object.keys(dto)];
      if (Number(despacho.total_tributos_gs ?? 0) !== Number(updated.total_tributos_gs ?? 0)) {
        diffKeys.push('total_tributos_gs');
      }
      const diff = buildBitacoraDiff(despacho, updated, diffKeys);
      await this.registerBitacora(tx, {
        embarqueId,
        empresaId,
        evento: 'DESPACHO_EDITADO',
        detalle: {
          despacho_id: despachoId,
          numero_despacho: updated.numero_despacho,
          cambios: diff,
        },
        usuarioId: userId,
      });
      return updated;
    });
  }

  async removeDespacho(embarqueId: string, despachoId: string, empresaId: string, userId?: string) {
    return this.prisma.$transaction(async (tx) => {
      const despacho = await this.ensureDespachoEditable(tx, embarqueId, despachoId, empresaId);
      await tx.imp_despachos.delete({ where: { id: despacho.id } });
      await this.registerBitacora(tx, {
        embarqueId,
        empresaId,
        evento: 'DESPACHO_ELIMINADO',
        detalle: { despacho_id: despachoId, numero_despacho: despacho.numero_despacho },
        usuarioId: userId,
      });
      return { message: 'Despacho eliminado' };
    });
  }

  async confirmarDespacho(embarqueId: string, despachoId: string, empresaId: string, userId?: string) {
    const result = await this.prisma.$transaction(async (tx) => {
      const embarque = await this.ensureEmbarqueExists(tx, embarqueId, empresaId);
      const despacho = await tx.imp_despachos.findFirst({
        where: { id: despachoId, embarque_id: embarqueId, empresa_id: empresaId },
      });
      if (!despacho) throw new NotFoundException('Despacho no encontrado');
      if (despacho.estado !== 'BORRADOR') {
        throw new BadRequestException('Solo se puede confirmar un despacho en estado BORRADOR');
      }
      if (embarque.estado !== 'EN_DESPACHO') {
        throw new BadRequestException(
          `Para confirmar el despacho el embarque debe estar en EN_DESPACHO (actual: ${embarque.estado})`,
        );
      }

      // Cotización: si los componentes USD ya están congelados, igual guardamos referencia del día del despacho
      let cotizacion: number | null = null;
      try {
        cotizacion = await this.tipoCambio.getTasa(empresaId, 'USD', new Date(despacho.fecha_despacho));
      } catch {
        cotizacion = null;
      }

      // Insertar componentes de costo por cada tributo > 0
      const conceptos = await tx.imp_conceptos_costo.findMany({
        where: {
          empresa_id: null,
          es_sistema: true,
          codigo: { in: CONCEPTO_TRIBUTO_BY_FIELD.map((c) => c.codigo) },
        },
        select: { id: true, codigo: true, nombre: true },
      });
      const conceptoIdByCodigo = new Map(conceptos.map((c) => [c.codigo, c.id]));

      for (const { field, codigo } of CONCEPTO_TRIBUTO_BY_FIELD) {
        const importe = Number(despacho[field] ?? 0);
        if (importe <= 0) continue;
        const conceptoId = conceptoIdByCodigo.get(codigo);
        if (!conceptoId) continue;
        const exists = await tx.imp_componentes_costo.findFirst({
          where: {
            empresa_id: empresaId,
            embarque_id: embarqueId,
            concepto_id: conceptoId,
            descripcion: `Despacho ${despacho.numero_despacho}`,
            activo: true,
          },
        });
        if (exists) {
          await tx.imp_componentes_costo.update({
            where: { id: exists.id },
            data: {
              importe,
              cotizacion: 1,
              importe_gs: importe,
              criterio_prorrateo: 'FOB',
              es_directo: false,
              estado: 'CONFIRMADO',
              updated_at: new Date(),
            },
          });
        } else {
          await tx.imp_componentes_costo.create({
            data: {
              empresa_id: empresaId,
              embarque_id: embarqueId,
              concepto_id: conceptoId,
              descripcion: `Despacho ${despacho.numero_despacho}`,
              importe,
              cotizacion: 1,
              importe_gs: importe,
              es_directo: false,
              criterio_prorrateo: 'FOB',
              estado: 'CONFIRMADO',
              usuario_id: userId || null,
            },
          });
        }
      }

      await tx.imp_despachos.update({
        where: { id: despachoId },
        data: {
          estado: 'CONFIRMADO',
          cotizacion_usada: cotizacion ?? null,
          confirmado_at: new Date(),
          updated_at: new Date(),
        },
      });

      await this.costeo.recalcular(tx, empresaId, embarqueId);

      // Embarque pasa a DESPACHADO
      await tx.imp_embarques.update({
        where: { id: embarqueId },
        data: { estado: 'DESPACHADO', updated_at: new Date() },
      });
      await this.registerBitacora(tx, {
        embarqueId,
        empresaId,
        evento: 'ESTADO_CAMBIO',
        estadoAnterior: 'EN_DESPACHO',
        estadoNuevo: 'DESPACHADO',
        usuarioId: userId,
      });
      await this.registerBitacora(tx, {
        embarqueId,
        empresaId,
        evento: 'DESPACHO_CONFIRMADO',
        detalle: {
          despacho_id: despachoId,
          numero_despacho: despacho.numero_despacho,
          total_tributos_gs: Number(despacho.total_tributos_gs ?? 0),
          cotizacion_usada: cotizacion,
        },
        usuarioId: userId,
      });
      return { despachoId, integraContabilidad: true };
    });

    if (result.integraContabilidad) {
      this.contabilidadIntegracion.integrarCierreDespacho?.(result.despachoId).catch(() => {
        // fail-silent: la contabilidad no debe interrumpir el flujo de negocio
      });
    }

    return this.findOne(embarqueId, empresaId);
  }

  /**
   * Reintenta la integración contable de un despacho ya CONFIRMADO.
   * `integrarCierreDespacho` se dispara al confirmar pero es fail-silent (un error
   * de contabilidad no debe cortar el flujo de negocio), así que sin esto un asiento
   * que no se generó quedaba sin forma de recuperarse. Es idempotente: la propia
   * integración corta si ya existe el documento contable del despacho.
   */
  async reintegrarContabilidadDespacho(embarqueId: string, despachoId: string, empresaId: string) {
    const despacho = await this.prisma.imp_despachos.findFirst({
      where: { id: despachoId, embarque_id: embarqueId, empresa_id: empresaId },
      select: { id: true, estado: true, numero_despacho: true },
    });
    if (!despacho) throw new NotFoundException('Despacho no encontrado');
    if (despacho.estado !== 'CONFIRMADO') {
      throw new BadRequestException(
        `Solo se puede integrar a contabilidad un despacho CONFIRMADO (actual: ${despacho.estado})`,
      );
    }

    const yaExistia = await this.prisma.cont_documentos.findFirst({
      where: { empresa_id: empresaId, origen_tipo: 'imp_despacho', origen_id: despachoId },
      select: { id: true },
    });
    if (yaExistia) {
      return {
        message: 'El despacho ya tenía su documento contable',
        creado: false,
        documento_id: yaExistia.id,
      };
    }

    // Acá SÍ esperamos el resultado (a diferencia del confirmar): el usuario pidió
    // explícitamente la integración, así que debe ver si funcionó o no.
    await this.contabilidadIntegracion.integrarCierreDespacho?.(despachoId);

    const doc = await this.prisma.cont_documentos.findFirst({
      where: { empresa_id: empresaId, origen_tipo: 'imp_despacho', origen_id: despachoId },
      select: { id: true, numero_documento: true },
    });
    if (!doc) {
      throw new BadRequestException(
        'No se pudo generar el asiento. Revisá que haya un período contable abierto para la fecha del despacho y que estén mapeadas las cuentas de Inventario, IVA Crédito y del proveedor.',
      );
    }
    return { message: 'Asiento contable generado', creado: true, documento_id: doc.id };
  }

  // ── Fase 2: Liberar embarque (transición DESPACHADO → LIBERADO) ──

  async liberarEmbarque(embarqueId: string, dto: LiberarEmbarqueDto, empresaId: string, userId?: string) {
    return this.prisma.$transaction(async (tx) => {
      const embarque = await this.ensureEmbarqueExists(tx, embarqueId, empresaId);
      if (embarque.estado !== 'DESPACHADO') {
        throw new BadRequestException(
          `Solo se puede liberar un embarque en DESPACHADO (actual: ${embarque.estado})`,
        );
      }

      const deposito = await tx.depositos.findFirst({
        where: { id: dto.deposito_destino_id, empresa_id: empresaId, active: true },
      });
      if (!deposito) throw new BadRequestException('Depósito destino inválido para la empresa');

      const items = await tx.imp_embarque_items.findMany({
        where: { embarque_id: embarqueId, empresa_id: empresaId, activo: true },
      });

      let productosCreados = 0;
      let stockActualizado = 0;

      if (embarque.perfil === 'AUTOS') {
        const defaults = await this.resolveProductoDefaults(tx, empresaId);
        const tipoMovimientoId = await this.resolveTipoMovimientoEntradaId(tx);
        for (const item of items) {
          if (!item.chasis) continue;

          let productoId = item.producto_id;
          if (!productoId) {
            const existente = await tx.productos.findFirst({
              where: { empresa_id: empresaId, cod_producto: item.chasis, deleted: false },
              select: { id: true },
            });
            if (existente) {
              productoId = existente.id;
            } else {
              const descripcionPartes = [item.marca, item.modelo, item.anio?.toString(), item.chasis].filter(Boolean);
              const descripcion = descripcionPartes.join(' ').trim() || `Vehículo ${item.chasis}`;
              const producto = await tx.productos.create({
                data: {
                  empresa_id: empresaId,
                  unidad_medida_id: defaults.unidadMedidaId,
                  afectacion_id: defaults.afectacionId,
                  descripcion,
                  cod_producto: item.chasis,
                  precio: 0,
                  precio_costo: Number(item.costo_unitario_gs ?? 0),
                  porcentaje_iva: 10,
                  proporcion_iva: 100,
                  maneja_inventario: true,
                  maneja_lote: false,
                  active: true,
                  deleted: false,
                  usuario: userId ?? null,
                },
              });
              productoId = producto.id;
              productosCreados += 1;
            }
          }

          await tx.stock_deposito.upsert({
            where: { deposito_id_producto_id: { deposito_id: deposito.id, producto_id: productoId } },
            update: { cantidad_disponible: { increment: Number(item.cantidad ?? 1) }, updated_at: new Date() },
            create: {
              deposito_id: deposito.id,
              producto_id: productoId,
              cantidad_disponible: Number(item.cantidad ?? 1),
              cantidad_reservada: 0,
              stock_minimo: 0,
            },
          });

          await tx.movimientos_inventario.create({
            data: {
              deposito_origen_id: deposito.id,
              producto_id: productoId,
              tipo_movimiento_id: tipoMovimientoId,
              cantidad: Number(item.cantidad ?? 1),
              costo_unitario: Number(item.costo_unitario_gs ?? 0),
              documento_origen: 'IMP',
              documento_id: embarqueId,
              usuario_id: userId ?? null,
              observaciones: `Liberación embarque ${embarque.numero ?? ''}`.trim(),
            },
          });

          await tx.imp_embarque_items.update({
            where: { id: item.id },
            data: { producto_id: productoId, estado: 'DISPONIBLE', updated_at: new Date() },
          });
          stockActualizado += 1;
        }
      } else {
        // CONSUMO_MASIVO: la mercadería entra al depósito contra el producto del
        // catálogo. A diferencia de AUTOS no se crea el producto acá: el ítem ya
        // viene mapeado a uno existente (`producto_id` o `sku_interno_id`), porque
        // un mismo SKU se repite entre embarques y crear uno nuevo por importación
        // duplicaría el catálogo.
        //
        // Antes esta rama sólo marcaba los ítems DISPONIBLE y no tocaba inventario:
        // un embarque liberado no sumaba una sola unidad al stock.
        const tipoMovimientoId = await this.resolveTipoMovimientoEntradaId(tx);

        // Se valida TODO antes de escribir. Liberar es irreversible desde la
        // pantalla, así que un embarque a medias dejaría stock inconsistente sin
        // forma de rehacerlo.
        const sinProducto = items.filter((i) => !(i.producto_id ?? i.sku_interno_id));
        if (sinProducto.length) {
          const detalle = sinProducto
            .slice(0, 5)
            .map((i) => i.sku_proveedor || i.descripcion_origen || i.id.substring(0, 8))
            .join(', ');
          throw new BadRequestException(
            `No se puede liberar: ${sinProducto.length} ítem(s) no tienen un producto del catálogo asociado ` +
              `(${detalle}${sinProducto.length > 5 ? ', …' : ''}). Asociálos desde los ítems del embarque y volvé a intentar.`,
          );
        }

        const sinCantidad = items.filter((i) => Number(i.cantidad ?? 0) <= 0);
        if (sinCantidad.length) {
          throw new BadRequestException(
            `No se puede liberar: ${sinCantidad.length} ítem(s) tienen cantidad en cero. Corregí la cantidad y volvé a intentar.`,
          );
        }

        // Un producto que no maneja inventario (un servicio) no puede tener stock:
        // se deja disponible pero no se le crea movimiento.
        const productoIds = [...new Set(items.map((i) => (i.producto_id ?? i.sku_interno_id) as string))];
        const productos = await tx.productos.findMany({
          where: { id: { in: productoIds }, empresa_id: empresaId, deleted: false },
          select: { id: true, maneja_inventario: true },
        });
        const manejaInventario = new Map(productos.map((pr) => [pr.id, pr.maneja_inventario !== false]));

        const faltantes = productoIds.filter((id) => !manejaInventario.has(id));
        if (faltantes.length) {
          throw new BadRequestException(
            `No se puede liberar: ${faltantes.length} producto(s) asociados a los ítems no existen o están eliminados.`,
          );
        }

        let sinInventario = 0;
        for (const item of items) {
          const productoId = (item.producto_id ?? item.sku_interno_id) as string;
          const cantidad = Number(item.cantidad ?? 0);

          if (manejaInventario.get(productoId)) {
            await tx.stock_deposito.upsert({
              where: { deposito_id_producto_id: { deposito_id: deposito.id, producto_id: productoId } },
              update: { cantidad_disponible: { increment: cantidad }, updated_at: new Date() },
              create: {
                deposito_id: deposito.id,
                producto_id: productoId,
                cantidad_disponible: cantidad,
                cantidad_reservada: 0,
                stock_minimo: 0,
              },
            });

            await tx.movimientos_inventario.create({
              data: {
                deposito_origen_id: deposito.id,
                producto_id: productoId,
                tipo_movimiento_id: tipoMovimientoId,
                cantidad,
                // Costo ya prorrateado por el costeo: FOB + flete + seguro + tributos.
                costo_unitario: Number(item.costo_unitario_gs ?? 0),
                documento_origen: 'IMP',
                documento_id: embarqueId,
                usuario_id: userId ?? null,
                observaciones: `Liberación embarque ${embarque.numero ?? ''}`.trim(),
              },
            });
            stockActualizado += 1;
          } else {
            sinInventario += 1;
          }

          await tx.imp_embarque_items.update({
            where: { id: item.id },
            data: { producto_id: productoId, estado: 'DISPONIBLE', updated_at: new Date() },
          });
        }

        if (sinInventario > 0) {
          this.logger.warn(
            `Embarque ${embarque.numero}: ${sinInventario} ítem(s) quedaron disponibles sin movimiento de inventario porque su producto no maneja stock.`,
          );
        }
      }

      await tx.imp_embarques.update({
        where: { id: embarqueId },
        data: { estado: 'LIBERADO', updated_at: new Date() },
      });
      await this.registerBitacora(tx, {
        embarqueId,
        empresaId,
        evento: 'ESTADO_CAMBIO',
        estadoAnterior: 'DESPACHADO',
        estadoNuevo: 'LIBERADO',
        usuarioId: userId,
      });
      await this.registerBitacora(tx, {
        embarqueId,
        empresaId,
        evento: 'EMBARQUE_LIBERADO',
        detalle: {
          deposito_id: deposito.id,
          deposito_nombre: deposito.descripcion ?? null,
          productos_creados: productosCreados,
          items_actualizados: stockActualizado,
        },
        usuarioId: userId,
      });

      return {
        message: 'Embarque liberado',
        deposito_destino_id: deposito.id,
        productos_creados: productosCreados,
        items_actualizados: stockActualizado,
      };
    });
  }

  private async ensureDespachoEditable(tx: Tx, embarqueId: string, despachoId: string, empresaId: string) {
    const despacho = await tx.imp_despachos.findFirst({
      where: { id: despachoId, embarque_id: embarqueId, empresa_id: empresaId },
    });
    if (!despacho) throw new NotFoundException('Despacho no encontrado');
    if (despacho.estado !== 'BORRADOR') {
      throw new BadRequestException('Solo se puede modificar/eliminar un despacho en BORRADOR');
    }
    return despacho;
  }

  private normalizeTributos(dto: Partial<CreateDespachoDto>) {
    return {
      arancel_gs: Number(dto.arancel_gs ?? 0),
      iva_importacion_gs: Number(dto.iva_importacion_gs ?? 0),
      isc_gs: Number(dto.isc_gs ?? 0),
      inc_gs: Number(dto.inc_gs ?? 0),
      anticipo_ire_gs: Number(dto.anticipo_ire_gs ?? 0),
      tasa_ana_gs: Number(dto.tasa_ana_gs ?? 0),
      otros_gs: Number(dto.otros_gs ?? 0),
    };
  }

  private sumTributos(t: ReturnType<ImportacionesService['normalizeTributos']>) {
    return (
      t.arancel_gs + t.iva_importacion_gs + t.isc_gs + t.inc_gs + t.anticipo_ire_gs + t.tasa_ana_gs + t.otros_gs
    );
  }

  private async resolveProductoDefaults(tx: Tx, empresaId: string) {
    const unidad = await tx.unidades_de_medida.findFirst({
      where: { OR: [{ representacion: 'UNI' }, { codigo: 77 }] },
      select: { id: true },
      orderBy: { codigo: 'asc' },
    });
    if (!unidad) {
      throw new BadRequestException('No se encontró unidad de medida default (UNI / código 77)');
    }
    const afectacion = await tx.afectacion_iva.findFirst({
      where: { OR: [{ codigo: 1 }, { descripcion: { contains: 'Gravado IVA', mode: 'insensitive' } }] },
      select: { id: true },
    });
    if (!afectacion) {
      throw new BadRequestException('No se encontró afectación IVA default (Gravado IVA)');
    }
    return { unidadMedidaId: unidad.id, afectacionId: afectacion.id, empresaId };
  }

  private async resolveTipoMovimientoEntradaId(tx: Tx) {
    const tipo = await tx.tipo_movimiento_inventario.findFirst({
      where: { codigo: 'ENTRADA' },
      select: { id: true },
    });
    if (!tipo) throw new BadRequestException('Tipo de movimiento ENTRADA no configurado');
    return tipo.id;
  }

  private async generarNumero(tx: Tx, empresaId: string): Promise<string> {
    const year = new Date().getFullYear();
    const prefijo = `IMP-${year}-`;
    const ultimo = await tx.imp_embarques.findFirst({
      where: { empresa_id: empresaId, numero: { startsWith: prefijo } },
      orderBy: { numero: 'desc' },
      select: { numero: true },
    });
    const correlativo = ultimo?.numero ? Number(ultimo.numero.slice(prefijo.length)) || 0 : 0;
    return `${prefijo}${String(correlativo + 1).padStart(6, '0')}`;
  }

  private async validateEmbarqueRefs(tx: Tx, empresaId: string, proveedorId?: string, monedaId?: string | null) {
    if (proveedorId) {
      const proveedor = await tx.proveedores.findFirst({
        where: { id: proveedorId, empresa_id: empresaId, activo: true },
        select: { id: true, tipo_entidad: true },
      });
      if (!proveedor) throw new BadRequestException('Proveedor inválido para la empresa');
      // FASE 2: solo proveedores (locales o del exterior) pueden operar importaciones.
      // Acreedores varios operan exclusivamente por Gastos.
      if ((proveedor as any).tipo_entidad === 'ACREEDOR_VARIO') {
        throw new BadRequestException(
          'Este tercero está clasificado como "Acreedor vario" y no puede usarse en importaciones.',
        );
      }
    }
    if (monedaId) {
      const moneda = await tx.moneda.findUnique({ where: { id: monedaId } });
      if (!moneda) throw new BadRequestException('Moneda inválida');
    }
  }

  private async hasImportacionesModuleInSubscription(tx: Tx, empresaId: string) {
    const moduloActivo = await tx.suscripcion_modulos.findFirst({
      where: {
        activo: true,
        suscripciones: {
          empresa_id: empresaId,
          estado: { in: ['Activo', 'EnGracia'] },
        },
        modulos: {
          codigo: 'IMPORTACIONES',
          active: true,
        },
      },
      select: { id: true },
    });
    return Boolean(moduloActivo);
  }

  private async getOrCreateConfig(tx: Tx, empresaId: string) {
    const config = await tx.imp_config_empresa.findUnique({ where: { empresa_id: empresaId } });
    if (config) return config;

    const hasModuloSuscripcion = await this.hasImportacionesModuleInSubscription(tx, empresaId);
    return tx.imp_config_empresa.create({
      data: {
        empresa_id: empresaId,
        modulo_operativo_activo: hasModuloSuscripcion,
        perfil_activo: 'AUTOS',
      },
    });
  }

  private buildConfigResponse(
    config: { modulo_operativo_activo: boolean; perfil_activo: string },
    hasModuloSuscripcion: boolean,
  ) {
    return {
      modulo_operativo_activo: Boolean(config.modulo_operativo_activo),
      perfil_activo: config.perfil_activo,
      has_subscription_module: hasModuloSuscripcion,
      can_operate: hasModuloSuscripcion && Boolean(config.modulo_operativo_activo),
    };
  }

  private assertPerfilConfigMatches(perfil: string, perfilActivo: string) {
    if (perfil !== perfilActivo) {
      throw new BadRequestException(
        `El perfil del embarque (${perfil}) debe coincidir con el perfil activo de la empresa (${perfilActivo})`,
      );
    }
  }

  private assertModuloOperativoActivo(moduloOperativoActivo: boolean) {
    if (!moduloOperativoActivo) {
      throw new BadRequestException('El módulo de importaciones está desactivado para esta empresa');
    }
  }

  private async getEditableEmbarque(tx: Tx, id: string, empresaId: string) {
    const embarque = await tx.imp_embarques.findFirst({ where: { id, empresa_id: empresaId, activo: true } });
    if (!embarque) throw new NotFoundException('Embarque no encontrado');
    if (!ESTADOS_EDITABLES.has(embarque.estado)) {
      throw new BadRequestException(`El embarque en estado ${embarque.estado} no admite cambios`);
    }
    return embarque;
  }

  private async ensureEmbarqueExists(db: PrismaService | Tx, id: string, empresaId: string) {
    const embarque = await db.imp_embarques.findFirst({ where: { id, empresa_id: empresaId, activo: true } });
    if (!embarque) throw new NotFoundException('Embarque no encontrado');
    return embarque;
  }

  private async validateItemPayload(tx: Tx, embarque: any, dto: any, excludeItemId?: string) {
    if (dto.producto_id) {
      const producto = await tx.productos.findFirst({
        where: {
          id: dto.producto_id,
          deleted: false,
          active: true,
          OR: [{ empresa_id: embarque.empresa_id }, { empresa_id: null }],
        },
      });
      if (!producto) throw new BadRequestException('Producto inválido para la empresa');
    }
    if (dto.sku_interno_id) {
      const sku = await tx.productos.findFirst({
        where: {
          id: dto.sku_interno_id,
          deleted: false,
          active: true,
          OR: [{ empresa_id: embarque.empresa_id }, { empresa_id: null }],
        },
      });
      if (!sku) throw new BadRequestException('SKU interno inválido para la empresa');
    }

    if (embarque.perfil === 'AUTOS') {
      if (!dto.chasis?.trim()) throw new BadRequestException('El chasis es obligatorio para importación de autos');
      const duplicated = await tx.imp_embarque_items.findFirst({
        where: {
          empresa_id: embarque.empresa_id,
          chasis: dto.chasis.trim(),
          activo: true,
          ...(excludeItemId ? { id: { not: excludeItemId } } : {}),
        },
      });
      if (duplicated) throw new BadRequestException('Ya existe un ítem activo con ese chasis');
    } else if (!dto.descripcion_origen?.trim() && !dto.producto_id && !dto.sku_proveedor?.trim()) {
      throw new BadRequestException('Debe indicar producto, SKU proveedor o descripción de origen');
    }
  }

  private async normalizeItem(
    tx: Tx,
    embarque: { perfil: string },
    dto: any,
    embarqueId: string,
    existing?: { orden?: number | null } | null,
  ) {
    const cantidad = embarque.perfil === 'AUTOS' ? 1 : Math.max(Number(dto.cantidad ?? 1), 0.0001);
    const fobUnit = Math.max(Number(dto.fob_unit_usd ?? dto.precio_fob_usd ?? 0), 0);
    const orden = await this.resolveOrdenItem(tx, embarqueId, dto.orden, existing?.orden ?? undefined);

    const isAutos = embarque.perfil === 'AUTOS';
    return {
      producto_id: dto.producto_id || null,
      sku_interno_id: !isAutos ? dto.sku_interno_id || null : null,
      chasis: isAutos ? dto.chasis?.trim() || null : null,
      motor: isAutos ? dto.motor?.trim() || null : null,
      marca: isAutos ? dto.marca?.trim() || null : null,
      modelo: isAutos ? dto.modelo?.trim() || null : null,
      version: isAutos ? dto.version?.trim() || null : null,
      anio: isAutos ? dto.anio ?? null : null,
      cilindrada_cc: isAutos ? dto.cilindrada_cc ?? null : null,
      kilometraje: isAutos ? dto.kilometraje ?? null : null,
      color: isAutos ? dto.color?.trim() || null : null,
      subasta_lote: isAutos ? dto.subasta_lote?.trim() || null : null,
      precio_fob_usd: isAutos ? dto.precio_fob_usd ?? fobUnit : null,
      inland_origen_usd: isAutos ? dto.inland_origen_usd ?? null : null,
      cantidad_duenios: isAutos ? dto.cantidad_duenios ?? null : null,
      sku_proveedor: !isAutos ? dto.sku_proveedor?.trim() || null : null,
      descripcion_origen: dto.descripcion_origen?.trim() || null,
      cantidad,
      unidad: dto.unidad?.trim() || 'UN',
      peso_unit_kg: !isAutos ? dto.peso_unit_kg ?? null : null,
      cbm_unit: !isAutos ? dto.cbm_unit ?? null : null,
      ncm: !isAutos ? dto.ncm?.trim() || null : null,
      arancel_pct: !isAutos ? dto.arancel_pct ?? null : null,
      isc_pct: !isAutos ? dto.isc_pct ?? null : null,
      mapeo_resuelto: !isAutos ? Boolean(dto.mapeo_resuelto) : false,
      fob_unit_usd: fobUnit,
      fob_total_usd: this.round(cantidad * fobUnit, 4),
      orden,
    };
  }

  private async resolveOrdenItem(tx: Tx, embarqueId: string, requested?: number | null, existing?: number | null) {
    if (requested !== undefined && requested !== null) return Number(requested);
    if (existing !== undefined && existing !== null) return Number(existing);
    const last = await tx.imp_embarque_items.aggregate({
      where: { embarque_id: embarqueId, activo: true },
      _max: { orden: true },
    });
    return Number(last._max.orden ?? 0) + 10;
  }

  private async normalizeCosto(
    tx: Tx,
    empresaId: string,
    embarqueId: string,
    estadoEmbarque: string,
    dto: any,
  ) {
    const concepto = await tx.imp_conceptos_costo.findFirst({
      where: {
        id: dto.concepto_id,
        activo: true,
        OR: [{ empresa_id: empresaId }, { empresa_id: null, es_sistema: true }],
      },
    });
    if (!concepto) throw new BadRequestException('Concepto de costo inválido');
    if (dto.moneda_id) {
      const moneda = await tx.moneda.findUnique({ where: { id: dto.moneda_id } });
      if (!moneda) throw new BadRequestException('Moneda inválida');
    }
    if (dto.proveedor_id) {
      const proveedor = await tx.proveedores.findFirst({
        where: { id: dto.proveedor_id, empresa_id: empresaId, activo: true },
      });
      if (!proveedor) throw new BadRequestException('Proveedor inválido para la empresa');
    }
    if (dto.cuenta_pagar_id) {
      const cxp = await tx.cuentas_pagar.findFirst({
        where: { id: dto.cuenta_pagar_id, empresa_id: empresaId },
      });
      if (!cxp) throw new BadRequestException('Cuenta a pagar inválida para la empresa');
    }

    const esDirecto = Boolean(dto.es_directo);
    let itemId: string | null = null;
    if (esDirecto) {
      if (!dto.item_id) throw new BadRequestException('Los costos directos requieren item_id');
      const item = await tx.imp_embarque_items.findFirst({
        where: { id: dto.item_id, embarque_id: embarqueId, empresa_id: empresaId, activo: true },
      });
      if (!item) throw new BadRequestException('El ítem indicado no pertenece al embarque');
      itemId = dto.item_id;
    } else if (!dto.criterio_prorrateo) {
      throw new BadRequestException('Los costos compartidos requieren criterio de prorrateo');
    }

    const esGastoExtra = Boolean(dto.es_gasto_extra);
    const motivo = (dto.motivo_gasto_extra || '').toString().trim();
    if (esGastoExtra && !motivo) {
      throw new BadRequestException('Los gastos extras requieren un motivo obligatorio');
    }

    const importe = Math.max(Number(dto.importe ?? 0), 0);
    const cotizacion = Math.max(Number(dto.cotizacion ?? 1), 0.000001);
    const esPosteriorCierre =
      dto.es_posterior_cierre !== undefined
        ? Boolean(dto.es_posterior_cierre)
        : ['CERRADO', 'SELLADO'].includes(estadoEmbarque);

    return {
      empresa_id: empresaId,
      embarque_id: embarqueId,
      item_id: itemId,
      concepto_id: dto.concepto_id,
      moneda_id: dto.moneda_id || null,
      proveedor_id: dto.proveedor_id || null,
      cuenta_pagar_id: dto.cuenta_pagar_id || null,
      descripcion: dto.descripcion?.trim() || null,
      numero_factura: dto.numero_factura?.trim() || null,
      importe,
      cotizacion,
      importe_gs: Math.round(importe * cotizacion),
      es_directo: esDirecto,
      criterio_prorrateo: esDirecto ? null : dto.criterio_prorrateo || 'FOB',
      fecha_devengo: dto.fecha_devengo ? new Date(dto.fecha_devengo) : null,
      fecha_pago: dto.fecha_pago ? new Date(dto.fecha_pago) : null,
      es_posterior_cierre: esPosteriorCierre,
      es_gasto_extra: esGastoExtra,
      motivo_gasto_extra: esGastoExtra ? motivo : null,
      observaciones: dto.observaciones?.trim() || null,
      estado: 'CONFIRMADO',
    };
  }

  private round(value: number, decimals = 4) {
    const factor = 10 ** decimals;
    return Math.round((Number(value) || 0) * factor) / factor;
  }

  private async registerBitacora(
    tx: Tx,
    params: {
      embarqueId: string;
      empresaId: string;
      evento: string;
      estadoAnterior?: string | null;
      estadoNuevo?: string | null;
      detalle?: Record<string, unknown> | null;
      observacion?: string | null;
      usuarioId?: string | null;
    },
  ) {
    await tx.imp_embarques_bitacora.create({
      data: {
        embarque_id: params.embarqueId,
        empresa_id: params.empresaId,
        evento: params.evento,
        estado_anterior: params.estadoAnterior ?? null,
        estado_nuevo: params.estadoNuevo ?? null,
        detalle: (params.detalle ?? Prisma.JsonNull) as Prisma.InputJsonValue,
        observacion: params.observacion ?? null,
        usuario_id: params.usuarioId ?? null,
      },
    });
  }

  private buildEmbarqueDiff(
    existing: Record<string, any>,
    dto: UpdateEmbarqueDto,
  ): Record<string, { anterior: unknown; nuevo: unknown }> | null {
    const tracked: (keyof UpdateEmbarqueDto)[] = [
      'proveedor_id',
      'moneda_id',
      'origen_pais',
      'origen_puerto',
      'destino_puerto',
      'modalidad',
      'booking',
      'bill_of_lading',
      'fecha_embarque',
      'fecha_eta',
      'fecha_arribo_real',
      'cotizacion_cierre',
      'notas',
    ];
    const sentKeys = tracked.filter(
      (key) => (dto as Record<string, unknown>)[key as string] !== undefined,
    ) as string[];
    return buildBitacoraDiff(existing, dto as Record<string, unknown>, sentKeys);
  }

  private summarizeItem(item: Record<string, any>): Record<string, unknown> {
    return {
      item_id: item.id,
      identificador: this.itemIdentifier(item),
      chasis: item.chasis ?? null,
      sku_proveedor: item.sku_proveedor ?? null,
      cantidad: Number(item.cantidad ?? 0),
      fob_total_usd: Number(item.fob_total_usd ?? 0),
    };
  }

  private itemIdentifier(item: Record<string, any>): string {
    return (
      item.chasis ||
      item.sku_proveedor ||
      item.descripcion_origen ||
      [item.marca, item.modelo].filter(Boolean).join(' ') ||
      String(item.id)
    );
  }

  private async summarizeCosto(tx: Tx, costo: Record<string, any>): Promise<Record<string, unknown>> {
    const concepto = await tx.imp_conceptos_costo.findUnique({
      where: { id: costo.concepto_id },
      select: { nombre: true, codigo: true },
    });
    return {
      componente_id: costo.id,
      concepto: concepto?.nombre ?? null,
      concepto_codigo: concepto?.codigo ?? null,
      es_directo: Boolean(costo.es_directo),
      importe: Number(costo.importe ?? 0),
      importe_gs: Number(costo.importe_gs ?? 0),
      item_id: costo.item_id ?? null,
    };
  }
}
