<?php
/**
 * Redirect After Login module.
 *
 * @package RedirectAfterLogin
 */

namespace RedirectAfterLogin;

use WP_PowerSuite\Core\AbstractModule;
use WP_PowerSuite\Core\ModuleManager;
use WP_PowerSuite\Core\OptionStoreModuleSettingsRepository;
use WP_Error;
use WP_User;

class Module extends AbstractModule {

	private const MODULE_ID      = 'redirect-after-login';
	private const MODULE_VERSION = '1.0.0';

	/**
	 * Passed as $requested_redirect_to to login_redirect so this module leaves the chain unchanged
	 * (used by Login as User when computing a default destination without applying role/fallback rules).
	 */
	public const REQUESTED_REDIRECT_LOGIN_AS_SENTINEL = '__wpps_login_as__';

	/** @var array<string, mixed>|null */
	private ?array $settings_cache = null;

	/** @var array<int, string>|null */
	private static ?array $canonical_ordered_roles_once = null;

	private static bool $redirect_hooks_registered = false;

	public function getId(): string {
		return self::MODULE_ID;
	}

	public function getName(): string {
		return __( 'Redirect After Login', 'wp-powersuite' );
	}

	public function getVersion(): string {
		return self::MODULE_VERSION;
	}

	public function getDescription(): string {
		return __( 'Pick where users land after login—global default, optional per-role URLs on supported plans, and WooCommerce-friendly behavior.', 'wp-powersuite' );
	}

	public function getContext(): string|array {
		return [ 'frontend', 'ajax' ];
	}

	public function getDependencies(): array {
		return [];
	}

	protected function registerHooks(): void {
		$settings = $this->getSettings();
		if ( ! $this->shouldRegisterLoginRedirect( $settings ) ) {
			return;
		}

		// Logged-in visitors hitting the custom login URL (GET) — not a credential POST.
		if ( OptionStoreModuleSettingsRepository::isModuleEnabled( 'change-login-url' ) ) {
			add_filter( 'wp_powersuite_change_login_logged_in_redirect', [ $this, 'filterChangeLoginLoggedInRedirect' ], 10, 3 );
		}

		add_action( 'login_init', [ $this, 'registerRedirectHooks' ], 0 );
		add_action( 'wp_loaded', [ $this, 'maybeRegisterRedirectHooks' ], 0 );
		add_action( 'wp_login', [ $this, 'registerRedirectHooks' ], 0 );
		add_action( 'wp_ajax_wp_powersuite_login_as_user', [ $this, 'registerRedirectHooks' ], 0 );
		add_action( 'wp_ajax_nopriv_wpps_2fa_verify', [ $this, 'registerRedirectHooks' ], 0 );
		add_action( 'wp_ajax_nopriv_wpps_2fa_login', [ $this, 'registerRedirectHooks' ], 0 );
	}

	/**
	 * Register login redirect filters once per request (wp-login, Woo POST, Login as User preview, etc.).
	 */
	public function registerRedirectHooks(): void {
		if ( self::$redirect_hooks_registered ) {
			return;
		}

		self::$redirect_hooks_registered = true;

		// Default 20: runs after core-style hooks (10) so other plugins can set $redirect_to first; use the
		// wp_powersuite_redirect_after_login_priority filter (e.g. 999) when this module must win over later filters.
		$priority = (int) apply_filters( 'wp_powersuite_redirect_after_login_priority', 20 );
		$priority = max( 1, min( 99999, $priority ) );

		add_filter( 'login_redirect', [ $this, 'filterLoginRedirect' ], $priority, 3 );

		$settings = $this->getSettings();
		if ( ! empty( $settings['woocommerce_sync'] ) && class_exists( 'WooCommerce' ) ) {
			add_filter( 'woocommerce_login_redirect', [ $this, 'filterWooCommerceLoginRedirect' ], $priority, 2 );
		}
	}

	/**
	 * Ensure Redirect After Login filters are registered, then run the standard post-login chain.
	 *
	 * Used by OAuth and other non–wp-login.php flows.
	 */
	public static function applyPostLoginRedirectFilters( string $redirect_to, string $requested_redirect_to, WP_User $user ): string {
		self::ensureRedirectHooksRegistered();

		$redirect_to = apply_filters( 'login_redirect', $redirect_to, $requested_redirect_to, $user );

		if ( class_exists( 'WooCommerce' ) ) {
			$redirect_to = apply_filters( 'woocommerce_login_redirect', (string) $redirect_to, $user );
		}

		return (string) wp_validate_redirect( $redirect_to, admin_url() );
	}

