<?php
/**
 * Test account manager, temporary subscriber accounts for CI/Playwright pipelines.
 *
 * @package CloudScale_DevTools
 */

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

class CSDT_Test_Accounts {

    public static function get_active_test_accounts(): array {
        $users = get_users(
            array(
				'meta_key'   => 'csdt_test_account', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- meta query required for user session lookup
				'meta_value' => '1', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- meta query required for user session lookup
				'fields'     => array( 'ID', 'user_login' ),
            )
        );

        $accounts = array();
        foreach ( $users as $u ) {
            $expires_at  = (int) get_user_meta( $u->ID, 'csdt_test_expires_at', true );
            $max_logins  = (int) get_user_meta( $u->ID, 'csdt_test_max_logins', true );
            $login_count = (int) get_user_meta( $u->ID, 'csdt_test_login_count', true );
            $accounts[]  = array(
                'user_id'     => $u->ID,
                'username'    => $u->user_login,
                'expires_at'  => $expires_at,
                'expires_in'  => max( 0, $expires_at - time() ),
                'max_logins'  => $max_logins,
                'login_count' => $login_count,
            );
        }

        return $accounts;
    }

    private static function create_test_account( int $ttl = 1800 ): array {
        $username = 'test-' . wp_generate_password( 8, false, false );
        $password = wp_generate_password( 20 );
        $email    = $username . '@test.local';
        $user_id  = wp_create_user( $username, $password, $email );

        if ( is_wp_error( $user_id ) ) {
            return array( 'error' => $user_id->get_error_message() );
        }

        $user = new WP_User( $user_id );
        $user->set_role( 'subscriber' );

        $expires_at = time() + $ttl;
        $max_logins = max( 0, (int) get_option( 'csdt_test_account_max_logins', '1' ) );

        update_user_meta( $user_id, 'csdt_test_account', '1' );
        update_user_meta( $user_id, 'csdt_test_expires_at', $expires_at );
        update_user_meta( $user_id, 'csdt_test_max_logins', $max_logins );
        update_user_meta( $user_id, 'csdt_test_login_count', 0 );

        [ $app_password, $item ] = WP_Application_Passwords::create_new_application_password(
            $user_id,
            array( 'name' => 'playwright-ci' )
        );

        if ( is_wp_error( $app_password ) ) {
            wp_delete_user( $user_id );
            return array( 'error' => $app_password->get_error_message() );
        }

        $formatted_pw = implode( ' ', str_split( $app_password, 4 ) );

        return array(
            'user_id'      => $user_id,
            'username'     => $username,
            'app_password' => $formatted_pw,
            'rest_url'     => rest_url( 'wp/v2/users/me' ),
            'expires_at'   => $expires_at,
            'accounts'     => self::get_active_test_accounts(),
        );
    }

    public static function ajax_create_test_account(): void {
        if ( ! current_user_can( 'manage_options' ) ) {
            wp_send_json_error( 'Forbidden', 403 );
        }
        check_ajax_referer( 'csdt_devtools_login_nonce', 'nonce' );

        $ttl    = (int) get_option( 'csdt_test_account_ttl', '1800' );
        $result = self::create_test_account( $ttl );

        if ( isset( $result['error'] ) ) {
            wp_send_json_error( $result['error'] );
        }

        wp_send_json_success( $result );
    }

    public static function ajax_revoke_test_account(): void {
        if ( ! current_user_can( 'manage_options' ) ) {
            wp_send_json_error( 'Forbidden', 403 );
        }
        check_ajax_referer( 'csdt_devtools_login_nonce', 'nonce' );

        $user_id = absint( wp_unslash( $_POST['user_id'] ?? 0 ) );
        if ( ! $user_id ) {
            wp_send_json_error( 'Missing user_id' );
        }

        if ( get_user_meta( $user_id, 'csdt_test_account', true ) !== '1' ) {
            wp_send_json_error( 'Not a test account' );
        }

        wp_delete_user( $user_id );

        wp_send_json_success( array( 'accounts' => self::get_active_test_accounts() ) );
    }

