<?php

/**
 * Impersonator Service
 *
 * Core session switching logic: cookie management, user switching,
 * capability checks, and return-to-admin flow.
 *
 * @package ArraySubs\Features\LoginAsUser\Services
 * @since 1.0.0
 */

namespace ArraySubs\Features\LoginAsUser\Services;

defined('ABSPATH') || exit;

/**
 * Handles user impersonation (login-as-user) session management.
 */
class Impersonator
{
    /**
     * Enable auto-loading.
     *
     * @var bool
     */
    public static $loadable = true;

    /**
     * Constructor — registers WordPress hooks for the impersonation flow.
     */
    public function __construct()
    {
        add_filter('user_has_cap', [$this, 'filterUserHasCap'], 10, 4);
        add_filter('map_meta_cap', [$this, 'filterMapMetaCap'], 10, 4);
        add_action('init', [$this, 'handleAction']);
        add_action('wp_logout', [$this, 'clearOldUserCookie']);
        add_action('wp_login', [$this, 'clearOldUserCookie']);
        add_filter('removable_query_args', [$this, 'filterRemovableQueryArgs']);

        // Register login_form_* hooks so wp-login.php recognises our custom
        // actions and does not silently reset them to the default login form.
        add_action('login_form_arraysubs_login_as_user', [$this, 'handleSwitchToUser']);
        add_action('login_form_arraysubs_login_as_olduser', [$this, 'handleSwitchBack']);
    }

    /**
     * Build the nonce-secured URL to start impersonating a user.
     *
     * @param \WP_User $target_user Target user to impersonate.
     * @return string|false URL or false if not allowed.
     */
    public static function buildLoginUrl(\WP_User $target_user)
    {
        $old_user = self::getOldUser();

        if ($old_user && $old_user->ID === $target_user->ID) {
            return self::buildBackUrl($old_user);
        }

        if (!current_user_can('arraysubs_login_as_user', $target_user->ID)) {
            return false;
        }

        $params = [
            'action'   => 'arraysubs_login_as_user',
            'user_id'  => $target_user->ID,
            '_wpnonce' => wp_create_nonce('arraysubs_login_as_user_' . $target_user->ID),
        ];

        // During REST requests the server-side URL is the API endpoint, not
        // the admin page the user is viewing.  Hash fragments (React routes)
        // are never sent to the server, so the JS caller must append back_url.
        if (!(defined('REST_REQUEST') && REST_REQUEST)) {
            // rawurlencode() is required because add_query_arg() does NOT encode
            // values (build_query passes $urlencode = false). Without it, any &
            // in the admin URL leaks into the parent query string and can override
            // the action parameter (e.g. &action=edit from the orders page).
            $params['back_url'] = rawurlencode(self::getCurrentAdminUrl());
        }

        return add_query_arg($params, wp_login_url());
    }

    /**
     * Build the nonce-secured URL to return to the original admin account.
     *
     * @param \WP_User $old_user The original admin user.
     * @return string
     */
    public static function buildBackUrl(\WP_User $old_user): string
    {
        return add_query_arg(
            [
                'action'   => 'arraysubs_login_as_olduser',
                '_wpnonce' => wp_create_nonce('arraysubs_login_as_olduser_' . $old_user->ID),
            ],
            wp_login_url()
        );
    }

    /**
     * Check whether the current session is an impersonated session.
     *
     * @return bool
     */
    public static function isImpersonating(): bool
    {
        return self::getOldUser() instanceof \WP_User;
    }

    /**
     * Route the init action to the correct handler (switch or switch-back).
     *
     * @return void
     */
    public function handleAction(): void
    {
        if (!isset($_REQUEST['action'])) {
            return;
        }

        // phpcs:ignore WordPress.Security.NonceVerification.Recommended
        $action = sanitize_text_field(wp_unslash($_REQUEST['action']));

        if ('arraysubs_login_as_user' === $action) {
            $this->handleSwitchToUser();
        }

        if ('arraysubs_login_as_olduser' === $action) {
            $this->handleSwitchBack();
        }
    }