	/**
	 * Register login_redirect filters when the module is enabled but hooks were not yet attached.
	 */
	public static function ensureRedirectHooksRegistered(): void {
		if ( ! OptionStoreModuleSettingsRepository::isModuleEnabled( self::MODULE_ID ) ) {
			return;
		}

		$manager = ModuleManager::getInstance();
		$module  = $manager->getModule( self::MODULE_ID );

		if ( ! $module instanceof self ) {
			try {
				$manager->loadModule( self::MODULE_ID );
				$module = $manager->getModule( self::MODULE_ID );
			} catch ( \Throwable $e ) {
				return;
			}
		}

		if ( $module instanceof self ) {
			$module->registerRedirectHooks();
		}
	}

	/**
	 * Front-end login forms (WooCommerce, etc.) do not run login_init.
	 */
	public function maybeRegisterRedirectHooks(): void {
		if ( self::$redirect_hooks_registered ) {
			return;
		}

		if ( function_exists( 'is_login' ) && is_login() ) {
			$this->registerRedirectHooks();
			return;
		}

		if ( apply_filters( 'wp_powersuite_is_custom_login_url_request', false ) ) {
			$this->registerRedirectHooks();
			return;
		}

		if ( strtoupper( (string) ( $_SERVER['REQUEST_METHOD'] ?? '' ) ) !== 'POST' ) {
			return;
		}

		if ( $this->isLoginCredentialPostRequest() ) {
			$this->registerRedirectHooks();
		}
	}

	/**
	 * Whether this POST carries login credentials (standard or WooCommerce).
	 */
	private function isLoginCredentialPostRequest(): bool {
		if ( strtoupper( (string) ( $_SERVER['REQUEST_METHOD'] ?? '' ) ) !== 'POST' ) {
			return false;
		}

		if ( isset( $_POST['log'], $_POST['pwd'] ) ) {
			return true;
		}

		if ( isset( $_POST['woocommerce-login-nonce'], $_POST['username'], $_POST['password'] ) ) {
			return true;
		}

		return (bool) apply_filters( 'wp_powersuite_redirect_after_login_credential_post', false );
	}

	/**
	 * When Change Login URL sends logged-in visitors away from the login page, match Redirect After Login rules.
	 *
	 * @param array<string, mixed> $query_params Parsed query string from the custom login URL.
	 */
	public function filterChangeLoginLoggedInRedirect( string $redirect, WP_User $user, array $query_params ): string {
		$req = '';
		if ( isset( $query_params['redirect_to'] ) && is_string( $query_params['redirect_to'] ) ) {
			$req = $query_params['redirect_to'];
		}

		return $this->applyLoginRedirectFilter( $redirect, $req, $user, 'change_login_url' );
	}

	/**
	 * Default settings (also used by admin.php).
	 *
	 * @return array<string, mixed>
	 */
	public static function getDefaultSettings(): array {
		return [
			'enable_role_redirects' => false,
			'honor_redirect_to'     => true,
			'woocommerce_sync'      => true,
			'fallback_url'          => '',
			'role_redirects'        => [],
		];
	}

	/**
	 * Whether per-role URLs are enabled (checkbox). If the option was never saved, treat as on when legacy data exists.
	 *
	 * @param array<string, mixed> $raw_stored Options as stored (before defaults merge).
	 * @param array<string, mixed> $merged     Merged with defaults.
	 */
	public static function isEnableRoleRedirectsEffective( array $raw_stored, array $merged ): bool {
		if ( array_key_exists( 'enable_role_redirects', $raw_stored ) ) {
			return ! empty( $merged['enable_role_redirects'] );
		}
		return self::hasNonEmptyRoleRedirectsArray( $merged['role_redirects'] ?? [] );
	}

