<?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\Pagination;

use MOWCPS\Common\Constants\Constants;
use MOWCPS\Common\Utils\Model_Utils;
use MOWCPS\Common\Models\Sync_Details_DAO;
use MOWCPS\Common\Services\Logging\Supplier_Logger;
use MOWCPS\Common\Models\Setting_Model;
use MOWCPS\Common\Models\Product_Sync_Audit_Model;

/**
 * Class Pagination_Service
 *
 * @package MOWCPS\Common\Services\Pagination
 */
class Pagination_Service {

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

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

	/**
	 * Page number.
	 *
	 * @var int
	 */
	public $page_number;


	/**
	 * To tell if is batch process or not.
	 *
	 * @var string
	 */
	protected $batch_flag;

	/**
	 * Batch number of current sync process.
	 *
	 * @var int
	 */
	protected $batch_number = 1;

	/**
	 * Batch size of current sync process.
	 *
	 * @var integer
	 */
	protected $batch_size;

	/**
	 * Page size of current sync process.
	 *
	 * @var string
	 */
	protected $page_param;

	/**
	 * Pagination type of current sync process.
	 *
	 * @var string
	 */
	protected $pagination_type;

	/**
	 * Pagination location.
	 *
	 * @var string
	 */
	protected $pagination_location;

	/**
	 * Offset param of current sync process.
	 *
	 * @var string
	 */
	protected $offset_param;

	/**
	 * Offset value of current sync process.
	 *
	 * @var string
	 */
	protected $offset_value;

	/**
	 * Cursor parameter name.
	 *
	 * @var string
	 */
	protected $cursor_parameter_name;

	/**
	 * Next cursor key in response.
	 *
	 * @var string
	 */
	protected $next_cursor_key_response;

	/**
	 * Next cursor key location, Headers or Body.
	 *
	 * @var string
	 */
	protected $next_cursor_key_location;

	/**
	 * End pagination key name in response.
	 *
	 * @var string
	 */
	protected $end_pagination_key = Constants::HAS_MORE;

	/**
	 * Is next cursor a full URL?
	 *
	 * @var string
	 */
	protected $is_next_cursor_url;

	/**
	 * The format to use to extract next cursor URL.
	 *
	 * @var string
	 */
	protected $cursor_extraction_format;

	/**
	 * Limit param of current sync process.
	 *
	 * @var string
	 */
	protected $limit_param;

	/**
	 * Limit value of current sync process.
	 *
	 * @var string
	 */
	protected $limit_value;

	/**
	 * Constructor.
	 *
	 * @param int   $supplier_id   The supplier ID.
	 * @param int   $connection_id The connection ID.
	 * @param int   $page_number   The page number.
	 * @param array $page_configuration The page configuration.
	 */
	public function __construct( $supplier_id, $connection_id, $page_number, $page_configuration ) {
		$this->connection_id = $connection_id;
		$this->supplier_id   = $supplier_id;
		$this->page_number   = $page_number;

		$this->pagination_type     = $page_configuration['type'];
		$this->pagination_location = $page_configuration['location'];

		if ( Constants::OFFSET === $this->pagination_type ) {
			$this->offset_param = $page_configuration['offset_param'];
			$this->offset_value = $page_configuration['offset_value'];
			$this->limit_param  = $page_configuration['offset_limit_param'];
			$this->limit_value  = $page_configuration['offset_limit_value'];
		} elseif ( Constants::CURSOR === $this->pagination_type ) {
			$this->cursor_parameter_name    = $page_configuration['cursor_parameter'];
			$this->next_cursor_key_response = $page_configuration['next_cursor_key'];
			$this->next_cursor_key_location = $page_configuration['next_cursor_key_location'];
			$this->end_pagination_key       = $page_configuration['end_pagination_key'];
			$this->is_next_cursor_url       = $page_configuration['is_next_cursor_url'];
			$this->cursor_extraction_format = $page_configuration['cursor_extraction_format'];
		} else {
			$this->page_param  = $page_configuration['page_param'];
			$this->limit_param = $page_configuration['limit_param'];
			$this->limit_value = $page_configuration['limit_value'];
		}
	}

