import { BadRequestException, ForbiddenException, Injectable, Logger } from '@nestjs/common';
import * as XLSX from 'xlsx';
import { AiProviderService } from '../../ai-dashboard/ai-provider/ai-provider.service';
import { extraerMensajeError, mensajeAmigableIA } from '../../ai-dashboard/shared/ai-error.util';
import { MovimientoNormalizado, ResultadoInterpretacion, TipoMov } from './tipos';
import { parseCsvMovimientos, parseFecha } from './csv-parse.util';

// eslint-disable-next-line @typescript-eslint/no-var-requires
const pdfParse = require('pdf-parse/lib/pdf-parse');

interface TransaccionIA {
  fecha?: string;
  concepto?: string;
  referencia?: string | null;
  importe?: number | string;
  tipo?: string;
  confianza?: number;
}

/**
 * Interpreta un archivo de movimientos (propio o extracto del banco) a una lista
 * normalizada, independiente del formato:
 *   - CSV / TXT estructurado  → parser de columnas (confianza 1)
 *   - XLSX / XLS              → se convierte a CSV y se parsea igual (confianza 1)
 *   - PDF / TXT libre         → pdf-parse/texto + IA (confianza por línea)
 *   - DOC / DOCX              → Fase 2 (mammoth); por ahora error guía
 */
@Injectable()
export class ArchivoMovimientosParserService {
  private readonly logger = new Logger(ArchivoMovimientosParserService.name);

  constructor(private readonly aiProvider: AiProviderService) {}

  async interpretar(
    empresaId: string,
    buffer: Buffer,
    filename: string,
    referenciaId?: string,
  ): Promise<ResultadoInterpretacion> {
    const ext = (filename.split('.').pop() || '').toLowerCase();

    if (ext === 'csv') {
      const movimientos = parseCsvMovimientos(buffer.toString('utf-8'));
      if (movimientos.length) return { movimientos, metodo: 'CSV' };
      // CSV sin columnas reconocibles → intentar IA sobre el texto.
      return this.interpretarConIA(empresaId, buffer.toString('utf-8'), filename, referenciaId);
    }

    if (ext === 'xlsx' || ext === 'xls') {
      let csv: string;
      try {
        csv = this.xlsxACsv(buffer);
      } catch (err) {
        this.logger.error(`No se pudo leer el Excel ${filename}: ${err instanceof Error ? err.message : 'unknown'}`);
        throw new BadRequestException('No se pudo leer el archivo Excel. Verificá que no esté dañado y que sea .xlsx/.xls válido.');
      }
      const movimientos = parseCsvMovimientos(csv);
      if (!movimientos.length) {
        throw new BadRequestException(
          'No se reconocieron columnas de movimientos en el Excel. Verificá que tenga columnas de fecha e importe (o débito/crédito).',
        );
      }
      return { movimientos, metodo: 'XLSX' };
    }

    if (ext === 'pdf') {
      const texto = await this.extraerTextoPdf(buffer, filename);
      return this.interpretarConIA(empresaId, texto, filename, referenciaId);
    }

    if (ext === 'txt') {
      // TXT: primero intento estructurado; si no, IA sobre el texto libre.
      const estructurado = parseCsvMovimientos(buffer.toString('utf-8'));
      if (estructurado.length) return { movimientos: estructurado, metodo: 'CSV' };
      return this.interpretarConIA(empresaId, buffer.toString('utf-8'), filename, referenciaId);
    }

    if (ext === 'doc' || ext === 'docx') {
      throw new BadRequestException('Word (.doc/.docx) estará disponible en la próxima versión. Por ahora usá PDF, Excel, CSV o TXT.');
    }

    throw new BadRequestException(`Formato .${ext} no soportado. Usá PDF, Excel, CSV o TXT.`);
  }

  private xlsxACsv(buffer: Buffer): string {
    const wb = XLSX.read(buffer, { type: 'buffer' });
    const sheet = wb.Sheets[wb.SheetNames[0]];
    return XLSX.utils.sheet_to_csv(sheet, { FS: ',' });
  }

