import { BadRequestException, Injectable, Logger } from '@nestjs/common';

/**
 * Cliente HTTP genérico para consumir marcaciones desde un reloj/marcador (M16).
 *
 * Soporta:
 *  - Métodos GET con auth NONE / BASIC / BEARER / API_KEY (header `X-Api-Key` por defecto).
 *  - Formatos de respuesta JSON o XML.
 *  - Extracción flexible de documento + timestamp configurando los nombres de campo.
 *
 * El parser de XML usa un enfoque muy básico para evitar agregar dependencias
 * (regexp por etiquetas). Si en el futuro un reloj requiere XML complejo,
 * conviene migrar a `fast-xml-parser`.
 */

export type RelojApiConfig = {
  url_base: string;
  tipo_auth: 'NONE' | 'BASIC' | 'BEARER' | 'API_KEY';
  api_key: string | null;
  formato_respuesta: 'JSON' | 'XML' | null;
  campo_documento: string | null;
  campo_timestamp: string | null;
};

export type RelojApiMarcacion = {
  documento_raw: string;
  timestamp_marcacion: Date;
};

export type RelojApiResult = {
  ok: boolean;
  marcaciones: RelojApiMarcacion[];
  errores_parse: number;
  http_status?: number;
  detalle?: string;
};

const DEFAULT_TIMEOUT_MS = 15_000;

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

  async consultar(
    config: RelojApiConfig,
    params: { desde?: Date; hasta?: Date },
  ): Promise<RelojApiResult> {
    if (!config.url_base) {
      throw new BadRequestException('url_base no configurada para el reloj');
    }

    const headers: Record<string, string> = {
      Accept: config.formato_respuesta === 'XML' ? 'application/xml' : 'application/json',
    };

    switch (config.tipo_auth) {
      case 'BASIC': {
        if (!config.api_key) throw new BadRequestException('BASIC requiere usuario:password en api_key');
        const enc = Buffer.from(config.api_key, 'utf8').toString('base64');
        headers.Authorization = `Basic ${enc}`;
        break;
      }
      case 'BEARER': {
        if (!config.api_key) throw new BadRequestException('BEARER requiere token en api_key');
        headers.Authorization = `Bearer ${config.api_key}`;
        break;
      }
      case 'API_KEY': {
        if (!config.api_key) throw new BadRequestException('API_KEY requiere valor en api_key');
        headers['X-Api-Key'] = config.api_key;
        break;
      }
      case 'NONE':
      default:
        break;
    }

    // Append parámetros de rango si los hay
    const url = new URL(config.url_base);
    if (params.desde) url.searchParams.set('desde', params.desde.toISOString());
    if (params.hasta) url.searchParams.set('hasta', params.hasta.toISOString());

    let response: Response;
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT_MS);
    try {
      response = await fetch(url.toString(), {
        method: 'GET',
        headers,
        signal: controller.signal,
      });
    } catch (err: any) {
      clearTimeout(timeoutId);
      const detalle = err?.name === 'AbortError'
        ? `Timeout al consultar el reloj (> ${DEFAULT_TIMEOUT_MS}ms)`
        : `Error de red al consultar el reloj: ${err?.message ?? err}`;
      this.logger.warn(`Reloj ${config.url_base}: ${detalle}`);
      return { ok: false, marcaciones: [], errores_parse: 0, detalle };
    }
    clearTimeout(timeoutId);

    if (!response.ok) {
      const body = await this.safeText(response);
      return {
        ok: false,
        marcaciones: [],
        errores_parse: 0,
        http_status: response.status,
        detalle: `HTTP ${response.status} ${response.statusText}: ${body.slice(0, 500)}`,
      };
    }

    const text = await response.text();
    const formato = config.formato_respuesta ?? 'JSON';
    const campoDoc = config.campo_documento ?? 'documento';
    const campoTs = config.campo_timestamp ?? 'timestamp';

    try {
      const items =
        formato === 'XML'
          ? this.parseXml(text, campoDoc, campoTs)
          : this.parseJson(text, campoDoc, campoTs);
      const marcaciones: RelojApiMarcacion[] = [];
      let errores = 0;
      for (const it of items) {
        if (!it.documento_raw || !it.timestamp_marcacion) {
          errores++;
          continue;
        }
        marcaciones.push(it);
      }
      return {
        ok: true,
        marcaciones,
        errores_parse: errores,
        http_status: response.status,
      };
    } catch (err: any) {
      return {
        ok: false,
        marcaciones: [],
        errores_parse: 0,
        http_status: response.status,
        detalle: `Error parseando respuesta del reloj: ${err?.message ?? err}`,
      };
    }
  }

  private async safeText(res: Response): Promise<string> {
    try {
      return await res.text();
    } catch {
      return '';
    }
  }

  private parseJson(body: string, campoDoc: string, campoTs: string): RelojApiMarcacion[] {
    const parsed = JSON.parse(body);
    let lista: any[];
    if (Array.isArray(parsed)) {
      lista = parsed;
    } else if (Array.isArray(parsed?.data)) {
      lista = parsed.data;
    } else if (Array.isArray(parsed?.marcaciones)) {
      lista = parsed.marcaciones;
    } else if (Array.isArray(parsed?.items)) {
      lista = parsed.items;
    } else if (Array.isArray(parsed?.result)) {
      lista = parsed.result;
    } else {
      throw new Error(
        'La respuesta JSON no contiene un array de marcaciones (claves esperadas: data | marcaciones | items | result).',
      );
    }
    return lista.map((item) => ({
      documento_raw: this.extraerString(item, campoDoc),
      timestamp_marcacion: this.parseTs(this.extraerString(item, campoTs)),
    }));
  }

  private extraerString(obj: any, key: string): string {
    if (!obj || typeof obj !== 'object') return '';
    // Soporta path con punto: "empleado.cedula"
    const path = key.split('.');
    let cur: any = obj;
    for (const p of path) {
      if (cur && typeof cur === 'object' && p in cur) cur = cur[p];
      else return '';
    }
    return cur == null ? '' : String(cur).trim();
  }

  private parseTs(s: string): Date | null {
    if (!s) return null;
    const d = new Date(s);
    return Number.isNaN(d.getTime()) ? null : d;
  }

  private parseXml(body: string, campoDoc: string, campoTs: string): RelojApiMarcacion[] {
    // Estrategia simple: buscamos repeticiones del bloque más profundo que contenga
    // a la vez `<campoDoc>...</campoDoc>` y `<campoTs>...</campoTs>`.
    const itemRegex = /<([a-zA-Z_][\w-]*)\b[^>]*>([\s\S]*?)<\/\1>/g;
    const items: RelojApiMarcacion[] = [];
    let match: RegExpExecArray | null;
    while ((match = itemRegex.exec(body)) !== null) {
      const inner = match[2];
      const docMatch = new RegExp(`<${campoDoc}[^>]*>([\\s\\S]*?)<\\/${campoDoc}>`).exec(inner);
      const tsMatch = new RegExp(`<${campoTs}[^>]*>([\\s\\S]*?)<\\/${campoTs}>`).exec(inner);
      if (docMatch && tsMatch) {
        items.push({
          documento_raw: docMatch[1].trim(),
          timestamp_marcacion: this.parseTs(tsMatch[1].trim()) as Date,
        });
      }
    }
    if (items.length === 0) {
      throw new Error(
        `No se encontraron elementos con campos ${campoDoc}/${campoTs} en la respuesta XML.`,
      );
    }
    return items;
  }
}