	/**
	 * Get custom details for pagination.
	 *
	 * @param array  $headers API response headers.
	 * @param array  $body    API response body.
	 * @param string $url     API request URL.
	 * @return mixed
	 */
	public function get_page_details( $headers, $body, $url, $page_details ) {

		$external_connection_config = $page_details;
		$configuration              = array(
			Constants::URL          => empty( $url ) ? ( $external_connection_config['endpoint'] ?? '' ) : $url,
			Constants::HEADERS      => empty( $headers ) ? ( $external_connection_config['headers'] ?? array() ) : $headers,
			Constants::REQUEST_BODY => empty( $body ) ? ( $external_connection_config['body'] ?? '' ) : $body,
		);

		switch ( $this->pagination_type ) {
			case Constants::REGULAR:
				$configuration = $this->handle_regular_pagination( $configuration );
				break;
			case Constants::OFFSET:
				$configuration = $this->handle_offset_pagination( $configuration );
				break;
			case Constants::CURSOR:
				$configuration = $this->handle_cursor_pagination( $configuration );
				break;
		}

		/**
		 * This filter position is to alter products URL in case of pagination completely.
		 *
		 * Additional args/details should be sent as an in 2nd parameter of apply filter.
		 * The filter should return the same structure as the 2nd parameter, so other filter potions are not affected by your add-on.
		 *
		 * $event_specifier   Event specifier for the filter.
		 * $config_url        URL configured in the External connection settings.
		 * $this->page_param  Same as Class DOC.
		 * $this->page_number Same as Class DOC.
		 * $next_url          URL to be used for next API Call
		 */
		if ( has_filter( 'mowcps_data_alteration_filter' ) ) {
			$returned_data        = apply_filters(
				'mowcps_data_alteration_filter',
				array(
					'event_specifier' => 'pagination_url_alter',
					'config_url'      => $configuration['url'],
					'page_param'      => $this->page_param,
					'page_number'     => $this->page_number,
					'returned_url'    => $configuration['url'],
				)
			);
			$configuration['url'] = $returned_data['returned_url'];
		}

		return $configuration;
	}

	/**
	 * Handle regular pagination.
	 *
	 * @param array $configuration The current connect configuration.
	 *
	 * @throws \Exception If error if pagination parameter is empty.
	 * @return array Updated configuration.
	 */
	private function handle_regular_pagination( $configuration ) {
		try {
			if ( empty( $this->page_param ) ) {
				throw new \Exception( Constants::EMPTY_PAGINATION_PARAMETER );
			}

			$params = array(
				$this->page_param  => $this->page_number,
				$this->limit_param => $this->limit_value,
			);

			return $this->get_next_page_from( $params, $configuration );

		} catch ( \Exception $e ) {
			$this->handle_failed_pagination( $e );
		}
	}

	/**
	 * Handle offset based pagination.
	 *
	 * @param array $configuration The current connect configuration.
	 *
	 * @throws \Exception If error if offset and offset limit parameter or value is empty.
	 * @return array Updated configuration.
	 */
	private function handle_offset_pagination( $configuration ) {
		try {
			if ( empty( $this->offset_param ) || ( empty( $this->offset_value ) && 0 !== (int) $this->offset_value ) ) {
				throw new \Exception( Constants::EMPTY_OFFSET_PARAMETER_AND_VALUE );
			}

			if ( empty( $this->limit_param ) || empty( $this->limit_value ) ) {
				throw new \Exception( Constants::EMPTY_OFFSET_LIMIT_PARAMETER_AND_VALUE );
			}

			$this->offset_value = $this->limit_value * ( $this->page_number - 1 );

			$params = array(
				$this->offset_param => $this->offset_value,
				$this->limit_param  => $this->limit_value,
			);

			return $this->get_next_page_from( $params, $configuration );

		} catch ( \Exception $e ) {
			$this->handle_failed_pagination( $e );
		}
	}

