import { InjectQueue } from '@nestjs/bullmq';
import { BadRequestException, Injectable, Logger, NotFoundException } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { Queue } from 'bullmq';
import { PrismaService } from 'src/prisma/prisma.service';
import * as XLSX from 'xlsx';
import { CrearAliasTipoGastoDto, GuardarMapeoDto } from './dto/gastos-importador.dto';
import { construirFilaEstructurada, normalizarTextoAlias, resolverMapeoColumnas } from './normalizadores/libro-compras-set.normalizador';

export const GASTOS_IMPORT_QUEUE = 'gastos-import-queue';

type EstadoFila = 'pendiente' | 'valida' | 'invalida' | 'omitida' | 'importada' | 'error';

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

  constructor(
    private readonly prisma: PrismaService,
    @InjectQueue(GASTOS_IMPORT_QUEUE) private readonly queue: Queue,
  ) {}

  async subirArchivo(empresaId: string, usuarioId: string, file: { originalname: string; buffer: Buffer }) {
    if (!file?.buffer?.length) throw new BadRequestException('Falta el archivo o está vacío');

    let workbook: XLSX.WorkBook;
    try {
      workbook = XLSX.read(file.buffer, { type: 'buffer', cellDates: true });
    } catch {
      throw new BadRequestException('No se pudo leer el archivo. Verificá que sea un .xlsx válido');
    }

    const sheetName = workbook.SheetNames[0];
    if (!sheetName) throw new BadRequestException('El archivo no tiene hojas');
    const rows: Record<string, unknown>[] = XLSX.utils.sheet_to_json(workbook.Sheets[sheetName], {
      defval: '',
      raw: true,
    });
    if (!rows.length) throw new BadRequestException('El archivo no tiene filas de datos');

    const headers = Object.keys(rows[0]);
    const mapeoColumnas = resolverMapeoColumnas(headers);
    const columnasFaltantes = Object.entries(mapeoColumnas)
      .filter(([, header]) => !header)
      .map(([campo]) => campo);

    const job = await this.prisma.gasto_importacion_jobs.create({
      data: {
        empresa_id: empresaId,
        usuario_id: usuarioId,
        nombre_archivo: file.originalname,
        headers,
        mapeo_columnas: mapeoColumnas,
        total_filas: rows.length,
        estado: 'preview',
      },
    });

    const aliasExistentes = await this.prisma.gasto_importacion_alias.findMany({
      where: { empresa_id: empresaId, campo: 'tipo_gasto' },
    });
    const aliasPorTexto = new Map(aliasExistentes.map((a) => [a.texto_origen, a]));

    const filasData = rows.map((row, idx) => {
      const estructurada = construirFilaEstructurada(row, mapeoColumnas);
      const textoNormalizado = normalizarTextoAlias(estructurada.tipoGastoTexto);
      const alias = aliasPorTexto.get(textoNormalizado);

      let estado: EstadoFila = 'pendiente';
      let motivoOmision: string | null = null;
      let tipoGastoId: string | null = null;

      if (estructurada.noGasto.esNoGasto) {
        estado = 'omitida';
        motivoOmision = estructurada.noGasto.motivo ?? null;
      } else if (alias?.accion === 'OMITIR') {
        estado = 'omitida';
        motivoOmision = 'ALIAS_OMITIR';
      } else if (alias?.accion === 'MAPEAR') {
        tipoGastoId = alias.tipo_gasto_id;
        estado = estructurada.errores.length > 0 ? 'invalida' : 'valida';
      } else if (estructurada.errores.length > 0) {
        estado = 'invalida';
      }

      return {
        job_id: job.id,
        empresa_id: empresaId,
        fila_numero: idx + 1,
        raw: row as unknown as Prisma.InputJsonValue,
        estado,
        motivo_omision: motivoOmision,
        errores: estructurada.errores.length ? estructurada.errores : undefined,
        ruc: estructurada.ruc || null,
        razon_social: estructurada.razonSocial || null,
        tipo_gasto_texto: estructurada.tipoGastoTexto || null,
        tipo_gasto_id: tipoGastoId,
        fecha_emision: estructurada.fechaEmision,
        timbrado: estructurada.timbrado || null,
        establecimiento: estructurada.comprobante.ok ? estructurada.comprobante.establecimiento : null,
        punto_expedicion: estructurada.comprobante.ok ? estructurada.comprobante.punto_expedicion : null,
        numero_factura: estructurada.comprobante.ok ? estructurada.comprobante.numero_factura : null,
        condicion_operacion: estructurada.condicionOperacion,
        forma_pago: estructurada.condicionOperacion,
        monto_gravado_10: estructurada.montoGravado10,
        iva_10: estructurada.iva10,
        monto_gravado_5: estructurada.montoGravado5,
        iva_5: estructurada.iva5,
        monto_no_gravado: estructurada.montoNoGravado,
        total_comprobante: estructurada.totalComprobante,
      };
    });

    await this.prisma.gasto_importacion_filas.createMany({ data: filasData });

    const filasOmitidas = filasData.filter((f) => f.estado === 'omitida').length;
    const filasInvalidas = filasData.filter((f) => f.estado === 'invalida').length;
    const filasValidas = filasData.filter((f) => f.estado === 'valida').length;
    await this.prisma.gasto_importacion_jobs.update({
      where: { id: job.id },
      data: { filas_omitidas: filasOmitidas, filas_invalidas: filasInvalidas, filas_validas: filasValidas },
    });

    return {
      job_id: job.id,
      nombre_archivo: file.originalname,
      total_filas: rows.length,
      headers,
      columnas_faltantes: columnasFaltantes,
      filas_omitidas_auto: filasOmitidas,
      filas_invalidas: filasInvalidas,
      filas_validas: filasValidas,
      valores_tipo_gasto: this.resumirValoresTipoGasto(filasData, aliasPorTexto),
      preview: rows.slice(0, 20),
    };
  }

  private resumirValoresTipoGasto(
    filas: Array<{ tipo_gasto_texto: string | null; estado: EstadoFila; motivo_omision: string | null }>,
    aliasPorTexto: Map<string, { accion: string; tipo_gasto_id: string | null }>,
  ) {
    const conteo = new Map<string, number>();
    for (const f of filas) {
      if (!f.tipo_gasto_texto || f.motivo_omision) continue; // las omitidas por keyword/alias no necesitan mapeo manual
      const norm = normalizarTextoAlias(f.tipo_gasto_texto);
      conteo.set(norm, (conteo.get(norm) ?? 0) + 1);
    }
    return Array.from(conteo.entries())
      .map(([texto, cantidad]) => ({ texto, cantidad, alias_existente: aliasPorTexto.get(texto) ?? null }))
      .sort((a, b) => b.cantidad - a.cantidad);
  }

  async guardarMapeo(empresaId: string, jobId: string, usuarioId: string, dto: GuardarMapeoDto) {
    const job = await this.getJobOrThrow(empresaId, jobId);
    if (job.estado !== 'preview' && job.estado !== 'mapeado') {
      throw new BadRequestException(`No se puede mapear un job en estado ${job.estado}`);
    }

    for (const item of dto.mapeos) {
      if (item.accion === 'MAPEAR' && !item.tipo_gasto_id) {
        throw new BadRequestException(`El valor "${item.tipo_gasto_texto}" requiere un tipo de gasto para mapear`);
      }
      const textoNorm = normalizarTextoAlias(item.tipo_gasto_texto);

      if (item.recordar) {
        await this.prisma.gasto_importacion_alias.upsert({
          where: { empresa_id_campo_texto_origen: { empresa_id: empresaId, campo: 'tipo_gasto', texto_origen: textoNorm } },
          create: {
            empresa_id: empresaId,
            campo: 'tipo_gasto',
            texto_origen: textoNorm,
            accion: item.accion,
            tipo_gasto_id: item.accion === 'MAPEAR' ? item.tipo_gasto_id : null,
            created_by: usuarioId,
          },
          update: {
            accion: item.accion,
            tipo_gasto_id: item.accion === 'MAPEAR' ? item.tipo_gasto_id : null,
          },
        });
      }

      const candidatas = await this.prisma.gasto_importacion_filas.findMany({
        where: { job_id: jobId, tipo_gasto_texto: { not: null } },
      });
      const idsAActualizar = candidatas
        .filter((f) => normalizarTextoAlias(f.tipo_gasto_texto ?? '') === textoNorm && f.motivo_omision !== 'IMPORTACION_EN_CURSO' && f.motivo_omision !== 'MERCADERIA_NO_GASTO' && f.motivo_omision !== 'REMUNERACION_PERSONAL')
        .map((f) => f);

      if (item.accion === 'OMITIR') {
        await this.prisma.gasto_importacion_filas.updateMany({
          where: { id: { in: idsAActualizar.map((f) => f.id) } },
          data: { estado: 'omitida', motivo_omision: 'MAPEO_MANUAL_OMITIR', tipo_gasto_id: null },
        });
      } else {
        const sinErrores = idsAActualizar.filter((f) => !f.errores || (f.errores as unknown[]).length === 0);
        const conErrores = idsAActualizar.filter((f) => f.errores && (f.errores as unknown[]).length > 0);
        if (sinErrores.length) {
          await this.prisma.gasto_importacion_filas.updateMany({
            where: { id: { in: sinErrores.map((f) => f.id) } },
            data: { estado: 'valida', tipo_gasto_id: item.tipo_gasto_id, motivo_omision: null },
          });
        }
        if (conErrores.length) {
          await this.prisma.gasto_importacion_filas.updateMany({
            where: { id: { in: conErrores.map((f) => f.id) } },
            data: { estado: 'invalida', tipo_gasto_id: item.tipo_gasto_id },
          });
        }
      }
    }

    const counts = await this.contarPorEstado(jobId);
    await this.prisma.gasto_importacion_jobs.update({
      where: { id: jobId },
      data: {
        estado: 'mapeado',
        mapeado_at: new Date(),
        config_defaults: dto.sucursal_id ? { sucursal_id: dto.sucursal_id } : undefined,
        filas_validas: counts.valida,
        filas_invalidas: counts.invalida,
        filas_omitidas: counts.omitida,
      },
    });

    return this.getJob(empresaId, jobId);
  }

  async confirmar(empresaId: string, jobId: string) {
    const job = await this.getJobOrThrow(empresaId, jobId);
    if (job.estado !== 'mapeado') {
      throw new BadRequestException('El job debe tener el mapeo guardado antes de confirmar');
    }
    await this.prisma.gasto_importacion_jobs.update({
      where: { id: jobId },
      data: { estado: 'procesando', confirmed_at: new Date() },
    });
    await this.queue.add('procesar-import', { jobId }, { removeOnComplete: 100, removeOnFail: 50 });
    return { job_id: jobId, estado: 'procesando' };
  }

  async getJob(empresaId: string, jobId: string) {
    const job = await this.getJobOrThrow(empresaId, jobId);
    const filasError = await this.prisma.gasto_importacion_filas.findMany({
      where: { job_id: jobId, estado: 'error' },
      take: 100,
    });
    return { ...job, filas_con_error: filasError };
  }

  async listarJobs(empresaId: string) {
    return this.prisma.gasto_importacion_jobs.findMany({
      where: { empresa_id: empresaId },
      orderBy: { created_at: 'desc' },
      take: 50,
    });
  }

  async listarFilas(empresaId: string, jobId: string, estado?: string) {
    await this.getJobOrThrow(empresaId, jobId);
    return this.prisma.gasto_importacion_filas.findMany({
      where: { job_id: jobId, ...(estado ? { estado: estado as EstadoFila } : {}) },
      orderBy: { fila_numero: 'asc' },
    });
  }

  async listarAlias(empresaId: string) {
    return this.prisma.gasto_importacion_alias.findMany({
      where: { empresa_id: empresaId },
      orderBy: { texto_origen: 'asc' },
    });
  }

  async crearAlias(empresaId: string, usuarioId: string, dto: CrearAliasTipoGastoDto) {
    if (dto.accion === 'MAPEAR' && !dto.tipo_gasto_id) {
      throw new BadRequestException('accion=MAPEAR requiere tipo_gasto_id');
    }
    const textoNorm = normalizarTextoAlias(dto.texto_origen);
    return this.prisma.gasto_importacion_alias.upsert({
      where: { empresa_id_campo_texto_origen: { empresa_id: empresaId, campo: 'tipo_gasto', texto_origen: textoNorm } },
      create: {
        empresa_id: empresaId,
        campo: 'tipo_gasto',
        texto_origen: textoNorm,
        accion: dto.accion,
        tipo_gasto_id: dto.accion === 'MAPEAR' ? dto.tipo_gasto_id : null,
        created_by: usuarioId,
      },
      update: {
        accion: dto.accion,
        tipo_gasto_id: dto.accion === 'MAPEAR' ? dto.tipo_gasto_id : null,
      },
    });
  }

  async eliminarAlias(empresaId: string, id: string) {
    const alias = await this.prisma.gasto_importacion_alias.findFirst({ where: { id, empresa_id: empresaId } });
    if (!alias) throw new NotFoundException('Alias no encontrado');
    await this.prisma.gasto_importacion_alias.delete({ where: { id } });
    return { ok: true };
  }

  private async getJobOrThrow(empresaId: string, jobId: string) {
    const job = await this.prisma.gasto_importacion_jobs.findFirst({ where: { id: jobId, empresa_id: empresaId } });
    if (!job) throw new NotFoundException('Job de importación no encontrado');
    return job;
  }

  private async contarPorEstado(jobId: string) {
    const grupos = await this.prisma.gasto_importacion_filas.groupBy({
      by: ['estado'],
      where: { job_id: jobId },
      _count: true,
    });
    const result: Record<EstadoFila, number> = {
      pendiente: 0,
      valida: 0,
      invalida: 0,
      omitida: 0,
      importada: 0,
      error: 0,
    };
    for (const g of grupos) result[g.estado as EstadoFila] = g._count;
    return result;
  }
}
