/**
 * Copyright IBM Corp. 2024, 2025
 */

import { HttpClient } from './http-client.js';
import { AxiosClient } from './axios-client.js';
import { VCM } from '../variable-context-manager/context-manager.js';
import { Request, Payload, AuthOptions } from '../../schemas/test.schema.js';
import qs from 'qs';
import { parseStringPromise } from 'xml2js';
import _get from 'lodash/get.js';
import { LogWrapper } from '../../service/log-wrapper.js';
import { uploadedFileModel } from '../../model-factories/fileupload.factory.js';
import FormDataNode from 'form-data';

// List of known system variables
const defaultSystemVars = [
  'response',
  'requestHeaders',
  'responseHeaders',
  'requestBody',
  'responseBody',
  'requestUrl',
  'requestMethod',
  'responseStatus',
  'responseStatusText',
  'responseTime',
];

export class RestHandler {
  constructor(private readonly httpClient: HttpClient = new AxiosClient()) {}

  /**
   * Remove properties that cause circular references from response/request objects
   * @param obj The object to sanitize
   * @returns A sanitized copy of the object
   */
  private removeCircularProperties(obj: any): any {
    if (!obj || typeof obj !== 'object') {
      return obj;
    }

    // Handle arrays
    if (Array.isArray(obj)) {
      return obj.map((item) => this.removeCircularProperties(item));
    }

    // List of properties known to cause circular references in HTTP responses
    const circularProps = new Set([
      'socket',
      '_httpMessage',
      'req',
      'request',
      'connection',
      'client',
      'res',
      'response',
      'agent',
      'httpAgent',
      'httpsAgent',
      '_events',
      '_eventsCount',
      '_maxListeners',
      'parser',
      '_consuming',
      '_dumped',
      'httpVersion',
      'httpVersionMajor',
      'httpVersionMinor',
      'complete',
      'rawHeaders',
      'rawTrailers',
      'aborted',
      'upgrade',
      '_readableState',
      '_writableState',
      'readable',
      'writable',
    ]);

    // Check if this object has any circular reference properties
    const hasCircularProps = Object.keys(obj).some((key) =>
      circularProps.has(key),
    );

    // If no circular properties found, return the object as-is (it's likely a simple error response)
    if (!hasCircularProps) {
      return obj;
    }

    // Create a shallow copy to avoid mutating the original
    const sanitized: any = {};

    for (const key in obj) {
      if (Object.prototype.hasOwnProperty.call(obj, key)) {
        // Skip circular reference properties
        if (circularProps.has(key)) {
          continue;
        }

        const value = obj[key];

        // Recursively sanitize nested objects
        if (value && typeof value === 'object') {
          // Only recurse for plain objects and arrays, not special objects
          if (Array.isArray(value) || value.constructor === Object) {
            sanitized[key] = this.removeCircularProperties(value);
          } else {
            // For other object types (like Buffer, Date, Error, etc.), keep as is
            sanitized[key] = value;
          }
        } else {
          sanitized[key] = value;
        }
      }
    }

    return sanitized;
  }

