<?php
/**
 * Swish API class.
 *
 * @package flo-checkout
 *
 * cURL offers more options than wp_remote_post, so we use cURL instead.
 * TODO: Maybe refactor to use wp_remote_post instead, if possible?
 *
 * @phpcs:disable WordPress.WP.AlternativeFunctions.curl_curl_init
 * @phpcs:disable WordPress.WP.AlternativeFunctions.curl_curl_setopt
 * @phpcs:disable WordPress.WP.AlternativeFunctions.curl_curl_exec
 * @phpcs:disable WordPress.WP.AlternativeFunctions.curl_curl_error
 * @phpcs:disable WordPress.WP.AlternativeFunctions.curl_curl_getinfo
 * @phpcs:disable WordPress.WP.AlternativeFunctions.curl_curl_close
 */

namespace Flo_Checkout\Gateways\Swish;

use Exception;
use Flo_Checkout\Shared;
use Northmill_Online_Shared_Checkout\Northmill_Online_Shared\Modules\Loggers\Log_Level_Enum;

/**
 * Swish API class.
 */
class Swish_API {

	/**
	 * Send an API request
	 *
	 * @param array  $request_data The payer alias.
	 * @param string $path         The API endpoint path.
	 * @param string $type         (Optional) The request type (defaults to 'POST').
	 * @param string $version      (Optional) The API version (defaults to 'v1').
	 *
	 * @return array Returns an array containing the response from the API
	 */
	public static function api_request( array $request_data, string $path, string $type = 'POST', string $version = 'v1' ): array {

		$end_point   = self::get_endpoint( $path, $version );
		$data_string = wp_json_encode( $request_data, JSON_UNESCAPED_SLASHES );

		return self::send_curl_request(
			Swish_Settings::get_swish_setting( 'root_certificate' ),
			Swish_Settings::get_swish_setting( 'public_certificate' ),
			Swish_Settings::get_swish_setting( 'private_certificate' ),
			Swish_Settings::get_swish_setting( 'private_key' ),
			$end_point,
			$data_string,
			$type
		);
	}

	/**
	 * Get the endpoint URL for the given path
	 *
	 * @param string $path The path to append to the endpoint URL.
	 * @param string $version The version of the API to use.
	 *
	 * @return string The complete URL of the endpoint.
	 */
	private static function get_endpoint( string $path, string $version ): string {
		return 'https://' . Swish_Settings::get_swish_setting( 'api_end_url' ) . "/swish-cpcapi/api/$version/$path";
	}


	/**
	 * Path to the root certificate.
	 *
	 * @var string
	 */
	private static string $root_certificate;

	/**
	 * Path to the public certificate.
	 *
	 * @var string
	 */
	private static string $public_certificate;

	/**
	 * The extension of the public certificate.
	 *
	 * @var string
	 */
	private static string $public_certificate_extension;

	/**
	 * Path to the private certificate.
	 *
	 * @var string
	 */
	private static string $private_certificate;

	/**
	 * The private key.
	 *
	 * @var string
	 */
	private static string $private_certificate_password;

	/**
	 * The URL of the endpoint to send the request to.
	 *
	 * @var string
	 */
	private static string $end_point;

	/**
	 * The type of request (for example "POST" or "PATCH").
	 *
	 * @var string
	 */
	private static string $type;

