import { Injectable } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { PrismaService } from 'src/prisma/prisma.service';
import { formatDateResponse } from 'src/utils/utilidades';

type TipoValorBitacora =
  | 'texto'
  | 'numero'
  | 'booleano'
  | 'fecha'
  | 'estado'
  | 'porcentaje'
  | 'moneda_gs'
  | 'moneda_usd';

type TipoReferencia = 'proveedor' | 'moneda' | 'concepto' | 'item' | 'producto' | 'deposito' | 'cxp';

interface CampoMeta {
  etiqueta: string;
  tipo: TipoValorBitacora;
  referencia?: TipoReferencia;
}

export interface BitacoraRawEvent {
  evento: string;
  estado_anterior: string | null;
  estado_nuevo: string | null;
  detalle: Prisma.JsonValue | null;
  observacion: string | null;
  usuario_id: string | null;
  created_at: Date;
}

export interface BitacoraCambioResponse {
  campo: string;
  tipo: TipoValorBitacora;
  anterior: unknown;
  nuevo: unknown;
  tiene_anterior: boolean;
}

export interface BitacoraDatoResponse {
  campo: string;
  tipo: TipoValorBitacora;
  valor: unknown;
}

export interface BitacoraEventResponse {
  evento: string;
  created_at: string | null;
  usuario_nombre: string;
  resumen: string;
  estado_anterior: string | null;
  estado_nuevo: string | null;
  observacion: string | null;
  cambios: BitacoraCambioResponse[];
  datos: BitacoraDatoResponse[];
}

