import {
  Fiel,
  HttpsWebClient,
  FielRequestBuilder,
  Service,
  QueryParameters,
  DateTimePeriod,
  DownloadType,
  RequestType,
  DocumentStatus,
  ServiceEndpoints,
} from '@nodecfdi/sat-ws-descarga-masiva';

export interface FielData {
  cerContent: string;
  keyContent: string;
  password: string;
}

/**
 * Timeout explícito para el cliente HTTP del SAT (ms).
 *
 * IMPORTANTE: la librería @nodecfdi/sat-ws-descarga-masiva@2.0.0 tiene un bug
 * en HttpsWebClient: si no se pasa un timeout explícito y ocurre un timeout
 * de red, rechaza con un `Error` nativo en vez de `WebClientException`.
 * Eso rompe el manejo de errores posterior y produce
 * `webError.getResponse is not a function`.
 *
 * Al pasar un timeout explícito, `_timeout` queda definido y la librería
 * envuelve el timeout como `WebClientException`, permitiendo reintentos sanos.
 *
 * El endpoint de verificación del SAT suele tardar >30s en responder; 5 minutos
 * da margen sin dejar la conexión colgada indefinidamente.
 */
const SAT_WEB_CLIENT_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutos

/**
 * Crea el servicio de descarga masiva del SAT usando los datos de la FIEL
 */
export function createSatService(fielData: FielData): Service {
  // Crear FIEL usando el método estático create
  const fiel = Fiel.create(fielData.cerContent, fielData.keyContent, fielData.password);

  // Verificar que la FIEL sea válida
  if (!fiel.isValid()) {
    throw new Error('La FIEL no es válida o está vencida');
  }

  // Crear cliente HTTP con timeout explícito para evitar el bug de la librería
  // cuando ocurre un timeout de red.
  const webClient = new (HttpsWebClient as any)(
    undefined,
    undefined,
    SAT_WEB_CLIENT_TIMEOUT_MS,
  );

  // Crear request builder con la FIEL
  const requestBuilder = new FielRequestBuilder(fiel);

  // Crear y retornar el servicio
  return new Service(requestBuilder, webClient, undefined, ServiceEndpoints.cfdi());
}

export interface QueryResult {
  success: boolean;
  requestId?: string;
  message: string;
  statusCode?: string;
}

export interface VerifyResult {
  success: boolean;
  status: 'pending' | 'processing' | 'ready' | 'failed' | 'rejected';
  packageIds: string[];
  totalCfdis: number;
  message: string;
  statusCode?: string;
}

export interface DownloadResult {
  success: boolean;
  packageContent: string; // Base64 encoded ZIP
  message: string;
}

/**
 * Realiza una consulta al SAT para solicitar CFDIs
 */
export async function querySat(
  service: Service,
  fechaInicio: Date,
  fechaFin: Date,
  tipo: 'emitidos' | 'recibidos',
  requestType: 'metadata' | 'cfdi' = 'cfdi'
): Promise<QueryResult> {
  try {
    // El SAT rechaza fechaInicial >= fechaFinal. Como formatDateForSat trunca
    // a medianoche en zona horaria de México, dos fechas dentro del mismo día
    // calendario mexicano resultan iguales. Ajustamos fechaFin al día siguiente
    // en hora México para evitar el error.
    let adjustedFechaFin = fechaFin;
    if (isSameMexicoDay(fechaInicio, fechaFin)) {
      // Sumar 24h en ms es suficiente porque formatDateForSat solo usa la fecha
      // calendaria de México, no la hora.
      adjustedFechaFin = new Date(fechaFin.getTime() + 24 * 60 * 60 * 1000);
    }

    const period = DateTimePeriod.createFromValues(
      formatDateForSat(fechaInicio),
      formatDateForSat(adjustedFechaFin)
    );

    const downloadType = new DownloadType(tipo === 'emitidos' ? 'issued' : 'received');
    const reqType = new RequestType(requestType === 'cfdi' ? 'xml' : 'metadata');

    // XMLs: solo vigentes (active). Metadata: todos (undefined = sin filtro).
    let parameters = QueryParameters.create(period, downloadType, reqType);
    if (requestType === 'cfdi') {
      parameters = parameters.withDocumentStatus(new DocumentStatus('active'));
    }
    const result = await service.query(parameters);

    if (!result.getStatus().isAccepted()) {
      return {
        success: false,
        message: result.getStatus().getMessage(),
        statusCode: result.getStatus().getCode().toString(),
      };
    }

    return {
      success: true,
      requestId: result.getRequestId(),
      message: 'Solicitud aceptada',
      statusCode: result.getStatus().getCode().toString(),
    };
  } catch (error: any) {
    // Errores tipo "EmptyResult (5004)" o "Se han agotado las solicitudes de por vida"
    // a veces vienen como excepción en vez de resultado aceptado. Los traducimos para
    // que el llamador los trate como "sin datos / no hay nada más que hacer" en lugar
    // de error fatal.
    const raw = error?.message || String(error);
    const emptyMatch = raw.match(/EmptyResult\s*\(?\s*(5004)\s*\)?/i) || raw.includes('5004');
    if (emptyMatch) {
      return {
        success: false,
        message: 'No se encontró la información',
        statusCode: '5004',
      };
    }

    const exhaustedMatch = raw.includes('Se han agotado las solicitudes de por vida');
    if (exhaustedMatch) {
      return {
        success: false,
        message: 'Se han agotado las solicitudes de por vida para este rango',
        statusCode: 'exhausted',
      };
    }

    console.error('[SAT Query Error]', error?.message, error?.stack || error);
    return {
      success: false,
      message: error.message || 'Error al realizar consulta',
    };
  }
}

