/**
 * Crypto.com OTC 2.0 API Digital Signature Implementation
 * Based on: https://exchange-docs.crypto.com/exchange/v1/rest-ws/index_OTC2.html#digital-signature
 *
 * The signature algorithm:
 * 1. If "params" exist in the request, sort the request parameter keys in ascending order
 * 2. Combine all the ordered parameter keys as key + value (no spaces, no delimiters) = parameter string
 * 3. Combine: method + id + api_key + parameter string + nonce
 * 4. Use HMAC-SHA256 to hash the above using the API Secret as the cryptographic key
 * 5. Encode the output as a hex string
 */

import crypto from 'crypto';

const MAX_PARAM_DEPTH = 3;

/**
 * Convert a value to string representation for signature calculation
 * Handles nested objects and arrays recursively up to MAX_PARAM_DEPTH
 */
function valueToString(value: unknown, level: number = 0): string {
  if (level >= MAX_PARAM_DEPTH) {
    return String(value);
  }

  if (value === null) {
    return 'null';
  }

  if (value === undefined) {
    return '';
  }

  if (Array.isArray(value)) {
    return value.map((item) => valueToString(item, level + 1)).join('');
  }

  if (typeof value === 'object') {
    return objectToString(value as Record<string, unknown>, level);
  }

  return String(value);
}

/**
 * Convert an object to string representation for signature calculation
 * Keys are sorted alphabetically
 */
function objectToString(obj: Record<string, unknown>, level: number = 0): string {
  if (level >= MAX_PARAM_DEPTH) {
    return String(obj);
  }

  const sortedKeys = Object.keys(obj).sort();
  let result = '';

  for (const key of sortedKeys) {
    const value = obj[key];
    result += key;
    result += valueToString(value, level + 1);
  }

  return result;
}

/**
 * Generate the parameter string from request params
 * Parameters are sorted alphabetically and concatenated without delimiters
 */
export function generateParamsString(params?: Record<string, unknown>): string {
  if (!params || Object.keys(params).length === 0) {
    return '';
  }

  return objectToString(params, 0);
}

/**
 * Generate HMAC-SHA256 digital signature for Crypto.com API
 *
 * @param method - The API method name
 * @param id - The request identifier
 * @param apiKey - The API key
 * @param params - The request parameters (optional)
 * @param nonce - Current timestamp in milliseconds
 * @param apiSecret - The API secret key
 * @returns The hex-encoded HMAC-SHA256 signature
 */
export function generateSignature(
  method: string,
  id: number,
  apiKey: string,
  params: Record<string, unknown> | undefined,
  nonce: number,
  apiSecret: string
): string {
  const paramsString = generateParamsString(params);

  // Construct the signature payload: method + id + api_key + params_string + nonce
  const sigPayload = `${method}${id}${apiKey}${paramsString}${nonce}`;

  // Generate HMAC-SHA256 hash and encode as hex
  return crypto
    .createHmac('sha256', apiSecret)
    .update(sigPayload)
    .digest('hex');
}

/**
 * Sign a request object with API key and digital signature
 *
 * @param request - The request object containing id, method, params, and nonce
 * @param apiKey - The API key
 * @param apiSecret - The API secret key
 * @returns Object containing api_key and sig to be merged into the request
 */
export function signRequest(
  request: {
    id: number;
    method: string;
    params?: Record<string, unknown>;
    nonce: number;
  },
  apiKey: string,
  apiSecret: string
): { api_key: string; sig: string } {
  const sig = generateSignature(
    request.method,
    request.id,
    apiKey,
    request.params,
    request.nonce,
    apiSecret
  );

  return {
    api_key: apiKey,
    sig,
  };
}

/**
 * Create a signed request ready to be sent to the API
 *
 * @param method - The API method name
 * @param params - The request parameters (optional)
 * @param apiKey - The API key
 * @param apiSecret - The API secret key
 * @param id - Optional request ID (will be auto-generated if not provided)
 * @returns A complete signed request object
 */
export function createSignedRequest(
  method: string,
  params: Record<string, unknown> | undefined,
  apiKey: string,
  apiSecret: string,
  id?: number
): {
  id: number;
  method: string;
  params?: Record<string, unknown>;
  api_key: string;
  sig: string;
  nonce: number;
} {
  const requestId = id ?? Math.floor(Math.random() * Number.MAX_SAFE_INTEGER);
  const nonce = Date.now();

  const request = {
    id: requestId,
    method,
    params,
    nonce,
  };

  const { api_key, sig } = signRequest(request, apiKey, apiSecret);

  return {
    ...request,
    api_key,
    sig,
  };
}

/**
 * Verify a signature (useful for testing)
 *
 * @param request - The request object
 * @param apiSecret - The API secret key
 * @returns true if the signature is valid
 */
export function verifySignature(
  request: {
    id: number;
    method: string;
    params?: Record<string, unknown>;
    nonce: number;
    api_key: string;
    sig: string;
  },
  apiSecret: string
): boolean {
  const expectedSig = generateSignature(
    request.method,
    request.id,
    request.api_key,
    request.params,
    request.nonce,
    apiSecret
  );

  return request.sig === expectedSig;
}
