<?php
/**
 * Authorization Helper.
 *
 * @since 1.0
 * @since 1.4 Added event_ids restriction.
 * @since 2.0 Rewrite of permission logic.
 */

declare( strict_types=1 );
namespace FeData\FastEvents\Helpers;

defined( 'ABSPATH' ) || exit;

use FeData\FastEvents\Accounts\FeAccount;
use FeData\FastEvents\Logging\LoggerFactory;
use FeData\FastEvents\Storage\PluginSettings;
use WP_Error;
use WP_REST_Request;
use WP_REST_Server;
use WP_User;

/**
 * A class with seats functions
 */
final class AuthHelper {

	private const FE_MAX_PERM = 100;

	public const NO_PERMISSION_NEEDED   = -1;
	public const EMAIL_LIST_CHANGE      = 0;
	public const EMAIL_LIST_READ        = 1;
	public const EMAIL_TEMPLATE_CHANGE  = 2;
	public const EMAIL_TEMPLATE_EXAMPLE = 3;
	public const EMAIL_TEMPLATE_READ    = 4;
	public const EVENT_ADD              = 5;
	public const EVENT_DELETE           = 6;
	public const EVENT_DELETE_ORDERS    = 7;
	public const EVENT_DETAILS          = 8;
	public const EVENT_EXPORT           = 9;
	public const EVENT_IMPORT           = 10;
	public const EVENT_READ             = 11;
	public const EVENT_SEATS_CHANGE     = 12;
	public const EVENT_SEATS_READ       = 13;
	public const EVENT_TRACKING_CHANGE  = 14;
	public const EVENT_TRACKING_READ    = 15;
	public const EVENT_TRACKING_UPLOAD  = 16;
	public const EVENT_SYNC             = 17;
	public const EVENT_UPDATE           = 18;
	public const INPUT_FIELDS_CHANGE    = 19;
	public const INPUT_FIELDS_READ      = 20;
	public const LOG_DELETE             = 21;
	public const LOG_READ               = 22;
	public const ORDER_ADD              = 23;
	public const ORDER_CHECKIN          = 24;
	public const ORDER_DELETE           = 25;
	public const ORDER_DETAILS          = 26;
	public const ORDER_DETAILS_VIA_QR   = 27;
	public const ORDER_EMAIL            = 28;
	public const ORDER_EXPORT           = 29;
	public const ORDER_LOG_DELETE       = 30;
	public const ORDER_LOG_READ         = 31;
	public const ORDER_PAYMENT_DETAILS  = 32;
	public const ORDER_READ             = 33;
	public const ORDER_REFUND           = 34;
	public const ORDER_UPDATE           = 35;
	public const PAYMENT_APP_CHANGE     = 36;
	public const PAYMENT_APP_READ       = 37;
	public const PDF_TEMPLATE_CHANGE    = 38;
	public const PDF_TEMPLATE_READ      = 39;
	public const SALES_DASHBOARD        = 40;
	public const SCAN_APP_CHANGE        = 41;
	public const SCAN_APP_READ          = 42;
	public const TICKETS_CREATE         = 43;
	public const TICKETS_DELETE         = 44;
	public const TICKETS_EXPORT         = 45;
	public const TICKETS_READ           = 46;
	public const TICKET_TYPES_CHANGE    = 47;
	public const TICKET_TYPES_READ      = 48;
	public const TOOLS_EMAIL            = 49;
	public const TOOLS_ORDERS           = 50;
	public const TOOLS_REFUND           = 51;
	public const TOTAL_SALES            = 52;
	public const TOTAL_SCANS            = 53;
	public const USER_SUB_ACCOUNTS      = 54;
	public const WEBHOOK_CHANGE         = 55;
	public const WEBHOOK_READ           = 56;
	public const COUPON_BULK_EMAIL      = 57;
	public const COUPON_CHANGE          = 58;
	public const COUPON_EMAIL           = 59;
	public const COUPON_EXPORT          = 60;
	public const COUPON_IMPORT          = 61;
	public const COUPON_READ            = 62;
	public const SCANS_EXPORT           = 63;
	public const ORDER_EMAIL_SEND       = 64;
	public const ORDER_EMAIL_DEBUG      = 65;
	public const ORDER_EMAIL_DELETE     = 66;

