<?php

namespace Simplyin\Simplyin_W_Plugin\Payments\Gateway;

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

use Exception;
use Simplyin\Simplyin_W_Plugin\Helper\Secret_Cipher;
use Simplyin\Simplyin_W_Plugin\Payments\Admin\Admin_Options;
use Simplyin\Simplyin_W_Plugin\Payments\Ajax\Payment_Methods\Payment_Methods_Ajax_Handler;
use Simplyin\Simplyin_W_Plugin\Payments\Gateway\Model\Order_Data;
use Simplyin\Simplyin_W_Plugin\Payments\Gateway\Model\Payment_Type_Interface;
use Simplyin\Simplyin_W_Plugin\Payments\Gateway\Model\Session_Data;
use Simplyin\Simplyin_W_Plugin\Payments\Gateway\Payment_Fields\Payment_Fields;
use Simplyin\Simplyin_W_Plugin\Payments\Gateway\Status\Payment_Status;
use WC_Order;
use WC_Payment_Gateway;


class Simplyin_Gateway extends WC_Payment_Gateway {

	/**
	 * WooCommerce gateway id — single source of truth; also consumed by
	 * Payment_Method_Label to detect Simply.IN-paid orders.
	 */
	public const GATEWAY_ID = 'simplyin';

	private static string $payment_fields_cache = '';

	private Admin_Options $admin_options;
	protected Order_Data $order_data;

	/**
	 *
	 * @throws Exception
	 */
	public function __construct() {
		$this->id           = self::GATEWAY_ID;
		$this->icon
		                    = simplyin()->get_plugin_url() . 'public/img/logo_smalln.svg';
		$this->has_fields
		                    = true;
		$this->method_title = __( 'Simply.IN',
			'simplyin' );
		$this->method_description
		                    = __( 'Boosts conversion with on-page payments — cards, BLIK, and a seamless 1-click experience without leaving the store.',
			'simplyin' );
		$this->supports     = [
			'products',
		];


		$this->title       = __( 'Simple and Safe payments',
			'simplyin' );
		$this->description = '';
		$this->enabled     = $this->get_option( 'enabled' );

		$this->admin_options = new Admin_Options();
		$this->init_form_fields();
		$this->init_settings();


		add_action( 'woocommerce_update_options_payment_gateways_' . $this->id,
			[ $this, 'process_admin_options' ] );

		add_action( 'woocommerce_before_thankyou',
			[ $this, 'before_thankyou_actions' ]
			,
			1 );


		$this->webhook();

		$this->register_ajax_hooks();
	}

	public function before_thankyou_actions( $order_id ): void {
		global $wp;

		if ( empty( $order_id ) ) {
			$this->log_error(
				'thankyou_guard',
				'Empty $order_id on woocommerce_before_thankyou',
				null,
				[
					'hook'        => 'woocommerce_before_thankyou',
					'request_uri' => Gateway_Helper::get_current_url(),
					'get'         => $wp->query_vars,
				]
			);

			return;
		}

		$order = wc_get_order( $order_id );
		if ( ! $order instanceof \WC_Order ) {
			$this->log_error(
				'thankyou_guard',
				'wc_get_order() did not return WC_Order',
				$order_id,
				[
					'hook'        => 'woocommerce_before_thankyou',
					'request_uri' => Gateway_Helper::get_current_url(),
				]
			);

			return;
		}

		$order_pm = $order->get_payment_method();
		if ( $order->get_payment_method() !== $this->id ) {
			$this->log(
				'thankyou_guard',
				$order_id,
				'Skip: payment method does not match gateway',
				[
					'order_payment_method' => $order_pm,
					'gateway_id'           => $this->id,
				]
			);

			return;
		}

		try {
			$this->order_received_page_action( $order );
		} catch ( Exception $error ) {
			$this->log_error(
				'thankyou_guard',
				'order_received_page_action exception: ' . $error->getMessage(),
				$order_id,
				[
					'trace'       => $error->getTraceAsString(),
					'request_uri' => Gateway_Helper::get_current_url(),
					'get'         => $wp->query_vars,
				]
			);
		}
	}

	public function get_icon() {
		$payment_fields = new Payment_Fields();
		$payment_fields->reload();
		$session_data = new Session_Data();
		$provider     = $session_data->get_payment_provider();
		if ( null === $provider ) {
			return parent::get_icon();
		}

		if ( ! $provider->get_logo_url() ) {
			return $this->icon
				? '<img class="simplyin-payment-provider-logo" src="' . esc_url( $this->icon ) . '" alt="Simply.IN">'
				: '';
		}

		ob_start();
		simplyin()->locate_template( 'gateway_icon.php',
			[
				'main_icon' => $this->icon,
				'provider'  => $provider,
			],
			'src/Payments/views/classic_checkout' );

		$output = ob_get_contents();

		ob_end_clean();

		return $output;
	}