    /**
     * Handle the "switch to customer" action.
     *
     * Also used as login_form_arraysubs_login_as_user callback.
     *
     * @return void
     */
    public function handleSwitchToUser(): void
    {
        $user_id = filter_input(INPUT_GET, 'user_id', FILTER_VALIDATE_INT);

        if (!$user_id) {
            $user_id = filter_input(INPUT_POST, 'user_id', FILTER_VALIDATE_INT);
        }

        if (!$user_id) {
            wp_die(esc_html__('Could not login as user.', 'arraysubs'));
        }

        if (!current_user_can('arraysubs_login_as_user', $user_id)) {
            wp_die(esc_html__('Could not login as user.', 'arraysubs'));
        }

        check_admin_referer('arraysubs_login_as_user_' . $user_id);

        $acting_admin_id = get_current_user_id();

        $this->preventWooCartClearing();

        $user = $this->switchToUser($user_id, self::rememberMe());
        if (!$user) {
            wp_die(esc_html__('Could not login as user.', 'arraysubs'));
        }

        do_action('arraysubs_login_as_user_started', $user_id, $acting_admin_id);

        $redirect_to = $this->getLoginRedirect($user);
        wp_safe_redirect($redirect_to, 302, 'ArraySubs');
        exit;
    }

    /**
     * Handle the "switch back to admin" action.
     *
     * Also used as login_form_arraysubs_login_as_olduser callback.
     *
     * @return void
     */
    public function handleSwitchBack(): void
    {
        $old_user = self::getOldUser();

        if (!$old_user) {
            wp_die(esc_html__('Could not switch back.', 'arraysubs'));
        }

        if (!self::authenticateOldUser($old_user)) {
            wp_die(esc_html__('Could not switch back.', 'arraysubs'));
        }

        check_admin_referer('arraysubs_login_as_olduser_' . $old_user->ID);

        $this->preventWooCartClearing();

        $impersonated_user_id = get_current_user_id();

        $switched = $this->switchToUser($old_user->ID, self::rememberMe(), false);

        if (!$switched) {
            wp_die(esc_html__('Could not switch back.', 'arraysubs'));
        }

        do_action('arraysubs_login_as_user_ended', $impersonated_user_id, $old_user->ID);

        $back_url = $this->getBackUrlCookie();
        $redirect = !empty($back_url) ? $back_url : admin_url('users.php');

        wp_safe_redirect($redirect, 302, 'ArraySubs');
        exit;
    }

    /**
     * Switch the current session to the given user.
     *
     * @param int  $user_id      Target user ID.
     * @param bool $remember     Persist session.
     * @param bool $set_old_user Whether to store the current user as the "old" user.
     * @return false|\WP_User
     */
    private function switchToUser(int $user_id, bool $remember = false, bool $set_old_user = true)
    {
        $user = get_userdata($user_id);

        if (!$user) {
            return false;
        }

        $old_user_id = is_user_logged_in() ? get_current_user_id() : false;
        $old_token   = function_exists('wp_get_session_token') ? wp_get_session_token() : '';
        $auth_cookie = $this->getAuthCookie();
        $cookie_parts = wp_parse_auth_cookie(end($auth_cookie));

        if ($set_old_user && $old_user_id) {
            $new_token = '';
            $this->setOldUserCookie($old_user_id, false, $old_token);
        } else {
            $new_token = isset($cookie_parts['token']) ? $cookie_parts['token'] : '';
            $this->clearOldUserCookie(false);
        }

        $session_filter = function (array $session) use ($old_user_id, $old_token) {
            $session['arraysubs_impersonated_from_id']    = $old_user_id;
            $session['arraysubs_impersonated_from_token'] = $old_token;
            return $session;
        };

        add_filter('attach_session_information', $session_filter, 99, 2);

        $this->manualClearAuthCookies();
        wp_set_auth_cookie($user_id, $remember, '', $new_token);
        wp_set_current_user($user_id);

        remove_filter('attach_session_information', $session_filter, 99);

        if (!$set_old_user && $old_token && $old_user_id) {
            $manager = \WP_Session_Tokens::get_instance($old_user_id);
            $manager->destroy($old_token);
        }

        if (function_exists('WC') && class_exists('WooCommerce')) {
            $wc = call_user_func('WC');
            if ($wc && apply_filters('arraysubs_login_as_user_forget_wc_session', true, $user_id, $old_user_id)) {
                if (property_exists($wc, 'session') && $wc->session && method_exists($wc->session, 'forget_session')) {
                    $wc->session->forget_session();
                }
            }
        }

        return $user;
    }