    public static function ajax_save_test_account_settings(): void {
        if ( ! current_user_can( 'manage_options' ) ) {
            wp_send_json_error( 'Forbidden', 403 );
        }
        check_ajax_referer( 'csdt_devtools_login_nonce', 'nonce' );

        $enabled    = ( sanitize_key( wp_unslash( $_POST['enabled'] ?? '0' ) ) ) === '1' ? '1' : '0';
        $ttl_raw    = sanitize_key( wp_unslash( $_POST['ttl'] ?? '1800' ) );
        $ttl        = in_array( $ttl_raw, array( '300', '600', '1800', '3600', '7200', '86400' ), true ) ? $ttl_raw : '1800';
        $single_use = ( sanitize_key( wp_unslash( $_POST['single_use'] ?? '0' ) ) ) === '1' ? '1' : '0';
        $max_logins = $single_use === '1' ? 1 : max( 0, absint( wp_unslash( $_POST['max_logins'] ?? 0 ) ) );

        update_option( 'csdt_test_accounts_enabled', $enabled );
        update_option( 'csdt_test_account_ttl', $ttl );
        update_option( 'csdt_test_account_single_use', $single_use );
        update_option( 'csdt_test_account_max_logins', (string) $max_logins );

        if ( $enabled === '1' ) {
            if ( ! wp_next_scheduled( 'csdt_cleanup_test_accounts' ) ) {
                wp_schedule_event( time() + 300, 'csdt_every_5min', 'csdt_cleanup_test_accounts' );
            }
        } else {
            wp_clear_scheduled_hook( 'csdt_cleanup_test_accounts' );
        }

        wp_send_json_success();
    }



    public static function cleanup_expired_test_accounts(): void {
        $now = time();

        // 1. Meta-tracked test accounts with an expiry timestamp.
        $users = get_users(
            array(
				'meta_key'   => 'csdt_test_account', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- meta query required for user session lookup
				'meta_value' => '1', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- meta query required for user session lookup
				'fields'     => array( 'ID' ),
            )
        );
        foreach ( $users as $u ) {
            $expires_at = (int) get_user_meta( $u->ID, 'csdt_test_expires_at', true );
            if ( $expires_at && $expires_at < $now ) {
                wp_delete_user( $u->ID );
            }
        }

        // 2. Orphaned test accounts not tracked by meta, sweep by known patterns.
        // @test.local email domain is never a real account; cs_devtools_test* and
        // temp-* usernames with no posts are plugin/debug artifacts safe to remove.
        $orphans = get_users(
            array(
				'fields' => array( 'ID', 'user_login', 'user_email', 'user_registered' ),
				'number' => 200,
            )
        );
        foreach ( $orphans as $u ) {
            $is_test_email = str_ends_with( strtolower( $u->user_email ), '@test.local' );
            $is_test_login = strncmp( $u->user_login, 'cs_devtools_test', 16 ) === 0;
            $is_temp_login = strncmp( $u->user_login, 'temp-', 5 ) === 0
                            && strtotime( $u->user_registered ) < $now - DAY_IN_SECONDS
                            && (int) count_user_posts( $u->ID ) === 0;
            if ( $is_test_email || $is_test_login || $is_temp_login ) {
                wp_delete_user( $u->ID );
            }
        }
    }

    /* ── Playwright Admin Roles ──────────────────────────────────────────────── */

    /**
     * Hard-block password-based logins for Playwright role accounts.
     *
     * The WP password for these accounts is a discarded random string, the only
     * intended auth path is the session API. This filter ensures that even if the
     * password hash were somehow extracted and cracked, the login form cannot be used.
     *
     * @param WP_User|WP_Error|null $user     Result of prior authenticate filters.
     * @param string                $username Submitted username.
     * @return WP_User|WP_Error|null
     */
    public static function block_playwright_password_login( $user, string $username ) {
        if ( ! ( $user instanceof WP_User ) ) {
            return $user;
        }
        $roles_data     = (array) get_option( 'csdt_playwright_roles', array() );
        $playwright_ids = array_column( $roles_data, 'user_id' );
        if ( in_array( $user->ID, $playwright_ids, true ) ) {
            return new WP_Error(
                'csdt_playwright_no_password_login',
                __( 'This account does not support password login.', 'cloudscale-cyber-devtools' )
            );
        }
        return $user;
    }

    public static function get_or_create_secret(): string {
        $secret = get_option( 'csdt_test_session_secret', '' );
        if ( ! $secret ) {
            $secret = wp_generate_password( 32, false, false );
            update_option( 'csdt_test_session_secret', $secret, false );
        }
        return $secret;
    }

