<?php
/**
 * Concurrent session limits by role.
 *
 * @package LoginSessionManager
 */

namespace LoginSessionManager\Includes;

use WP_User;

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

class Session_Limiter {

	private const NOTICE_META_KEY = '_wpps_lsm_limit_notice';

	private Session_Repository $repository;

	/** @var callable(): array<string, mixed> */
	private $settings_provider;

	private static bool $notice_rendered = false;

	/**
	 * @param callable(): array<string, mixed> $settings_provider
	 */
	public function __construct( Session_Repository $repository, callable $settings_provider ) {
		$this->repository        = $repository;
		$this->settings_provider = $settings_provider;
	}

	public function register_hooks(): void {
		if ( empty( $this->get_settings()['manager_enabled'] ) ) {
			return;
		}

		add_action( 'wp_login', [ $this, 'enforce_on_login' ], 20, 2 );
		add_action( 'set_logged_in_cookie', [ $this, 'enforce_on_cookie' ], 20, 6 );
		add_action( 'init', [ $this, 'enforce_on_init' ], 20 );
		add_action( 'admin_notices', [ $this, 'show_limit_notice' ] );
		add_action( 'wp_enqueue_scripts', [ $this, 'maybe_enqueue_notice_assets' ], 20 );
		add_action( 'wp_body_open', [ $this, 'show_frontend_limit_notice' ], 5 );
		add_action( 'wp_footer', [ $this, 'show_frontend_limit_notice' ], 5 );

		if ( class_exists( 'WooCommerce' ) ) {
			add_action( 'woocommerce_before_account_navigation', [ $this, 'show_wc_limit_notice' ], 5 );
		}
	}

	/**
	 * @return array<string, mixed>
	 */
	private function get_settings(): array {
		$settings = ( $this->settings_provider )();

		return is_array( $settings ) ? $settings : [];
	}

	public function enforce_on_login( string $user_login, WP_User $user ): void {
		unset( $user_login );
		$this->enforce_for_user( $user, true );
	}

	/**
	 * @param mixed $logged_in_cookie
	 * @param mixed $expire
	 * @param mixed $expiration
	 * @param mixed $user_id
	 * @param mixed $scheme
	 * @param mixed $token
	 */
	public function enforce_on_cookie( $logged_in_cookie, $expire, $expiration, $user_id, $scheme, $token ): void {
		unset( $logged_in_cookie, $expire, $expiration, $scheme, $token );

		$user_id = (int) $user_id;
		if ( $user_id < 1 ) {
			return;
		}

		$user = get_userdata( $user_id );
		if ( ! $user instanceof WP_User ) {
			return;
		}

		$this->enforce_for_user( $user, true );
	}

	public function enforce_on_init(): void {
		if ( ! is_user_logged_in() || wp_doing_cron() || wp_doing_ajax() ) {
			return;
		}

		$user = wp_get_current_user();
		if ( ! $user instanceof WP_User || $user->ID < 1 ) {
			return;
		}

		$settings = $this->get_settings();
		$throttle = max( 1, min( 60, (int) ( $settings['init_throttle_minutes'] ?? 10 ) ) );
		$key      = 'wpps_lsm_enforce_' . $user->ID;

		if ( get_transient( $key ) ) {
			return;
		}

		set_transient( $key, 1, $throttle * MINUTE_IN_SECONDS );

		$this->enforce_for_user( $user, false );
	}

	private function enforce_for_user( WP_User $user, bool $is_new_login = false ): void {
		$settings = $this->get_settings();

		if ( empty( $settings['manager_enabled'] ) ) {
			return;
		}

		if ( $this->should_skip_user( $user, $settings ) ) {
			return;
		}

		$max = $this->get_max_sessions_for_user( $user, $settings );
		if ( $max < 1 ) {
			return;
		}

		if ( $is_new_login ) {
			wp_cache_delete( $user->ID, 'user_meta' );
		}

		$count = $this->repository->count_active_sessions( $user->ID );
		if ( $count <= $max ) {
			return;
		}

		$action = (string) ( $settings['limit_exceeded_action'] ?? 'logout_oldest' );

		if ( $action === 'block_login' && $is_new_login ) {
			$this->block_new_login( $user );
			return;
		}

		$destroyed = $this->repository->destroy_oldest_sessions( $user->ID, $max );

		if ( $destroyed !== [] ) {
			Session_Audit_Log::dispatch(
				'limit_enforced',
				0,
				$user->ID,
				'',
				[
					'destroyed_count' => count( $destroyed ),
					'max_sessions'    => $max,
				]
			);

			if ( ! empty( $settings['show_limit_notice'] ) ) {
				$this->flag_limit_notice( $user->ID );
			}
		}
	}