  async setValues(
    start: number,
    response: any,
    request: any,
    contextId: string,
    step: Request,
  ) {
    const responseTime = Date.now() - start;

    const vcm = VCM.getContext(contextId);

    // Sanitize request and response objects before storing in VCM
    const sanitizedRequest = this.removeCircularProperties(request);
    const sanitizedResponse = this.removeCircularProperties(response);

    // Store all information for assertions
    vcm.set('request', sanitizedRequest);
    vcm.set('response', sanitizedResponse);
    vcm.set('requestHeaders', sanitizedRequest.headers);
    vcm.set('responseHeaders', sanitizedResponse.headers);
    vcm.set('requestBody', sanitizedRequest.data);

    let parsedResponseData: any = sanitizedResponse.data;
    const contentType = sanitizedResponse.headers?.['content-type'] || '';
    const isXML = contentType.includes('application/xml');

    if (isXML) {
      parsedResponseData = await parseStringPromise(sanitizedResponse.data, {
        explicitArray: false,
      });
      // @deprecated
      vcm.set('xml()', parsedResponseData);
    } else {
      // @deprecated
      vcm.set('json()', parsedResponseData);
    }

    vcm.set('responseBody', parsedResponseData);
    vcm.set('requestUrl', sanitizedRequest.url);
    vcm.set('requestMethod', sanitizedRequest.method);
    vcm.set('responseStatus', sanitizedResponse.status);
    vcm.set('responseStatusText', sanitizedResponse.statusText);
    sanitizedResponse.responseTime = responseTime;
    vcm.set('responseTime', responseTime);

    // Mark this as @deprecated. which should use __response_status__
    vcm.set('code', sanitizedResponse.status);
    vcm.set('headers()', sanitizedResponse.headers);
    vcm.set('responseTime', responseTime);

    // For storing results based on step variable to use for chaining
    if (step.var) {
      if (Array.isArray(step.var)) {
        step.var.forEach(
          (obj: Record<string, string> | { key: string; value: string }) => {
            if ('key' in obj && 'value' in obj) {
              const { key, value } = obj;
              let resolvedValue: any;
              // Handle system variable style references (like responseBody.id)
              if (typeof value === 'string' && value.includes('.')) {
                // Extract system variable name and property path
                const [systemVar, ...pathParts] = value.split('.');
                const path = pathParts.join('.');
                const isKnownSystemVar = defaultSystemVars.includes(systemVar);
                // Get the base value from VCM
                const baseValue = vcm.get(systemVar);

                if (baseValue === undefined) {
                  // Different log message based on whether it's a known system variable
                  const message = isKnownSystemVar
                    ? `System variable "${systemVar}" exists but has no value yet`
                    : `Unknown system variable "${systemVar}"`;
                  LogWrapper.logWarn(
                    '0003',
                    `Variable resolution warning: ${message}`,
                  );
                  resolvedValue = undefined;
                } else {
                  const unwrappedValue =
                    baseValue?.value !== undefined
                      ? baseValue.value
                      : baseValue;
                  resolvedValue = path
                    ? _get(unwrappedValue, path)
                    : unwrappedValue;
                }
              } else if (
                typeof value === 'string' &&
                defaultSystemVars.includes(value)
              ) {
                const baseValue = vcm.get(value);
                if (baseValue === undefined) {
                  const message = `Unknown system variable "${value}"`;
                  LogWrapper.logWarn(
                    '0003',
                    `Variable resolution warning: ${message}`,
                  );
                  resolvedValue = undefined;
                } else {
                  resolvedValue =
                    baseValue?.value !== undefined
                      ? baseValue.value
                      : baseValue;
                }
              } else {
                resolvedValue = null;
              }
              vcm.set(key, resolvedValue);
            } else {
              const [key, jsonPath] = Object.entries(obj)[0];
              vcm.set(key, _get(parsedResponseData, jsonPath));
            }
          },
        );
      } else {
        vcm.set(step.var, parsedResponseData);
      }
    }
  }

  async execute(step: Request, contextId: string): Promise<any> {
    const {
      headers: stepHeaders,
      auth,
      payload,
      settings,
      endpoint: url,
      parameters,
      ...rest
    } = step;

    if (!url) {
      throw new Error('Endpoint is required');
    }
    const start = Date.now();
    let data;
    const headers = {
      ...this.constructRecord(stepHeaders),
      ...this.constructAuthHeaders(contextId, auth),
    };

    try {
      data = this.constructData(this.constructRecord(stepHeaders), payload);
    } catch (error: any) {
      const errorResponse = {
        status: 0,
        statusText: 'Invalid file path error',
        headers: {},
        data: { error: error?.message || 'Invalid file path error' },
        error: error,
      };
      await this.setValues(
        start,
        errorResponse,
        {
          ...rest,
          headers,
        },
        contextId,
        step,
      );
      throw error;
    }
    const stepRequest = {
      ...rest,
      url,
      headers,
      validateSSL: settings?.sslVerification,
      data,
      params: this.constructRecord(parameters),
    };

    delete stepRequest.assertions;

    let request;
    try {
      // Resolve variables in the request
      request = VCM.resolve(contextId, stepRequest);
    } catch (error: any) {
      // Create a structured error response for variable resolution failures
      const errorResponse = {
        status: 0,
        statusText: 'Variable Resolution Error',
        headers: {},
        data: { error: error?.message || 'Unknown variable resolution error' },
        error: error,
      };
      await this.setValues(start, errorResponse, stepRequest, contextId, step);
      throw error;
    }

    try {
      const isFormDataAvailable: boolean = this.checkIfFormData(request);
      const response = await this.httpClient.request(
        request,
        isFormDataAvailable,
      );
      await this.setValues(start, response, request, contextId, step);
      return response;
    } catch (error) {
      const err = error as any;
      const response = err.response || err;
      await this.setValues(start, response, request, contextId, step);
      throw error;
    }
  }

