<?php

namespace DigitalApps\PaymentsForAdyen;

defined('ABSPATH') || exit;

/** Durable, bound, one-time continuation state for Adyen Web actions. */
class Adyen_Session_Manager
{
	private const SESSION_PREFIX = 'digitalapps_payments_for_adyen_3ds_';
	private const SESSION_EXPIRY = 900;

	/** @var Logger */
	private $logger;

	public function __construct()
	{
		$this->logger = new Logger();
	}

	/**
	 * Create a continuation before `/payments`, then attach action data afterwards.
	 *
	 * @param int         $order_id Order ID.
	 * @param string|null $payment_data Adyen paymentData.
	 * @param array       $action Adyen action, or an empty array before the response.
	 * @return string|false
	 */
	public function create_session($order_id, $payment_data = null, $action = [])
	{
		$order = wc_get_order($order_id);
		if (!$order || !is_array($action)) {
			return false;
		}

		try {
			$session_key = self::SESSION_PREFIX . wp_generate_password(32, false, false);
			$now = time();
			$wrapper = [
				'action' => $action,
				'bindingHash' => $this->binding_hash(),
			];

			global $wpdb;
			$table = $wpdb->prefix . 'digitalapps_payments_for_adyen_3ds_sessions';
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Custom durable action table.
			$inserted = $wpdb->insert(
				$table,
				[
					'session_key' => $session_key,
					'order_id' => (int) $order_id,
					'user_id' => (int) get_current_user_id(),
					'payment_data' => (string) $payment_data,
					'action_data' => wp_json_encode($wrapper),
					'status' => 'pending',
					'created_at' => gmdate('Y-m-d H:i:s', $now),
					'expires_at' => gmdate('Y-m-d H:i:s', $now + self::SESSION_EXPIRY),
				],
				['%s', '%d', '%d', '%s', '%s', '%s', '%s', '%s']
			);

			return false !== $inserted ? $session_key : false;
		} catch (\Throwable $exception) {
			$this->logger->log('ERROR', [
				'message' => 'Adyen action continuation could not be created',
				'order_id' => (int) $order_id,
			]);
			return false;
		}
	}

	/** Return a pending continuation only when it belongs to this shopper session. */
	public function get_session($session_key)
	{
		$row = $this->get_row($session_key);
		if (!$row || 'pending' !== $row->status || strtotime($row->expires_at . ' UTC') < time()) {
			return false;
		}
		return $this->hydrate_and_verify($row);
	}

	/** Atomically claim a continuation before submitting details. */
	public function claim_session($session_key)
	{
		$session = $this->get_session($session_key);
		if (!$session) {
			return false;
		}

		global $wpdb;
		$table = $wpdb->prefix . 'digitalapps_payments_for_adyen_3ds_sessions';
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Atomic one-time claim.
		$updated = $wpdb->query($wpdb->prepare(
			"UPDATE {$table} SET status = 'processing' WHERE session_key = %s AND status = 'pending' AND expires_at >= %s",
			$session_key,
			gmdate('Y-m-d H:i:s')
		));

		return 1 === (int) $updated ? $session : false;
	}

	/** Attach the action returned from `/payments` to a pending continuation. */
	public function update_session($session_key, $data)
	{
		$session = $this->get_session($session_key);
		if (!$session || !is_array($data)) {
			return false;
		}

		$wrapper = [
			'action' => array_key_exists('action', $data) ? $data['action'] : $session['action'],
			'bindingHash' => $session['binding_hash'],
		];
		$update = ['action_data' => wp_json_encode($wrapper)];
		$formats = ['%s'];
		if (array_key_exists('payment_data', $data)) {
			$update['payment_data'] = (string) $data['payment_data'];
			$formats[] = '%s';
		}

		global $wpdb;
		$table = $wpdb->prefix . 'digitalapps_payments_for_adyen_3ds_sessions';
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Custom durable action table.
		return false !== $wpdb->update($table, $update, ['session_key' => $session_key, 'status' => 'pending'], $formats, ['%s', '%s']);
	}