	/**
	 * @param array<string, mixed> $role_redirects
	 */
	public static function hasNonEmptyRoleRedirectsArray( array $role_redirects ): bool {
		foreach ( $role_redirects as $url ) {
			if ( is_string( $url ) && trim( $url ) !== '' ) {
				return true;
			}
		}
		return false;
	}

	/**
	 * @return array<string, mixed>
	 */
	public function getSettings(): array {
		if ( $this->settings_cache !== null ) {
			return $this->settings_cache;
		}

		$raw     = $this->moduleSettings()->all( [] );
		$raw_arr = is_array( $raw ) ? $raw : [];
		$this->settings_cache = wp_parse_args( $raw_arr, self::getDefaultSettings() );

		if ( ! is_array( $this->settings_cache['role_redirects'] ) ) {
			$this->settings_cache['role_redirects'] = [];
		}

		$this->settings_cache['enable_role_redirects'] = self::isEnableRoleRedirectsEffective( $raw_arr, $this->settings_cache );

		return $this->settings_cache;
	}

	/**
	 * Whether any fallback or licensed per-role URL will change login destinations.
	 *
	 * @param array<string, mixed> $settings Normalized settings (enable_role_redirects resolved).
	 */
	public static function hasActionableRedirectRules( array $settings ): bool {
		$has_fallback = isset( $settings['fallback_url'] ) && trim( (string) $settings['fallback_url'] ) !== '';
		if ( $has_fallback ) {
			return true;
		}

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

		if ( ! function_exists( 'wp_powersuite_is_license_valid' ) || ! wp_powersuite_is_license_valid() ) {
			return false;
		}

		return self::hasNonEmptyRoleRedirectsArray( $settings['role_redirects'] ?? [] );
	}

	/**
	 * Sanitize a redirect destination for storage or runtime use.
	 *
	 * Returns empty string when the URL is invalid or points at a login page (loop prevention).
	 */
	public static function sanitizeRedirectDestinationUrl( string $url ): string {
		$url = trim( $url );
		if ( $url === '' ) {
			return '';
		}

		$url = esc_url_raw( $url );
		if ( $url === '' ) {
			return '';
		}

		$validated = wp_validate_redirect( $url, false );
		if ( ! is_string( $validated ) || $validated === '' ) {
			return '';
		}

		if ( self::isLoginPageUrl( $validated ) ) {
			return '';
		}

		return $validated;
	}

	/**
	 * Validate a redirect URL for settings save; returns WP_Error when input is non-empty but rejected.
	 *
	 * @return string|WP_Error Sanitized URL, empty string, or error.
	 */
	public static function validateRedirectDestinationUrl( string $url ) {
		$raw = trim( $url );
		if ( $raw === '' ) {
			return '';
		}

		$normalized = esc_url_raw( $raw );
		if ( $normalized !== '' && self::isLoginPageUrl( $normalized ) ) {
			return new WP_Error(
				'login_page_redirect',
				__( 'Redirect URLs cannot point to the login page.', 'wp-powersuite' )
			);
		}

		$sanitized = self::sanitizeRedirectDestinationUrl( $raw );
		if ( $sanitized === '' ) {
			return new WP_Error(
				'invalid_redirect_url',
				__( 'Enter a valid same-site URL (absolute or path, e.g. /my-account/).', 'wp-powersuite' )
			);
		}

		return $sanitized;
	}

	/**
	 * Whether a URL resolves to wp-login.php or the Change Login URL slug.
	 */
	public static function isLoginPageUrl( string $url ): bool {
		$url = trim( $url );
		if ( $url === '' ) {
			return false;
		}

		if ( str_starts_with( $url, '/' ) && ! str_starts_with( $url, '//' ) ) {
			$url = home_url( $url );
		}

		$path = wp_parse_url( $url, PHP_URL_PATH );
		if ( ! is_string( $path ) || $path === '' ) {
			return false;
		}

		$path = untrailingslashit( strtolower( $path ) );

		if ( str_ends_with( $path, '/wp-login.php' ) || $path === 'wp-login.php' ) {
			return true;
		}

		if ( ! OptionStoreModuleSettingsRepository::isModuleEnabled( 'change-login-url' ) ) {
			return false;
		}

		$clu_settings = OptionStoreModuleSettingsRepository::forModule( 'change-login-url' )->all( [] );
		$slug         = sanitize_key( (string) ( is_array( $clu_settings ) ? ( $clu_settings['login_slug'] ?? 'login' ) : 'login' ) );
		if ( $slug === '' ) {
			return false;
		}

		$home_path = wp_parse_url( home_url( '/' ), PHP_URL_PATH );
		$home_path = is_string( $home_path ) ? untrailingslashit( strtolower( $home_path ) ) : '';
		$custom    = ( $home_path !== '' ? $home_path : '' ) . '/' . $slug;

		return $path === $custom || str_ends_with( $path, '/' . $slug );
	}

