<?php
/**
 * This file is for performing the API service functionality.
 *
 * @package    ProductSync
 * @subpackage API_Service
 * @author     miniOrange <info@miniorange.com>
 * @link       https://miniorange.com
 */

namespace MOWCPS\Common\Services\API;

use MOWCPS\Common\Constants\Constants;
use MOWCPS\Common\Utils\Utils;
use MOWCPS\Common\Utils\Sync_Utils;
use MOWCPS\Common\Services\Logging\Supplier_Logger;
use MOWCPS\Common\Models\Sync_Details_DAO;

/**
 * API Service Class.
 *
 * Handles API calls to external suppliers.
 */
class API_Service {

	/**
	 * Auth details.
	 *
	 * @var int
	 */
	public $supplier_id;

	/**
	 * API details.
	 *
	 * @var int
	 */
	public $connection_id;

	/**
	 * Constructor.
	 *
	 * @param int $supplier_id   The supplier ID.
	 * @param int $connection_id The connection ID.
	 */
	public function __construct( $supplier_id = null, $connection_id = null ) {
		$this->connection_id = $connection_id;
		$this->supplier_id   = $supplier_id;
	}

	/**
	 * Execute an API call using provided details.
	 *
	 * @param array $api_details API details including URL, method, headers, and body.
	 *
	 * @return array|WP_Error The API response or WP_Error on failure.
	 */
	public function execute_api_call( $api_details = array() ) {
		if ( isset( $api_details['endpoint'] ) || ! empty( isset( $api_details['endpoint'] ) ) ) {
			// Request URL: $request_url.
			$request_url = htmlspecialchars_decode( $api_details['endpoint'] );
			if ( ! empty( $url ) ) {
				$request_url = $url;
			}
			$request_url = str_replace( '&amp;', '&', $request_url );

			// All request arguments.
			$arguments = array(
				'method'  => $api_details['method'],
				'headers' => $api_details['headers'],
				'timeout' => 300, // execution time.
				// 'stream'   => true, //to set file download.
				// 'filename' => utils, //name of to be written in.
			);

			if ( Constants::GET_REQUEST !== $api_details['method'] ) {
				$arguments['body'] = is_array( $api_details['body']['data'] ) ? json_encode( $api_details['body']['data'] ) : $api_details['body']['data'];
			}

			if ( ! empty( $api_details['time_delay'] ) ) {
				sleep( (int) $api_details['time_delay'] );
			}

			// Hook for SNSActivewear to prevent Rate Limiting.
			if ( has_filter( 'mowcps_api_call_alteration_filter' ) ) {

				$api_request = apply_filters(
					'mowcps_api_call_alteration_filter',
					array(
						'event_specifier' => 'alter_pre_api_call_data',
						'request_url'     => $request_url,
						'api_arguments'   => $arguments,
						'connection_id'   => $this->connection_id,
						'supplier_id'     => $this->supplier_id,
					)
				);

				if ( ! empty( $api_request['api_response'] ) ) {
					return $api_request['api_response'];
				} else {
					$request_url = $api_request['request_url'];
					$arguments   = $api_request['api_arguments'];
				}
			}

			$api_response = self::make_api_call( $request_url, $arguments );
			if ( is_wp_error( $api_response ) ) {
				$error_message = $api_response->get_error_message();
				$error_code    = $api_response->get_error_code();

				Supplier_Logger::debug( $this->supplier_id, 'API call failed for connection: ' . $this->connection_id );
				Supplier_Logger::debug( $this->supplier_id, 'API endpoint: ' . $request_url );
				Supplier_Logger::debug( $this->supplier_id, 'Error code: ' . $error_code );
				Supplier_Logger::debug( $this->supplier_id, 'Error message: ' . $error_message );
				unset( $api_response );
				unset( $arguments );

				return $error_message;
			} else {

				/**
				 * This filter position is to perform any operations on the API response of the current API before starting sync.
				 *
				 * Additional args/details should be sent as an in 2nd parameter of apply filter. It may contain contain event_specifier to specify the event of the filter.
				 * The filter should return the same structure as the 2nd parameter, so other filter potions are not affected by your add-on.
				 *
				 * $api_response    @JSON Object Complete response from wp_remote_request which contains headers and body of the API response.
				 * self::$connection_id @String      Name of the specific connections.
				 */
				if ( has_filter( 'mowcps_api_call_alteration_filter' ) ) {

					$returned_data = apply_filters(
						'mowcps_api_call_alteration_filter',
						array(
							'event_specifier' => 'mowcps_post_api_call_filter',
							'api_response'    => $api_response,
							'request_url'     => $request_url,
							'connection_id'   => $this->connection_id,
						)
					);

					$api_response = $returned_data['api_response'];
				}

				$response_headers = wp_remote_retrieve_headers( $api_response );
				if ( is_object( $response_headers ) && $response_headers instanceof \WpOrg\Requests\Utility\CaseInsensitiveDictionary ) {
					$response_headers = $response_headers->getAll();
				}
				$returned_response = wp_remote_retrieve_body( $api_response );
				unset( $api_response );

				if ( has_filter( 'mowcps_api_call_alteration_filter' ) ) {

					$returned_data = apply_filters(
						'mowcps_api_call_alteration_filter',
						array(
							'event_specifier'  => 'alter_header_and_body_post_api_call_filter',
							'response_body'    => $returned_response,
							'response_headers' => $response_headers,
							'connection_id'    => $this->connection_id,
						)
					);

					$response_headers  = $returned_data['response_headers'];
					$returned_response = $returned_data['response_body'];
					unset( $returned_data );
				}

				$content_type = isset( $response_headers['content-type'] ) ? $response_headers['content-type'] : '';
				if ( stripos( $content_type, 'xml' ) !== false ) {
					$returned_response = self::parse_xml( $returned_response );
				}
				if ( null !== $returned_response ) {
					$returned_response = Utils::safe_json_decode( $returned_response );
				} else {
					$returned_response = array();
				}
				// Trigger garbage collection.
				Sync_Utils::trigger_garbage_collection();
				unset( $response_headers );
				unset( $content_type );

				/**
				 * This filter position is to perform any operations on the API response of the current API before starting sync.
				 *
				 * Additional args/details should be sent as an in 2rd parameter of apply filter. It may contain contain event_specifier to specify the event of the filter.
				 * The filter should return the same structure as the 2nd parameter, so other filter potions are not affected by your add-on.
				 *
				 * $api_response    @JSON Object Complete response from wp_remote_request which contains headers and body of the API response.
				 * self::$connection_id @String      Name of the specific connections.
				 */
				if ( has_filter( 'mowcps_api_call_alteration_filter' ) ) {

					$returned_data = apply_filters(
						'mowcps_api_call_alteration_filter',
						array(
							'event_specifier' => 'alter_curated_api_response_data',
							'response_body'   => $returned_response,
							'connection_id'   => $this->connection_id,
						)
					);

					$returned_response = $returned_data['response_body'];
					unset( $returned_data );
				}

				if ( has_filter( 'mowcps_api_call_alteration_filter' ) ) {
					$returned_data = apply_filters(
						'mowcps_api_call_alteration_filter',
						array(
							'event_specifier' => 'alter_processed_api_data',
							'connection_id'   => $this->connection_id,
							'api_response'    => $returned_response,
							'request_url'     => $request_url,
						)
					);

					$returned_response = $returned_data['api_response'];
					unset( $returned_data );
				}

				/**
				 * Filter with request context: same as alter_processed_api_data but includes request_url and api_arguments
				 * so handlers can retry or alter the response using the original request (e.g. auth headers already in api_arguments).
				 */
				if ( has_filter( 'mowcps_api_call_alteration_filter' ) ) {
					$returned_data = apply_filters(
						'mowcps_api_call_alteration_filter',
						array(
							'event_specifier'  => 'alter_processed_api_data_with_request',
							'connection_id'   => $this->connection_id,
							'supplier_id'      => $this->supplier_id,
							'api_response'     => $returned_response,
							'request_url'      => $request_url,
							'api_arguments'    => $arguments,
						)
					);

					$returned_response = $returned_data['api_response'];
					unset( $returned_data );
				}
				unset( $arguments );
				unset( $request_url );

				return $returned_response;
			}
		} else {
			Supplier_Logger::debug( $this->supplier_id, 'API call failed - Invalid or missing endpoint' );
			Supplier_Logger::debug( $this->supplier_id, 'Connection ID: ' . $this->connection_id );
			Supplier_Logger::debug( $this->supplier_id, 'Check your connection configuration and ensure the API endpoint is properly set' );
			unset( $api_details );
			wp_die( 'Invalid API Name passed in external api connection hook :' . esc_html( $this->connection_id ) );
		}
	}