	public function show_limit_notice(): void {
		$this->render_limit_notice_markup( 'admin' );
	}

	public function maybe_enqueue_notice_assets(): void {
		if ( is_admin() || ! is_user_logged_in() ) {
			return;
		}

		$user_id = get_current_user_id();
		if ( $user_id < 1 || ! get_user_meta( $user_id, self::NOTICE_META_KEY, true ) ) {
			return;
		}

		$base     = defined( 'WP_POWERSUITE_PLUGIN_URL' ) ? WP_POWERSUITE_PLUGIN_URL : '';
		$css_path = dirname( __DIR__ ) . '/assets/login-session-manager.css';
		$css_ver  = file_exists( $css_path ) ? (string) filemtime( $css_path ) : '1.1.0';

		wp_enqueue_style(
			'wpps-login-session-manager',
			$base . 'modules/login-session-manager/assets/login-session-manager.css',
			[],
			$css_ver
		);
	}

	public function show_frontend_limit_notice(): void {
		if ( is_admin() ) {
			return;
		}

		$this->render_limit_notice_markup( 'frontend' );
	}

	public function show_wc_limit_notice(): void {
		if ( ! function_exists( 'is_account_page' ) || ! is_account_page() ) {
			return;
		}

		$this->render_limit_notice_markup( 'wc' );
	}

	private function flag_limit_notice( int $user_id ): void {
		if ( $user_id < 1 ) {
			return;
		}

		update_user_meta( $user_id, self::NOTICE_META_KEY, time() );
	}

	private function consume_limit_notice( int $user_id ): bool {
		if ( $user_id < 1 ) {
			return false;
		}

		$flag = get_user_meta( $user_id, self::NOTICE_META_KEY, true );
		if ( empty( $flag ) ) {
			return false;
		}

		delete_user_meta( $user_id, self::NOTICE_META_KEY );

		return true;
	}

	private function render_limit_notice_markup( string $context ): void {
		if ( self::$notice_rendered || ! is_user_logged_in() ) {
			return;
		}

		$user_id = get_current_user_id();
		if ( ! $this->consume_limit_notice( $user_id ) ) {
			return;
		}

		self::$notice_rendered = true;

		$message = __(
			'An older session was ended because this account reached its session limit.',
			'wp-powersuite'
		);

		if ( $context === 'wc' && function_exists( 'wc_print_notice' ) ) {
			wc_print_notice( esc_html( $message ), 'notice' );
			return;
		}

		if ( $context === 'admin' && is_admin() ) {
			printf(
				'<div class="notice notice-info is-dismissible"><p>%s</p></div>',
				esc_html( $message )
			);
			return;
		}

		if ( $context === 'frontend' && ! is_admin() ) {
			printf(
				'<div class="wpps-lsm-limit-notice" role="status"><p>%s</p></div>',
				esc_html( $message )
			);
		}
	}

	private function block_new_login( WP_User $user ): void {
		$verifier = $this->repository->get_current_verifier( $user->ID );
		if ( $verifier !== null ) {
			$this->repository->destroy_session( $user->ID, $verifier );
		}

		wp_logout();

		wp_safe_redirect(
			add_query_arg(
				'wpps_lsm_blocked',
				'1',
				wp_login_url()
			)
		);
		exit;
	}

	/**
	 * @param array<string, mixed> $settings
	 */
	private function should_skip_user( WP_User $user, array $settings ): bool {
		if ( $this->repository->is_temporary_login_session( $user->ID ) ) {
			return true;
		}

		if ( Session_Repository::has_temporary_login_cookie() ) {
			return true;
		}

		if ( $this->repository->is_login_as_session( $user->ID ) ) {
			return true;
		}

		if ( Session_Repository::has_login_as_cookie() ) {
			return true;
		}

		$roles    = is_array( $user->roles ) ? $user->roles : [];
		$excluded = $settings['excluded_roles'] ?? [];

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

		foreach ( $roles as $role ) {
			if ( in_array( $role, $excluded, true ) ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * @param array<string, mixed> $settings
	 */
	private function get_max_sessions_for_user( WP_User $user, array $settings ): int {
		$role_limits = $settings['role_max_sessions'] ?? [];
		if ( ! is_array( $role_limits ) ) {
			return 0;
		}

		$roles = is_array( $user->roles ) ? $user->roles : [];
		$max   = 0;

		foreach ( $roles as $role ) {
			if ( ! isset( $role_limits[ $role ] ) ) {
				continue;
			}

			$role_max = max( 0, (int) $role_limits[ $role ] );
			if ( $role_max < 1 ) {
				continue;
			}

			if ( $max < 1 || $role_max > $max ) {
				$max = $role_max;
			}
		}

		return $max;
	}
}