    /**
     * Get the redirect URL after impersonating a user.
     *
     * @param \WP_User $user The target user.
     * @return string
     */
    private function getLoginRedirect(\WP_User $user): string
    {
        if (!current_user_can('read')) {
            return home_url('/');
        }

        $redirect_to = apply_filters('arraysubs_login_as_user_redirect', home_url('/'), $user);

        return $redirect_to;
    }

    /**
     * Prevent WooCommerce from clearing carts during impersonation switches.
     *
     * @return void
     */
    private function preventWooCartClearing(): void
    {
        add_filter('woocommerce_clear_cart_on_logout', '__return_false', 999);
        add_filter('woocommerce_clear_cart_on_login', '__return_false', 999);
        add_filter('woocommerce_clear_cart_on_new_login', '__return_false', 999);
    }

    /**
     * Returns whether the current user is being "remembered" via a persistent cookie.
     *
     * @return bool
     */
    public static function rememberMe(): bool
    {
        $cookie_life = apply_filters('auth_cookie_expiration', 259200, get_current_user_id(), false);
        $current     = wp_parse_auth_cookie('', 'logged_in');

        if (!$current || !isset($current['expiration'])) {
            return false;
        }

        return (($current['expiration'] - time()) > $cookie_life);
    }

    /**
     * Grant the arraysubs_login_as_user capability to admins who can edit the target user.
     *
     * @param bool[]   $user_caps     User capabilities.
     * @param string[] $required_caps Required caps.
     * @param array    $args          Arguments.
     * @param \WP_User $user          The user being checked.
     * @return bool[]
     */
    public function filterUserHasCap(array $user_caps, array $required_caps, array $args, \WP_User $user): array
    {
        if (!isset($args[0], $args[2]) || 'arraysubs_login_as_user' !== $args[0]) {
            return $user_caps;
        }

        $target_user_id = (int) $args[2];

        // Only users who can edit other users may impersonate.
        if (!user_can($user->ID, 'edit_user', $target_user_id)) {
            return $user_caps;
        }

        // Cannot impersonate yourself.
        if ($target_user_id === $user->ID) {
            return $user_caps;
        }

        // Prevent impersonating other administrators.
        $target_user = get_userdata($target_user_id);
        if ($target_user && user_can($target_user, 'manage_options')) {
            return $user_caps;
        }

        $user_caps['arraysubs_login_as_user'] = true;

        return $user_caps;
    }

    /**
     * Disallow a Super Admin from impersonating themselves.
     *
     * @param string[] $required_caps Capabilities.
     * @param string   $cap           Capability being checked.
     * @param int      $user_id       User ID.
     * @param array    $args          Extra arguments.
     * @return string[]
     */
    public function filterMapMetaCap(array $required_caps, string $cap, int $user_id, array $args): array
    {
        if ('arraysubs_login_as_user' === $cap && isset($args[0]) && (int) $args[0] === $user_id) {
            $required_caps[] = 'do_not_allow';
        }

        return $required_caps;
    }

    /**
     * Remove query args left by the switch action from the URL.
     *
     * @param string[] $args Removable query args.
     * @return string[]
     */
    public function filterRemovableQueryArgs(array $args): array
    {
        $args[] = 'arraysubs_logged_in_as';
        return $args;
    }

    // ------------------------------------------------------------------
    // Cookie helpers
    // ------------------------------------------------------------------

    /**
     * Set the old-user cookies that track the original admin.
     *
     * @param int    $old_user_id Original admin user ID.
     * @param bool   $pop         Pop the latest entry instead of pushing.
     * @param string $token       Session token.
     * @return void
     */
    private function setOldUserCookie(int $old_user_id, bool $pop = false, string $token = ''): void
    {
        $secure_auth    = self::secureAuthCookie();
        $secure_olduser = self::secureOldUserCookie();
        $expiration     = time() + 259200; // 3 days.
        $auth_cookie    = $this->getAuthCookie();
        $olduser_cookie = wp_generate_auth_cookie($old_user_id, $expiration, 'logged_in', $token);

        if ($secure_auth) {
            $auth_cookie_name = 'arraysubs_lau_secure_' . COOKIEHASH;
            $scheme           = 'secure_auth';
        } else {
            $auth_cookie_name = 'arraysubs_lau_' . COOKIEHASH;
            $scheme           = 'auth';
        }

        if ($pop) {
            array_pop($auth_cookie);
        } else {
            $auth_cookie[] = wp_generate_auth_cookie($old_user_id, $expiration, $scheme, $token);
        }

        $auth_cookie_json = wp_json_encode($auth_cookie);

        if (!apply_filters('send_auth_cookies', true)) {
            return;
        }

        setcookie($auth_cookie_name, $auth_cookie_json, $expiration, SITECOOKIEPATH, COOKIE_DOMAIN, $secure_auth, true);
        setcookie('arraysubs_lau_olduser_' . COOKIEHASH, $olduser_cookie, $expiration, COOKIEPATH, COOKIE_DOMAIN, $secure_olduser, true);

        $get_back_url = filter_input(INPUT_GET, 'back_url', FILTER_SANITIZE_URL);
        $get_back_url = $get_back_url ? esc_url_raw($get_back_url) : '';
        setcookie('arraysubs_lau_backurl_' . COOKIEHASH, $get_back_url, $expiration, COOKIEPATH, COOKIE_DOMAIN, $secure_olduser, true);
    }