const CAMPOS: Record<string, CampoMeta> = {
  proveedor_id: { etiqueta: 'Proveedor', tipo: 'texto', referencia: 'proveedor' },
  despachante_id: { etiqueta: 'Despachante', tipo: 'texto', referencia: 'proveedor' },
  moneda_id: { etiqueta: 'Moneda', tipo: 'texto', referencia: 'moneda' },
  concepto_id: { etiqueta: 'Concepto de costo', tipo: 'texto', referencia: 'concepto' },
  item_id: { etiqueta: 'Ítem', tipo: 'texto', referencia: 'item' },
  producto_id: { etiqueta: 'Producto', tipo: 'texto', referencia: 'producto' },
  sku_interno_id: { etiqueta: 'SKU interno', tipo: 'texto', referencia: 'producto' },
  deposito_id: { etiqueta: 'Depósito', tipo: 'texto', referencia: 'deposito' },
  deposito_destino_id: { etiqueta: 'Depósito', tipo: 'texto', referencia: 'deposito' },
  cuenta_pagar_id: { etiqueta: 'Cuenta por pagar', tipo: 'texto', referencia: 'cxp' },
  numero: { etiqueta: 'Número de embarque', tipo: 'texto' },
  perfil: { etiqueta: 'Perfil', tipo: 'estado' },
  origen_pais: { etiqueta: 'País de origen', tipo: 'texto' },
  origen_puerto: { etiqueta: 'Puerto de origen', tipo: 'texto' },
  destino_puerto: { etiqueta: 'Puerto de destino', tipo: 'texto' },
  modalidad: { etiqueta: 'Modalidad', tipo: 'texto' },
  booking: { etiqueta: 'Booking', tipo: 'texto' },
  bill_of_lading: { etiqueta: 'Bill of Lading', tipo: 'texto' },
  fecha_embarque: { etiqueta: 'Fecha de embarque', tipo: 'fecha' },
  fecha_eta: { etiqueta: 'ETA', tipo: 'fecha' },
  fecha_arribo_real: { etiqueta: 'Arribo real', tipo: 'fecha' },
  cotizacion_cierre: { etiqueta: 'Cotización de cierre', tipo: 'numero' },
  notas: { etiqueta: 'Notas', tipo: 'texto' },
  identificador: { etiqueta: 'Ítem', tipo: 'texto' },
  chasis: { etiqueta: 'Chasis', tipo: 'texto' },
  motor: { etiqueta: 'Motor', tipo: 'texto' },
  marca: { etiqueta: 'Marca', tipo: 'texto' },
  modelo: { etiqueta: 'Modelo', tipo: 'texto' },
  version: { etiqueta: 'Versión', tipo: 'texto' },
  anio: { etiqueta: 'Año', tipo: 'numero' },
  cilindrada_cc: { etiqueta: 'Cilindrada', tipo: 'numero' },
  kilometraje: { etiqueta: 'Kilometraje', tipo: 'numero' },
  color: { etiqueta: 'Color', tipo: 'texto' },
  subasta_lote: { etiqueta: 'Lote de subasta', tipo: 'texto' },
  precio_fob_usd: { etiqueta: 'Precio FOB', tipo: 'moneda_usd' },
  inland_origen_usd: { etiqueta: 'Inland de origen', tipo: 'moneda_usd' },
  cantidad_duenios: { etiqueta: 'Dueños anteriores', tipo: 'numero' },
  sku_proveedor: { etiqueta: 'SKU del proveedor', tipo: 'texto' },
  descripcion_origen: { etiqueta: 'Descripción de origen', tipo: 'texto' },
  cantidad: { etiqueta: 'Cantidad', tipo: 'numero' },
  unidad: { etiqueta: 'Unidad', tipo: 'texto' },
  peso_unit_kg: { etiqueta: 'Peso unitario (kg)', tipo: 'numero' },
  cbm_unit: { etiqueta: 'CBM unitario', tipo: 'numero' },
  ncm: { etiqueta: 'NCM', tipo: 'texto' },
  arancel_pct: { etiqueta: 'Arancel', tipo: 'porcentaje' },
  isc_pct: { etiqueta: 'ISC', tipo: 'porcentaje' },
  fob_unit_usd: { etiqueta: 'FOB unitario', tipo: 'moneda_usd' },
  fob_total_usd: { etiqueta: 'FOB total', tipo: 'moneda_usd' },
  orden: { etiqueta: 'Orden', tipo: 'numero' },
  mapeo_resuelto: { etiqueta: 'Mapeo resuelto', tipo: 'booleano' },
  concepto: { etiqueta: 'Concepto', tipo: 'texto' },
  concepto_codigo: { etiqueta: 'Código de concepto', tipo: 'texto' },
  descripcion: { etiqueta: 'Descripción', tipo: 'texto' },
  numero_factura: { etiqueta: 'Número de factura', tipo: 'texto' },
  importe: { etiqueta: 'Importe', tipo: 'numero' },
  importe_gs: { etiqueta: 'Importe en guaraníes', tipo: 'moneda_gs' },
  cotizacion: { etiqueta: 'Cotización', tipo: 'numero' },
  es_directo: { etiqueta: 'Costo directo', tipo: 'booleano' },
  criterio_prorrateo: { etiqueta: 'Criterio de prorrateo', tipo: 'estado' },
  fecha_devengo: { etiqueta: 'Fecha de devengo', tipo: 'fecha' },
  fecha_pago: { etiqueta: 'Fecha de pago', tipo: 'fecha' },
  es_posterior_cierre: { etiqueta: 'Posterior al cierre', tipo: 'booleano' },
  es_gasto_extra: { etiqueta: 'Gasto extra', tipo: 'booleano' },
  motivo_gasto_extra: { etiqueta: 'Motivo del gasto extra', tipo: 'texto' },
  observaciones: { etiqueta: 'Observaciones', tipo: 'texto' },
  numero_despacho: { etiqueta: 'Número de despacho', tipo: 'texto' },
  fecha_despacho: { etiqueta: 'Fecha de despacho', tipo: 'fecha' },
  arancel_gs: { etiqueta: 'Arancel', tipo: 'moneda_gs' },
  iva_importacion_gs: { etiqueta: 'IVA importación', tipo: 'moneda_gs' },
  isc_gs: { etiqueta: 'ISC', tipo: 'moneda_gs' },
  inc_gs: { etiqueta: 'INC', tipo: 'moneda_gs' },
  anticipo_ire_gs: { etiqueta: 'Anticipo IRE', tipo: 'moneda_gs' },
  tasa_ana_gs: { etiqueta: 'Tasa ANA', tipo: 'moneda_gs' },
  otros_gs: { etiqueta: 'Otros tributos', tipo: 'moneda_gs' },
  total_tributos_gs: { etiqueta: 'Total de tributos', tipo: 'moneda_gs' },
  cotizacion_usada: { etiqueta: 'Cotización utilizada', tipo: 'numero' },
  deposito_nombre: { etiqueta: 'Depósito', tipo: 'texto' },
  productos_creados: { etiqueta: 'Productos creados', tipo: 'numero' },
  items_actualizados: { etiqueta: 'Ítems actualizados', tipo: 'numero' },
  total_gs: { etiqueta: 'Total de la CxP', tipo: 'moneda_gs' },
  cuotas: { etiqueta: 'Cuotas generadas', tipo: 'numero' },
};