	public const FAST_EVENTS_AUTH_QUERY_EVENTS       = 0;
	public const FAST_EVENTS_AUTH_QUERY_ORDERS       = 1;
	public const FAST_EVENTS_AUTH_QUERY_ORDER_LOOKUP = 2;

	private const OPTIONS = 'fast_events_options';

	/**
	 * Is this an Admin user?
	 *
	 * @since 2.0
	 * @var bool
	 */
	private bool $is_admin = false;

	/**
	 * Fast_Events_Account class.
	 *
	 * @since 2.0
	 * @var ?FeAccount
	 */
	private ?FeAccount $account;

	/**
	 * Index in the subaccount array.
	 *
	 * @since 2.0
	 * @var bool|int
	 */
	private bool|int $idx;

	/**
	 * Is the (sub)account blocked?
	 *
	 * @since 2.0
	 * @var bool
	 */
	private bool $blocked;

	/**
	 * The list of allowed events.
	 *
	 * @since 2.0
	 * @var array
	 */
	private array $allowed_events = array();

	/**
	 * The list of allowed endpoints.
	 *
	 * @since 2.0
	 * @var array
	 */
	private array $allowed_endpoints = array();

	/**
	 * The list of allowed permissions.
	 *
	 * @since 2.0
	 * @var string
	 */
	private string $permission_list;


	/**
	 * SeatsHelper constructor.
	 *
	 * @since 3.0.0
	 *
	 * @param LoggerFactory  $logger   Logger.
	 * @param PluginSettings $settings Plugin settings.
	 */
	public function __construct( private readonly LoggerFactory $logger, private readonly PluginSettings $settings ) {
	}


	/**
	 * User logged in with an application password
	 * Save the User object and application password array
	 *
	 * @since 1.7
	 * @since 2.0 Update to a new permission system
	 *
	 * @param WP_User $user The current user.
	 * @param array   $item The details about the application password.
	 */
	public function apw_save_info( WP_User $user, array $item ): void {
		if ( in_array( 'administrator', $user->roles, true ) ) {
			$this->is_admin = true;
			return;
		}

		// Get the Fast_Events_Account class.
		$this->get_user_account( $user->ID, $item['uuid'] );
	}


	/**
	 * Fired trigger from a delete application password via the admin ui or REST API
	 *
	 * @since 2.0
	 *
	 * @param int   $user_id The user ID.
	 * @param array $item The details about the application password.
	 */
	public function apw_delete( int $user_id, array $item ): void {
		$this->get_user_account( $user_id, $item['uuid'] );
		if ( isset( $this->account->api_key ) && false !== $this->idx ) {
			array_splice( $this->account->sub_accounts, $this->idx, 1 );
			$this->account->save();
		}
	}


	/**
	 * Check if the user is allowed to use this endpoint
	 *
	 * @since 2.0
	 * @since 2.5.0 Using 'current_user_can'.
	 *
	 * @param null            $response Result to send to the client.
	 * @param WP_REST_Server  $server   Route handler used for the request.
	 * @param WP_REST_Request $request  Request used to generate the response.
	 * @return WP_Error|null
	 */
	public function apw_check_endpoint( $response, WP_REST_Server $server, WP_REST_Request $request ): WP_Error|null {
		// We need this part for the cookie-based authentication.
		if ( is_user_logged_in() && current_user_can( 'manage_options' ) ) {
			$this->is_admin = true;
		}

		if ( ! is_user_logged_in() || $this->is_admin_user() || empty( $this->allowed_endpoints ) ) {
			return null;
		}
		$route = $request->get_route();
		foreach ( $this->allowed_endpoints as $endpoint ) {
			if ( str_contains( $route, $endpoint ) ) {
				return null;
			}
		}
		return new WP_Error( 'not_authorized', __( 'No authorization for this API-endpoint', 'fast-events' ), array( 'status' => 401 ) );
	}


	/**
	 * Check if the user is allowed to log in to the WordPress dashboard
	 *
	 * @since 2.0
	 *
	 * @param object $user Potential WP_User object.
	 *
	 * @return object The event ID to be used by the Payment App
	 */
	public function user_login( object $user ): object {
		if ( is_a( $user, 'WP_User' ) ) {
			$account = new FeAccount( $user->ID );
			if ( 0 === $account->get_user_id() ) {
				return $user;
			}
			if ( isset( $account->api_key ) && true === $account->disable_login ) {
				$this->logger->warning( 'User tried to login with disabled login', array( 'user_id' => $user->ID ) );
				return new WP_Error( 'no_login_allowed', __( 'No authorization for login', 'fast-events' ), array( 'status' => 401 ) );
			}
		}
		return $user;
	}