    /**
     * Clear old-user cookies.
     *
     * @param bool $clear_all Whether to wipe all cookies or just pop the latest.
     * @return void
     */
    public function clearOldUserCookie($clear_all = true): void
    {
        $auth_cookie = $this->getAuthCookie();

        if (!empty($auth_cookie)) {
            array_pop($auth_cookie);
        }

        if ($clear_all || empty($auth_cookie)) {
            if (!apply_filters('send_auth_cookies', true)) {
                return;
            }

            $expire = time() - YEAR_IN_SECONDS;
            setcookie('arraysubs_lau_' . COOKIEHASH, ' ', $expire, SITECOOKIEPATH, COOKIE_DOMAIN);
            setcookie('arraysubs_lau_secure_' . COOKIEHASH, ' ', $expire, SITECOOKIEPATH, COOKIE_DOMAIN);
            setcookie('arraysubs_lau_olduser_' . COOKIEHASH, ' ', $expire, COOKIEPATH, COOKIE_DOMAIN);
            setcookie('arraysubs_lau_backurl_' . COOKIEHASH, ' ', $expire, COOKIEPATH, COOKIE_DOMAIN);
        } else {
            $scheme = self::secureAuthCookie() ? 'secure_auth' : 'auth';

            $old_cookie  = end($auth_cookie);
            $old_user_id = wp_validate_auth_cookie($old_cookie, $scheme);

            if ($old_user_id) {
                $parts = wp_parse_auth_cookie($old_cookie, $scheme);
                $this->setOldUserCookie($old_user_id, true, $parts['token'] ?? '');
            }
        }
    }

    /**
     * Get the old-user (admin) from the stored cookie.
     *
     * @return false|\WP_User
     */
    public static function getOldUser()
    {
        $cookie_name = 'arraysubs_lau_olduser_' . COOKIEHASH;

        if (!isset($_COOKIE[$cookie_name])) {
            return false;
        }

        $cookie = wp_unslash($_COOKIE[$cookie_name]);
        $old_user_id = wp_validate_auth_cookie($cookie, 'logged_in');

        if ($old_user_id) {
            return get_userdata($old_user_id);
        }

        return false;
    }

    /**
     * Authenticate the old user from the auth cookie stack.
     *
     * @param \WP_User $user The claimed old user.
     * @return bool
     */
    private static function authenticateOldUser(\WP_User $user): bool
    {
        $secure = self::secureAuthCookie();
        $cookie_name = $secure
            ? 'arraysubs_lau_secure_' . COOKIEHASH
            : 'arraysubs_lau_' . COOKIEHASH;

        if (!isset($_COOKIE[$cookie_name]) || !is_string($_COOKIE[$cookie_name])) {
            return false;
        }

        $cookie = json_decode(wp_unslash($_COOKIE[$cookie_name]), true);

        if (!is_array($cookie) || empty($cookie)) {
            return false;
        }

        $scheme = $secure ? 'secure_auth' : 'auth';
        $old_user_id = wp_validate_auth_cookie(end($cookie), $scheme);

        return $old_user_id && ($user->ID === $old_user_id);
    }

    /**
     * Get the back-URL cookie value.
     *
     * @return string
     */
    private function getBackUrlCookie(): string
    {
        $cookie_name = 'arraysubs_lau_backurl_' . COOKIEHASH;

        if (isset($_COOKIE[$cookie_name])) {
            return wp_unslash($_COOKIE[$cookie_name]);
        }

        return '';
    }

