import { BadRequestException, Injectable } from '@nestjs/common';
import * as XLSX from 'xlsx';

/**
 * Parser de planilla de marcaciones (CSV/XLSX/XLS) usado por el módulo
 * Presentismo (M16). Acepta dos formatos y autodetecta cuál usar:
 *
 *  - Formato A (fila-por-día): `documento_funcionario, fecha, hora_entrada, hora_salida`.
 *    Cada fila se expande a dos marcaciones (entrada + salida) salvo que
 *    alguna hora venga vacía.
 *  - Formato B (marcación individual): `documento_funcionario, timestamp_marcacion`.
 *    Una fila = una marcación. Útil para exports directos de relojes.
 *
 * Las filas con error no abortan el lote: quedan reportadas en `errores`
 * y el resto se procesa normalmente (regla de M16: "filas con errores se
 * reportan en pantalla y no se importan; el resto se procesa normalmente").
 */

export type RawMarcacionParsed = {
  /** Número de fila en el archivo (1-indexed, sin contar el encabezado). */
  fila: number;
  documento_raw: string;
  timestamp_marcacion: Date;
  tipo_marcacion?: 'ENTRADA' | 'SALIDA';
};

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

export type ParseResult = {
  formato: 'POR_DIA' | 'POR_MARCACION';
  marcaciones: RawMarcacionParsed[];
  errores: RowError[];
  total_filas: number;
};

const normalizeKey = (k: string) =>
  String(k || '')
    .trim()
    .toLowerCase()
    .replace(/[^a-z0-9_]+/g, '_')
    .replace(/^_+|_+$/g, '');

const ALIASES: Record<string, string[]> = {
  documento_funcionario: [
    'documento',
    'documento_funcionario',
    'cedula',
    'cedula_identidad',
    'ci',
    'numero_documento',
    'nro_documento',
    'dni',
  ],
  fecha: ['fecha', 'dia', 'fecha_marcacion'],
  hora_entrada: ['hora_entrada', 'entrada', 'hora_in', 'hr_entrada'],
  hora_salida: ['hora_salida', 'salida', 'hora_out', 'hr_salida'],
  timestamp_marcacion: [
    'timestamp_marcacion',
    'timestamp',
    'fecha_hora',
    'datetime',
    'fechahora',
    'hora',
  ],
};

function resolverColumna(headers: string[], alias: string): string | null {
  const candidatos = ALIASES[alias] ?? [alias];
  for (const c of candidatos) {
    if (headers.includes(c)) return c;
  }
  return null;
}

const HHMM = /^\d{1,2}:\d{2}(:\d{2})?$/;

function parseFecha(v: unknown): Date | null {
  if (v == null || v === '') return null;
  if (v instanceof Date) {
    if (Number.isNaN(v.getTime())) return null;
    // Normalizar a 00:00:00 UTC para evitar drift de TZ
    return new Date(Date.UTC(v.getUTCFullYear(), v.getUTCMonth(), v.getUTCDate()));
  }
  const s = String(v).trim();
  // YYYY-MM-DD
  let m = s.match(/^(\d{4})-(\d{2})-(\d{2})$/);
  if (m) return new Date(Date.UTC(+m[1], +m[2] - 1, +m[3]));
  // DD/MM/YYYY o DD-MM-YYYY
  m = s.match(/^(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{4})$/);
  if (m) return new Date(Date.UTC(+m[3], +m[2] - 1, +m[1]));
  // Número serial Excel (días desde 1899-12-30)
  if (/^\d+(\.\d+)?$/.test(s)) {
    const serial = Number(s);
    const epoch = Date.UTC(1899, 11, 30);
    const ms = serial * 86400 * 1000;
    return new Date(epoch + ms);
  }
  return null;
}

