<?php

namespace Simplyin\Simplyin_W_Plugin\Sync;

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly
}

use Simplyin\Simplyin_W_Plugin\Helper\Secret_Cipher;
use Simplyin\Simplyin_W_Plugin\Lib\Remote\Sin_Core_Api\Simplyin_Api_Legacy_Client;
use Simplyin\Simplyin_W_Plugin\Plugin;
use Simplyin\Simplyin_W_Plugin\Payments\Gateway\Webhook_Context;
use WC_Order;
use WC_Order_Query;
use WP_Async_Request;

/**
 * BI-directional interface between plugin and BE
 */
class Simplyin_Sync {

	private $plugin_name;
	private $version;


	/**
	 * @var SimplyIn_Background_Process
	 */
	protected $process_all;
	protected $simplyin_api;

	protected $logs = [];

	protected WP_Async_Request $process_single;


	public function __construct( $plugin_name, $version ) {

		add_action( 'plugins_loaded', [ $this, 'init' ] );
		add_action( 'init', [ $this, 'process_handler' ] );
		// Path B (priority 10, async): queues update_order_status to background process
		// for orders with SimplyInOrderId only. See also Path A in simplyin.php (priority 1).
		add_action( 'woocommerce_order_status_changed',
			[ $this, 'change_status' ],
			10,
			3 );
		add_filter( 'pre_update_option_simplyin_api_key',
			[ $this, 'force_update_api_key' ],
			10,
			2 );

		add_action( 'update_option_simplyin_api_key',
			[ $this, 'simplyin_update_merchand_settings' ],
			10,
			2 );
		add_action( 'add_option_simplyin_api_key',
			[ $this, 'simplyin_update_merchand_settings' ],
			10,
			2 );
		$this->plugin_name = $plugin_name;
		$this->version     = $version;
	}

	public function force_update_api_key( $new_value, $old_value ) {
		if ( $new_value === $old_value ) {
			$this->simplyin_update_merchand_settings( $old_value, $new_value );
		}


		return $new_value;
	}

	/**
	 * Init
	 */
	public function init() {
		$this->simplyin_api = new Simplyin_Api_Legacy_Client( $this->plugin_name,
			$this->version );

		$this->process_single = new SimplyIn_Background_Request();
		$this->process_single->setSimplyInApi( $this->simplyin_api );

		$this->process_all = new SimplyIn_Background_Process();
		$this->process_all->setSimplyInApi( $this->simplyin_api );
	}

