<?php

namespace Inpsyde\BackWPup\Notice;

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

class DismissibleNoticeOption {

	/**
	 * The default expiration time in hours.
	 *
	 * @var int
	 */
	public const DEFAULT_EXPIRATION = 12;
	/**
	 * The option prefix.
	 *
	 * @var string
	 */
	public const OPTION_PREFIX = 'backwpup_dinotopt_';
	/**
	 * Dismiss notice permanently (global action).
	 *
	 * @var string
	 */
	public const FOR_GOOD_ACTION = 'dismiss_admin_notice_for_good';
	/**
	 * Dismiss notice temporarily (global action).
	 *
	 * @var string
	 */
	public const FOR_NOW_ACTION = 'dismiss_admin_notice_for_now';
	/**
	 * Dismiss notice permanently (per user action).
	 *
	 * @var string
	 */
	public const FOR_USER_FOR_GOOD_ACTION = 'dismiss_admin_notice_for_good_user';
	/**
	 * Skip dismissal handling.
	 *
	 * @var string
	 */
	public const SKIP = 'skip_action';

	/**
	 * Supported dismissal actions.
	 *
	 * @var string[]
	 */
	private const ALL_ACTIONS = [
		self::FOR_GOOD_ACTION,
		self::FOR_NOW_ACTION,
		self::FOR_USER_FOR_GOOD_ACTION,
	];

	/**
	 * Setup state by scope.
	 *
	 * @var array{sitewide: array<string, string>, blog: array<string, string>}
	 * @phpstan-var array{
	 *     sitewide: array<string, string>,
	 *     blog: array<string, string>,
	 * }
	 */
	private static $setup = [
		'sitewide' => [],
		'blog'     => [],
	];

	/**
	 * Whether to use sitewide options.
	 *
	 * @var bool
	 */
	private $sitewide = false;

	/**
	 * Creates a dismissible notice option helper.
	 *
	 * @param bool $sitewide Whether to use sitewide options.
	 */
	public function __construct( bool $sitewide = false ) {
		$this->sitewide = $sitewide;
	}

	/**
	 * Registers dismissal actions for a notice.
	 *
	 * @param bool   $sitewide   Whether to use sitewide options.
	 * @param string $notice_id  Notice identifier.
	 * @param string $capability Capability required to dismiss the notice.
	 */
	public static function setup_actions( bool $sitewide, string $notice_id, string $capability = 'read' ): void {
		if ( ! is_string( $notice_id ) ) {
			return;
		}

		$sitewide = $sitewide && ( function_exists( 'is_multisite' ) ? is_multisite() : false );

		$key = $sitewide ? 'sitewide' : 'blog';

		if ( array_key_exists( $notice_id, self::$setup[ $key ] ) ) {
			return;
		}

		if ( [] === self::$setup[ $key ] ) {
			$option = new self( $sitewide );
			add_action(
				'admin_post_' . self::FOR_GOOD_ACTION,
				function () use ( $option ): void {
					$option->dismiss();
				}
				);
			add_action(
				'admin_post_' . self::FOR_NOW_ACTION,
				function () use ( $option ): void {
					$option->dismiss();
				}
				);
			add_action(
				'admin_post_' . self::FOR_USER_FOR_GOOD_ACTION,
				function () use ( $option ): void {
					$option->dismiss();
				}
				);
		}

		self::$setup[ $key ][ $notice_id ] = $capability;
	}

	/**
	 * Returns the URL that can be used to dismiss a given notice for good or temporarily according to given action.
	 *
	 * @param string   $notice_id  Notice identifier.
	 * @param string   $action     Dismissal action.
	 * @param int|null $expiration Expiration in hours.
	 *
	 * @return string The dismissal action URL.
	 */
	public static function dismiss_action_url( string $notice_id, string $action, ?int $expiration = null ): string {
		return add_query_arg(
			[
				'action'     => $action,
				'notice'     => $notice_id,
				'blog'       => get_current_blog_id(),
				'expiration' => $expiration,
				$action      => wp_create_nonce( $action ),
			],
			admin_url( 'admin-post.php' )
		);
	}

	/**
	 * Returns true when given notice is dismissed for good or temporarily for current user.
	 *
	 * @param string $notice_id Notice identifier.
	 *
	 * @return bool True when dismissed, false otherwise.
	 */
	public function is_dismissed( string $notice_id ): bool {
		$option_name = self::OPTION_PREFIX . $notice_id;
		// Dismissed for good?
		$option = $this->sitewide ? get_site_option( $option_name ) : get_option( $option_name );
		if ( $option ) {
			return true;
		}

		// Dismissed for good for user?
		if ( get_user_option( $option_name ) ) {
			return true;
		}

		// Dismissed for now for user?
		$transient_name = self::OPTION_PREFIX . $notice_id . get_current_user_id();
		$transient      = $this->sitewide ? get_site_transient( $transient_name ) : get_transient( $transient_name );
		return (bool) $transient;
	}