const CAMPOS_IGNORADOS = new Set([
  'componente_id',
  'despacho_id',
  'embarque_id',
  'empresa_id',
  'usuario_id',
]);

const RESUMEN_POR_EVENTO: Record<string, string> = {
  EMBARQUE_CREADO: 'Se creó el embarque',
  EMBARQUE_EDITADO: 'Se editaron los datos generales del embarque',
  ESTADO_CAMBIO: 'Cambió el estado del embarque',
  EMBARQUE_ELIMINADO: 'Se eliminó el embarque',
  ITEM_AGREGADO: 'Se agregó un ítem',
  ITEM_EDITADO: 'Se editó un ítem',
  ITEM_ELIMINADO: 'Se eliminó un ítem',
  COSTO_AGREGADO: 'Se agregó un componente de costo',
  COSTO_EDITADO: 'Se editó un componente de costo',
  COSTO_ELIMINADO: 'Se eliminó un componente de costo',
  DESPACHO_CREADO: 'Se creó el despacho aduanero',
  DESPACHO_EDITADO: 'Se editó el despacho aduanero',
  DESPACHO_ELIMINADO: 'Se eliminó el despacho aduanero',
  DESPACHO_CONFIRMADO: 'Se confirmó el despacho aduanero',
  EMBARQUE_LIBERADO: 'Se liberó el embarque al depósito',
  CXP_GENERADA: 'Se generó una cuenta por pagar',
};

type Referencias = Record<TipoReferencia, Map<string, string>>;

function normalizeAuditValue(value: unknown): unknown {
  if (value === null || value === undefined || value === '') return null;
  if (value instanceof Date) return value.toISOString().slice(0, 10);
  if (typeof value === 'object') {
    if ('toNumber' in value && typeof value.toNumber === 'function') {
      return value.toNumber();
    }
    const serialized = JSON.stringify(value);
    return serialized ? JSON.parse(serialized) : null;
  }
  return value;
}

export function buildBitacoraDiff(
  existing: Record<string, any>,
  updated: Record<string, any>,
  tracked: string[],
): Record<string, { anterior: unknown; nuevo: unknown }> | null {
  const changes: Record<string, { anterior: unknown; nuevo: unknown }> = {};
  for (const key of [...new Set(tracked)]) {
    const previous = normalizeAuditValue(existing[key]);
    const next = normalizeAuditValue(updated[key]);
    if (JSON.stringify(previous) !== JSON.stringify(next)) {
      changes[key] = { anterior: previous, nuevo: next };
    }
  }
  return Object.keys(changes).length ? changes : null;
}

@Injectable()
export class ImportacionesBitacoraPresenter {
  constructor(private readonly prisma: PrismaService) {}

  async present(
    events: BitacoraRawEvent[],
    empresaId: string,
    embarqueId: string,
  ): Promise<BitacoraEventResponse[]> {
    const references = await this.resolveReferences(events, empresaId, embarqueId);
    const userIds = [...new Set(events.map((event) => event.usuario_id).filter(Boolean))] as string[];
    const users = userIds.length
      ? await this.prisma.usuario.findMany({
          where: { id: { in: userIds } },
          select: { id: true, nombres: true, apellidos: true },
        })
      : [];
    const userMap = new Map(
      users.map((user) => [
        user.id,
        [user.nombres, user.apellidos].filter(Boolean).join(' ').trim() || 'Usuario',
      ]),
    );

    return events.map((event) => {
      const detail = this.asRecord(event.detalle);
      return {
        evento: event.evento,
        created_at: formatDateResponse(event.created_at),
        usuario_nombre: event.usuario_id ? userMap.get(event.usuario_id) || 'Usuario no disponible' : 'Sistema',
        resumen: this.buildSummary(event.evento, detail),
        estado_anterior: event.estado_anterior,
        estado_nuevo: event.estado_nuevo,
        observacion: event.observacion,
        cambios: this.buildChanges(detail.cambios, references),
        datos: this.buildData(detail, references),
      };
    });
  }