	/**
	 * Send a POST request to a specified endpoint with the given data string.
	 *
	 * @param mixed  $root_certificate             The path to the root certificate.
	 * @param mixed  $public_certificate           The path to the public certificate.
	 * @param mixed  $private_certificate          The path to the private certificate.
	 * @param mixed  $private_certificate_password The private key.
	 * @param string $end_point                    The URL of the endpoint to send the request to.
	 * @param string $data_string                  The data string to send in the request body.
	 * @param string $type                         The type of request (for example "POST" or "PATCH").
	 *
	 * @return array  An array with the following keys:
	 *                - status: A boolean indicating if the request was successful or not.
	 *                - response: A string with the response message.
	 *                - content: The content of the response.
	 *                - response_technical: A string with the technical information of the response.
	 */
	public static function send_curl_request(
		mixed $root_certificate,
		mixed $public_certificate,
		mixed $private_certificate,
		mixed $private_certificate_password,
		string $end_point,
		string $data_string,
		string $type
	): array {
		$extension = strtoupper( pathinfo( $public_certificate, PATHINFO_EXTENSION ) );

		self::$root_certificate             = (string) $root_certificate;
		self::$public_certificate           = (string) $public_certificate;
		self::$public_certificate_extension = $extension;
		self::$private_certificate          = (string) $private_certificate;
		self::$private_certificate_password = (string) $private_certificate_password;
		self::$end_point                    = $end_point;
		self::$type                         = $type;

		$cert_check_valid = self::verify_certificate();

		if ( true !== $cert_check_valid ) {
			Swish_Settings::log( 'Swish certificate error', array( 'result' => $cert_check_valid ), Log_Level_Enum::WARNING );

			return array(
				'status'             => false,
				'response'           => __( 'Could not connect to Swish.', 'flo-checkout-plugin' ),
				'response_technical' => $cert_check_valid,
			);
		}

		$ch = curl_init();

		curl_setopt( $ch, CURLOPT_HEADER, true );
		curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true );

		curl_setopt( $ch, CURLOPT_SSL_VERIFYHOST, 2 );
		curl_setopt( $ch, CURLOPT_SSL_VERIFYPEER, true );
		curl_setopt( $ch, CURLINFO_HEADER_OUT, true );

		curl_setopt( $ch, CURLOPT_CAINFO, $root_certificate );
		curl_setopt( $ch, CURLOPT_SSLCERT, $public_certificate );
		curl_setopt( $ch, CURLOPT_URL, $end_point );
		curl_setopt( $ch, CURLOPT_CUSTOMREQUEST, $type );
		curl_setopt( $ch, CURLOPT_POSTFIELDS, $data_string );

		if ( 'P12' === $extension ) {
			curl_setopt( $ch, CURLOPT_SSLCERTTYPE, 'P12' );
			curl_setopt( $ch, CURLOPT_SSLKEYPASSWD, $private_certificate_password );

			if ( ! $private_certificate_password ) {
				Swish_Settings::log( 'Private key not set for p12', null, Log_Level_Enum::WARNING );
			}
		} elseif ( 'PEM' === $extension ) {
			curl_setopt( $ch, CURLOPT_SSLKEY, $private_certificate );
			if ( $private_certificate_password ) {
				curl_setopt( $ch, CURLOPT_SSLKEYPASSWD, $private_certificate_password );
			}
		}

		if ( 'POST' === $type ) {
			curl_setopt( $ch, CURLOPT_POST, true );
		}

		$content_type = 'application/json';
		if ( 'PATCH' === $type ) {
			$content_type = 'application/json-patch+json';
		}

		curl_setopt(
			$ch,
			CURLOPT_HTTPHEADER,
			array(
				'Content-Type: ' . $content_type,
				'Content-Length: ' . strlen( $data_string ),
			)
		);

		$curl_response = curl_exec( $ch );
		$curl_error    = curl_error( $ch );
		$response_code = (int) curl_getinfo( $ch, CURLINFO_HTTP_CODE );

		$header_size = curl_getinfo( $ch, CURLINFO_HEADER_SIZE );

		$result           = array();
		$result['header'] = substr( $curl_response, 0, $header_size );
		$result['body']   = substr( $curl_response, $header_size );

		curl_close( $ch );