	/**
	 * Process handler
	 */
	public function process_handler() {
		if ( ! isset( $_GET['process'], $_GET['_wpnonce'] ) ) {
			return;
		}

		if ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ),
			'process' ) ) {
			return;
		}

		$process = sanitize_text_field( wp_unslash( $_GET['process'] ) );

		if ( 'all' === $process ) {
			$this->handle_all();
		}

		if ( 'initial' === $process ) {
			$this->simplyin_initial_user_sync();
		}

		if ( 'clean' === $process ) {
			$this->clean_queue();
		}
	}

	protected function clean_queue() {
		//todo: verify
		if ( is_object( $this->process_all->get_data() ) ) {
			$this->process_all->get_data()->delete_all();
		}

	}

	/**
	 * Handle status change
	 */
	public function change_status( $order_id, $old_status, $new_status ) {
		if ( Webhook_Context::is_processing( (int) $order_id ) ) {
			return;
		}

		$order = $this->get_wc_order_instance( (int) $order_id );

		if ( ! $order ) {
			return;
		}

		$simplyin_order_id = $order->get_meta( 'SimplyInOrderId' );
		if ( $simplyin_order_id ) {
			$this->process_all->push_to_queue( [
				'action'            => 'update_order_status',
				'order_id'          => $order_id,
				'simplyin_order_id' => $simplyin_order_id,
				'status'            => $new_status,
				'old_status'        => $old_status,
				'order'             => $order,

			] );
		}
		$this->process_all->save()->dispatch();
	}

	private function get_wc_order_instance( int $order_id ): ?WC_Order {
		$order = wc_get_order( $order_id );

		if ( false === $order instanceof WC_Order ) {
			return null;
		}

		return $order;
	}

	/**
	 * Handle all requests
	 */
	protected function handle_all() {
		$page_size = 500;
		$offset    = 0;

		do {
			$user_ids = get_users( [
				'role__in' => [ 'customer', 'subscriber' ],
				'fields'   => 'ID',
				'number'   => $page_size,
				'offset'   => $offset,
			] );

			if ( empty( $user_ids ) ) {
				break;
			}

			foreach ( $user_ids as $user_id ) {
				$this->process_all->push_to_queue( [
					'action'  => 'check_user',
					'user_id' => (int) $user_id,
				] );
			}

			$this->process_all->save();
			$offset += count( $user_ids );

		} while ( count( $user_ids ) === $page_size );

		$this->process_all->dispatch();
	}

	/**
	 * Get all users and sync with SimplyIn
	 */
	public function simplyin_initial_user_sync() {
		$page_size = 500;
		$offset    = 0;

		do {
			$users = get_users( [
				'role__in' => [ 'customer', 'subscriber' ],
				'fields'   => [ 'ID', 'user_email' ],
				'number'   => $page_size,
				'offset'   => $offset,
			] );

			if ( empty( $users ) ) {
				break;
			}

			foreach ( $users as $user ) {
				$this->process_all->push_to_queue( [
					'action'     => 'verify_user',
					'user_email' => $user->user_email,
					'user_id'    => (int) $user->ID,
				] );
			}

			$this->process_all->save();
			$offset += count( $users );

		} while ( count( $users ) === $page_size );

		$this->process_all->dispatch();
	}

	public function get_api(): Simplyin_Api_Legacy_Client {
		if ( ! $this->simplyin_api ) {
			$this->simplyin_api = new Simplyin_Api_Legacy_Client( $this->plugin_name,
				$this->version );
		}

		return $this->simplyin_api;
	}

	/**
	 * when key is updated try to fetch merchand settings, fired when api key
	 * is updated
	 */
	public function simplyin_update_merchand_settings(
		$old_api_key,
		$new_api_key
	) {


		simplyin()->get_simplyin_logger()->log_if_wpdebug_on(
			sprintf( "[SimplyInSync] [hook: update_option_simplyin_api_key]" ),
			"sync" );


		// check if API urls exist in settings, if not activate it
		if ( empty( get_option( 'SimplyIn_Backend_Url' ) ) ) {
			simplyin()->get_simplyin_logger()->log_if_wpdebug_on(
				sprintf( "[SimplyInSync] [hook: update_option_simplyin_api_key] [API urls don\'t exist in settings. Now activate it]" ),
				"sync" );

			simplyinActivate();
		}

		if ( empty( $new_api_key ) ) {
			$response = null;
			simplyin()->update_option( 'SimplyInSettingsSuccessMsg',
				0 );

			simplyin()->update_option( 'SimplyInSettingsErrorMsg',
				__( 'Enter API key.',
					'simplyin' ) );
			simplyin()->update_option( 'SimplyInApiConnection',
				0,
				'sync' );
			simplyin()->update_option( 'SimplyInApiConnectedLastTry',
				time(),
				'sync' );

			return;
		} else {
			$response = $this->get_api()->sendRequest( 'plugin/sync',
				'POST',
				self::get_plugin_info_to_sync_request(),
				[],
				$new_api_key );
		}

		if ( $response && isset( $response->transactionHistoryLengthDays ) ) { // this means API responsed correctly
			simplyin()->update_option( 'SimplyInSettingsErrorMsg',
				0 );

			simplyin()->update_option( 'SimplyInSettingsSuccessMsg',
				__( 'Connected with Simply.IN',
					'simplyin' ),
				null,
				true );

			simplyin()
				->get_simplyin_logger()
				->log_if_wpdebug_on( "Successfull connection", "settings" );
			simplyin()->update_option( 'SimplyInApiConnection',
				1,
				'sync' );
			simplyin()->update_option( 'SimplyInApiConnectionLastTry',
				time(),
				'sync' );
			simplyin()->update_option( 'SimplyInTransactionHistoryLengthDay',
				$response->transactionHistoryLengthDays,
				'sync' );
			simplyin()->update_option( 'SimplyInSwitchLastChange',
				$response->switchInfo->lastChange,
				'sync' );
			simplyin()->update_option( 'SimplyInLogisticsPartnersOrder',
				$response->logisticsPartnersOrder->order,
				'sync' );

			if ( ! $response->switchInfo->isChecked ) {
				$current_value = get_option( 'simplyin_register_by_default' );
				// Always backup before overwriting; use sentinel when option was never set.
				$backup_value = ( false === $current_value ) ? '__not_set__' : (string) $current_value;
				simplyin()->update_option( 'SimplyInRegisterByDefaultStateOld', $backup_value, 'sync' );
				simplyin()->update_option( 'SimplyInRegisterByDefaultDisabled', true, 'sync' );
				simplyin()->update_option( 'simplyin_register_by_default', 'off', 'sync' );
			} else {
				$was_disabled_by_api = '1' === (string) get_option( 'SimplyInRegisterByDefaultDisabled' );
				simplyin()->update_option( 'SimplyInRegisterByDefaultDisabled', false, 'sync' );

				if ( $was_disabled_by_api ) {
					$backup = get_option( 'SimplyInRegisterByDefaultStateOld' );

					if ( false === $backup || '__not_set__' === $backup ) {
						// Option was never set before API disabled — apply sensible default.
						simplyin()->update_option( 'simplyin_register_by_default', 'on', 'sync' );
					} else {
						// Restore merchant's previous setting; normalize legacy values.
						if ( '1' === $backup ) {
							$backup = 'on'; // Pre-5.5.29 slider format.
						} elseif ( '' === $backup ) {
							$backup = 'off'; // Normalize empty to explicit canonical value.
						}
						simplyin()->update_option( 'simplyin_register_by_default', $backup, 'sync' );
					}

					simplyin()->update_option( 'SimplyInRegisterByDefaultStateOld', '', 'sync' );
				}
			}

			if ( isset( $response->docsInfo ) && ! empty( $response->docsInfo ) && is_array( $response->docsInfo ) ) {
				foreach ( $response->docsInfo as $doc ) {
					if ( isset( $doc->key ) ) {
						simplyin()->update_option( 'SimplyInDoc_' . $doc->key,
							$doc->url,
							'sync' );
					}
				}
			}

			// Persist the per-shop HMAC webhook signing secret from THIS (reachable) response,
			// so HMAC-signed webhook verification is available as soon as the connection succeeds.
			self::persist_webhook_signing_secret( $response );

			// Kick zero-touch RSA registration the moment the connection is established, so signing
			// becomes active within seconds of "Save" instead of only on a later admin_init /
			// daily-cron backstop tick. Scheduled off-request, NOT inline like the HMAC secret
			// above: RSA is two synchronous backend round-trips and must stay off the Save request
			// so a slow backend can never freeze it into a max_execution_time fatal on shared
			// hosting. The shared event also piggybacks a webhook-secret refresh; here that is a
			// harmless idempotent no-op (we just persisted it above), bounded to the one-time
			// pre-registration window since the scheduler no-ops once paired.
			Plugin::schedule_rsa_auto_register();

		} else {
			simplyin()->get_simplyin_logger()->log_if_wpdebug_on(
				sprintf( "[SimplyInSync] [hook: update_option_simplyin_api_key] [failed] [response: %s]",
					wp_json_encode( $response ) ),
				"sync" );

			simplyin()->update_option( 'SimplyInSettingsSuccessMsg',
				0 );

			simplyin()->update_option( 'SimplyInSettingsErrorMsg',
				__( 'Error connecting to Simply.IN - check the API key.',
					'simplyin' ),
				null,
				true );

			simplyin()->update_option( 'SimplyInApiConnection',
				0,
				'sync' );
			simplyin()->update_option( 'SimplyInApiConnectedLastTry',
				time(),
				'sync' );
		}
	}

	/**
	 * Synchronize everything
	 */
	public function sync() {
		if ( get_option( 'SimplyInApiConnection' ) ) {
			$this->sync_orders();
			$this->sync_merchand_settings();
		}
	}

	protected function sync_merchand_settings() {
		//SimplyinLogger::log( "Sync merchant settings", "sync" );

		$this->process_all->push_to_queue( [ 'action' => 'update_merchant_settings' ] );
		$this->process_all->save()->dispatch();
	}

	protected function sync_shipping() {
		simplyin()
			->get_simplyin_logger()
			->log_if_wpdebug_on( "Sync shipping started", "sync" );

		$this->process_all->push_to_queue( [ 'action' => 'update_shipping_methods' ] );
		$this->process_all->save()->dispatch();
	}

	/**
	 * Sync orders from last time.
	 */
	protected function sync_orders() {

		simplyin()
			->get_simplyin_logger()
			->log_if_wpdebug_on( "Sync orders started", "sync" );
		$lastSyncTimestamp = get_option( 'SimplyInLastSyncTimestamp' );
		if ( ! $lastSyncTimestamp || $lastSyncTimestamp <= 0 ) { // if never done, take it back from $transactionHistoryLengthDay days.
			$transactionHistoryLengthDay = get_option( 'SimplyInTransactionHistoryLengthDay',
				180 );
			simplyin()
				->get_simplyin_logger()
				->log_if_wpdebug_on( "History length: " . $transactionHistoryLengthDay,
					"settings" );
			$lastSyncTimestamp = time() - 60 * 60 * 24 * $transactionHistoryLengthDay;
		}

		simplyin()
			->get_simplyin_logger()
			->log_if_wpdebug_on( "Last Synchronisation Timestamp: " . $lastSyncTimestamp,
				"sync" );
		simplyin()->get_simplyin_logger()->log_if_wpdebug_on(
			sprintf( "[SimplyInSync] [sync_orders] [date_query after: %s]",
				wp_json_encode( gmdate( "Y-m-d H:i:s",
					$lastSyncTimestamp ) ) ),
			"sync" );

		$page       = 1;
		$batch_size = 200;
		$total      = 0;

		// Earliest date_created among orders queued this run that are not yet confirmed exported
		// (SimplyInOrderExported=1). The watermark below must not advance past it: at this point the
		// order has only been queued, not synced — if the background process later fails on it (see
		// SimplyIn_Background_Process::createOrder()), advancing the watermark to "now" would push
		// the sync window past its date_created and permanently drop it from every future run.
		// Held at null when nothing unconfirmed was found this run.
		//
		// Accepted tradeoff: if an order NEVER gets a CONFIRMED result (e.g. it is deterministically
		// rejected by SIN.CORE, or checkUserExist() itself keeps failing to confirm an answer,
		// exhausting SimplyIn_Background_Process::MAX_CREATE_ORDER_SYNC_ATTEMPTS every single cron
		// cycle), the watermark stays pinned at it — but only until that attempt sequence gives up
		// (create_order_task() clears SimplyInOrderInQueue on giving up, so the NEXT sync_orders()
		// run picks it back up for another bounded 5-attempt pass; it does not pin forever with no
		// further action). That is deliberate, not an oversight: the alternative — giving up on the
		// watermark globally — is exactly the permanent-data-loss bug this ticket fixes.
		// Explicitly NOT covered by this tradeoff: an order checkUserExist() CONFIRMED has no
		// Simply.IN account (SimplyInOrderNotApplicable=1) is resolved terminally below and never
		// pins the watermark at all — it will never be exported, so there is nothing to keep
		// rescanning for.
		// The cost of the genuinely-pinned case is bounded: already-resolved orders (exported or
		// not-applicable) in the widened range are skipped on one cheap meta check (no API call), so
		// this only grows the WC_Order_Query page-scan, not the number of SIN.CORE calls made.
		$earliest_unconfirmed_timestamp = null;
		$earliest_unconfirmed_order_id  = null;

		do {
			$query     = new WC_Order_Query(
				[
					'limit'        => $batch_size,
					'paged'        => $page,
					'date_created' => '>=' . gmdate( "Y-m-d H:i:s", $lastSyncTimestamp ),
					'return'       => 'ids',
				]
			);
			$order_ids = $query->get_orders();

			foreach ( $order_ids as $order_id ) {
				$order = wc_get_order( $order_id );
				if ( ! $order ) {
					continue;
				}
				if ( 1 === (int) $order->get_meta( 'SimplyInOrderExported', true ) ) {
					continue;
				}
				// Terminal, not a transient failure: SimplyIn_Background_Process::createOrder()
				// sets this when checkUserExist() came back false. Distinct from SimplyInOrderExported
				// above — this order will never be exported, so it must not hold back the watermark
				// or be re-queued, same as an exported one.
				if ( 1 === (int) $order->get_meta( 'SimplyInOrderNotApplicable', true ) ) {
					continue;
				}

				$order_created    = $order->get_date_created();
				$order_created_ts = $order_created ? $order_created->getTimestamp() : $lastSyncTimestamp;
				if ( null === $earliest_unconfirmed_timestamp || $order_created_ts < $earliest_unconfirmed_timestamp ) {
					$earliest_unconfirmed_timestamp = $order_created_ts;
					$earliest_unconfirmed_order_id  = $order_id;
				}

				// Already dispatched to the background process and not yet resolved: still
				// genuinely unconfirmed (counted above), but don't push a second copy while the
				// first is still in flight. WP_Background_Process's own cron healthcheck recovers a
				// stalled dispatch on its own; createOrder() clears this flag as soon as it runs.
				if ( 1 === (int) $order->get_meta( 'SimplyInOrderInQueue', true ) ) {
					continue;
				}

				$this->process_all->push_to_queue( [
					'action'   => 'create_order',
					'order_id' => $order_id,
				] );
				$order->update_meta_data( 'SimplyInOrderInQueue', 1 );
				$order->save_meta_data();
			}

			$total += count( $order_ids );
			$page++;
		} while ( count( $order_ids ) === $batch_size );

		simplyin()
			->get_simplyin_logger()
			->log_if_wpdebug_on( "Number of orders to sync: " . $total, "sync" );

		$this->process_all->save()->dispatch();

		if ( null !== $earliest_unconfirmed_timestamp ) {
			// Informational, not a WARNING: an order sitting unconfirmed right after being queued is
			// normal (the background process runs asynchronously after this returns). A genuine sync
			// failure is logged at WARNING at the point it happens, in createOrder()/create_order_task().
			simplyin()
				->get_simplyin_logger()
				->log_if_wpdebug_on(
					sprintf( "[SimplyInSync] [sync_orders] [watermark held back by unconfirmed order] [order_id: %s] [held_at: %s]",
						wp_json_encode( $earliest_unconfirmed_order_id ),
						wp_json_encode( gmdate( "Y-m-d H:i:s", $earliest_unconfirmed_timestamp ) ) ),
					"sync" );

			// max(): a null get_date_created() (WooCommerce can return that) falls back to
			// $lastSyncTimestamp above, which would otherwise regress the watermark by 1 second on
			// every single run forever (Copilot review) — clamp so it can never move earlier than
			// where this run started. A real dated order's timestamp is always >= $lastSyncTimestamp
			// (the query filter guarantees it), so the clamp is a no-op for one strictly later than
			// the boundary; it only changes the result for the null-date fallback, or for an order
			// dated exactly at the boundary (harmless either way — both re-include it next run).
			update_option( "SimplyInLastSyncTimestamp", max( $lastSyncTimestamp, $earliest_unconfirmed_timestamp - 1 ) );
		} else {
			update_option( "SimplyInLastSyncTimestamp", time() );
		}
	}

	public static function get_plugin_info_to_sync_request(
		array $body_data = []
	): array {

		$body_data['pluginVersion'] = SimplyInGetPluginVersion();
		$body_data['platform']      = 'WooCommerce';
		$body_data['shopName']      = addslashes( sanitize_text_field( get_bloginfo( 'name' ) ) );

		return $body_data;
	}

	/** Throttle: at most one out-of-band webhook-secret sync per this window (seconds). */
	const WEBHOOK_SECRET_SYNC_THROTTLE = 'simplyin_webhook_secret_sync_throttle';

	/**
	 * Persist the per-shop HMAC webhook signing secret from a plugin/sync response.
	 * Shared by EVERY path that reads a plugin/sync response so the secret is never dropped on
	 * one path and stored on another.
	 * Model B semantics: field present + non-empty => store (encrypted at rest);
	 * present + empty => explicit rotate/clear; absent => no change.
	 *
	 * @param mixed $response json_decode()'d plugin/sync response (stdClass) or null.
	 */
	public static function persist_webhook_signing_secret( $response ): void {
		if ( ! is_object( $response ) || ! property_exists( $response, 'paymentWebhookSigningSecret' ) ) {
			return;
		}
		$secret = (string) $response->paymentWebhookSigningSecret;
		if ( '' === $secret ) {
			delete_option( 'simplyin_plugin_payment_webhook_secret' );

			return;
		}
		update_option(
			'simplyin_plugin_payment_webhook_secret',
			Secret_Cipher::encrypt( $secret ),
			false
		);
	}

	/**
	 * Out-of-band, synchronous, best-effort refresh of the webhook signing secret — for the
	 * plugin-update and admin-backstop triggers, so a shop picks up the secret WITHOUT re-saving the
	 * API key. Mirrors the RSA auto-registration triggers: runs from upgrader_process_complete
	 * (auto + manual update, via Plugin) and from the admin backstop. Connection-gated + throttled so
	 * it does not call plugin/sync on every request; never fatal. Uses the stored API key (RSA path).
	 */
	public static function sync_webhook_secret_now(): void {
		if ( (string) get_option( 'SimplyInApiConnection' ) !== '1' ) {
			return;
		}
		if ( '' === (string) get_option( 'simplyin_api_key', '' ) ) {
			return;
		}
		if ( false !== get_transient( self::WEBHOOK_SECRET_SYNC_THROTTLE ) ) {
			return;
		}
		set_transient( self::WEBHOOK_SECRET_SYNC_THROTTLE, 1, 5 * MINUTE_IN_SECONDS );
		try {
			$response = ( new Simplyin_Api_Legacy_Client() )->sendRequest(
				'plugin/sync',
				'POST',
				self::get_plugin_info_to_sync_request()
			);
			self::persist_webhook_signing_secret( $response );
		} catch ( \Throwable $e ) {
			// Best-effort: the next update / admin visit retries.
		}
	}

}