	/**
	 * Make an API call.
	 *
	 * @param string $request_url The request URL.
	 * @param array  $arguments   The request arguments.
	 *
	 * @return array|WP_Error The API response or WP_Error on failure.
	 */
	public static function make_api_call( $request_url, $arguments ) {
		ini_set( 'memory_limit', '1024M' ); //phpcs:ignore
		Sync_Utils::cleanup_memory();
		$api_response = wp_remote_request( $request_url, $arguments );

		return $api_response;
	}

	/**
	 * Parse SOAP response and convert it to an array.
	 *
	 * @param string $response SOAP response string.
	 * @return string Parsed response as an array.
	 */
	public static function parse_xml( $response ): string {
		// Remove namespace prefixes from xml tags so it can be decoded without SOAP namespace.
		$content = preg_replace( '/(<\/?)(?:[\w\-]+):([^>]*>)/', '$1$2$3', $response );
		unset( $response );
		$xml = new \SimpleXMLElement( $content, LIBXML_NOCDATA | LIBXML_DTDATTR | LIBXML_DTDLOAD | LIBXML_XINCLUDE | LIBXML_SCHEMA_CREATE | LIBXML_PARSEHUGE | LIBXML_NOERROR | LIBXML_NOWARNING );
		unset( $content );

		// Check if the XML is a SOAP response.
		$is_soap = self::is_soap_response( $xml );
		if ( $is_soap ) {
			$xml = $xml->xpath( '//Body' );
		}

		$json = Utils::safe_json_encode( (array) $xml );
		unset( $xml );
		unset( $is_soap );
		Sync_Utils::trigger_garbage_collection();
		return $json;
	}

	/**
	 * Check if the XML is a SOAP response.
	 *
	 * @param \SimpleXMLElement $xml XML element to check.
	 * @return bool True if it is a SOAP response, false otherwise.
	 */
	public static function is_soap_response( $xml ): bool {
		libxml_use_internal_errors( true );

		try {
			return $xml->getName() === 'Envelope';
		} catch ( \Exception $e ) {
			$error_msg = 'SOAP check error: ' . $e->getMessage();
			Supplier_Logger::debug( Sync_Details_DAO::get_instance()->get_supplier_id(), $error_msg );
			return false;
		}
	}
}