/**
 * Verifica el estado de una solicitud
 */
export async function verifySatRequest(
  service: Service,
  requestId: string
): Promise<VerifyResult> {
  try {
    const result = await service.verify(requestId);
    const statusRequest = result.getStatusRequest();

    // `codeRequest` es el código SAT específico del estado de la solicitud
    // (5000 Accepted, 5002 Exhausted, 5003 MaximumLimit, 5004 EmptyResult,
    // 5005 Duplicated) y su mensaje explica POR QUÉ el SAT rechazó. Es la
    // pieza clave para diagnosticar rejections — el `getStatus().getCode()`
    // solo devuelve el wrapper HTTP (5000 genérico "Aceptada").
    //
    // Fuente: docs phpcfdi + lib @nodecfdi/sat-ws-descarga-masiva (`CodeRequest`).
    const codeReqObj = typeof (result as any).getCodeRequest === 'function'
      ? (result as any).getCodeRequest()
      : null;
    const codeRequestValue = codeReqObj ? codeReqObj.getValue() : null;
    const codeRequestMessage = codeReqObj ? codeReqObj.getMessage() : null;
    const codeRequestEntry = codeReqObj ? codeReqObj.getEntryId() : null;

    // Debug logging
    console.log('[SAT Verify Debug]', {
      statusRequestValue: statusRequest.getValue(),
      statusRequestEntryId: statusRequest.getEntryId(),
      cfdis: result.getNumberCfdis(),
      packages: result.getPackageIds(),
      statusCode: result.getStatus().getCode(),
      statusMsg: result.getStatus().getMessage(),
      codeRequestValue,
      codeRequestEntry,
      codeRequestMessage,
    });

    // Usar isTypeOf para determinar el estado
    let status: VerifyResult['status'];
    if (statusRequest.isTypeOf('Finished')) {
      status = 'ready';
    } else if (statusRequest.isTypeOf('InProgress')) {
      status = 'processing';
    } else if (statusRequest.isTypeOf('Accepted')) {
      status = 'pending';
    } else if (statusRequest.isTypeOf('Failure')) {
      status = 'failed';
    } else if (statusRequest.isTypeOf('Rejected')) {
      status = 'rejected';
    } else {
      // Default: check by entryId
      const entryId = statusRequest.getEntryId();
      if (entryId === 'Finished') status = 'ready';
      else if (entryId === 'InProgress') status = 'processing';
      else if (entryId === 'Accepted') status = 'pending';
      else if (entryId === 'Unknown' && result.getStatus().getCode().toString() === '404') status = 'failed';
      else status = 'pending';
    }

    // Para estados terminales no-felices, construir mensaje informativo.
    // `codeRequest` (si está disponible) es la razón SAT real del rechazo.
    const statusCode = result.getStatus().getCode().toString();
    const statusMsg = result.getStatus().getMessage();
    const reqValue = statusRequest.getValue();
    const reqEntry = statusRequest.getEntryId();

    // EmptyResult (5004) o Exhausted (5002, "solicitudes de por vida"): el SAT
    // aceptó la solicitud pero no generó paquetes (rango sin info) o ya agotamos
    // las solicitudes de ese rango. Tratarlos como "ready" con 0 paquetes para
    // NO fallar la etapa ni quemar reintentos — es un resultado benigno.
    // Se comparan value/entry/mensaje de forma defensiva porque getValue() puede
    // venir como number o string según la versión de la librería.
    const codeValueStr = codeRequestValue != null ? String(codeRequestValue) : '';
    const codeEntryStr = codeRequestEntry != null ? String(codeRequestEntry) : '';
    const codeMsgStr = codeRequestMessage != null ? String(codeRequestMessage) : '';
    const isEmptyResult =
      codeValueStr === '5004' ||
      codeEntryStr === '5004' ||
      /EmptyResult/i.test(codeEntryStr) ||
      /\b5004\b/.test(codeMsgStr);
    const isExhausted =
      codeValueStr === '5002' ||
      /Exhausted/i.test(codeEntryStr) ||
      /solicitudes de por vida/i.test(codeMsgStr);
    if (isEmptyResult || isExhausted) {
      return {
        success: true,
        status: 'ready',
        packageIds: [],
        totalCfdis: 0,
        message: isExhausted
          ? 'Se han agotado las solicitudes de por vida para este rango'
          : 'No se encontró información para el rango solicitado',
        statusCode,
      };
    }

    let message = statusMsg;
    if (status === 'rejected' || status === 'failed') {
      const codeReqStr = codeRequestValue
        ? ` codeRequest=${codeRequestEntry}(${codeRequestValue}) — ${codeRequestMessage}`
        : '';
      message = `SAT request=${reqEntry}(${reqValue})${codeReqStr} wrapperCode=${statusCode} wrapperMsg="${statusMsg}"`;
    }

    return {
      success: result.getStatus().isAccepted(),
      status,
      packageIds: result.getPackageIds(),
      totalCfdis: result.getNumberCfdis(),
      message,
      statusCode,
    };
  } catch (error: any) {
    console.error('[SAT Verify Error]', error?.message, error?.stack || error);
    // Errores de la librería (ej. webError.getResponse is not a function)
    // no son fallos del SAT — devolver 'pending' para reintentar polling
    return {
      success: false,
      status: 'pending',
      packageIds: [],
      totalCfdis: 0,
      message: error.message || 'Error al verificar solicitud',
    };
  }
}