	/**
	 * Check if the user is allowed to do a password reset
	 *
	 * @since 2.0
	 *
	 * @param bool $allow   Whether to allow the password to be reset.
	 * @param int  $user_id The unique id of the user.
	 * @return bool
	 */
	public function password_reset( bool $allow, int $user_id ): bool {
		$account = new FeAccount( $user_id );
		if ( 0 === $account->get_user_id() ) {
			return true;
		}
		if ( isset( $account->api_key ) && true === $account->disable_login ) {
			return false;
		}
		return true;
	}


	/**
	 * Is this an admin account?
	 *
	 * @since 2.0
	 * @since 2.5.0 Now using 'current_user_can'.
	 *
	 * @return bool Is this an admin account?
	 */
	public function is_admin_user(): bool {
		return ( $this->is_admin || current_user_can( 'manage_options' ) );
	}


	/**
	 * Get the permission array.
	 *
	 * @since 1.6
	 * @since 2.0 New permission structure.
	 *
	 * @return string Permission string.
	 */
	public function get_permissions(): string {
		if ( $this->is_admin ) {
			return str_repeat( '1', self::FE_MAX_PERM );
		} else {
			return $this->permission_list;
		}
	}


	/**
	 * Get the partial query if the authorized events are restricted
	 *
	 * @since 1.4
	 *
	 * @param int    $query_type  The emailaddress and optional event_ids constraints.
	 * @param string $event_table Optional event table name.
	 *
	 * @return string
	 */
	public function get_restricted_events( int $query_type, string $event_table = '' ): string {

		// Is the event array empty e.g. all are allowed?
		if ( empty( $this->allowed_events ) ) {
			return '';
		}

		$query = '';
		switch ( $query_type ) {
			case self::FAST_EVENTS_AUTH_QUERY_EVENTS:
				$query = ' WHERE id IN (' . implode( ',', $this->allowed_events ) . ')';
				break;
			case self::FAST_EVENTS_AUTH_QUERY_ORDERS:
				$query = " AND $event_table.id IN (" . implode( ',', $this->allowed_events ) . ')';
				break;
			case self::FAST_EVENTS_AUTH_QUERY_ORDER_LOOKUP:
				$query = ' AND event_id IN (' . implode( ',', $this->allowed_events ) . ')';
				break;
		}
		return $query;
	}


	/**
	 * Check if the event_id is in the restricted event_ids array.
	 *
	 * @since 1.4
	 *
	 * @param int $id The event id.
	 *
	 * @return bool|WP_Error
	 */
	public function check_event_authorization( int $id ): WP_Error|bool {
		if ( empty( $this->allowed_events ) || in_array( strval( $id ), $this->allowed_events, true ) ) {
			return true;
		} else {
			/* translators: 1: event ID  */
			return new WP_Error( 'not_authorized', sprintf( __( 'No authorization for event [%1$s]', 'fast-events' ), $id ), array( 'status' => 401 ) );
		}
	}


	/**
	 * Return array of authorised event ids.
	 *
	 * @since 1.4
	 *
	 * @return array
	 */
	public function get_authorized_event_ids(): array {
		return $this->allowed_events;
	}