    // The path token makes the endpoint URL itself non-guessable, an attacker
    // must enumerate a 32-char alphanumeric string before they can even attempt
    // the secret. Both layers are required to obtain a session.
    public static function get_or_create_path_token(): string {
        $token = get_option( 'csdt_test_session_path_token', '' );
        if ( ! $token ) {
            $token = wp_generate_password( 32, false, false );
            update_option( 'csdt_test_session_path_token', $token, false );
        }
        return $token;
    }

    public static function register_rest_routes(): void {
        // The path_token baked into the URL is the primary auth gate, unguessable without
        // server access. A non-empty secret param is required as a second factor; full
        // hash_equals() validation and rate-limiting happen inside the callback.
        $perm       = static function ( \WP_REST_Request $request ): bool {
            return '' !== (string) $request->get_param( 'secret' );
        };
        $path_token = self::get_or_create_path_token();
        register_rest_route(
            'csdt/v1',
            '/test-session-' . $path_token,
            array(
				'methods'             => 'POST',
				'callback'            => array( __CLASS__, 'rest_test_session' ),
				'permission_callback' => $perm,
			)
        );
        register_rest_route(
            'csdt/v1',
            '/test-logout-' . $path_token,
            array(
				'methods'             => 'POST',
				'callback'            => array( __CLASS__, 'rest_test_logout' ),
				'permission_callback' => $perm,
			)
        );
    }

    public static function rest_test_session( WP_REST_Request $request ): WP_REST_Response {
        // Global lock check (set after 5 bad attempts)
        if ( get_transient( 'csdt_ts_locked' ) ) {
            return new WP_REST_Response( array( 'error' => 'Temporarily locked. Try again in 10 minutes.' ), 429 );
        }

        $secret = (string) ( $request->get_param( 'secret' ) ?? '' );
        $role   = sanitize_key( (string) ( $request->get_param( 'role' ) ?? '' ) );
        $ttl    = min( 3600, max( 60, (int) ( $request->get_param( 'ttl' ) ?? 1200 ) ) );

        $stored = get_option( 'csdt_test_session_secret', '' );

        if ( ! $stored || ! hash_equals( $stored, $secret ) ) {
            $fails = (int) get_transient( 'csdt_tsf' ) + 1;
            set_transient( 'csdt_tsf', $fails, 10 * MINUTE_IN_SECONDS );
            $ip = self::get_client_ip();
            $cc = $ip ? CSDT_Geo::get_country( $ip ) : '';
            // Record every failed API attempt, include country so map works without re-resolving.
            CSDT_Login::record_security_event(
                'api_attack',
                "Test Session API: bad secret (attempt {$fails}/5)",
                "IP: {$ip}" . ( $cc ? " · {$cc}" : '' )
            );
            if ( $fails >= 5 ) {
                set_transient( 'csdt_ts_locked', true, 10 * MINUTE_IN_SECONDS );
                delete_transient( 'csdt_tsf' );
                CSDT_Login::record_security_event( 'api_attack', 'Test Session API LOCKED, 5 bad attempts', "IP: {$ip}" . ( $cc ? " · {$cc}" : '' ) );
                self::send_security_alert( 'Test Session API: 5 failed auth attempts. API locked for 10 min.' );
            }
            return new WP_REST_Response( array( 'error' => 'Invalid secret' ), 401 );
        }

        delete_transient( 'csdt_tsf' );

        if ( ! $role ) {
            return new WP_REST_Response( array( 'error' => 'role is required' ), 400 );
        }

        $roles_data = get_option( 'csdt_playwright_roles', array() );
        if ( ! isset( $roles_data[ $role ] ) ) {
            return new WP_REST_Response( array( 'error' => 'Role not found, create it in the Test Account Manager first.' ), 404 );
        }

        $user_id  = (int) $roles_data[ $role ]['user_id'];
        $userdata = get_userdata( $user_id );
        if ( ! $userdata ) {
            return new WP_REST_Response( array( 'error' => 'Test user missing, re-create the role.' ), 500 );
        }

        // Ensure the user has the expected WP role (heals users created by older code with custom roles).
        $expected_role = $roles_data[ $role ]['wp_role'] ?? 'administrator';
        $user_obj      = new WP_User( $user_id );
        if ( ! in_array( $expected_role, (array) $user_obj->roles, true ) ) {
            $user_obj->set_role( $expected_role );
        }

        $expiration = time() + $ttl;
        $token      = WP_Session_Tokens::get_instance( $user_id )->create( $expiration );
        update_user_meta( $user_id, 'csdt_playwright_last_login', time() );

        $resp = array(
            'username'                => $userdata->user_login,
            'session_token'           => $token,
            'secure_auth_cookie'      => wp_generate_auth_cookie( $user_id, $expiration, 'secure_auth', $token ),
            'logged_in_cookie'        => wp_generate_auth_cookie( $user_id, $expiration, 'logged_in', $token ),
            'secure_auth_cookie_name' => SECURE_AUTH_COOKIE,
            'logged_in_cookie_name'   => LOGGED_IN_COOKIE,
            'cookie_domain'           => wp_parse_url( home_url(), PHP_URL_HOST ),
            'expires_at'              => $expiration,
        );

        // Optional: create a one-time Application Password for REST API calls (e.g. help-docs runner).
        if ( $request->get_param( 'create_app_password' ) === '1' ) {
            [ $raw_pw, $item ] = WP_Application_Passwords::create_new_application_password(
                $user_id,
                array( 'name' => 'helpdocs-' . substr( $token, 0, 8 ) )
            );
            if ( ! is_wp_error( $raw_pw ) ) {
                $resp['app_password']      = $raw_pw;
                $resp['app_password_uuid'] = $item['uuid'];
            }
        }

        return new WP_REST_Response( $resp );
    }