/**
 * Descarga un paquete de CFDIs
 */
export async function downloadSatPackage(
  service: Service,
  packageId: string
): Promise<DownloadResult> {
  try {
    const result = await service.download(packageId);

    if (!result.getStatus().isAccepted()) {
      return {
        success: false,
        packageContent: '',
        message: result.getStatus().getMessage(),
      };
    }

    return {
      success: true,
      packageContent: result.getPackageContent(),
      message: 'Paquete descargado',
    };
  } catch (error: any) {
    console.error('[SAT Download Error]', error);
    return {
      success: false,
      packageContent: '',
      message: error.message || 'Error al descargar paquete',
    };
  }
}

/**
 * Formatea una fecha para el SAT (YYYY-MM-DD HH:mm:ss).
 * El SAT requiere hora 00:00:00; cualquier otra hora causa
 * "Fecha final invalida" / "Fecha inicial invalida".
 *
 * IMPORTANTE: las fechas deben interpretarse en la zona horaria de México
 * (America/Mexico_City) porque el SAT opera en esa zona. El servidor corre
 * en UTC, así que usamos Intl.DateTimeFormat para obtener los componentes
 * locales a México.
 */
function formatDateForSat(date: Date): string {
  const fmt = new Intl.DateTimeFormat('es-MX', {
    timeZone: 'America/Mexico_City',
    year: 'numeric',
    month: '2-digit',
    day: '2-digit',
  });
  const parts = fmt.formatToParts(date);
  const get = (type: string) => parts.find(p => p.type === type)?.value || '00';
  return `${get('year')}-${get('month')}-${get('day')} 00:00:00`;
}

/**
 * Devuelve true si dos fechas (interpretadas en zona horaria de México)
 * caen en el mismo día calendario.
 */
function isSameMexicoDay(a: Date, b: Date): boolean {
  const fmt = new Intl.DateTimeFormat('es-MX', {
    timeZone: 'America/Mexico_City',
    year: 'numeric',
    month: '2-digit',
    day: '2-digit',
  });
  return fmt.format(a) === fmt.format(b);
}