  private constructRecord(
    data?: Array<{ key: string; value: any }>,
  ): Record<string, any> {
    const result: Record<string, any> = {};
    for (const { key, value } of data ?? []) {
      result[key] = value;
    }
    return result;
  }

  private constructData(headers: any, payload?: Payload) {
    if (!payload) {
      return;
    }
    const { raw, urlEncodedFormData, formData } = payload;
    if (raw) {
      // Prioritize these types in this order
      const order: (keyof typeof raw)[] = ['json', 'xml', 'js', 'html'];
      for (const key of order) {
        const value = raw[key];
        if (value) return value;
      }
    } else if (urlEncodedFormData) {
      return qs.stringify(this.constructRecord(urlEncodedFormData));
    } else if (formData) {
      /* eslint-disable @typescript-eslint/no-unused-expressions */
      let openAPIVersion = 2;
      const contentType =
        headers['Content-Type']?.toLowerCase() ||
        headers['content-type']?.toLowerCase();
      if (['application/octet-stream', 'image/png'].includes(contentType)) {
        openAPIVersion = 3;
      }
      const form = openAPIVersion === 3 ? new FormDataNode() : new FormData();
      const uploadedFiles = uploadedFileModel.getAllUploadedFiles();
      const uploadedFileKey = new Set<string>();
      // Add uploaded files to the form and track their keys
      if (Array.isArray(uploadedFiles) && uploadedFiles.length > 0) {
        uploadedFiles.forEach((ele: any) => {
          if (typeof Buffer !== 'undefined' && Buffer.isBuffer(ele.value)) {
            // eslint-disable-next-line @typescript-eslint/ban-ts-comment
            // @ts-ignore: FormData in browser doesn't support Buffer, but Node.js `form-data` does
            openAPIVersion === 3
              ? form.append(ele.fileName, ele.value, ele.fileName)
              : form.append('file', ele.value);
            uploadedFileKey.add(ele.fileName);
          }
        });
      }
      // Add regular form fields, avoiding duplicates with uploaded files

      if (Array.isArray(formData) && formData.length > 0) {
        formData.forEach(({ key, value }) => {
          if (!uploadedFileKey.has(key) && key !== 'file') {
            form.append(key, value);
          }
        });
        /* eslint-enable @typescript-eslint/no-unused-expressions */
      }
      return form;
    }
    return;
  }

  private constructAuthHeaders(
    contextId: string,
    auth?: AuthOptions,
  ): Record<string, string> {
    const headers: Record<string, string> = {};
    if (auth) {
      if (auth.bearerToken) {
        headers['Authorization'] = `Bearer ${auth.bearerToken}`;
        return headers;
      }
      if (auth.basicAuth) {
        const { username = '', password = '' } = auth.basicAuth;
        let basicString = `${username}:${password}`;

        try {
          // Resolve any variables in the username:password string
          basicString = VCM.resolve(contextId, basicString);
        } catch (error) {
          // If variable resolution fails, use the original string
          // This allows basic auth to work even if variables are not defined
          LogWrapper.logWarn(
            '0004',
            `Failed to resolve variables in basic auth credentials: ${error instanceof Error ? error.message : 'Unknown error'}`,
          );
        }

        const encoded = Buffer.from(basicString).toString('base64');
        headers['Authorization'] = `Basic ${encoded}`;
        return headers;
      }
    }
    return headers;
  }

  private checkIfFormData(request: any): boolean {
    const data = request.data;
    const contentType =
      request.headers?.['Content-Type'] ||
      request.headers?.['content-type'] ||
      '';

    return (
      data instanceof FormData ||
      data instanceof FormDataNode ||
      contentType.includes('multipart/form-data') ||
      contentType.includes('application/octet-stream') ||
      contentType.includes('image/png')
    );
  }
}