  /**
   * Extrae texto de un PDF de forma tolerante. pdf-parse (pdf.js) puede fallar con
   * PDFs de XRef malformado ("bad XRef entry") o escaneos sin texto; en esos casos
   * damos un mensaje accionable en vez de un 500.
   */
  private async extraerTextoPdf(buffer: Buffer, filename: string): Promise<string> {
    let parsed: { text?: string };
    try {
      parsed = await pdfParse(buffer);
    } catch (err) {
      const msg = err instanceof Error ? err.message : String(err);
      this.logger.error(`No se pudo leer el PDF ${filename}: ${msg}`);
      throw new BadRequestException(
        'No se pudo leer el PDF (archivo dañado o con formato no estándar). Probá volver a descargarlo del homebanking, ' +
          'o subí el extracto en CSV o Excel.',
      );
    }
    const texto = (parsed?.text || '').trim();
    if (!texto) {
      throw new BadRequestException(
        'El PDF no tiene texto seleccionable (parece un escaneo o imagen). Subí el extracto en CSV o Excel, o un PDF con texto.',
      );
    }
    return texto;
  }

  private static readonly LIMITE_CHUNK = 6000; // caracteres por fragmento enviado a la IA
  private static readonly MAX_CHUNKS = 12; // tope de fragmentos (extractos muy largos)

  /**
   * Extrae transacciones de texto libre con IA. Para documentos largos (extractos
   * de muchas páginas) el texto se parte en fragmentos por línea y se interpreta
   * cada uno; los fragmentos son disjuntos, así que las transacciones se concatenan
   * sin riesgo de duplicados. Cada fragmento es una llamada medida (feature CONCILIACION_IA).
   */
  private async interpretarConIA(
    empresaId: string,
    textoCrudo: string,
    filename: string,
    referenciaId?: string,
  ): Promise<ResultadoInterpretacion> {
    const todos = this.chunkTexto(textoCrudo, ArchivoMovimientosParserService.LIMITE_CHUNK);
    const chunks = todos.slice(0, ArchivoMovimientosParserService.MAX_CHUNKS);
    if (todos.length > chunks.length) {
      this.logger.warn(
        `Documento ${filename} muy largo (${todos.length} fragmentos): se procesan los primeros ${chunks.length}.`,
      );
    }

    // Los fragmentos son disjuntos → se interpretan en paralelo (mucho más rápido en
    // extractos de varias páginas). Promise.all preserva el orden, así que la lista
    // final queda cronológica. Un fragmento transitorio que falla devuelve [] y no
    // corta el resto; un error sistémico (401/403/sin saldo) sí propaga y aborta.
    const porChunk = await Promise.all(
      chunks.map((chunk, i) =>
        this.extraerChunkConIA(empresaId, chunk, filename, referenciaId, chunks.length, i + 1),
      ),
    );
    const acumuladas: TransaccionIA[] = porChunk.flat();

    const movimientos = this.normalizarTransaccionesIA(acumuladas);
    if (!movimientos.length) {
      throw new BadRequestException('La IA no encontró transacciones en el archivo. Verificá el contenido o probá con CSV/Excel.');
    }
    return { movimientos, metodo: 'IA' };
  }

  /** Parte el texto en fragmentos de ~maxChars respetando límites de línea (no corta transacciones). */
  private chunkTexto(texto: string, maxChars: number): string[] {
    if (texto.length <= maxChars) return [texto];
    const lineas = texto.split('\n');
    const chunks: string[] = [];
    let actual = '';
    for (const linea of lineas) {
      if (actual.length + linea.length + 1 > maxChars && actual) {
        chunks.push(actual);
        actual = '';
      }
      // Línea suelta más larga que el tope: se recorta (caso extremo, no debería pasar en extractos).
      actual += (actual ? '\n' : '') + (linea.length > maxChars ? linea.slice(0, maxChars) : linea);
    }
    if (actual) chunks.push(actual);
    return chunks;
  }