    public static function rest_test_logout( WP_REST_Request $request ): WP_REST_Response {
        $secret = (string) ( $request->get_param( 'secret' ) ?? '' );
        $role   = sanitize_key( (string) ( $request->get_param( 'role' ) ?? '' ) );
        $token  = (string) ( $request->get_param( 'session_token' ) ?? '' );

        $stored = get_option( 'csdt_test_session_secret', '' );
        if ( ! $stored || ! hash_equals( $stored, $secret ) ) {
            return new WP_REST_Response( array( 'error' => 'Invalid secret' ), 401 );
        }
        if ( ! $role ) {
            return new WP_REST_Response( array( 'error' => 'role is required' ), 400 );
        }

        $roles_data = get_option( 'csdt_playwright_roles', array() );
        if ( ! isset( $roles_data[ $role ] ) ) {
            return new WP_REST_Response( array( 'error' => 'Role not found' ), 404 );
        }

        $user_id  = (int) $roles_data[ $role ]['user_id'];
        $sessions = WP_Session_Tokens::get_instance( $user_id );
        if ( $token ) {
            $sessions->destroy( $token );
        } else {
            $sessions->destroy_all();
        }

        // Clean up any associated application password.
        $app_uuid = sanitize_text_field( (string) ( $request->get_param( 'app_password_uuid' ) ?? '' ) );
        if ( $app_uuid ) {
            WP_Application_Passwords::delete_application_password( $user_id, $app_uuid );
        }

        return new WP_REST_Response( array( 'ok' => true ) );
    }

    public static function get_test_users_with_sessions(): array {
        $roles_data = get_option( 'csdt_playwright_roles', array() );
        $result     = array();
        $now        = time();
        foreach ( $roles_data as $name => $info ) {
            $user_id      = (int) ( $info['user_id'] ?? 0 );
            $userdata     = $user_id ? get_userdata( $user_id ) : false;
            $all_sessions = $user_id ? WP_Session_Tokens::get_instance( $user_id )->get_all() : array();
            $active       = array_filter( $all_sessions, fn( $s ) => $s['expiration'] > $now );
            $result[]     = array(
                'name'          => $name,
                'user_id'       => $user_id,
                'username'      => $userdata ? $userdata->user_login : '(deleted)',
                'wp_role'       => $info['wp_role'] ?? '',
                'session_count' => count( $active ),
                'last_login'    => $user_id ? (int) get_user_meta( $user_id, 'csdt_playwright_last_login', true ) : 0,
            );
        }
        return $result;
    }

    private static function get_client_ip(): string {
        // Delegate to the central helper so we never trust client-supplied
        // proxy headers from a non-Cloudflare peer (would let an attacker
        // spoof IPs and bypass the per-IP lockout).
        return CloudScale_DevTools::get_client_ip();
    }