	private function register_ajax_hooks() {
		$payment_methods_ajax_handler = new Payment_Methods_Ajax_Handler();
		$payment_methods_ajax_handler->register_ajax_hooks();
	}

	public function init_form_fields() {

		$this->form_fields = $this->admin_options->get_form_config();

	}

	public function admin_options() {
		$this->admin_options->render(
			$this->generate_settings_html( $this->get_form_fields(), false )
		);
	}


	public function is_available() {
		if ( ! is_admin() && empty( self::$payment_fields_cache ) ) {

			ob_start();
			$this->payment_fields();

			self::$payment_fields_cache = ob_get_contents();
			ob_end_clean();

			if ( empty( self::$payment_fields_cache ) ) {
				return false;

			}
		}

		return parent::is_available();
	}

	/**
	 * @return void
	 * @throws Exception
	 */
	public function payment_fields() {
		if ( empty( self::$payment_fields_cache ) ) {
			try {
				$service = ( new Payment_Fields() );
				$service->render_payment_fields_checkout();
			} catch ( Exception $exception ) {

				$this->log_error( 'payment_fields',
					$exception->getMessage(),
					null );

				return;
			}


		}
		// phpcs:disable
		// Output payment fields template
		echo self::$payment_fields_cache;
		// phpcs:enable
	}

	private function perform_post_process_cleaning() {
		if ( ! WC()->session || ! WC()->cart ) {
			return;
		}
		wc()->session->set( 'store_api_draft_order', 0 );
		WC()->cart->empty_cart();
		WC()->session->save_data();
	}