	/**
	 * Register hooks only when role or fallback URLs are set (otherwise WordPress defaults are unchanged).
	 *
	 * @param array<string, mixed> $settings Normalized settings.
	 */
	private function shouldRegisterLoginRedirect( array $settings ): bool {
		return self::hasActionableRedirectRules( $settings );
	}

	/**
	 * Per-role URLs are Premium: only apply when a valid license is active.
	 *
	 * @param array<string, mixed> $settings Normalized settings.
	 * @return array<string, string>
	 */
	private function getLicensedRoleRedirects( array $settings ): array {
		if ( empty( $settings['enable_role_redirects'] ) ) {
			return [];
		}
		if ( ! function_exists( 'wp_powersuite_is_license_valid' ) || ! wp_powersuite_is_license_valid() ) {
			return [];
		}
		$r = $settings['role_redirects'] ?? [];
		return is_array( $r ) ? $r : [];
	}

	/**
	 * @param string           $redirect_to           URL WordPress will use if the filter leaves it unchanged (after core processing).
	 * @param string           $requested_redirect_to Original redirect_to from the request (may be empty).
	 * @param \WP_User|\WP_Error $user              User on success; may be error in edge cases — bail if not WP_User.
	 */
	public function filterLoginRedirect( string $redirect_to, string $requested_redirect_to, $user ): string {
		return $this->applyLoginRedirectFilter( $redirect_to, $requested_redirect_to, $user, 'wordpress' );
	}