    /**
     * Get the auth cookie stack (array of auth cookies for nested impersonation).
     *
     * @return array
     */
    private function getAuthCookie(): array
    {
        $secure = self::secureAuthCookie();
        $cookie_name = $secure
            ? 'arraysubs_lau_secure_' . COOKIEHASH
            : 'arraysubs_lau_' . COOKIEHASH;

        if (isset($_COOKIE[$cookie_name]) && is_string($_COOKIE[$cookie_name])) {
            $cookie = json_decode(wp_unslash($_COOKIE[$cookie_name]), true);
        }

        if (!isset($cookie) || !is_array($cookie)) {
            $cookie = [];
        }

        return $cookie;
    }

    /**
     * Should the auth cookie be secure?
     *
     * @return bool
     */
    private static function secureAuthCookie(): bool
    {
        return is_ssl() && 'https' === wp_parse_url(wp_login_url(), PHP_URL_SCHEME);
    }

    /**
     * Should the old-user cookie be secure?
     *
     * @return bool
     */
    private static function secureOldUserCookie(): bool
    {
        return is_ssl() && 'https' === wp_parse_url(home_url(), PHP_URL_SCHEME);
    }

    /**
     * Get the current admin page URL for back-URL tracking.
     *
     * @return string
     */
    private static function getCurrentAdminUrl(): string
    {
        if (!isset($_SERVER['HTTP_HOST'], $_SERVER['REQUEST_URI'])) {
            return admin_url('users.php');
        }

        $protocol = is_ssl() ? 'https://' : 'http://';

        return $protocol . sanitize_text_field(wp_unslash($_SERVER['HTTP_HOST'])) . sanitize_text_field(wp_unslash($_SERVER['REQUEST_URI']));
    }

    /**
     * Manually clear WordPress authentication cookies without triggering the clear_auth_cookie action.
     * Prevents conflicts with other plugins that hook into that action.
     *
     * @return void
     */
    private function manualClearAuthCookies(): void
    {
        if (!apply_filters('send_auth_cookies', true, 0, 0, 0, '', '')) {
            return;
        }

        $user_id = get_current_user_id();

        setcookie(AUTH_COOKIE, ' ', time() - YEAR_IN_SECONDS, ADMIN_COOKIE_PATH, COOKIE_DOMAIN);
        setcookie(SECURE_AUTH_COOKIE, ' ', time() - YEAR_IN_SECONDS, ADMIN_COOKIE_PATH, COOKIE_DOMAIN);
        setcookie(AUTH_COOKIE, ' ', time() - YEAR_IN_SECONDS, PLUGINS_COOKIE_PATH, COOKIE_DOMAIN);
        setcookie(SECURE_AUTH_COOKIE, ' ', time() - YEAR_IN_SECONDS, PLUGINS_COOKIE_PATH, COOKIE_DOMAIN);
        setcookie(LOGGED_IN_COOKIE, ' ', time() - YEAR_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN);
        setcookie(LOGGED_IN_COOKIE, ' ', time() - YEAR_IN_SECONDS, SITECOOKIEPATH, COOKIE_DOMAIN);

        setcookie('wp-settings-' . $user_id, ' ', time() - YEAR_IN_SECONDS, SITECOOKIEPATH);
        setcookie('wp-settings-time-' . $user_id, ' ', time() - YEAR_IN_SECONDS, SITECOOKIEPATH);

        setcookie(AUTH_COOKIE, ' ', time() - YEAR_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN);
        setcookie(AUTH_COOKIE, ' ', time() - YEAR_IN_SECONDS, SITECOOKIEPATH, COOKIE_DOMAIN);
        setcookie(SECURE_AUTH_COOKIE, ' ', time() - YEAR_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN);
        setcookie(SECURE_AUTH_COOKIE, ' ', time() - YEAR_IN_SECONDS, SITECOOKIEPATH, COOKIE_DOMAIN);

        setcookie(USER_COOKIE, ' ', time() - YEAR_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN);
        setcookie(PASS_COOKIE, ' ', time() - YEAR_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN);
        setcookie(USER_COOKIE, ' ', time() - YEAR_IN_SECONDS, SITECOOKIEPATH, COOKIE_DOMAIN);
        setcookie(PASS_COOKIE, ' ', time() - YEAR_IN_SECONDS, SITECOOKIEPATH, COOKIE_DOMAIN);

        setcookie('wp-postpass_' . COOKIEHASH, ' ', time() - YEAR_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN);
    }
}