	/**
	 * @param $order_id
	 *
	 * @return array
	 * @throws Exception
	 */
	public function process_payment( $order_id ): array {
		$order = wc_get_order( $order_id );
		if ( ! $order ) {
			return [ 'result' => 'failure' ];
		}
		$this->order_data = new Order_Data( $order );

		if ( isset( $_POST['simplyin_checkout_token'] ) && ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['simplyin_checkout_token'] ) ),
				'simplyin_checkout_nonce' ) ) {
			$this->log_error( 'process_payment',
				'wp_verify_nonce classic checkout failed ',
				$order_id );

			return [ 'status' => 'failure' ];

		} elseif ( isset( $_POST['simplyin_blocks_token'] ) && ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['simplyin_blocks_token'] ) ),
				'simplyin_blocks_nonce' ) ) {
			$this->log_error( 'process_payment',
				'wp_verify_nonce block checkout failed ',
				$order_id );

			return [ 'status' => 'failure' ];

		} elseif ( ! isset( $_POST['simplyin_checkout_token'] ) && ! isset( $_POST['simplyin_blocks_token'] ) ) {
			$this->log_error( 'process_payment',
				'Nonce not found',
				$order_id );

			return [ 'status' => 'failure' ];
		}


		if ( isset( $_POST['simplyin-payment-option'] ) ) {
			$payment_channel_id = sanitize_text_field( wp_unslash( $_POST['simplyin-payment-option'] ) );
		} elseif ( isset( $_POST['simplyin_payment_option_id'] ) ) {//blocks
			$payment_channel_id = sanitize_text_field( wp_unslash( $_POST['simplyin_payment_option_id'] ) );
		} else {

			$this->log_error( 'process_payment',
				'Missing payment_channel_id',
				$order_id );

			return [ 'status' => 'failure' ];

		}


		$session_data = new Session_Data();
		try {
			$payment_method_model = $session_data->get_payment_method_info_by_id( $payment_channel_id );
		} catch ( \Throwable $e ) {
			$this->log_error( 'process_payment',
				'Payment method not found or cache missing: ' . $e->getMessage(),
				$order_id );

			return [ 'status' => 'failure' ];
		}


		if ( $payment_method_model instanceof Payment_Type_Interface ) {
			try {
				$payment_method_model->configure_on_checkout();
			} catch ( Exception $exception ) {
				$this->log_error( 'configure_on_checkout',
					$order_id,
					$exception->getMessage() );

				return [
					'status' => 'failure',
				];
			}
		}

		$this->order_data->update_selected_payment_method( $payment_method_model );

		$url       = $order->get_checkout_order_received_url();
		$to_return = [
			'result'   => 'success',
			'redirect' => $url,
		];


		$this->log( 'process_payment',
			$order_id,
			'',
			[
				'payment_method_model' => $payment_method_model,
			]
		);

		// If this is a retry on a previously failed order, reset to pending before attempting payment.
		if ( 'failed' === $order->get_status() ) {
			$order->update_status( 'pending' );
			$order->save();
		}

		try {
			$payment_method_model->process_payment( $this->order_data );
			$this->perform_post_process_cleaning();
		} catch ( Exception $exception ) {
			$this->log_error( 'process_payment failed',
				$exception->getMessage(),
				$order_id );

			$this->order_data->update_order_status( 'failed', sprintf( 'Simply.IN payment failed with method: %s', $payment_method_model->get_name() ) );
			$order->save();

			return [ 'result' => 'failure' ];
		}

		return $to_return;
	}

	public function order_received_page_action( WC_Order $order ): void {
		$order_id         = $order->get_id();
		$this->order_data = new Order_Data( $order );

		if ( ! $this->order_data->has_order_received_flag() ) {
			$this->order_data->add_order_received_flag();
		}


		$serialized_method = (string) $this->order_data->get_from_order_meta( '_simplyin_selected_payment_method' );
		if ( empty( $serialized_method ) ) {
			$this->log_error( 'order_received_page_action',
				'Missing _simplyin_selected_payment_method meta',
				$order_id );

			return;
		}

		try {
			$selected_payment_method = $this->order_data->get_selected_payment_method();
		} catch ( Exception $e ) {
			$this->log_error( 'order_received_page_action',
				'Failed to deserialize selected payment method: ' . $e->getMessage(),
				$order_id );

			return;
		}

		if ( $selected_payment_method instanceof Payment_Type_Interface ) {
			$selected_payment_method->order_received_actions( $this->order_data );
		}

		$this->log( 'order_received_page_action',
			$order_id,
			'',
			[
				'selected_payment_method' => $selected_payment_method,
				'payment_uuid'            => $selected_payment_method->get_payment_uuid(),
				'payment_status'          => $selected_payment_method->get_payment_status(),
				'provider_payment_id'     => $selected_payment_method->get_provider_payment_id(),
				'registered_at'           => $selected_payment_method->get_registered_at(),
			]
		);

	}


	private function log_error(
		string $action,
		string $message,
		$order_id,
		array $args = []
	) {

		$this->log( $action, $order_id, $message, $args, true );

	}

	private function log(
		string $action,
		$order_id,
		string $message = '',
		array $args = [],
		bool $error = false
	) {

		if ( $order_id ) {
			$args['order_id'] = $order_id;
		}

		$content = sprintf( "[%s] [%s] [%s]",
			$action,
			$message,
			wp_json_encode( $args ),
		);

		$woo_logger = simplyin()
			->get_woocommerce_logger( 'payments' );

		if ( $error ) {
			$woo_logger->log_error(
				$content );
		} else {
			$woo_logger->log_debug(
				$content );
		}
	}

	/**
	 * Verify the incoming payment webhook against the per-shop HMAC secret.
	 *
	 * Checks a signed request header against the stored secret and rejects stale requests
	 * (replay guard). The secret is synced from plugin-info into
	 * `simplyin_plugin_payment_webhook_secret`.
	 * Returns false when either header is missing, when the timestamp is stale, or on
	 * signature mismatch. Whether a failed check rejects the request is decided
	 * by the caller, which only enforces once the secret has been synced (see webhook()).
	 *
	 * The signing/verification scheme is a fixed cross-service contract shared with the
	 * backend and the other Simply.IN plugins — do not change the header names, the signed
	 * string format, or the replay window independently of the backend.
	 */
	private function verify_webhook_signature( string $raw_body, ?string $secret = null ): bool {
		// Decrypt on demand only when the caller did not already (e.g. unit tests). webhook()
		// passes the already-decrypted secret so we do not repeat the decryption work per
		// call.
		if ( null === $secret ) {
			$secret = Secret_Cipher::decrypt( (string) get_option( 'simplyin_plugin_payment_webhook_secret', '' ) );
		}
		if ( '' === $secret ) {
			return false;
		}

		// These are cryptographic header values, not display text: read them raw
		// (trim + wp_unslash) and validate by strict format below. sanitize_text_field()
		// is the wrong tool — it strips characters and would run BEFORE the format checks,
		// validating an already-mutated value rather than the signed bytes.
		$signature = isset( $_SERVER['HTTP_X_SIMPLYIN_WEBHOOK_SIGNATURE'] )
			? trim( (string) wp_unslash( $_SERVER['HTTP_X_SIMPLYIN_WEBHOOK_SIGNATURE'] ) ) // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- validated by strict hex/length format checks below, not sanitize_text_field() (see comment above).
			: '';
		$timestamp = isset( $_SERVER['HTTP_X_SIMPLYIN_WEBHOOK_TIMESTAMP'] )
			? trim( (string) wp_unslash( $_SERVER['HTTP_X_SIMPLYIN_WEBHOOK_TIMESTAMP'] ) ) // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- validated as a Unix timestamp below, not sanitize_text_field() (see comment above).
			: '';

		if ( '' === $signature || '' === $timestamp ) {
			return false;
		}
		// Signature must be a 64-char SHA-256 hex digest. Normalize case before comparison:
		// hash_equals() is case-sensitive while hash_hmac() emits lowercase, so an
		// uppercase-hex sender would otherwise be wrongly rejected.
		if ( 64 !== strlen( $signature ) || ! ctype_xdigit( $signature ) ) {
			return false;
		}
		$signature = strtolower( $signature );
		// Timestamp must be a plain integer string — the same bytes the HMAC was
		// computed over. Reject non-numeric input rather than coercing via (int),
		// which would desync the freshness check from the signed payload.
		// Bound the length before casting: an arbitrarily long digit string can overflow (int)
		// (especially on 32-bit) and make the freshness window behave unpredictably. A Unix-seconds
		// timestamp is ~10 digits; 20 is a safe ceiling.
		if ( ! ctype_digit( $timestamp ) || strlen( $timestamp ) > 20 ) {
			return false;
		}
		if ( abs( time() - (int) $timestamp ) > 900 ) {
			return false;
		}

		$expected = hash_hmac( 'sha256', $timestamp . '.' . $raw_body, $secret );

		return hash_equals( $expected, $signature );
	}

	private function parse_webhook_params(): ?array {
		$order_key = null;

		/**
		 * Note for Plugin Reviewer: `order_key` maps the request to a WooCommerce order via
		 * `wc_get_order_id_by_order_key()`. Once the per-shop HMAC signing secret has been synced
		 * from SIN.CORE, authentication is the HMAC-SHA256 signature (`x-simplyin-webhook-signature`,
		 * verified in `verify_webhook_signature()`) and `order_key` is only routing. During the
		 * zero-downtime migration window — before that first sync — no secret is stored, so
		 * `order_key` acts as the token (legacy behaviour), see the enforcement gate in `webhook()`.
		 * A standard WordPress nonce is not applicable for server-to-server communication.
		 */
		if ( isset( $_GET['order_key'] ) ) {// phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.NonceVerification.Recommended
			$order_key = sanitize_text_field( wp_unslash( $_GET['order_key'] ) );// phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.NonceVerification.Recommended
		}

		if ( $order_key ) {
			return [
				'order_key' => $order_key,
			];
		}

		return null;
	}

	public function webhook(): void {
		add_action( 'woocommerce_api_wc_gateway_simplyin', function () {
			$params = $this->parse_webhook_params();
			if ( $params ) {
				$order_id = null;
				try {
					$order_key = $params['order_key'];
					$order_id  = wc_get_order_id_by_order_key( wc_clean( wp_unslash( $order_key ) ) );
					$order     = wc_get_order( $order_id );
					if ( $order ) {
						$this->order_data = new Order_Data( $order );
					} else {
						throw new Exception( "Order: $order_id not found" );
					}

					// file_get_contents('php://input') can return false; cast to string
					// once so signature verification and json_decode below stay consistent
					// and never hit a PHP 8 TypeError.
					$body = (string) file_get_contents( 'php://input' );

					// Log the receipt at handler entry so an audit can see the webhook arrived at
					// all, independently of whether it later passes auth (the previous code logged
					// nothing until the 401 or the final success).
					$this->log( 'webhook', $order_id, 'received' );

					// Authenticate the webhook with the HMAC signature. order_key
					// only routes to the order; the signature is what proves it came from SIN.CORE.
					//
					// Migration (zero-downtime): enforce HMAC only once the signing secret has
					// been synced from SIN.CORE into `simplyin_plugin_payment_webhook_secret`.
					// Until then fall back to order_key as the token (legacy behaviour) so a
					// freshly-updated shop is never rejected during the window before its first
					// plugin/sync. SIN.CORE is rolled out first, so the secret arrives quickly
					// and signature enforcement turns on automatically, per shop.
					//
					// Gate on the DECRYPTED secret (the actual ability to verify), not the stored
					// ciphertext: if the stored value can't be decrypted (changed WP salts /
					// missing libsodium), we must fall back to order_key rather than hard-fail
					// every webhook with 401.
					$webhook_secret = Secret_Cipher::decrypt( (string) get_option( 'simplyin_plugin_payment_webhook_secret', '' ) );
					if ( '' === $webhook_secret ) {
						// Migration window: no signing secret synced yet, so order_key is the token
						// (legacy). Log the fallback explicitly so an audit can distinguish
						// "authenticated by HMAC" from "HMAC not yet enforced on this shop".
						$this->log( 'webhook', $order_id, 'signature check skipped — signing secret not synced yet (order_key fallback)' );
					} elseif ( ! $this->verify_webhook_signature( $body, $webhook_secret ) ) {
						$this->log_error( 'webhook', 'Invalid or missing HMAC signature', $order_id );
						status_header( 401 );
						exit;
					} else {
						// Explicit positive result: this webhook was authenticated by HMAC. Without
						// this line a valid signature was silent and indistinguishable from the fallback.
						$this->log( 'webhook', $order_id, 'signature valid (HMAC)' );
					}

					$data = json_decode( $body, true );

					if ( json_last_error() !== JSON_ERROR_NONE ) {
						$this->log_error( 'webhook',
							'Invalid JSON payload',
							null );
						status_header( 400 );
						exit;
					}

					if ( ! is_array( $data ) ) {
						$this->log_error( 'webhook', 'Payload is not a JSON object', $order_id );
						status_header( 400 );
						exit;
					}

					array_walk_recursive( $data, static function ( &$value ) {
						if ( is_string( $value ) ) {
							$value = sanitize_text_field( $value );
						}
					} );

					$payment_status_from_webhook = isset( $data['status'] ) ? (string) $data['status'] : '';
					if ( '' === $payment_status_from_webhook ) {
						$this->log_error( 'webhook', 'Missing or empty status in webhook payload', $order_id );
						status_header( 400 );
						exit;
					}

					$event_id = $data['event_id'] ?? $data['eventId'] ?? null;
					if ( null === $event_id ) {
						$event_id = hash( 'sha256', $order_key . '|' . ( $data['status'] ?? '' ) );
						$this->log( 'webhook', $order_id, 'event_id missing in payload — using deterministic fallback', [
							'fallback_event_id' => $event_id,
						] );
					}
					$processed_events = $order->get_meta( '_simplyin_processed_webhook_events' );

					if ( ! is_array( $processed_events ) ) {
						$processed_events = [];
					}

					if ( in_array( $event_id, $processed_events, true ) ) {
						$this->log( 'webhook',
							0,
							'',
							[
								'order_id'  => $order_id,
								'event_id'  => $event_id,
								'status'    => 'already_processed',
							]
						);
						wp_send_json_success( [ 'status' => 'already_processed', 'event_id' => $event_id ] );
					}

					$payment_status_service = new Payment_Status( $this->order_data );
					$payment_status_service->update_payment_status( $payment_status_from_webhook );
					$mapped_wc_order_status = $payment_status_service->map_wc_status( $payment_status_from_webhook );
					$note                   = "Simply.IN webhook status received: $payment_status_from_webhook. Mapped to Woocommerce status: $mapped_wc_order_status";

					Webhook_Context::set_processing( $order_id );
					$this->order_data->update_order_status( $mapped_wc_order_status,
						$note );
					Webhook_Context::clear( $order_id );

					$processed_events[] = $event_id;
					$processed_events   = array_slice( array_unique( $processed_events ), -20 );
					$order->update_meta_data( '_simplyin_processed_webhook_events', $processed_events );
					$order->save_meta_data();

					$this->log( 'webhook',
						0,
						'',
						[
							'order_id'  => $order_id,
							'order_key' => $order_key,
							'data'      => $data,
							'status from webhook'    => $payment_status_from_webhook,
						]
					);
				} catch ( Exception $exception ) {
					$this->log_error( 'webhook',
						$exception->getMessage(),
						$order_id
					);
				}

			} else {
				$this->log_error( 'webhook',
					'Order not found in request',
					null,
					[
						'url' => Gateway_Helper::get_current_url(),
					],
				);
			}
		} );


	}


}