	/**
	 * Check if the user is allowed to execute this action.
	 *
	 * @since 1.0
	 * @since 2.0 New permission system
	 *
	 * @param int $action The required action.
	 *
	 * @return bool|WP_Error
	 */
	public function authorize( int $action ): bool|WP_Error {
		if ( ! is_user_logged_in() ) {
			return new WP_Error( 'rest_no_login', __( 'Not logged in', 'fast-events' ), array( 'status' => rest_authorization_required_code() ) );
		}
		if ( ! isset( $_SERVER['HTTP_X_FE_API_KEY'] ) ) {
			return new WP_Error( 'rest_no_api_key', __( 'No API key found', 'fast-events' ), array( 'status' => rest_authorization_required_code() ) );
		}
		$api_key = sanitize_text_field( wp_unslash( $_SERVER['HTTP_X_FE_API_KEY'] ) );

		// Check the admin user API key.
		if ( $this->is_admin_user() ) {
			if ( 16 !== strlen( $api_key ) || $api_key !== $this->settings->admin_api_key() ) {
				return new WP_Error( 'rest_invalid_api_key', __( 'Invalid Admin API key', 'fast-events' ), array( 'status' => rest_authorization_required_code() ) );
			}
			return true;
		}

		// Do we have a valid Fast_Events_Account account?
		if ( is_null( $this->account ) ) {
			return new WP_Error( 'rest_no_permission', __( 'No permissions found', 'fast-events' ), array( 'status' => rest_authorization_required_code() ) );
		}

		// Has this account a 'fe_account' user role.
		if ( ! in_array( 'fe_account', wp_get_current_user()->roles, true ) ) {
			return new WP_Error( 'rest_wrong_user_role', __( 'Wrong user role for this account', 'fast-events' ), array( 'status' => rest_authorization_required_code() ) );
		}

		// Check the normal user API key.
		if ( 16 !== strlen( $api_key ) || $api_key !== $this->account->api_key ) {
			return new WP_Error( 'rest_invalid_api_key', __( 'Invalid API key', 'fast-events' ), array( 'status' => rest_authorization_required_code() ) );
		}

		// Sub-account blocked?
		if ( $this->blocked ) {
			return new WP_Error( 'rest_user_blocked', __( 'This sub-account is temporarily blocked', 'fast-events' ), array( 'status' => rest_authorization_required_code() ) );
		}

		// Account blocked?
		if ( $this->account->blocked ) {
			return new WP_Error( 'rest_user_blocked', __( 'This account is temporarily blocked', 'fast-events' ), array( 'status' => rest_authorization_required_code() ) );
		}

		if ( self::NO_PERMISSION_NEEDED === $action ) {
			return true;
		}

		// First check if allowed and then, just to be sure on the account level if the permission is allowed.
		if ( '1' === substr( $this->permission_list, $action, 1 ) ) {
			return '1' === substr( $this->account->permission_list, $action, 1 );
		} else {
			return false;
		}
	}

	/**
	 * Return permission structure for the (sub)account.
	 *
	 * @since 2.0
	 *
	 * @param int    $user_id The user ID.
	 * @param string $uuid    The application password uuid.
	 */
	private function get_user_account( int $user_id, string $uuid ): void {
		$this->account = new FeAccount( $user_id );
		if ( 0 === $this->account->get_user_id() || ! isset( $this->account->api_key ) ) { // No account found -> no permission.
			$this->account = null;
			return;
		}

		// Find permission meta-data of (sub)account.
		$this->idx = array_search( $uuid, array_column( $this->account->sub_accounts, 'uuid' ), true );
		if ( false === $this->idx ) { // It must be a Fast_Events_Account.
			if ( $uuid === $this->account->uuid ) {
				$this->blocked         = $this->account->blocked;
				$this->permission_list = $this->account->permission_list;
				if ( ! empty( $this->account->allowed_events ) ) {
					$this->allowed_events = explode( ',', $this->account->allowed_events );
				}
			} else {
				$this->account = null;
				return;
			}
		} else { // It is a Fast_Events_Sub_Account.
			$this->blocked         = $this->account->sub_accounts[ $this->idx ]->blocked || $this->account->blocked;
			$this->permission_list = $this->account->sub_accounts[ $this->idx ]->permission_list;
			if ( ! empty( $this->account->sub_accounts[ $this->idx ]->allowed_events ) ) {
				$this->allowed_events = explode( ',', $this->account->sub_accounts[ $this->idx ]->allowed_events );
			}

			// Check if events are allowed on the account level.
			if ( ! empty( $this->account->allowed_events ) ) {
				if ( empty( $this->allowed_events ) ) {
					$this->allowed_events = explode( ',', $this->account->allowed_events );
				} else {
					$events   = explode( ',', $this->account->allowed_events );
					$new_list = array();
					foreach ( $this->allowed_events as $allowed_event ) {
						if ( in_array( $allowed_event, $events, true ) ) {
							$new_list[] = $allowed_event;
						}
					}
					$this->allowed_events = $new_list;
				}
			}
		}
		$this->allowed_endpoints = explode( ',', $this->account->allowed_endpoints );
	}
}