  private async extraerChunkConIA(
    empresaId: string,
    texto: string,
    filename: string,
    referenciaId: string | undefined,
    totalChunks: number,
    nChunk: number,
  ): Promise<TransaccionIA[]> {
    const SYSTEM_PROMPT = `Sos un extractor de datos bancarios experto en extractos y libros banco de Paraguay.
Tu única tarea es devolver un JSON válido con las transacciones. No incluyas texto extra, markdown ni explicaciones. Solo el JSON puro.`;

    const nota = totalChunks > 1 ? ` — fragmento ${nChunk} de ${totalChunks}; extraé solo las transacciones que aparezcan en este fragmento` : '';
    const USER_PROMPT = `Extraé todas las transacciones del siguiente documento (extracto bancario o libro banco de Paraguay).

Reglas:
- Para cada transacción identificá: fecha (YYYY-MM-DD), concepto (descripción), referencia (nro de comprobante si existe, sino null), importe (número positivo, sin puntos ni comas de miles), tipo (DEBITO si sale dinero/cargo/debe, CREDITO si entra/abono/haber).
- Agregá "confianza": número 0..1 que refleje qué tan seguro estás de esa línea (1 = clarísima; <0.5 = dato borroso/ilegible/ambiguo).
- Ignorá filas de totales, saldos, encabezados, textos legales y publicidad.
- Los montos en guaraníes usan punto como separador de miles: "18.500.000" = 18500000.
- Si una fecha tiene solo día, inferí mes/año del contexto.

Devolvé exactamente este JSON:
{ "transacciones": [ { "fecha": "YYYY-MM-DD", "concepto": "texto", "referencia": "nro o null", "importe": 400000, "tipo": "CREDITO", "confianza": 0.95 } ] }

DOCUMENTO (${filename})${nota}:
${texto}`;

    try {
      const resultado = await this.aiProvider.completar(empresaId, USER_PROMPT, SYSTEM_PROMPT, {
        feature: 'CONCILIACION_IA',
        referenciaId,
      });
      if (
        !resultado.content ||
        resultado.content.includes('[Sin API key') ||
        resultado.content.includes('[Proveedor')
      ) {
        throw new BadRequestException('No hay API key de IA configurada. Configurá una en Ajustes → IA, o subí un CSV/Excel estructurado.');
      }
      const jsonText = resultado.content.trim().replace(/^```json?\n?/, '').replace(/\n?```$/, '');
      const parsed = JSON.parse(jsonText);
      return Array.isArray(parsed) ? parsed : parsed.transacciones ?? [];
    } catch (err) {
      // El límite de tokens (403) y la falta de API key cortan todo el proceso.
      if (err instanceof BadRequestException || err instanceof ForbiddenException) throw err;

      // Error del proveedor (key inválida 401, modelo inexistente, sin saldo, límite de tasa…):
      // es sistémico (falla en todos los fragmentos) → abortar con mensaje claro y accionable.
      if (typeof (err as { status?: number })?.status === 'number') {
        this.logger.error(
          `IA (proveedor) falló al interpretar ${filename}: ${err instanceof Error ? err.message : 'unknown'}`,
        );
        throw new BadRequestException(mensajeAmigableIA(extraerMensajeError(err)));
      }

      // Error transitorio / JSON inválido de un fragmento: no perder el resto del extracto.
      this.logger.error(
        `IA no pudo interpretar el fragmento ${nChunk}/${totalChunks} de ${filename}: ${err instanceof Error ? err.message : 'unknown'}`,
      );
      if (totalChunks > 1) return [];
      throw new BadRequestException('No se pudo interpretar el archivo con IA. Probá con un CSV o Excel estructurado.');
    }
  }

  private normalizarTransaccionesIA(transacciones: TransaccionIA[]): MovimientoNormalizado[] {
    return (transacciones || [])
      .map((t) => {
        const fecha = t.fecha ? parseFecha(this.normalizarFecha(t.fecha)) : null;
        const importe =
          typeof t.importe === 'string'
            ? parseFloat(String(t.importe).replace(/\./g, '').replace(',', '.')) || 0
            : Number(t.importe || 0);
        const tipo: TipoMov = String(t.tipo).toUpperCase() === 'DEBITO' ? 'DEBITO' : 'CREDITO';
        const confianza = typeof t.confianza === 'number' ? Math.max(0, Math.min(1, t.confianza)) : 0.85;
        return {
          fecha,
          concepto: (t.concepto || '').trim() || '(sin concepto)',
          tipo,
          importe: Math.abs(importe),
          referencia: t.referencia ? String(t.referencia).trim() : null,
          confianza_ia: confianza,
        };
      })
      .filter((m): m is MovimientoNormalizado => Boolean(m.fecha) && m.importe > 0);
  }

  /** Acepta ya YYYY-MM-DD o DD/MM/YYYY; deja pasar el resto para que parseFecha lo maneje. */
  private normalizarFecha(raw: string): string {
    return (raw || '').trim().split(' ')[0].split('T')[0];
  }
}