    private static function send_security_alert( string $message ): void {
        CloudScale_Telegram::send( "Security Alert\n\n" . $message, 'Test Accounts', 'warning' );
    }

    public static function ajax_create_playwright_role(): void {
        if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( 'Forbidden', 403 ); }
        check_ajax_referer( 'csdt_devtools_login_nonce', 'nonce' );

        $name    = sanitize_key( wp_unslash( $_POST['name'] ?? $_POST['role_slug'] ?? '' ) );
        $wp_role = sanitize_key( wp_unslash( $_POST['wp_role'] ?? 'administrator' ) );

        if ( ! $name ) {
			wp_send_json_error( 'Name is required' ); }
        if ( strlen( $name ) < 3 || strlen( $name ) > 40 ) {
            wp_send_json_error( 'Name must be 3 to 40 characters' );
        }
        if ( ! get_role( $wp_role ) ) {
            wp_send_json_error( 'Invalid WordPress role' );
        }

        $roles_data = get_option( 'csdt_playwright_roles', array() );
        if ( isset( $roles_data[ $name ] ) ) {
            wp_send_json_error( "A test user named '{$name}' already exists" );
        }

        $username = 'csdt-playwright-' . $name;
        if ( username_exists( $username ) ) {
            wp_send_json_error( "Username '{$username}' is already taken" );
        }

        $user_id = wp_create_user( $username, wp_generate_password( 24 ), $username . '@test.local' );
        if ( is_wp_error( $user_id ) ) {
            wp_send_json_error( $user_id->get_error_message() );
        }

        ( new WP_User( $user_id ) )->set_role( $wp_role );

        $roles_data[ $name ] = array(
			'user_id' => $user_id,
			'wp_role' => $wp_role,
		);
        update_option( 'csdt_playwright_roles', $roles_data, false );
        self::get_or_create_secret();