  private async resolveReferences(
    events: BitacoraRawEvent[],
    empresaId: string,
    embarqueId: string,
  ): Promise<Referencias> {
    const ids = {
      proveedor: new Set<string>(),
      moneda: new Set<string>(),
      concepto: new Set<string>(),
      item: new Set<string>(),
      producto: new Set<string>(),
      deposito: new Set<string>(),
      cxp: new Set<string>(),
    };

    for (const event of events) {
      this.collectReferenceIds(this.asRecord(event.detalle), ids);
    }

    const [proveedores, monedas, conceptos, items, productos, depositos, cuentas] = await Promise.all([
      ids.proveedor.size
        ? this.prisma.proveedores.findMany({
            where: { id: { in: [...ids.proveedor] }, empresa_id: empresaId },
            select: { id: true, personas: { select: { razon_social: true } } },
          })
        : [],
      ids.moneda.size
        ? this.prisma.moneda.findMany({
            where: { id: { in: [...ids.moneda] } },
            select: { id: true, codigo: true, descripcion: true },
          })
        : [],
      ids.concepto.size
        ? this.prisma.imp_conceptos_costo.findMany({
            where: {
              id: { in: [...ids.concepto] },
              OR: [{ empresa_id: empresaId }, { empresa_id: null, es_sistema: true }],
            },
            select: { id: true, codigo: true, nombre: true },
          })
        : [],
      ids.item.size
        ? this.prisma.imp_embarque_items.findMany({
            where: { id: { in: [...ids.item] }, empresa_id: empresaId, embarque_id: embarqueId },
            select: { id: true, chasis: true, sku_proveedor: true, descripcion_origen: true, marca: true, modelo: true },
          })
        : [],
      ids.producto.size
        ? this.prisma.productos.findMany({
            where: {
              id: { in: [...ids.producto] },
              OR: [{ empresa_id: empresaId }, { empresa_id: null }],
            },
            select: { id: true, cod_producto: true, descripcion: true },
          })
        : [],
      ids.deposito.size
        ? this.prisma.depositos.findMany({
            where: { id: { in: [...ids.deposito] }, empresa_id: empresaId },
            select: { id: true, codigo: true, descripcion: true },
          })
        : [],
      ids.cxp.size
        ? this.prisma.cuentas_pagar.findMany({
            where: { id: { in: [...ids.cxp] }, empresa_id: empresaId },
            select: { id: true, numero_cuota: true, total_cuotas: true },
          })
        : [],
    ]);

    return {
      proveedor: new Map(
        proveedores.map((row) => [
          row.id,
          row.personas?.razon_social || 'Proveedor',
        ]),
      ),
      moneda: new Map(monedas.map((row) => [row.id, row.codigo || row.descripcion || 'Moneda'])),
      concepto: new Map(
        conceptos.map((row) => [row.id, row.nombre || row.codigo || 'Concepto de costo']),
      ),
      item: new Map(
        items.map((row) => [
          row.id,
          row.chasis ||
            row.sku_proveedor ||
            row.descripcion_origen ||
            [row.marca, row.modelo].filter(Boolean).join(' ') ||
            'Ítem',
        ]),
      ),
      producto: new Map(
        productos.map((row) => [
          row.id,
          [row.cod_producto, row.descripcion].filter(Boolean).join(' · ') || 'Producto',
        ]),
      ),
      deposito: new Map(
        depositos.map((row) => [
          row.id,
          [row.codigo, row.descripcion].filter(Boolean).join(' · ') || 'Depósito',
        ]),
      ),
      cxp: new Map(
        cuentas.map((row) => [
          row.id,
          row.numero_cuota && row.total_cuotas
            ? `CxP cuota ${row.numero_cuota}/${row.total_cuotas}`
            : 'CxP vinculada',
        ]),
      ),
    };
  }

