import { BadRequestException, Injectable, Logger, NotFoundException } from '@nestjs/common';
import type { Prisma } from '@prisma/client';
import * as XLSX from 'xlsx';
import { PrismaService } from '../../prisma/prisma.service';
import { MapeoColumnasDto } from '../dto/importar-cartera.dto';

interface ParseResultado {
  headers: string[];
  filas: Record<string, unknown>[];
}

interface FilaValidada {
  raw: Record<string, unknown>;
  data?: {
    cliente_externo_id: string;
    cliente_nombre: string;
    cliente_documento: string | null;
    cliente_telefono: string | null;
    cliente_email: string | null;
    documento_numero: string;
    documento_tipo: string;
    moneda_codigo: string;
    monto_total: number;
    saldo_pendiente: number;
    fecha_emision: Date;
    fecha_vencimiento: Date | null;
    cuotas_total: number | null;
  };
  errores: string[];
}

const MAX_PREVIEW_FILAS = 20;
const MAX_ERRORES_GUARDADOS = 200;

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

  constructor(private readonly prisma: PrismaService) {}

  // ─── Paso 1: subir archivo y crear el job en estado "preview" ────────────
  async subirArchivo(
    empresa_id: string,
    usuario_id: string,
    file: { originalname: string; buffer: Buffer; size: number },
  ) {
    if (!file) throw new BadRequestException('Archivo requerido');
    if (file.size > 10 * 1024 * 1024) {
      throw new BadRequestException('Archivo demasiado grande (máx. 10 MB)');
    }

    const { headers, filas } = this.parsearArchivo(file.buffer);

    if (headers.length === 0 || filas.length === 0) {
      throw new BadRequestException('El archivo está vacío o no se pudo leer');
    }

    const job = await this.prisma.cartera_importada_jobs.create({
      data: {
        empresa_id,
        usuario_id,
        nombre_archivo: file.originalname,
        estado: 'preview',
        total_filas: filas.length,
        mapeo_columnas: {
          headers,
          preview_filas: filas.slice(0, MAX_PREVIEW_FILAS) as unknown as Prisma.InputJsonValue,
          filas_totales_almacenadas: filas as unknown as Prisma.InputJsonValue,
        } as unknown as Prisma.InputJsonValue,
      },
    });

    return {
      job_id: job.id,
      nombre_archivo: file.originalname,
      headers,
      preview: filas.slice(0, MAX_PREVIEW_FILAS),
      total_filas: filas.length,
      mapeo_sugerido: this.sugerirMapeo(headers),
    };
  }

  // ─── Paso 2: confirmar import con el mapeo elegido ───────────────────────
  async confirmar(empresa_id: string, jobId: string, mapeo: MapeoColumnasDto) {
    const job = await this.prisma.cartera_importada_jobs.findFirst({
      where: { id: jobId, empresa_id },
    });
    if (!job) throw new NotFoundException('Job no encontrado');
    if (job.estado !== 'preview') {
      throw new BadRequestException(`Job ya está en estado ${job.estado}`);
    }
    const meta = job.mapeo_columnas as { filas_totales_almacenadas?: Record<string, unknown>[] } | null;
    const filas = meta?.filas_totales_almacenadas ?? [];
    if (filas.length === 0) {
      throw new BadRequestException('No hay filas para procesar');
    }

    const validadas = filas.map((f) => this.validarFila(f, mapeo));
    const validas = validadas.filter((v) => v.data && v.errores.length === 0);
    const invalidas = validadas.filter((v) => v.errores.length > 0);

    await this.prisma.cartera_importada_jobs.update({
      where: { id: jobId },
      data: {
        estado: 'importando',
        mapeo_columnas: mapeo as unknown as object,
        filas_validas: validas.length,
        filas_invalidas: invalidas.length,
        confirmed_at: new Date(),
      },
    });

    let creados = 0;
    let actualizados = 0;
    try {
      for (const v of validas) {
        if (!v.data) continue;
        const res = await this.prisma.cartera_importada_documentos.upsert({
          where: {
            empresa_id_cliente_externo_id_documento_numero: {
              empresa_id,
              cliente_externo_id: v.data.cliente_externo_id,
              documento_numero: v.data.documento_numero,
            },
          },
          create: {
            empresa_id,
            job_id: jobId,
            ...v.data,
            active: true,
          },
          update: {
            ...v.data,
            job_id: jobId,
            active: true,
          },
        });
        if (res.created_at.getTime() === res.updated_at.getTime()) creados++;
        else actualizados++;
      }

      await this.prisma.cartera_importada_jobs.update({
        where: { id: jobId },
        data: {
          estado: 'completado',
          finished_at: new Date(),
          errores: invalidas.slice(0, MAX_ERRORES_GUARDADOS).map((v, idx) => ({
            fila: idx + 1,
            errores: v.errores,
            raw: v.raw,
          })) as unknown as Prisma.InputJsonValue,
        },
      });

      return {
        job_id: jobId,
        estado: 'completado',
        creados,
        actualizados,
        validas: validas.length,
        invalidas: invalidas.length,
        errores: invalidas.slice(0, 50),
      };
    } catch (err: any) {
      this.logger.error(`Job ${jobId} falló: ${err?.message ?? err}`);
      await this.prisma.cartera_importada_jobs.update({
        where: { id: jobId },
        data: {
          estado: 'error',
          finished_at: new Date(),
          error_msg: err?.message ?? String(err),
        },
      });
      throw err;
    }
  }

  // ─── Listar jobs recientes ───────────────────────────────────────────────
  async listarJobs(empresa_id: string) {
    return this.prisma.cartera_importada_jobs.findMany({
      where: { empresa_id },
      orderBy: { created_at: 'desc' },
      take: 50,
      select: {
        id: true,
        nombre_archivo: true,
        estado: true,
        total_filas: true,
        filas_validas: true,
        filas_invalidas: true,
        created_at: true,
        confirmed_at: true,
        finished_at: true,
        error_msg: true,
      },
    });
  }

  // ─── Helpers ─────────────────────────────────────────────────────────────

  private parsearArchivo(buffer: Buffer): ParseResultado {
    const workbook = XLSX.read(buffer, { type: 'buffer', cellDates: true });
    const firstSheetName = workbook.SheetNames[0];
    if (!firstSheetName) return { headers: [], filas: [] };
    const sheet = workbook.Sheets[firstSheetName];
    const json = XLSX.utils.sheet_to_json<Record<string, unknown>>(sheet, {
      defval: null,
      raw: false,
    });
    const headers = json.length > 0 ? Object.keys(json[0]) : [];
    return { headers, filas: json };
  }

  /**
   * Sugiere un mapeo automático buscando coincidencias por nombre de columna.
   * El frontend lo pre-llena para que el usuario ajuste si hace falta.
   */
  private sugerirMapeo(headers: string[]): Partial<MapeoColumnasDto> {
    const lower = headers.map((h) => ({ original: h, lower: h.toLowerCase().trim() }));
    const find = (...candidates: string[]): string | undefined => {
      for (const c of candidates) {
        const hit = lower.find((h) => h.lower === c.toLowerCase() || h.lower.includes(c.toLowerCase()));
        if (hit) return hit.original;
      }
      return undefined;
    };
    return {
      cliente_externo_id: find('cliente_id', 'cod cliente', 'codigo cliente', 'id cliente'),
      cliente_nombre: find('razon social', 'razon_social', 'nombre cliente', 'cliente', 'nombre'),
      cliente_documento: find('ruc', 'cedula', 'documento', 'ci'),
      cliente_telefono: find('telefono', 'celular', 'whatsapp'),
      cliente_email: find('email', 'correo'),
      documento_numero: find('nro factura', 'numero factura', 'nro documento', 'documento', 'factura'),
      moneda_codigo: find('moneda', 'codigo moneda'),
      monto_total: find('total', 'monto total', 'monto'),
      saldo_pendiente: find('saldo', 'saldo pendiente', 'pendiente'),
      fecha_emision: find('fecha emision', 'emision', 'fecha'),
      fecha_vencimiento: find('vencimiento', 'fecha vencimiento', 'venc'),
      cuotas_total: find('cuotas', 'nro cuotas', 'cantidad cuotas'),
    };
  }

  private validarFila(raw: Record<string, unknown>, mapeo: MapeoColumnasDto): FilaValidada {
    const errores: string[] = [];
    const get = (col: string | undefined): unknown => (col ? raw[col] : null);
    const reqStr = (col: string | undefined, etiqueta: string): string => {
      const v = get(col);
      if (v === null || v === undefined || String(v).trim() === '') {
        errores.push(`${etiqueta} requerido`);
        return '';
      }
      return String(v).trim();
    };
    const optStr = (col: string | undefined): string | null => {
      const v = get(col);
      if (v === null || v === undefined || String(v).trim() === '') return null;
      return String(v).trim();
    };
    const parseNum = (col: string | undefined, etiqueta: string, requerido: boolean): number => {
      const v = get(col);
      if (v === null || v === undefined || String(v).trim() === '') {
        if (requerido) errores.push(`${etiqueta} requerido`);
        return 0;
      }
      const limpio = String(v).replace(/\./g, '').replace(/,/g, '.').replace(/[^\d.-]/g, '');
      const n = Number(limpio);
      if (!Number.isFinite(n)) {
        errores.push(`${etiqueta} inválido`);
        return 0;
      }
      return n;
    };
    const parseFecha = (col: string | undefined, etiqueta: string, requerido: boolean): Date | null => {
      const v = get(col);
      if (v === null || v === undefined || String(v).trim() === '') {
        if (requerido) errores.push(`${etiqueta} requerido`);
        return null;
      }
      if (v instanceof Date && !Number.isNaN(v.getTime())) return v;
      const txt = String(v).trim();
      const iso = new Date(txt);
      if (!Number.isNaN(iso.getTime())) return iso;
      const m = txt.match(/^(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{2,4})$/);
      if (m) {
        const dd = parseInt(m[1], 10);
        const mm = parseInt(m[2], 10) - 1;
        let yy = parseInt(m[3], 10);
        if (yy < 100) yy += 2000;
        const d = new Date(yy, mm, dd);
        if (!Number.isNaN(d.getTime())) return d;
      }
      errores.push(`${etiqueta} con formato inválido (usar dd/mm/aaaa)`);
      return null;
    };

    const cliente_externo_id = reqStr(mapeo.cliente_externo_id, 'cliente_externo_id');
    const cliente_nombre = reqStr(mapeo.cliente_nombre, 'cliente_nombre');
    const documento_numero = reqStr(mapeo.documento_numero, 'documento_numero');
    const monto_total = parseNum(mapeo.monto_total, 'monto_total', true);
    const saldo_pendiente = parseNum(mapeo.saldo_pendiente, 'saldo_pendiente', true);
    const fecha_emision = parseFecha(mapeo.fecha_emision, 'fecha_emision', true);
    const fecha_vencimiento = parseFecha(mapeo.fecha_vencimiento, 'fecha_vencimiento', false);
    const cuotas_total_n = mapeo.cuotas_total ? parseNum(mapeo.cuotas_total, 'cuotas_total', false) : 0;

    if (saldo_pendiente < 0) errores.push('saldo_pendiente no puede ser negativo');
    if (monto_total < 0) errores.push('monto_total no puede ser negativo');

    if (errores.length > 0 || !fecha_emision) {
      return { raw, errores };
    }

    return {
      raw,
      errores: [],
      data: {
        cliente_externo_id,
        cliente_nombre,
        cliente_documento: optStr(mapeo.cliente_documento),
        cliente_telefono: optStr(mapeo.cliente_telefono),
        cliente_email: optStr(mapeo.cliente_email),
        documento_numero,
        documento_tipo: optStr(mapeo.documento_tipo) ?? 'factura',
        moneda_codigo: (optStr(mapeo.moneda_codigo) ?? 'PYG').toUpperCase().slice(0, 3),
        monto_total,
        saldo_pendiente,
        fecha_emision,
        fecha_vencimiento,
        cuotas_total: cuotas_total_n > 0 ? cuotas_total_n : null,
      },
    };
  }
}