function parseHoraComoSegundos(v: unknown): number | null {
  if (v == null || v === '') return null;
  if (v instanceof Date) {
    return v.getUTCHours() * 3600 + v.getUTCMinutes() * 60 + v.getUTCSeconds();
  }
  const s = String(v).trim();
  if (HHMM.test(s)) {
    const [hh, mm, ss = '0'] = s.split(':');
    const h = Number(hh);
    const m = Number(mm);
    const sec = Number(ss);
    if (h < 0 || h > 23 || m < 0 || m > 59 || sec < 0 || sec > 59) return null;
    return h * 3600 + m * 60 + sec;
  }
  // Excel también puede devolver fracción del día (0–1).
  if (/^\d*\.\d+$/.test(s)) {
    const frac = Number(s);
    if (frac < 0 || frac >= 1) return null;
    return Math.round(frac * 86400);
  }
  return null;
}

function parseTimestamp(v: unknown): Date | null {
  if (v == null || v === '') return null;
  if (v instanceof Date) {
    if (Number.isNaN(v.getTime())) return null;
    return v;
  }
  const s = String(v).trim();
  // ISO con T o espacio
  let m = s.match(/^(\d{4})-(\d{2})-(\d{2})[T\s](\d{1,2}):(\d{2})(?::(\d{2}))?/);
  if (m) {
    return new Date(
      Date.UTC(+m[1], +m[2] - 1, +m[3], +m[4], +m[5], m[6] ? +m[6] : 0),
    );
  }
  // DD/MM/YYYY HH:MM(:SS)?
  m = s.match(/^(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{4})\s+(\d{1,2}):(\d{2})(?::(\d{2}))?/);
  if (m) {
    return new Date(
      Date.UTC(+m[3], +m[2] - 1, +m[1], +m[4], +m[5], m[6] ? +m[6] : 0),
    );
  }
  // Serial de Excel (días con fracción)
  if (/^\d+(\.\d+)?$/.test(s)) {
    const serial = Number(s);
    const epoch = Date.UTC(1899, 11, 30);
    const ms = Math.round(serial * 86400 * 1000);
    return new Date(epoch + ms);
  }
  return null;
}

function validarDocumento(s: string): boolean {
  // Cédula paraguaya: sólo dígitos, longitud 5–9. Aceptamos también guiones/puntos
  // que limpiamos antes.
  return /^\d{5,9}$/.test(s);
}

@Injectable()
export class MarcacionesParserService {
  /**
   * Parsea el buffer del archivo cargado por el usuario.
   * Lanza `BadRequestException` sólo si el archivo no es legible o no tiene
   * encabezados. Las filas con errores quedan en `errores`.
   */
  parseBuffer(buffer: Buffer): ParseResult {
    if (!buffer || buffer.length === 0) {
      throw new BadRequestException('El archivo está vacío');
    }

    // Detección de formato: XLSX/XLS empiezan con magic bytes específicos.
    // Cualquier otro buffer lo tratamos como CSV/TXT para evitar la conversión
    // automática de fechas que aplica XLSX al parsear CSVs (interpreta MM/DD/YYYY).
    const isXlsx = buffer.length >= 4 && buffer[0] === 0x50 && buffer[1] === 0x4b; // PK
    const isXls = buffer.length >= 8 && buffer[0] === 0xd0 && buffer[1] === 0xcf; // D0 CF 11 E0

    let rows: Record<string, unknown>[];
    if (isXlsx || isXls) {
      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');
      rows = XLSX.utils.sheet_to_json(workbook.Sheets[sheetName], {
        defval: '',
        raw: true,
      }) as Record<string, unknown>[];
    } else {
      rows = parseCsv(buffer.toString('utf8'));
    }
    if (rows.length === 0) {
      throw new BadRequestException('El archivo está vacío o no tiene encabezados válidos');
    }

    const headers = Object.keys(rows[0]).map(normalizeKey);

    const colDocumento = resolverColumna(headers, 'documento_funcionario');
    if (!colDocumento) {
      throw new BadRequestException(
        'Falta la columna documento_funcionario (alias aceptados: ' +
          ALIASES.documento_funcionario.join(', ') +
          ')',
      );
    }

    const colTimestamp = resolverColumna(headers, 'timestamp_marcacion');
    const colFecha = resolverColumna(headers, 'fecha');
    const colEntrada = resolverColumna(headers, 'hora_entrada');
    const colSalida = resolverColumna(headers, 'hora_salida');

    if (colTimestamp) {
      return this.parsePorMarcacion(rows, colDocumento, colTimestamp);
    }
    if (colFecha && (colEntrada || colSalida)) {
      return this.parsePorDia(rows, colDocumento, colFecha, colEntrada, colSalida);
    }
    throw new BadRequestException(
      'No se reconoce el formato. Use uno de: ' +
        '(A) columnas documento_funcionario, fecha, hora_entrada, hora_salida; ' +
        '(B) columnas documento_funcionario, timestamp_marcacion.',
    );
  }