	/** Mark a token as consumed so a replay cannot submit details again. */
	public function delete_session($session_key)
	{
		if (!$this->validate_session_key($session_key)) {
			return false;
		}
		global $wpdb;
		$table = $wpdb->prefix . 'digitalapps_payments_for_adyen_3ds_sessions';
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Custom durable action table.
		return false !== $wpdb->update($table, ['status' => 'consumed'], ['session_key' => $session_key], ['%s'], ['%s']);
	}

	/** Release a claimed token after a retryable local failure. */
	public function release_session($session_key)
	{
		global $wpdb;
		$table = $wpdb->prefix . 'digitalapps_payments_for_adyen_3ds_sessions';
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Custom durable action table.
		return false !== $wpdb->update($table, ['status' => 'pending'], ['session_key' => $session_key, 'status' => 'processing'], ['%s'], ['%s', '%s']);
	}

	/** Delete expired and old consumed continuation records. */
	public function cleanup_expired_sessions()
	{
		global $wpdb;
		$table = $wpdb->prefix . 'digitalapps_payments_for_adyen_3ds_sessions';
		$cutoff = gmdate('Y-m-d H:i:s', time() - DAY_IN_SECONDS);
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Scheduled custom-table retention cleanup.
		$deleted = $wpdb->query($wpdb->prepare(
			"DELETE FROM {$table} WHERE expires_at < %s OR (status = 'consumed' AND created_at < %s)",
			gmdate('Y-m-d H:i:s'),
			$cutoff
		));
		return false === $deleted ? 0 : (int) $deleted;
	}

	public static function get_session_expiry()
	{
		return self::SESSION_EXPIRY;
	}

	/** Build the merchant-hosted action page URL for a continuation. */
	public static function action_url($session_key)
	{
		return add_query_arg('digitalapps_payments_for_adyen_action', rawurlencode($session_key), home_url('/'));
	}

	private function get_row($session_key)
	{
		if (!$this->validate_session_key($session_key)) {
			return null;
		}
		global $wpdb;
		$table = $wpdb->prefix . 'digitalapps_payments_for_adyen_3ds_sessions';
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- One durable continuation lookup.
		return $wpdb->get_row($wpdb->prepare("SELECT * FROM {$table} WHERE session_key = %s LIMIT 1", $session_key));
	}

	private function hydrate_and_verify($row)
	{
		$wrapper = json_decode((string) $row->action_data, true);
		if (!is_array($wrapper) || empty($wrapper['bindingHash'])) {
			return false;
		}
		$current_binding = $this->binding_hash();
		if ('' === $current_binding || !hash_equals((string) $wrapper['bindingHash'], $current_binding)) {
			return false;
		}
		return [
			'order_id' => (int) $row->order_id,
			'user_id' => (int) $row->user_id,
			'payment_data' => (string) $row->payment_data,
			'action' => is_array($wrapper['action'] ?? null) ? $wrapper['action'] : [],
			'binding_hash' => (string) $wrapper['bindingHash'],
			'created_at' => strtotime($row->created_at . ' UTC'),
		];
	}

	private function binding_hash()
	{
		$user_id = (int) get_current_user_id();
		if ($user_id > 0) {
			$binding = 'user:' . $user_id;
		} elseif (function_exists('WC') && WC() && WC()->session) {
			$binding = 'guest:' . (string) WC()->session->get_customer_id();
		} else {
			return '';
		}
		return hash_hmac('sha256', $binding, wp_salt('auth'));
	}

	private function validate_session_key($session_key)
	{
		return is_string($session_key)
			&& 1 === preg_match('/^' . preg_quote(self::SESSION_PREFIX, '/') . '[A-Za-z0-9]{32}$/', $session_key);
	}
}