	/**
	 * Handle cursor based pagination.
	 *
	 * @param array $configuration The current connect configuration.
	 *
	 * @throws \Exception If any error occurs while fetching cursor value.
	 * @return array Updated configuration.
	 */
	private function handle_cursor_pagination( $configuration ) {
		try {
			if ( empty( $this->next_cursor_key_response ) ) {
				throw new \Exception( Constants::EMPTY_NEXT_CURSOR_KEY_RESPONSE );
			}

			if ( 1 < $this->page_number ) {

				$pagination_details = Model_Utils::get_option( $this->connection_id . '_page_details' );

				if ( is_wp_error( $pagination_details ) ) {
					throw new \Exception( Constants::FAILED_TO_GET_PAGINATION_DETAILS );
				}

				// Validate pagination details before JSON decoding.
				if ( empty( $pagination_details ) || ! is_string( $pagination_details ) ) {
					Supplier_Logger::debug( $this->supplier_id, 'Invalid pagination details - empty or not a string' );
					throw new \Exception( 'Invalid pagination details format' );
				}

				if ( Constants::REQUEST_BODY === $this->next_cursor_key_location ) {
					$pagination_details = json_decode( $pagination_details, true );

					// Check for JSON decoding errors.
					if ( json_last_error() !== JSON_ERROR_NONE ) {
						Supplier_Logger::debug( $this->supplier_id, 'JSON decoding failed for pagination details: ' . json_last_error_msg() );
						throw new \Exception( 'Failed to decode pagination response: ' . json_last_error_msg() );
					}

					if ( ! is_array( $pagination_details ) ) {
						Supplier_Logger::debug( $this->supplier_id, 'Decoded pagination details is not an array' );
						throw new \Exception( Constants::FAILED_TO_DECODE_RESPONSE_BODY );
					}

					$next_cursor_value = $pagination_details[ $this->next_cursor_key_response ] ?? Constants::MO_NO_OPERATION;
					$is_pagination_end = $pagination_details[ $this->end_pagination_key ] ?? false;
				} elseif ( Constants::REQUEST_HEADER === $this->next_cursor_key_location ) {
					$pagination_details = json_decode( $pagination_details, true );
					$next_cursor_value = $pagination_details[ strtolower( $this->next_cursor_key_response ) ] ?? Constants::MO_NO_OPERATION;
					$is_pagination_end = $pagination_details[ strtolower( $this->end_pagination_key ) ] ?? false;
				} else {
					throw new \Exception( Constants::INVALID_CURSOR_LOCATION );
				}

				if ( empty( $next_cursor_value ) && ! $is_pagination_end ) {
					throw new \Exception( Constants::INVALID_NEXT_CURSOR_VALUE );
				}

				if ( Constants::CONFIGURATION_ACTIVE === $this->is_next_cursor_url ) {
					$configuration = $this->get_next_cursor_url( $configuration, $next_cursor_value );
				} else {
					$params = array(
						$this->cursor_parameter_name => $next_cursor_value,
					);

					$configuration = $this->get_next_page_from( $params, $configuration );
				}
			}
		} catch ( \Exception $e ) {
			$this->handle_failed_pagination( $e );
		}

		return $configuration;
	}

	/**
	 * Get next page details from respective pagination location.
	 *
	 * @param array $parameters    Parameters to be used for next page.
	 * @param array $configuration Configuration details.
	 *
	 * @return array Next page details.
	 */
	private function get_next_page_from( $parameters, $configuration ) {
		if ( Constants::QUERY_PARAMETER === $this->pagination_location ) {
			$configuration[ Constants::URL ] = $this->get_pagination_query_params( $parameters, $configuration[ Constants::URL ] );
		} elseif ( Constants::REQUEST_HEADER === $this->pagination_location ) {
			$configuration[ Constants::HEADERS ] = $this->get_pagination_headers( $parameters, $configuration[ Constants::HEADERS ] );
		} elseif ( Constants::REQUEST_BODY === $this->pagination_location ) {
			$configuration[ Constants::REQUEST_BODY ] = $this->get_pagination_body( $parameters, $configuration[ Constants::REQUEST_BODY ] );
		}

		return $configuration;
	}

	/**
	 * Get the next page URL with new query parameters.
	 *
	 * @param array  $query_parameters Query parameters to be added to the URL.
	 * @param string $url              The request URL to be modified.
	 *
	 * @return string The modified URL with new query parameters.
	 */
	private function get_pagination_query_params( $query_parameters, $url ) {
		return add_query_arg( $query_parameters, $url );
	}

	/**
	 * Get the next page headers with new parameters.
	 *
	 * @param array $parameters Pagination parameters to be added to the headers.
	 * @param array $headers    The request headers to be modified.
	 *
	 * @return array The modified headers with new pagination parameters.
	 */
	private function get_pagination_headers( $parameters, $headers ) {
		return array_merge( $headers, $parameters );
	}