  private collectReferenceIds(value: unknown, ids: Record<TipoReferencia, Set<string>>, parentKey?: string) {
    if (typeof value === 'string' && parentKey) {
      const meta = CAMPOS[parentKey];
      if (meta?.referencia && this.isUuid(value)) ids[meta.referencia].add(value);
      return;
    }
    if (!value || typeof value !== 'object') return;
    if (Array.isArray(value)) {
      value.forEach((item) => this.collectReferenceIds(item, ids, parentKey));
      return;
    }
    for (const [key, nested] of Object.entries(value as Record<string, unknown>)) {
      this.collectReferenceIds(nested, ids, key === 'anterior' || key === 'nuevo' ? parentKey : key);
    }
  }

  private buildChanges(value: unknown, references: Referencias): BitacoraCambioResponse[] {
    const changes = this.asRecord(value);
    return Object.entries(changes).flatMap(([key, raw]) => {
      const meta = CAMPOS[key];
      if (!meta || CAMPOS_IGNORADOS.has(key)) return [];
      const record = this.asRecord(raw);
      const hasStructuredDiff =
        Object.prototype.hasOwnProperty.call(record, 'anterior') ||
        Object.prototype.hasOwnProperty.call(record, 'nuevo');
      const previous = hasStructuredDiff ? record.anterior : undefined;
      const next = hasStructuredDiff ? record.nuevo : raw;
      return [
        {
          campo: meta.etiqueta,
          tipo: meta.tipo,
          anterior: this.presentValue(previous, meta, references),
          nuevo: this.presentValue(next, meta, references),
          tiene_anterior: hasStructuredDiff,
        },
      ];
    });
  }

  private buildData(detail: Record<string, unknown>, references: Referencias): BitacoraDatoResponse[] {
    return Object.entries(detail).flatMap(([key, value]) => {
      if (key === 'cambios' || CAMPOS_IGNORADOS.has(key)) return [];
      if (key === 'deposito_id' && detail.deposito_nombre) return [];
      if (key === 'item_id' && detail.identificador) return [];
      const meta = CAMPOS[key];
      if (!meta) return [];
      return [
        {
          campo: meta.etiqueta,
          tipo: meta.tipo,
          valor: this.presentValue(value, meta, references),
        },
      ];
    });
  }

  private presentValue(value: unknown, meta: CampoMeta, references: Referencias): unknown {
    if (value === null || value === undefined || value === '') return null;
    if (typeof value === 'string' && this.isUuid(value) && !meta.referencia) {
      return 'Referencia no disponible';
    }
    if (!meta.referencia) return value;
    if (meta.referencia === 'cxp') {
      if (typeof value === 'boolean') return value ? 'CxP vinculada' : null;
      return typeof value === 'string'
        ? references.cxp.get(value) || 'Referencia no disponible'
        : 'Referencia no disponible';
    }
    return typeof value === 'string'
      ? references[meta.referencia].get(value) || 'Referencia no disponible'
      : 'Referencia no disponible';
  }

  private buildSummary(event: string, detail: Record<string, unknown>): string {
    const base = RESUMEN_POR_EVENTO[event] || event.toLowerCase().replace(/_/g, ' ');
    const subject =
      this.stringValue(detail.concepto) ||
      this.stringValue(detail.identificador) ||
      this.stringValue(detail.numero_despacho) ||
      this.stringValue(detail.numero) ||
      this.stringValue(detail.deposito_nombre);
    return subject ? `${base}: ${subject}` : base;
  }

  private stringValue(value: unknown): string | null {
    return typeof value === 'string' && value.trim() && !this.isUuid(value) ? value.trim() : null;
  }

  private asRecord(value: unknown): Record<string, any> {
    return value && typeof value === 'object' && !Array.isArray(value)
      ? (value as Record<string, any>)
      : {};
  }

  private isUuid(value: string): boolean {
    return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value);
  }
}