		if ( $curl_response ) {
			if ( $response_code >= 200 && $response_code <= 299 ) {
				preg_match( '/Location:(.*?)\n/', $result['header'], $matches );
				$location = trim( array_pop( $matches ) );
				Swish_Settings::log( 'Swish request successful', array( 'location' => $location ) );
				return array(
					'status'   => true,
					'response' => $location,
					'content'  => $curl_response,
				);
			} elseif ( $response_code >= 400 && $response_code <= 499 ) {
				$errors = json_decode( $result['body'] );

				if ( is_array( $errors ) ) {
					$errors = $errors[0];
				}

				$response           = null;
				$response_technical = null;

				if ( ! empty( $errors->errorCode ) ) {
					$response           = Swish_Errors::get_message_by_code( $errors->errorCode );
					$response_technical = $errors->errorCode . ' - ' . $response;
				} elseif ( ! empty( $errors->error ) ) {
					$response_technical = $errors->error;
				}

				return self::request_error(
					$response_technical,
					array(
						'response_code' => $response_code,
						'full_response' => $result,
					),
					$response
				);
			} else {
				return self::request_error(
					'Unknown response code',
					array(
						'response_code' => $response_code,
					)
				);
			}
		}

		return self::request_error(
			$curl_error,
			array(
				'response_code' => $response_code,
			)
		);
	}

	/**
	 * Handle errors from the API request
	 *
	 * @param string|null $response_technical The technical response from the API.
	 * @param array       $extras             (Optional) Extra data to log.
	 * @param string|null $response           (Optional) The response message.
	 *
	 * @return array An array with the following keys:
	 *               - status: A boolean indicating if the request was successful or not.
	 *               - response: A string with the response message.
	 *               - response_technical: A string with the technical information of the response.
	 */
	private static function request_error(
		string|null $response_technical,
		array $extras = array(),
		string|null $response = null,
	): array {
		$response = $response ?? __( 'Could not connect to Swish.', 'flo-checkout-plugin' );
		$error    = array(
			'status'             => false,
			'response'           => $response,
			'response_technical' => $response_technical,
		);

		$extras['response']           = $response;
		$extras['response_technical'] = $response_technical;
		$extras['end_point']          = self::$end_point;
		$extras['type']               = self::$type;

		if ( Swish_Settings::is_setting_enabled( 'log_enabled' ) ) {
			self::debug_ssl_environment(
				$extras,
			);
		} else {
			Swish_Settings::log(
				'Swish Error',
				$extras,
				Log_Level_Enum::ERROR
			);
		}

		return $error;
	}


	/**
	 * Verify a certificate
	 *
	 * @return true|string Returns true if the certificate is valid, otherwise returns an error message
	 */
	private static function verify_certificate(): true|string {

		if ( ! file_exists( self::$root_certificate ) ) {
			return 'Root certificate not found: ' . self::$root_certificate;

		} elseif ( ! file_exists( self::$public_certificate ) ) {
			return 'Public certificate not found: ' . self::$public_certificate;

		} elseif ( 'P12' === self::$public_certificate_extension ) {
			// phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
			$p12file           = file_get_contents( self::$public_certificate ); // Public and private key in one file.
			$certs             = array();
			$cert_parse_result = openssl_pkcs12_read( $p12file, $certs, self::$private_certificate_password );
			if ( ! $cert_parse_result ) {
				return 'Certificate parse failed with error: ' . openssl_error_string();
			} else {
				return true;
			}
		} elseif ( 'PEM' === self::$public_certificate_extension ) {
			if ( ! file_exists( self::$private_certificate ) ) {
				return 'Private key not found: ' . self::$private_certificate;
			}

			// TODO: Write working test for PEM file.
			return true;
		} else {
			// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r
			return 'Unknown certificate extension.' . print_r( pathinfo( self::$public_certificate ), true );
		}
	}

	/**
	 * Debugging function to check SSL environment, file existence, and configuration.
	 *
	 * @param array $debug_log Any additional data to log.
	 */
	private static function debug_ssl_environment( array $debug_log ): void {
		$debug_log = array_merge( $debug_log, self::get_debug_ssl_env() );

		Swish_Settings::log(
			'Swish Error environment debug',
			$debug_log,
			Log_Level_Enum::ERROR
		);
	}

	/**
	 * Get SSL Debug data.
	 *
	 * @phpcs:disable WordPress.PHP.DiscouragedPHPFunctions.system_calls_shell_exec
	 * @phpcs:disable WordPress.WP.AlternativeFunctions.file_system_operations_fopen
	 * @phpcs:disable WordPress.WP.AlternativeFunctions.file_system_operations_fclose
	 * TODO: We should implement testing the password.
	 *
	 * @return array
	 */
	public static function get_debug_ssl_env(): array {
		if ( ! isset( self::$root_certificate ) ) {
			self::$root_certificate             = (string) Swish_Settings::get_swish_setting( 'root_certificate' );
			self::$public_certificate           = (string) Swish_Settings::get_swish_setting( 'public_certificate' );
			$extension                          = strtoupper( pathinfo( self::$public_certificate, PATHINFO_EXTENSION ) );
			self::$public_certificate_extension = $extension;
			self::$private_certificate          = (string) Swish_Settings::get_swish_setting( 'private_certificate' );
			self::$private_certificate_password = (string) Swish_Settings::get_swish_setting( 'private_key' );
			$req_id                             = Swish_Request_IDs::generate();
			self::$end_point                    = self::get_endpoint( "paymentrequests/$req_id", 'v2' );
			self::$type                         = 'PUT';
		}

		$debug_data = array();

		$debug_data['swish_env']   = Swish_Settings::get_swish_setting( 'swish_environment' );
		$debug_data['swish_payee'] = Swish_Settings::get_swish_setting( 'payee_number' );
		// Log PHP Version.
		$debug_data['php_version'] = PHP_VERSION;

		$debug_data['wp_version']  = get_bloginfo( 'version' );
		$debug_data['wc_version']  = defined( 'WC_VERSION' ) ? constant( 'WC_VERSION' ) : 'WooCommerce not active';
		$debug_data['nmc_version'] = Shared::instance()->package_info->package_data->get_version();

		// Log OpenSSL Details.
		$debug_data['openssl_version'] = defined( 'OPENSSL_VERSION_TEXT' ) ? OPENSSL_VERSION_TEXT : 'OpenSSL not available in PHP';
		$debug_data['openssl_support'] = function_exists( 'openssl_x509_parse' ) ? 'Enabled' : 'Disabled';

		// Log PHP cURL Information.
		if ( function_exists( 'curl_version' ) ) {
			$curl_info                             = curl_version();
			$debug_data['curl_version']            = $curl_info['version'];
			$debug_data['ssl_version']             = $curl_info['ssl_version'];
			$debug_data['ssl_supported_protocols'] = implode( ', ', $curl_info['protocols'] );
		} else {
			$debug_data['curl_version'] = 'cURL not available';
		}

		// Check File Existence and Permissions.
		$files_to_check = array(
			'root_certificate'    => self::$root_certificate,
			'public_certificate'  => self::$public_certificate,
			'private_certificate' => self::$private_certificate,
		);

		foreach ( $files_to_check as $type => $file_path ) {
			if ( file_exists( $file_path ) ) {
				$debug_data[ "{$type}_exists" ]      = 'Yes';
				$debug_data[ "{$type}_readable" ]    = is_readable( $file_path ) ? 'Yes' : 'No';
				$debug_data[ "{$type}_permissions" ] = substr( sprintf( '%o', fileperms( $file_path ) ), - 4 );
				$debug_data[ "{$type}_owner" ]       = fileowner( $file_path );
			} else {
				$debug_data[ "{$type}_exists" ] = 'No';
			}

			$debug_data[ "{$type}_path" ] = $file_path;
		}

		// Check Certificate Expiry.
		$public_cert_expiry = shell_exec(
			'openssl x509 -enddate -noout -in ' . escapeshellarg( self::$public_certificate )
		);
		$root_cert_expiry   = shell_exec(
			'openssl x509 -enddate -noout -in ' . escapeshellarg( self::$root_certificate )
		);

		$debug_data['public_cert_expiry'] = $public_cert_expiry ? trim( $public_cert_expiry ) : 'Error retrieving public certificate expiry date';
		$debug_data['root_cert_expiry']   = $root_cert_expiry ? trim( $root_cert_expiry ) : 'Error retrieving root certificate expiry date';

		// Validate Intermediate Certificates.
		$intermediate_cert_check                 = shell_exec(
			'openssl s_client -connect ' . escapeshellarg( wp_parse_url( self::$end_point, PHP_URL_HOST ) . ':443' ) . ' -showcerts </dev/null 2>/dev/null'
		);
		$debug_data['intermediate_certificates'] = $intermediate_cert_check ? $intermediate_cert_check : 'Error retrieving intermediate certificates';

		// Log Available Ciphers.
		$available_ciphers                       = shell_exec( 'openssl ciphers -v' );
		$debug_data['openssl_available_ciphers'] = $available_ciphers ? trim( $available_ciphers ) : 'Error: Could not retrieve available ciphers';

		// Verify Hostname Matching.
		$hostname_verification             = shell_exec(
			'openssl s_client -servername ' . escapeshellarg( wp_parse_url( self::$end_point, PHP_URL_HOST ) ) .
			' -connect ' . escapeshellarg( wp_parse_url( self::$end_point, PHP_URL_HOST ) . ':443' ) . ' </dev/null 2>/dev/null | openssl x509 -noout -subject'
		);
		$debug_data['hostname_validation'] = $hostname_verification ? trim( $hostname_verification ) : 'Hostname validation failed';

		// Validate Public/Private Key Pair.
		if ( file_exists( self::$public_certificate ) && file_exists( self::$private_certificate ) ) {
			$public_cert_modulus                = shell_exec( 'openssl x509 -noout -modulus -in ' . escapeshellarg( self::$public_certificate ) . ' | openssl md5' );
			$private_key_modulus                = shell_exec( 'openssl rsa -noout -modulus -in ' . escapeshellarg( self::$private_certificate ) . ' | openssl md5' );
			$debug_data['public_cert_modulus']  = $public_cert_modulus ? trim( $public_cert_modulus ) : 'Error retrieving modulus';
			$debug_data['private_cert_modulus'] = $private_key_modulus ? trim( $private_key_modulus ) : 'Error retrieving modulus';
			$debug_data['key_match']            = $public_cert_modulus === $private_key_modulus ? 'Yes' : 'No';
		} else {
			$debug_data['key_match'] = 'Skipped (files missing)';
		}

		// Check System Time.
		$debug_data['system_time'] = gmdate( 'Y-m-d H:i:s' );
		$debug_data['timezone']    = date_default_timezone_get();

		// Test cURL with OpenSSL Configuration.
		try {
			if ( function_exists( 'curl_init' ) ) {
				$ch = curl_init();
				curl_setopt( $ch, CURLOPT_URL, self::$end_point );
				curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true );
				curl_setopt( $ch, CURLOPT_CAINFO, self::$root_certificate );
				curl_setopt( $ch, CURLOPT_SSLCERT, self::$public_certificate );
				curl_setopt( $ch, CURLOPT_SSLKEY, self::$private_certificate );
				curl_setopt( $ch, CURLOPT_HEADER, true );

				// Enable verbose cURL output.
				curl_setopt( $ch, CURLOPT_VERBOSE, true );
				$verbose = fopen( 'php://temp', 'w+' );
				curl_setopt( $ch, CURLOPT_STDERR, $verbose );

				$test_response   = curl_exec( $ch );
				$test_curl_error = curl_error( $ch ); // Store any cURL errors.
				curl_close( $ch );

				rewind( $verbose );
				$debug_data['curl_verbose_output'] = stream_get_contents( $verbose );
				fclose( $verbose );

				$debug_data['curl_test'] = empty( $test_curl_error ) ? 'cURL request succeeded' : ( 'cURL error: ' . $test_curl_error );
			} else {
				$debug_data['curl_test'] = 'cURL function not available in PHP';
			}
		} catch ( Exception $e ) {
			$debug_data['curl_test'] = 'cURL error: ' . $e->getMessage();
		}

		return $debug_data;
	}
	// phpcs:enable WordPress.PHP.DiscouragedPHPFunctions.system_calls_shell_exec
	// phpcs:enable WordPress.WP.AlternativeFunctions.file_system_operations_fopen
	// phpcs:enable WordPress.WP.AlternativeFunctions.file_system_operations_fclose
}
