import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import * as XLSX from 'xlsx';
import { PrismaService } from '../../prisma/prisma.service';
import { AnularPlanillaDto, ImportarPlanillaDto, QueryPlanillasDto } from '../dto/planillas.dto';

// Formato esperado del archivo (.xlsx, .xls, .csv):
// Columnas obligatorias (encabezado en primera fila, case-insensitive):
//   numero_empleado | codigo_concepto | tipo (INGRESO/EGRESO) | monto
// Columnas opcionales:
//   observacion
const HEADER_REQUIRED = ['numero_empleado', 'codigo_concepto', 'tipo', 'monto'];

type ParsedRow = {
  fila: number;
  numero_empleado?: string;
  codigo_concepto?: string;
  tipo?: string;
  monto?: number;
  observacion?: string;
  rawError?: string;
};

type ErrorImportacion = {
  fila: number;
  mensaje: string;
  datos: Record<string, unknown>;
};

const normalizeKey = (k: string) => String(k || '').trim().toLowerCase().replace(/\s+/g, '_');

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

  // ── Listado ───────────────────────────────────────────
  async listar(empresaId: string, query: QueryPlanillasDto) {
    const page = query.page ?? 1;
    const limit = query.limit ?? 20;
    const skip = (page - 1) * limit;

    const where: Prisma.rrhh_planillas_externasWhereInput = { empresa_id: empresaId };
    if (query.estado) where.estado = query.estado;
    if (query.periodo_anio) where.periodo_anio = query.periodo_anio;
    if (query.periodo_mes) where.periodo_mes = query.periodo_mes;

    const [data, total] = await this.prisma.$transaction([
      this.prisma.rrhh_planillas_externas.findMany({
        where,
        skip,
        take: limit,
        orderBy: [{ periodo_anio: 'desc' }, { periodo_mes: 'desc' }, { created_at: 'desc' }],
      }),
      this.prisma.rrhh_planillas_externas.count({ where }),
    ]);

    return { data, total, page, limit };
  }

  async obtener(id: string, empresaId: string) {
    const planilla = await this.prisma.rrhh_planillas_externas.findFirst({
      where: { id, empresa_id: empresaId },
      include: {
        detalles: {
          orderBy: { fila_origen: 'asc' },
          include: {
            empleado: {
              select: { id: true, numero_empleado: true, nombres: true, apellidos: true },
            },
            concepto: { select: { id: true, codigo: true, nombre: true } },
          },
        },
      },
    });
    if (!planilla) throw new NotFoundException('Planilla no encontrada');
    return planilla;
  }

  // ── Importación ───────────────────────────────────────
  private parsearArchivo(buffer: Buffer): ParsedRow[] {
    let workbook: XLSX.WorkBook;
    try {
      workbook = XLSX.read(buffer, { type: 'buffer', cellDates: false, cellNF: false });
    } catch {
      throw new BadRequestException('No se pudo leer el archivo. Formatos soportados: .xlsx, .xls, .csv');
    }
    const sheetName = workbook.SheetNames[0];
    if (!sheetName) throw new BadRequestException('El archivo no tiene hojas');

    const sheet = workbook.Sheets[sheetName];
    const rows: Record<string, unknown>[] = XLSX.utils.sheet_to_json(sheet, {
      defval: '',
      raw: false,
    });

    if (rows.length === 0) {
      throw new BadRequestException('El archivo está vacío o no tiene encabezados válidos');
    }

    const firstNormalized = Object.keys(rows[0]).map(normalizeKey);
    const faltantes = HEADER_REQUIRED.filter((h) => !firstNormalized.includes(h));
    if (faltantes.length) {
      throw new BadRequestException(
        `Faltan columnas obligatorias: ${faltantes.join(', ')}. Encabezados aceptados: ${HEADER_REQUIRED.join(', ')}, observacion`,
      );
    }

    return rows.map((raw, idx): ParsedRow => {
      const row: Record<string, unknown> = {};
      for (const [k, v] of Object.entries(raw)) row[normalizeKey(k)] = v;

      const numero_empleado = String(row.numero_empleado ?? '').trim();
      const codigo_concepto = String(row.codigo_concepto ?? '').trim();
      const tipo = String(row.tipo ?? '').trim().toUpperCase();
      const observacion = String(row.observacion ?? '').trim() || undefined;

      const montoRaw = String(row.monto ?? '').replace(/[^0-9.,-]/g, '').replace(/,/g, '.');
      const monto = Number(montoRaw);

      return {
        fila: idx + 2, // +1 por header, +1 base 1
        numero_empleado: numero_empleado || undefined,
        codigo_concepto: codigo_concepto || undefined,
        tipo: tipo || undefined,
        monto: Number.isFinite(monto) ? monto : undefined,
        observacion,
      };
    });
  }

  async importar(
    empresaId: string,
    dto: ImportarPlanillaDto,
    archivo: Express.Multer.File,
    userId?: string,
  ) {
    if (!archivo?.buffer) throw new BadRequestException('No se recibió ningún archivo');
    const MAX_BYTES = 10 * 1024 * 1024;
    if (archivo.size > MAX_BYTES) {
      throw new BadRequestException('El archivo supera el límite de 10 MB');
    }

    const filas = this.parsearArchivo(archivo.buffer);
    if (filas.length === 0) {
      throw new BadRequestException('El archivo no tiene filas de datos');
    }

    // Cargo catálogos de la empresa para validar
    const [empleados, conceptos] = await Promise.all([
      this.prisma.rrhh_empleados.findMany({
        where: { empresa_id: empresaId },
        select: { id: true, numero_empleado: true, estado: true },
      }),
      this.prisma.rrhh_conceptos_liquidacion.findMany({
        where: { empresa_id: empresaId, activo: true },
        select: { id: true, codigo: true },
      }),
    ]);

    const mapEmpleados = new Map(empleados.map((e) => [e.numero_empleado, e]));
    const mapConceptos = new Map(conceptos.map((c) => [c.codigo, c]));

    const detallesValidos: {
      empleado_id: string;
      concepto_id: string;
      tipo: string;
      monto: number;
      observacion?: string;
      fila_origen: number;
    }[] = [];
    const errores: ErrorImportacion[] = [];
    let totalIngresos = 0;
    let totalEgresos = 0;

    for (const f of filas) {
      const datos = {
        numero_empleado: f.numero_empleado,
        codigo_concepto: f.codigo_concepto,
        tipo: f.tipo,
        monto: f.monto,
        observacion: f.observacion,
      };

      if (!f.numero_empleado) {
        errores.push({ fila: f.fila, mensaje: 'numero_empleado vacío', datos });
        continue;
      }
      if (!f.codigo_concepto) {
        errores.push({ fila: f.fila, mensaje: 'codigo_concepto vacío', datos });
        continue;
      }
      if (!f.tipo || (f.tipo !== 'INGRESO' && f.tipo !== 'EGRESO')) {
        errores.push({ fila: f.fila, mensaje: `tipo inválido (debe ser INGRESO o EGRESO)`, datos });
        continue;
      }
      if (typeof f.monto !== 'number' || !Number.isFinite(f.monto) || f.monto <= 0) {
        errores.push({ fila: f.fila, mensaje: 'monto inválido (> 0)', datos });
        continue;
      }

      const emp = mapEmpleados.get(f.numero_empleado);
      if (!emp) {
        errores.push({
          fila: f.fila,
          mensaje: `empleado "${f.numero_empleado}" no existe en esta empresa`,
          datos,
        });
        continue;
      }
      if (emp.estado === 'EGRESADO') {
        errores.push({
          fila: f.fila,
          mensaje: `empleado "${f.numero_empleado}" está EGRESADO`,
          datos,
        });
        continue;
      }
      const concepto = mapConceptos.get(f.codigo_concepto);
      if (!concepto) {
        errores.push({
          fila: f.fila,
          mensaje: `concepto "${f.codigo_concepto}" no existe o está inactivo`,
          datos,
        });
        continue;
      }

      detallesValidos.push({
        empleado_id: emp.id,
        concepto_id: concepto.id,
        tipo: f.tipo,
        monto: f.monto,
        observacion: f.observacion,
        fila_origen: f.fila,
      });
      if (f.tipo === 'INGRESO') totalIngresos += f.monto;
      else totalEgresos += f.monto;
    }

    if (detallesValidos.length === 0) {
      throw new BadRequestException(
        `Ninguna fila válida (${errores.length} errores). Corregí el archivo y reintentá.`,
      );
    }

    return this.prisma.$transaction(async (tx) => {
      const planilla = await tx.rrhh_planillas_externas.create({
        data: {
          empresa_id: empresaId,
          periodo_anio: dto.periodo_anio,
          periodo_mes: dto.periodo_mes,
          nombre: dto.nombre,
          archivo_url: archivo.originalname || null,
          estado: 'IMPORTADA',
          total_registros: filas.length,
          registros_validos: detallesValidos.length,
          registros_invalidos: errores.length,
          total_ingresos: totalIngresos,
          total_egresos: totalEgresos,
          errores_importacion: errores.length ? (errores as unknown as Prisma.JsonArray) : Prisma.JsonNull,
          creado_por: userId,
        },
      });

      await tx.rrhh_planillas_externas_det.createMany({
        data: detallesValidos.map((d) => ({ ...d, planilla_id: planilla.id })),
      });

      return { ...planilla, errores };
    });
  }

  // ── Anulación ─────────────────────────────────────────
  async anular(id: string, empresaId: string, dto: AnularPlanillaDto, userId?: string) {
    const planilla = await this.prisma.rrhh_planillas_externas.findFirst({
      where: { id, empresa_id: empresaId },
    });
    if (!planilla) throw new NotFoundException('Planilla no encontrada');
    if (planilla.estado === 'ANULADA') {
      throw new BadRequestException('La planilla ya está anulada');
    }
    if (planilla.estado === 'APLICADA') {
      throw new BadRequestException(
        'La planilla ya fue aplicada a una liquidación cerrada y no puede anularse',
      );
    }

    return this.prisma.rrhh_planillas_externas.update({
      where: { id },
      data: {
        estado: 'ANULADA',
        anulado_por: userId,
        fecha_anulacion: new Date(),
        updated_at: new Date(),
      },
    });
  }
}