	/**
	 * Action callback to dismiss an action for good.
	 */
	public function dismiss(): void {
		[$action, $notice_id, $is_ajax, $expiration] = $this->assert_allowed();
		$end_request                                 = true;

		switch ( $action ) {
			case self::FOR_GOOD_ACTION:
				$this->dismiss_for_good( $notice_id );
				break;

			case self::FOR_USER_FOR_GOOD_ACTION:
				$this->dismiss_for_user_for_good( $notice_id );
				break;

			case self::FOR_NOW_ACTION:
				$this->dismiss_for_now( $notice_id, $expiration );
				break;

			case self::SKIP:
				$end_request = false;
				break;
		}

		if ( $end_request ) {
			$this->end_request( $is_ajax );
		}
	}

	/**
	 * Action callback to dismiss an action for good.
	 *
	 * @param string $notice_id Notice identifier.
	 */
	private function dismiss_for_good( string $notice_id ): void {
		$option_name = self::OPTION_PREFIX . $notice_id;

		$this->sitewide
			? update_site_option( $option_name, 1 )
			: update_option( $option_name, 1, false );
	}

	/**
	 * Action callback to dismiss an action definitively for current user.
	 *
	 * @param string $notice_id Notice identifier.
	 */
	private function dismiss_for_user_for_good( string $notice_id ): void {
		update_user_option(
			get_current_user_id(),
			self::OPTION_PREFIX . $notice_id,
			1,
			$this->sitewide
		);
	}

	/**
	 * Action callback to dismiss an action temporarily for current user.
	 *
	 * @param string $notice_id Notice identifier.
	 * @param int    $delay     Hours to dismiss the notice.
	 */
	private function dismiss_for_now( string $notice_id, int $delay ): void {
		$transient_name = self::OPTION_PREFIX . $notice_id . get_current_user_id();
		$expiration     = $delay * HOUR_IN_SECONDS;

		$this->sitewide
			? set_site_transient( $transient_name, 1, $expiration )
			: set_transient( $transient_name, 1, $expiration );
	}

	/**
	 * Ends a request redirecting to referer page.
	 *
	 * @param bool $no_redirect Whether to skip redirect.
	 */
	private function end_request( bool $no_redirect = false ): void {
		if ( $no_redirect ) {
			exit();
		}

		$referer = wp_get_raw_referer();
		if ( ! $referer ) {
			$referer = $this->sitewide && is_super_admin() ? network_admin_url() : admin_url();
		}

		wp_safe_redirect( $referer );

		exit();
	}

	/**
	 * Validates the dismissal request data.
	 *
	 * @phpstan-return array{string, string, bool}
	 */
	private function assert_allowed(): array {
		if ( ! is_admin() ) {
			$this->end_request();
		}

		$definition = [
			'action'     => FILTER_DEFAULT,
			'notice'     => FILTER_DEFAULT,
			'blog'       => FILTER_SANITIZE_NUMBER_INT,
			'isAjax'     => FILTER_VALIDATE_BOOLEAN,
			'expiration' => FILTER_SANITIZE_NUMBER_INT,
		];

		$data = array_merge(
			array_filter( (array) filter_input_array( INPUT_GET, $definition ) ),
			array_filter( (array) filter_input_array( INPUT_POST, $definition ) )
		);

		$is_ajax    = ! empty( $data['isAjax'] );
		$action     = empty( $data['action'] ) ? '' : $data['action'];
		$notice     = empty( $data['notice'] ) ? '' : $data['notice'];
		$expiration = empty( $data['expiration'] ) ? self::DEFAULT_EXPIRATION : $data['expiration'];

		if ( ! $action
			|| ! $notice
			|| ! is_string( $notice )
			|| ! in_array( $action, self::ALL_ACTIONS, true )
		) {
			$this->end_request( $is_ajax );
		}

		$key        = $this->sitewide ? 'sitewide' : 'blog';
		$swap_key   = $this->sitewide ? 'blog' : 'sitewide';
		$capability = empty( self::$setup[ $key ][ $notice ] ) ? '' : self::$setup[ $key ][ $notice ];

		if ( ! $capability && ! empty( self::$setup[ $swap_key ][ $notice ] ) ) {
			return [ self::SKIP, '', $is_ajax ];
		}

		if ( ! $capability || ! current_user_can( $capability ) ) {
			$this->end_request( $is_ajax );
		}
		$nonce = filter_input( INPUT_POST, $action )
			?: filter_input( INPUT_GET, $action );

		if ( ! is_string( $nonce ) || ! wp_verify_nonce( $nonce, $action ) ) {
			$this->end_request( $is_ajax );
		}

		if ( ! $this->sitewide
			&& ( empty( $data['blog'] ) || get_current_blog_id() !== (int) $data['blog'] )
		) {
			$this->end_request( $is_ajax );
		}

		return [ $action, $notice, $is_ajax, $expiration ];
	}
}