  // ─────────────────────────────────────────────────────────────────
  // Formato A — una fila = un día (con entrada y/o salida)
  // ─────────────────────────────────────────────────────────────────

  private parsePorDia(
    rows: Record<string, unknown>[],
    colDoc: string,
    colFecha: string,
    colEntrada: string | null,
    colSalida: string | null,
  ): ParseResult {
    const marcaciones: RawMarcacionParsed[] = [];
    const errores: RowError[] = [];

    rows.forEach((raw, idx) => {
      const fila = idx + 2; // +2 = 1 por header + 1 por base-1
      const norm: Record<string, unknown> = {};
      for (const [k, v] of Object.entries(raw)) norm[normalizeKey(k)] = v;

      const documento_raw = String(norm[colDoc] ?? '')
        .trim()
        .replace(/[.\-\s]/g, '');
      if (!documento_raw) {
        errores.push({ fila, mensaje: 'documento_funcionario vacío', datos: norm });
        return;
      }
      if (!validarDocumento(documento_raw)) {
        errores.push({
          fila,
          mensaje: `documento_funcionario inválido: "${documento_raw}" (debe ser sólo dígitos, 5-9 chars)`,
          datos: norm,
        });
        return;
      }

      const fecha = parseFecha(norm[colFecha]);
      if (!fecha) {
        errores.push({
          fila,
          mensaje: `fecha inválida: "${norm[colFecha]}". Use YYYY-MM-DD o DD/MM/YYYY.`,
          datos: norm,
        });
        return;
      }

      const entradaSec = colEntrada ? parseHoraComoSegundos(norm[colEntrada]) : null;
      const salidaSec = colSalida ? parseHoraComoSegundos(norm[colSalida]) : null;

      if (entradaSec == null && salidaSec == null) {
        errores.push({
          fila,
          mensaje: 'No hay hora_entrada ni hora_salida válida',
          datos: norm,
        });
        return;
      }
      if (entradaSec != null && salidaSec != null && salidaSec <= entradaSec) {
        errores.push({
          fila,
          mensaje: `hora_salida (${formatSec(salidaSec)}) debe ser posterior a hora_entrada (${formatSec(entradaSec)})`,
          datos: norm,
        });
        return;
      }

      if (entradaSec != null) {
        marcaciones.push({
          fila,
          documento_raw,
          tipo_marcacion: 'ENTRADA',
          timestamp_marcacion: new Date(fecha.getTime() + entradaSec * 1000),
        });
      }
      if (salidaSec != null) {
        marcaciones.push({
          fila,
          documento_raw,
          tipo_marcacion: 'SALIDA',
          timestamp_marcacion: new Date(fecha.getTime() + salidaSec * 1000),
        });
      }
    });

    return {
      formato: 'POR_DIA',
      marcaciones,
      errores,
      total_filas: rows.length,
    };
  }

  // ─────────────────────────────────────────────────────────────────
  // Formato B — una fila = una marcación individual
  // ─────────────────────────────────────────────────────────────────