	/**
	 * Get the next page body with new parameters.
	 *
	 * @param array        $parameters Pagination parameters to be added to the body.
	 * @param array|string $body       The request body to be modified.
	 *
	 * @throws \Exception If any error occurs while fetching XML value.
	 * @return string The modified body with new pagination parameters.
	 */
	private function get_pagination_body( $parameters, $body ) {
		$body_format    = $body['type'];
		$extracted_body = $body['data'];

		// Try decoding as JSON.
		$decoded_body = json_decode( $extracted_body, true );

		if ( null === $decoded_body && null !== $extracted_body ) {
			// Handle as XML.
			try {
				libxml_use_internal_errors( true );
				$xml = simplexml_load_string( $extracted_body );

				if ( false === $xml ) {
					$errors = libxml_get_errors();
					libxml_clear_errors();

					$error_messages = array_map(
						function ( $error ) {
							return sprintf(
								'Line %d, Column %d: %s',
								$error->line,
								$error->column,
								trim( $error->message )
							);
						},
						$errors
					);

					throw new \Exception( implode( ' | ', $error_messages ) );
				}

				// Find <args> node, regardless of namespaces.
				$args_nodes = $xml->xpath( '//*[local-name()="args"]' );
				if ( $args_nodes && isset( $args_nodes[0] ) ) {
					$args = $args_nodes[0];

					// Update pagination values.
					foreach ( $parameters as $key => $value ) {
						if ( ! empty( $key ) && ! empty( $value ) ) {
							if ( isset( $args->$key ) ) {
								$args->$key = $value;
							} else {
								$args->addChild( $key, $value );
							}
						}
					}
				}

				$extracted_body = $xml->asXML();
				$extracted_body = html_entity_decode( $extracted_body );

				$body['type'] = $body_format;
				$body['data'] = $extracted_body;

				return $body;
			} catch ( \Exception $e ) {
				$this->handle_failed_pagination( $e );
			}
		}

		// Handle JSON.
		if ( is_array( $decoded_body ) ) {
			foreach ( $parameters as $key => $value ) {
				$decoded_body[ $key ] = $value;
			}

			if ( Constants::JSON === $body_format ) {
				$body = wp_json_encode( $decoded_body );
			} elseif ( Constants::X_WWW_FORM_URLENCODED === $body_format ) {
				$body = $decoded_body;
			}

			$body['type'] = $body_format;
			$body['data'] = $body;

			return $body;
		}

		$body['type'] = $body_format;
		$body['data'] = $extracted_body;

		return $body;
	}


	/**
	 * Get the next next page URL from next page cursor.
	 *
	 * @param array  $configuration The configuration array.
	 * @param string $next_cursor   The next page cursor value.
	 *
	 * @return array The updated configuration.
	 */
	private function get_next_cursor_url( $configuration, $next_cursor ) {
		if ( Constants::NONE === $this->cursor_extraction_format ) {
			$configuration['url'] = $next_cursor;
		} elseif ( Constants::REL_NEXT === $this->cursor_extraction_format ) {
			if ( false !== strpos( $next_cursor, 'rel="next"' ) ) {

				// Extract URL from rel="next" link.
				$next_cursor = explode( ',', $next_cursor );
				$next_cursor = end( $next_cursor );
				preg_match( '/<([^;>]*)>/', $next_cursor, $matches );

				if ( 1 < $this->page_number && ! empty( $matches[1] ) ) {
					$configuration['url'] = trim( $matches[1] );
				}
			} else {
				// If next URL does not exist, then set next URL to MO_NO_OPERATION so pagination can end.
				$configuration['url'] = Constants::MO_NO_OPERATION;
			}
		} elseif ( 'custom' === $this->cursor_extraction_format ) {
			// TODO: [RESTAPI-218] - Add support for custom cursor extraction format in pagination.
		}

		return $configuration;
	}

	/**
	 * Abort sync if any exception is thrown during pagination.
	 *
	 * @param \Exception $error The exception.
	 * @return void
	 */
	private function handle_failed_pagination( $error ) {
		$schedule_id = Sync_Details_DAO::get_instance()->get_schedule_id();

		// Log detailed pagination error
		Supplier_Logger::debug( $this->supplier_id, 'Pagination error occurred - sync aborted' );
		Supplier_Logger::debug( $this->supplier_id, 'Connection ID: ' . $this->connection_id );
		Supplier_Logger::debug( $this->supplier_id, 'Pagination type: ' . $this->pagination_type );
		Supplier_Logger::debug( $this->supplier_id, 'Current page: ' . $this->page_number );
		Supplier_Logger::debug( $this->supplier_id, 'Error details: ' . $error->getMessage() );
		Supplier_Logger::debug( $this->supplier_id, 'Check your pagination configuration settings' );

		// Update sync status
		Product_Sync_Audit_Model::update_sync_running_status( $this->supplier_id, $schedule_id, Constants::SYNC_RUNNING_STATUS_OFF );
		$connection_sync_analytics                      = Sync_Details_DAO::get_instance()->get_connection_analytics( $this->supplier_id, $schedule_id, $this->connection_id );
		$connection_sync_analytics->sync_status_message = Constants::SYNC_ABORTED;
		$connection_sync_analytics->save();
		$sync_analytics                      = Sync_Details_DAO::get_instance()->get_connection_analytics( $this->supplier_id, $schedule_id, $this->connection_id );
		$sync_analytics->sync_status_message = Constants::SYNC_ABORTED;
		$sync_analytics->save();
	}
}