	/**
	 * @param string  $redirect WC default redirect.
	 * @param WP_User $user     Logged-in user.
	 */
	public function filterWooCommerceLoginRedirect( string $redirect, $user ): string {
		if ( ! $user instanceof WP_User ) {
			return $redirect;
		}

		$requested = isset( $_REQUEST['redirect'] ) ? sanitize_text_field( wp_unslash( (string) $_REQUEST['redirect'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
		if ( $requested === '' && isset( $_REQUEST['redirect_to'] ) ) {
			$requested = sanitize_text_field( wp_unslash( (string) $_REQUEST['redirect_to'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
		}

		return $this->applyLoginRedirectFilter( $redirect, $requested, $user, 'woocommerce' );
	}

	/**
	 * @param 'wordpress'|'woocommerce'|'change_login_url' $context
	 */
	private function applyLoginRedirectFilter( string $redirect_to, string $requested_redirect_to, $user, string $context ): string {
		if ( ! $user instanceof WP_User ) {
			return $redirect_to;
		}

		$allowed_ctx = [ 'wordpress', 'woocommerce', 'change_login_url' ];
		if ( ! in_array( $context, $allowed_ctx, true ) ) {
			$context = 'wordpress';
		}

		if ( ! apply_filters( 'wp_powersuite_redirect_after_login_should_apply', true, $context, $redirect_to, $requested_redirect_to, $user ) ) {
			return $redirect_to;
		}

		if ( self::REQUESTED_REDIRECT_LOGIN_AS_SENTINEL === $requested_redirect_to ) {
			return wp_validate_redirect( $redirect_to, home_url( '/' ) );
		}

		if ( $this->isSensitiveLoginFlow() ) {
			return $this->resolveSensitiveLoginRedirect( $redirect_to, $requested_redirect_to );
		}

		$resolved = $this->resolveRedirect( $redirect_to, $requested_redirect_to, $user );
		$fallback = wp_validate_redirect( $redirect_to, admin_url() );

		$final = wp_validate_redirect( $resolved, $fallback );
		if ( self::isLoginPageUrl( $final ) ) {
			$final = $fallback;
		}

		return (string) apply_filters(
			'wp_powersuite_login_redirect_url',
			$final,
			$user,
			$this->getSettings()
		);
	}

	/**
	 * Reauth and interim-login must preserve the intended admin/deep-link destination.
	 */
	private function isSensitiveLoginFlow(): bool {
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended
		if ( ! empty( $_REQUEST['reauth'] ) ) {
			return true;
		}

		// phpcs:ignore WordPress.Security.NonceVerification.Recommended
		$interim = isset( $_REQUEST['interim-login'] ) ? (string) wp_unslash( $_REQUEST['interim-login'] ) : '';
		if ( $interim === '1' ) {
			return true;
		}

		return false;
	}

	private function resolveSensitiveLoginRedirect( string $redirect_to, string $requested_redirect_to ): string {
		if ( $requested_redirect_to !== '' ) {
			$validated_request = wp_validate_redirect( $requested_redirect_to, false );
			if ( is_string( $validated_request ) && $validated_request !== '' && ! self::isLoginPageUrl( $validated_request ) ) {
				return $validated_request;
			}
		}

		$validated = wp_validate_redirect( $redirect_to, admin_url() );
		return self::isLoginPageUrl( $validated ) ? admin_url() : $validated;
	}

	private function resolveRedirect( string $redirect_to, string $requested_redirect_to, WP_User $user ): string {
		$settings = $this->getSettings();

		if ( ! empty( $settings['honor_redirect_to'] ) && $requested_redirect_to !== '' ) {
			$validated_request = wp_validate_redirect( $requested_redirect_to, false );
			if ( is_string( $validated_request ) && $validated_request !== '' && ! self::isLoginPageUrl( $validated_request ) ) {
				return $validated_request;
			}
		}

		$role_url = $this->pickRoleRedirectUrl( $user, $this->getLicensedRoleRedirects( $settings ) );
		if ( $role_url !== '' ) {
			$validated = self::sanitizeRedirectDestinationUrl( $role_url );
			if ( $validated !== '' ) {
				return $validated;
			}
		}

		$fb = isset( $settings['fallback_url'] ) ? trim( (string) $settings['fallback_url'] ) : '';
		if ( $fb !== '' ) {
			$validated_fb = self::sanitizeRedirectDestinationUrl( $fb );
			if ( $validated_fb !== '' ) {
				return $validated_fb;
			}
		}

		return $redirect_to;
	}

	/**
	 * Pick the first matching role URL using a stable role order (core roles first, then alphabetical).
	 *
	 * @param array<string, string> $role_redirects
	 */
	private function pickRoleRedirectUrl( WP_User $user, array $role_redirects ): string {
		if ( empty( $role_redirects ) || empty( $user->roles ) ) {
			return '';
		}

		foreach ( $this->orderedRoleKeys( array_keys( $role_redirects ) ) as $role ) {
			if ( ! isset( $role_redirects[ $role ] ) ) {
				continue;
			}
			if ( ! in_array( $role, (array) $user->roles, true ) ) {
				continue;
			}
			$url = trim( (string) $role_redirects[ $role ] );
			if ( $url !== '' ) {
				return $url;
			}
		}

		return '';
	}

	/**
	 * @param array<int, string> $roles_with_rules Role keys that have a configured URL.
	 * @return array<int, string>
	 */
	private function orderedRoleKeys( array $roles_with_rules ): array {
		if ( self::$canonical_ordered_roles_once === null ) {
			if ( ! function_exists( 'wp_roles' ) ) {
				require_once ABSPATH . 'wp-admin/includes/user.php';
			}

			$all = array_keys( wp_roles()->roles );
			sort( $all, SORT_STRING );

			$core_priority = [ 'administrator', 'editor', 'author', 'contributor', 'subscriber' ];
			$ordered       = array_values( array_intersect( $core_priority, $all ) );
			$rest          = array_diff( $all, $ordered );
			sort( $rest, SORT_STRING );

			self::$canonical_ordered_roles_once = array_merge( $ordered, $rest );
		}

		$canonical_order = self::$canonical_ordered_roles_once;

		$with_rules = array_intersect( $canonical_order, $roles_with_rules );
		$extras     = array_diff( $roles_with_rules, $with_rules );
		sort( $extras, SORT_STRING );

		return array_merge( array_values( $with_rules ), $extras );
	}
}