  private parsePorMarcacion(
    rows: Record<string, unknown>[],
    colDoc: string,
    colTimestamp: string,
  ): ParseResult {
    const marcaciones: RawMarcacionParsed[] = [];
    const errores: RowError[] = [];

    rows.forEach((raw, idx) => {
      const fila = idx + 2;
      const norm: Record<string, unknown> = {};
      for (const [k, v] of Object.entries(raw)) norm[normalizeKey(k)] = v;

      const documento_raw = String(norm[colDoc] ?? '')
        .trim()
        .replace(/[.\-\s]/g, '');
      if (!documento_raw) {
        errores.push({ fila, mensaje: 'documento_funcionario vacío', datos: norm });
        return;
      }
      if (!validarDocumento(documento_raw)) {
        errores.push({
          fila,
          mensaje: `documento_funcionario inválido: "${documento_raw}" (debe ser sólo dígitos, 5-9 chars)`,
          datos: norm,
        });
        return;
      }

      const timestamp = parseTimestamp(norm[colTimestamp]);
      if (!timestamp) {
        errores.push({
          fila,
          mensaje: `timestamp_marcacion inválido: "${norm[colTimestamp]}". Use YYYY-MM-DD HH:MM o DD/MM/YYYY HH:MM.`,
          datos: norm,
        });
        return;
      }
      marcaciones.push({
        fila,
        documento_raw,
        timestamp_marcacion: timestamp,
      });
    });

    return {
      formato: 'POR_MARCACION',
      marcaciones,
      errores,
      total_filas: rows.length,
    };
  }
}

function formatSec(total: number): string {
  const h = Math.floor(total / 3600);
  const m = Math.floor((total % 3600) / 60);
  return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`;
}

/**
 * Parser CSV simple. Soporta comillas dobles para campos con coma y comillas
 * dobles escapadas como `""`. Detecta separadores `,` o `;`. Devuelve un array
 * de records `{ header: value }` igual que XLSX.sheet_to_json.
 */
export function parseCsv(text: string): Record<string, string>[] {
  // BOM
  if (text.charCodeAt(0) === 0xfeff) text = text.slice(1);
  const lines = splitCsvRows(text);
  if (lines.length === 0) return [];
  const sep = detectSeparator(lines[0]);
  const headers = splitCsvLine(lines[0], sep);
  const out: Record<string, string>[] = [];
  for (let i = 1; i < lines.length; i++) {
    const raw = lines[i];
    if (raw.trim() === '') continue;
    const cols = splitCsvLine(raw, sep);
    const row: Record<string, string> = {};
    for (let j = 0; j < headers.length; j++) {
      row[headers[j]] = (cols[j] ?? '').trim();
    }
    out.push(row);
  }
  return out;
}

function detectSeparator(headerLine: string): string {
  const candidates = [',', ';', '\t'];
  let best = ',';
  let max = -1;
  for (const c of candidates) {
    const count = (headerLine.match(new RegExp(`\\${c}`, 'g')) ?? []).length;
    if (count > max) {
      max = count;
      best = c;
    }
  }
  return best;
}

function splitCsvRows(text: string): string[] {
  // Manejo simple: cuenta comillas para no cortar dentro de campos quoted.
  const rows: string[] = [];
  let cur = '';
  let inQuotes = false;
  for (let i = 0; i < text.length; i++) {
    const ch = text[i];
    if (ch === '"') {
      // Doble comilla escapada
      if (inQuotes && text[i + 1] === '"') {
        cur += '"';
        i++;
        continue;
      }
      inQuotes = !inQuotes;
      cur += ch;
      continue;
    }
    if (!inQuotes && (ch === '\n' || ch === '\r')) {
      if (ch === '\r' && text[i + 1] === '\n') i++;
      rows.push(cur);
      cur = '';
      continue;
    }
    cur += ch;
  }
  if (cur.length > 0) rows.push(cur);
  return rows;
}

function splitCsvLine(line: string, sep: string): string[] {
  const out: string[] = [];
  let cur = '';
  let inQuotes = false;
  for (let i = 0; i < line.length; i++) {
    const ch = line[i];
    if (ch === '"') {
      if (inQuotes && line[i + 1] === '"') {
        cur += '"';
        i++;
        continue;
      }
      inQuotes = !inQuotes;
      continue;
    }
    if (!inQuotes && ch === sep) {
      out.push(cur);
      cur = '';
      continue;
    }
    cur += ch;
  }
  out.push(cur);
  return out;
}