        wp_send_json_success(
            array(
				'name'     => $name,
				'username' => $username,
				'user_id'  => $user_id,
				'wp_role'  => $wp_role,
				'users'    => self::get_test_users_with_sessions(),
            )
        );
    }

    public static function ajax_delete_playwright_role(): void {
        if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( 'Forbidden', 403 ); }
        check_ajax_referer( 'csdt_devtools_login_nonce', 'nonce' );

        $name = sanitize_key( wp_unslash( $_POST['name'] ?? $_POST['role_slug'] ?? '' ) );
        if ( ! $name ) {
			wp_send_json_error( 'name required' ); }

        $roles_data = get_option( 'csdt_playwright_roles', array() );
        if ( isset( $roles_data[ $name ] ) ) {
            $user_id = (int) ( $roles_data[ $name ]['user_id'] ?? 0 );
            if ( $user_id ) {
                wp_delete_user( $user_id );
            }
            // Clean up any custom WP role created by the old system.
            if ( get_role( $name ) ) {
                remove_role( $name );
            }
            unset( $roles_data[ $name ] );
            update_option( 'csdt_playwright_roles', $roles_data, false );
        }

        wp_send_json_success( array( 'users' => self::get_test_users_with_sessions() ) );
    }

    public static function ajax_kill_test_sessions(): void {
        if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( 'Forbidden', 403 ); }
        check_ajax_referer( 'csdt_devtools_login_nonce', 'nonce' );

        $name = sanitize_key( wp_unslash( $_POST['name'] ?? '' ) );
        if ( ! $name ) {
			wp_send_json_error( 'name required' ); }

        $roles_data = get_option( 'csdt_playwright_roles', array() );
        if ( ! isset( $roles_data[ $name ] ) ) {
			wp_send_json_error( 'Test user not found' ); }

        $user_id = (int) $roles_data[ $name ]['user_id'];
        WP_Session_Tokens::get_instance( $user_id )->destroy_all();

        wp_send_json_success( array( 'users' => self::get_test_users_with_sessions() ) );
    }

    public static function ajax_regen_test_secret(): void {
        if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( 'Forbidden', 403 ); }
        check_ajax_referer( 'csdt_devtools_login_nonce', 'nonce' );

        $secret = wp_generate_password( 32, false, false );
        update_option( 'csdt_test_session_secret', $secret, false );
        wp_send_json_success( array( 'secret' => $secret ) );
    }

    public static function ajax_regen_path_token(): void {
        if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( 'Forbidden', 403 ); }
        check_ajax_referer( 'csdt_devtools_login_nonce', 'nonce' );

        $token = wp_generate_password( 32, false, false );
        update_option( 'csdt_test_session_path_token', $token, false );
        $session_url = rest_url( 'csdt/v1/test-session-' . $token );
        $logout_url  = rest_url( 'csdt/v1/test-logout-' . $token );
        wp_send_json_success(
            array(
				'path_token'  => $token,
				'session_url' => $session_url,
				'logout_url'  => $logout_url,
            )
        );
    }

    public static function ajax_rename_test_user(): void {
        if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( 'Forbidden', 403 ); }
        check_ajax_referer( 'csdt_devtools_login_nonce', 'nonce' );

        $user_id   = isset( $_POST['user_id'] ) ? (int) $_POST['user_id'] : 0;
        $new_login = isset( $_POST['new_login'] ) ? sanitize_user( wp_unslash( $_POST['new_login'] ), true ) : '';

        if ( ! $user_id || ! $new_login ) {
            wp_send_json_error( __( 'User ID and new username are required.', 'cloudscale-cyber-devtools' ) );
        }
        if ( strlen( $new_login ) < 3 || strlen( $new_login ) > 60 ) {
            wp_send_json_error( __( 'Username must be 3 to 60 characters.', 'cloudscale-cyber-devtools' ) );
        }

        // Must be one of our test users.
        $roles_data = get_option( 'csdt_playwright_roles', array() );
        $role_key   = null;
        foreach ( $roles_data as $key => $r ) {
            if ( (int) $r['user_id'] === $user_id ) {
				$role_key = $key;
				break; }
        }
        if ( $role_key === null ) {
            wp_send_json_error( __( 'User is not a registered test account.', 'cloudscale-cyber-devtools' ) );
        }

        // Check new login not already taken.
        if ( username_exists( $new_login ) && get_user_by( 'login', $new_login )->ID !== $user_id ) {
            wp_send_json_error( __( 'That username is already in use.', 'cloudscale-cyber-devtools' ) );
        }

        // WP does not expose a direct way to change user_login, use wpdb.
        global $wpdb;
        $old_login = get_userdata( $user_id )->user_login;
        $updated   = $wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
            $wpdb->users,
            array( 'user_login' => $new_login ),
            array( 'ID' => $user_id ),
            array( '%s' ),
            array( '%d' )
        );

        if ( $updated === false ) {
            wp_send_json_error( __( 'Database error, could not rename user.', 'cloudscale-cyber-devtools' ) );
        }

        // Update display name and nicename to match.
        wp_update_user(
            array(
				'ID'            => $user_id,
				'display_name'  => $new_login,
				'user_nicename' => sanitize_title( $new_login ),
            )
        );

        // Kill all sessions for this user so they re-auth with the new credentials.
        WP_Session_Tokens::get_instance( $user_id )->destroy_all();

        // Update the stored username in roles data.
        $roles_data[ $role_key ]['username'] = $new_login;
        update_option( 'csdt_playwright_roles', $roles_data, false );

        wp_send_json_success(
            array(
				'old_login' => $old_login,
				'new_login' => $new_login,
            )
        );
    }

    /**
     * Returns the number of non-test users who have at least one app password, plus the total password count.
     *
     * @return array{ users: int, passwords: int }
     */
    public static function get_app_password_usage(): array {
        global $wpdb;
        $rows  = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
            "SELECT user_id, meta_value FROM {$wpdb->usermeta}
             WHERE meta_key = '_application_passwords' AND meta_value NOT LIKE 'a:0:%'"
        );
        $users = 0;
        $total = 0;
        foreach ( $rows as $row ) {
            $test = get_user_meta( (int) $row->user_id, 'csdt_test_account', true );
            if ( $test === '1' ) {
                continue;
            }
            $list = maybe_unserialize( $row->meta_value );
            if ( is_array( $list ) && count( $list ) > 0 ) {
                ++$users;
                $total += count( $list );
            }
        }
        return array(
			'users'     => $users,
			'passwords' => $total,
		);
    }

    public static function ajax_toggle_block_basic_auth(): void {
        if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( 'Forbidden', 403 ); }
        check_ajax_referer( 'csdt_devtools_login_nonce', 'nonce' );

        $enabled = ( sanitize_key( wp_unslash( $_POST['enabled'] ?? '0' ) ) ) === '1' ? '1' : '0';
        update_option( 'csdt_block_basic_auth', $enabled );
        wp_send_json_success(
            array(
				'enabled' => $enabled,
				'usage'   => self::get_app_password_usage(),
            )
        );
    }

    public static function filter_app_pw_for_user( $available, $user ): bool {
        if ( get_user_meta( $user->ID, 'csdt_test_account', true ) === '1' ) {
            return true;
        }
        return false;
    }

    public static function get_app_password_details(): array {
        global $wpdb;
        $rows   = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
            "SELECT user_id, meta_value FROM {$wpdb->usermeta}
             WHERE meta_key = '_application_passwords' AND meta_value NOT LIKE 'a:0:%'"
        );
        $result = array();
        foreach ( $rows as $row ) {
            if ( get_user_meta( (int) $row->user_id, 'csdt_test_account', true ) === '1' ) {
                continue;
            }
            $list = maybe_unserialize( $row->meta_value );
            if ( ! is_array( $list ) || empty( $list ) ) {
                continue;
            }
            $user = get_userdata( (int) $row->user_id );
            if ( ! $user ) {
				continue; }
            $passwords = array();
            foreach ( $list as $pw ) {
                $passwords[] = array(
                    'uuid'      => (string) ( $pw['uuid'] ?? '' ),
                    'name'      => (string) ( $pw['name'] ?? '' ),
                    'created'   => (int) ( $pw['created'] ?? 0 ),
                    'last_used' => isset( $pw['last_used'] ) ? (int) $pw['last_used'] : null,
                    'last_ip'   => isset( $pw['last_ip'] ) ? (string) $pw['last_ip'] : null,
                );
            }
            $result[] = array(
                'user_id'      => (int) $row->user_id,
                'display_name' => $user->display_name,
                'passwords'    => $passwords,
            );
        }
        return $result;
    }

    public static function ajax_delete_app_password(): void {
        if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( 'Forbidden', 403 ); }
        check_ajax_referer( 'csdt_devtools_login_nonce', 'nonce' );

        $user_id = absint( wp_unslash( $_POST['user_id'] ?? 0 ) );
        $uuid    = sanitize_text_field( wp_unslash( $_POST['uuid'] ?? '' ) );

        if ( ! $user_id || ! $uuid ) {
			wp_send_json_error( 'Missing params' ); }

        $deleted = WP_Application_Passwords::delete_application_password( $user_id, $uuid );
        if ( is_wp_error( $deleted ) ) {
            wp_send_json_error( $deleted->get_error_message() );
        }

        wp_send_json_success( array( 'usage' => self::get_app_password_usage() ) );
    }

    public static function ajax_bulk_delete_app_passwords(): void {
        if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( 'Forbidden', 403 ); }
        check_ajax_referer( 'csdt_devtools_login_nonce', 'nonce' );

        $items_raw = wp_unslash( $_POST['items'] ?? '' ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- JSON-decoded; user_id cast to int and uuid sanitized individually below
        $items     = json_decode( $items_raw, true );
        if ( ! is_array( $items ) || empty( $items ) ) {
			wp_send_json_error( 'Missing items' ); }

        $deleted = 0;
        foreach ( $items as $item ) {
            $user_id = (int) ( $item['user_id'] ?? 0 );
            $uuid    = sanitize_text_field( $item['uuid'] ?? '' );
            if ( ! $user_id || ! $uuid ) {
				continue; }
            $result = WP_Application_Passwords::delete_application_password( $user_id, $uuid );
            if ( ! is_wp_error( $result ) ) {
				++$deleted; }
        }

        wp_send_json_success(
            array(
				'deleted' => $deleted,
				'usage'   => self::get_app_password_usage(),
            )
        );
    }

    public static function test_account_after_auth( $user, $app_password ): void {
        if ( get_user_meta( $user->ID, 'csdt_test_account', true ) !== '1' ) {
            return;
        }
        $max_logins = (int) get_user_meta( $user->ID, 'csdt_test_max_logins', true );
        if ( $max_logins <= 0 ) {
            return; // unlimited
        }
        $count = (int) get_user_meta( $user->ID, 'csdt_test_login_count', true ) + 1;
        if ( $count >= $max_logins ) {
            wp_delete_user( $user->ID );
        } else {
            update_user_meta( $user->ID, 'csdt_test_login_count', $count );
        }
    }
}
