<?php
/**
 * Session repository — reads and revokes WordPress native session tokens.
 *
 * @package LoginSessionManager
 */

namespace LoginSessionManager\Includes;

use WP_Session_Tokens;
use WP_User;

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

class Session_Repository {

	public const LOGIN_AS_COOKIE = 'wpps_login_as';
	public const TEMP_LOGIN_COOKIE = 'wpps_tl_session';

	/** @var array<int, int> */
	private array $count_cache = [];

	/** @var array<string, bool> */
	private array $privacy = [
		'hide_ip'         => false,
		'hide_user_agent' => false,
	];

	/**
	 * @param array<string, mixed> $options
	 */
	public function set_privacy_options( array $options ): void {
		$this->privacy = [
			'hide_ip'         => ! empty( $options['hide_ip'] ),
			'hide_user_agent' => ! empty( $options['hide_user_agent'] ),
		];
	}

	/**
	 * @return list<array<string, mixed>>
	 */
	public function get_sessions_for_user( int $user_id ): array {
		if ( $user_id < 1 ) {
			return [];
		}

		$sessions = $this->get_session_map( $user_id );

		if ( $sessions === [] ) {
			return [];
		}

		$current_verifier = $this->get_current_verifier( $user_id );
		$rows             = [];

		foreach ( $sessions as $verifier => $session ) {
			if ( ! is_array( $session ) ) {
				continue;
			}

			$rows[] = $this->format_session_row( $user_id, (string) $verifier, $session, $current_verifier );
		}

		usort(
			$rows,
			static function ( array $a, array $b ): int {
				return (int) ( $b['login'] ?? 0 ) <=> (int) ( $a['login'] ?? 0 );
			}
		);

		return $this->apply_privacy_to_sessions( $rows );
	}

	/**
	 * @param array<string, mixed> $args
	 * @return array{sessions:list<array<string,mixed>>,stats:array{total_sessions:int,users_logged_in:int,sessions_by_role:array<string,int>},pagination:array{page:int,per_page:int,total:int,total_pages:int}}
	 */
	public function query_sessions( array $args = [] ): array {
		$filter_user_id = isset( $args['filter_user_id'] ) ? absint( $args['filter_user_id'] ) : 0;
		$page           = max( 1, absint( $args['page'] ?? 1 ) );
		$per_page       = max( 1, min( 100, absint( $args['per_page'] ?? 50 ) ) );
		$search         = isset( $args['search'] ) ? sanitize_text_field( (string) $args['search'] ) : '';

		$sessions = $this->get_all_active_sessions( $filter_user_id > 0 ? $filter_user_id : null );

		if ( $search !== '' ) {
			$needle   = strtolower( $search );
			$sessions = array_values(
				array_filter(
					$sessions,
					static function ( array $session ) use ( $needle ): bool {
						$user = $session['user'] ?? [];
						$hay  = strtolower(
							( (string) ( $user['login'] ?? '' ) ) . ' ' . ( (string) ( $user['display_name'] ?? '' ) )
						);

						return str_contains( $hay, $needle );
					}
				)
			);
		}

		$stats  = $this->build_stats_from_sessions( $sessions );
		$total  = count( $sessions );
		$offset = ( $page - 1 ) * $per_page;

		return [
			'sessions'   => $this->apply_privacy_to_sessions( array_slice( $sessions, $offset, $per_page ) ),
			'stats'      => $stats,
			'pagination' => [
				'page'        => $page,
				'per_page'    => $per_page,
				'total'       => $total,
				'total_pages' => $total > 0 ? (int) ceil( $total / $per_page ) : 0,
			],
		];
	}

	/**
	 * @return list<array<string, mixed>>
	 */
	public function get_all_active_sessions( ?int $filter_user_id = null ): array {
		global $wpdb;

		$rows = [];

		if ( $filter_user_id !== null && $filter_user_id > 0 ) {
			$user = get_userdata( $filter_user_id );
			if ( ! $user instanceof WP_User ) {
				return [];
			}

			foreach ( $this->get_sessions_for_user( $filter_user_id ) as $session ) {
				$session['user'] = $this->format_user_brief( $user );
				$rows[]          = $session;
			}

			return $rows;
		}

		$user_ids = $wpdb->get_col(
			$wpdb->prepare(
				"SELECT user_id FROM {$wpdb->usermeta} WHERE meta_key = %s AND meta_value != '' AND meta_value != %s",
				'session_tokens',
				'a:0:{}'
			)
		);

		if ( ! is_array( $user_ids ) ) {
			return [];
		}

		foreach ( $user_ids as $user_id ) {
			$user_id = (int) $user_id;
			$user    = get_userdata( $user_id );
			if ( ! $user instanceof WP_User ) {
				continue;
			}

			foreach ( $this->get_sessions_for_user( $user_id ) as $session ) {
				$session['user'] = $this->format_user_brief( $user );
				$rows[]          = $session;
			}
		}

		usort(
			$rows,
			static function ( array $a, array $b ): int {
				return (int) ( $b['login'] ?? 0 ) <=> (int) ( $a['login'] ?? 0 );
			}
		);

		return $rows;
	}

	/**
	 * @return array{total_sessions:int,users_logged_in:int,sessions_by_role:array<string,int>}
	 */
	public function get_overview_stats(): array {
		return $this->build_stats_from_sessions( $this->get_all_active_sessions() );
	}

	/**
	 * @param list<array<string, mixed>> $sessions
	 * @return array{total_sessions:int,users_logged_in:int,sessions_by_role:array<string,int>}
	 */
	public function build_stats_from_sessions( array $sessions ): array {
		$user_ids = [];
		$by_role  = [];

		foreach ( $sessions as $session ) {
			$user_id = (int) ( $session['user_id'] ?? 0 );
			if ( $user_id > 0 ) {
				$user_ids[ $user_id ] = true;
			}

			$role = (string) ( $session['user']['primary_role'] ?? 'unknown' );
			if ( ! isset( $by_role[ $role ] ) ) {
				$by_role[ $role ] = 0;
			}
			++$by_role[ $role ];
		}

		return [
			'total_sessions'   => count( $sessions ),
			'users_logged_in'  => count( $user_ids ),
			'sessions_by_role' => $by_role,
		];
	}

	public function count_active_sessions( int $user_id ): int {
		if ( isset( $this->count_cache[ $user_id ] ) ) {
			return $this->count_cache[ $user_id ];
		}

		$count                          = count( $this->get_session_map( $user_id ) );
		$this->count_cache[ $user_id ]  = $count;

		return $count;
	}

	public function destroy_session( int $user_id, string $verifier ): bool {
		if ( $user_id < 1 || $verifier === '' ) {
			return false;
		}

		$sessions = get_user_meta( $user_id, 'session_tokens', true );

		if ( ! is_array( $sessions ) || ! array_key_exists( $verifier, $sessions ) ) {
			return false;
		}

		unset( $sessions[ $verifier ] );

		if ( $sessions === [] ) {
			delete_user_meta( $user_id, 'session_tokens' );
		} else {
			update_user_meta( $user_id, 'session_tokens', $sessions );
		}

		unset( $this->count_cache[ $user_id ] );

		return true;
	}

	public function destroy_all_for_user( int $user_id ): int {
		if ( $user_id < 1 ) {
			return 0;
		}

		$this->ensure_session_tokens_loaded();

		$manager = WP_Session_Tokens::get_instance( $user_id );
		$count   = count( $this->get_session_map( $user_id ) );

		if ( $count > 0 ) {
			$manager->destroy_all();
			unset( $this->count_cache[ $user_id ] );
		}

		return $count;
	}

	/**
	 * @return array{destroyed_users:int,destroyed_sessions:int}
	 */
	public function destroy_all_site_wide(): array {
		global $wpdb;

		$user_ids = $wpdb->get_col(
			$wpdb->prepare(
				"SELECT user_id FROM {$wpdb->usermeta} WHERE meta_key = %s AND meta_value != '' AND meta_value != %s",
				'session_tokens',
				'a:0:{}'
			)
		);

		$destroyed_users    = 0;
		$destroyed_sessions = 0;

		if ( ! is_array( $user_ids ) ) {
			return [
				'destroyed_users'    => 0,
				'destroyed_sessions' => 0,
			];
		}

		foreach ( $user_ids as $user_id ) {
			$user_id = (int) $user_id;
			$count   = $this->destroy_all_for_user( $user_id );
			if ( $count > 0 ) {
				++$destroyed_users;
				$destroyed_sessions += $count;
			}
		}

		return [
			'destroyed_users'    => $destroyed_users,
			'destroyed_sessions' => $destroyed_sessions,
		];
	}

	/**
	 * Destroy oldest sessions until count is at or below $max.
	 *
	 * @return list<string> Verifiers destroyed.
	 */
	public function destroy_oldest_sessions( int $user_id, int $max ): array {
		$sessions  = $this->get_sessions_for_user( $user_id );
		$destroyed = [];

		if ( count( $sessions ) <= $max ) {
			return $destroyed;
		}

		usort(
			$sessions,
			static function ( array $a, array $b ): int {
				return (int) ( $a['login'] ?? 0 ) <=> (int) ( $b['login'] ?? 0 );
			}
		);

		$to_remove = count( $sessions ) - $max;

		for ( $i = 0; $i < $to_remove; $i++ ) {
			$verifier = (string) ( $sessions[ $i ]['verifier'] ?? '' );
			if ( $verifier !== '' && $this->destroy_session( $user_id, $verifier ) ) {
				$destroyed[] = $verifier;
			}
		}

		return $destroyed;
	}

	public function get_current_verifier( ?int $user_id = null ): ?string {
		$cookie = wp_parse_auth_cookie( '', 'logged_in' );
		if ( ! is_array( $cookie ) || empty( $cookie['token'] ) || empty( $cookie['user_id'] ) ) {
			return null;
		}

		$cookie_user_id = (int) $cookie['user_id'];
		if ( $user_id !== null && $user_id > 0 && $cookie_user_id !== $user_id ) {
			return null;
		}

		return hash( 'sha256', (string) $cookie['token'] );
	}

	public function is_current_session( int $user_id, string $verifier ): bool {
		$current = $this->get_current_verifier( $user_id );
		return $current !== null && hash_equals( $current, $verifier );
	}

	public static function has_login_as_cookie(): bool {
		return ! empty( $_COOKIE[ self::LOGIN_AS_COOKIE ] );
	}

	public static function has_temporary_login_cookie(): bool {
		return ! empty( $_COOKIE[ self::TEMP_LOGIN_COOKIE ] );
	}

	public function is_login_as_session( int $user_id ): bool {
		if ( get_current_user_id() !== $user_id ) {
			return false;
		}

		if ( ! self::has_login_as_cookie() ) {
			return false;
		}

		return (int) get_user_meta( $user_id, '_wpps_original_user', true ) > 0
			|| (int) get_user_meta( $user_id, '_wpps_login_as_time', true ) > 0;
	}

	public function is_temporary_login_session( int $user_id ): bool {
		if ( get_current_user_id() !== $user_id ) {
			return false;
		}

		return self::has_temporary_login_cookie();
	}

	public static function parse_user_agent( string $user_agent ): array {
		$user_agent = trim( $user_agent );
		$browser    = __( 'Unknown browser', 'wp-powersuite' );
		$os         = __( 'Unknown OS', 'wp-powersuite' );

		if ( $user_agent === '' ) {
			return [
				'browser' => $browser,
				'os'      => $os,
				'label'   => $browser . ' / ' . $os,
			];
		}

		if ( preg_match( '/Edg\//i', $user_agent ) ) {
			$browser = 'Edge';
		} elseif ( preg_match( '/Chrome\//i', $user_agent ) ) {
			$browser = 'Chrome';
		} elseif ( preg_match( '/Firefox\//i', $user_agent ) ) {
			$browser = 'Firefox';
		} elseif ( preg_match( '/Safari\//i', $user_agent ) && ! preg_match( '/Chrome\//i', $user_agent ) ) {
			$browser = 'Safari';
		} elseif ( preg_match( '/MSIE|Trident/i', $user_agent ) ) {
			$browser = 'Internet Explorer';
		}

		if ( preg_match( '/Windows NT/i', $user_agent ) ) {
			$os = 'Windows';
		} elseif ( preg_match( '/Macintosh|Mac OS X/i', $user_agent ) ) {
			$os = 'macOS';
		} elseif ( preg_match( '/Linux/i', $user_agent ) ) {
			$os = 'Linux';
		} elseif ( preg_match( '/Android/i', $user_agent ) ) {
			$os = 'Android';
		} elseif ( preg_match( '/iPhone|iPad|iPod/i', $user_agent ) ) {
			$os = 'iOS';
		}

		return [
			'browser' => $browser,
			'os'      => $os,
			'label'   => $browser . ' / ' . $os,
		];
	}

	public static function get_client_ip(): string {
		$ip = '';
		if ( ! empty( $_SERVER['REMOTE_ADDR'] ) ) {
			$ip = sanitize_text_field( wp_unslash( (string) $_SERVER['REMOTE_ADDR'] ) );
		}

		/**
		 * Filter the client IP used for session audit entries.
		 *
		 * @param string $ip Client IP address.
		 */
		return (string) apply_filters( 'wpps_session_manager_client_ip', $ip );
	}

	/**
	 * @param list<array<string, mixed>> $sessions
	 * @return list<array<string, mixed>>
	 */
	private function apply_privacy_to_sessions( array $sessions ): array {
		if ( empty( $this->privacy['hide_ip'] ) && empty( $this->privacy['hide_user_agent'] ) ) {
			return $sessions;
		}

		return array_map(
			function ( array $session ): array {
				if ( ! empty( $this->privacy['hide_ip'] ) ) {
					$session['ip'] = '—';
				}

				if ( ! empty( $this->privacy['hide_user_agent'] ) ) {
					unset( $session['user_agent'] );
				}

				return $session;
			},
			$sessions
		);
	}

	/**
	 * @param array<string, mixed> $session
	 * @return array<string, mixed>
	 */
	private function format_session_row( int $user_id, string $verifier, array $session, ?string $current_verifier ): array {
		$login      = isset( $session['login'] ) ? (int) $session['login'] : 0;
		$expiration = isset( $session['expiration'] ) ? (int) $session['expiration'] : 0;
		$ip         = isset( $session['ip'] ) ? (string) $session['ip'] : '';
		$ua         = isset( $session['ua'] ) ? (string) $session['ua'] : '';
		$parsed_ua  = self::parse_user_agent( $ua );
		$is_current = $current_verifier !== null && hash_equals( $current_verifier, $verifier );

		$badges = [];
		if ( $is_current ) {
			$badges[] = 'current';
		}
		if ( $is_current && $this->is_login_as_session( $user_id ) ) {
			$badges[] = 'login_as';
		}
		if ( $is_current && $this->is_temporary_login_session( $user_id ) ) {
			$badges[] = 'temporary_login';
		}

		return [
			'user_id'              => $user_id,
			'verifier'             => $verifier,
			'verifier_prefix'      => substr( $verifier, 0, 8 ),
			'login'                => $login,
			'login_formatted'      => $this->format_timestamp( $login ),
			'expiration'           => $expiration,
			'expiration_formatted' => $this->format_timestamp( $expiration ),
			'ip'                   => $ip !== '' ? $ip : '—',
			'user_agent'           => $ua,
			'device'               => $parsed_ua['label'],
			'browser'              => $parsed_ua['browser'],
			'os'                   => $parsed_ua['os'],
			'is_current'           => $is_current,
			'badges'               => $badges,
		];
	}

	/**
	 * @return array<string, mixed>
	 */
	private function format_user_brief( WP_User $user ): array {
		$roles = is_array( $user->roles ) ? $user->roles : [];
		$role  = $roles[0] ?? 'none';

		return [
			'id'           => (int) $user->ID,
			'login'        => $user->user_login,
			'display_name' => $user->display_name,
			'roles'        => $roles,
			'primary_role' => $role,
			'role_label'   => translate_user_role( ucfirst( $role ) ),
			'edit_url'     => get_edit_user_link( $user->ID ) ?: '',
			'sessions_url' => get_edit_user_link( $user->ID ) ? get_edit_user_link( $user->ID ) . '#wpps-lsm-profile-sessions' : '',
		];
	}

	private function format_timestamp( int $timestamp ): string {
		if ( $timestamp < 1 ) {
			return '—';
		}

		$format = get_option( 'date_format', 'F j, Y' ) . ' ' . get_option( 'time_format', 'g:i a' );

		if ( function_exists( 'wp_date' ) ) {
			return wp_date( $format, $timestamp );
		}

		return date_i18n( $format, $timestamp );
	}

	/**
	 * Active sessions keyed by verifier hash.
	 *
	 * WordPress WP_Session_Tokens::get_all() returns array_values() and drops verifier keys,
	 * so session tokens are read from usermeta directly.
	 *
	 * @return array<string, array<string, mixed>>
	 */
	private function get_session_map( int $user_id ): array {
		if ( $user_id < 1 ) {
			return [];
		}

		$sessions = get_user_meta( $user_id, 'session_tokens', true );

		if ( ! is_array( $sessions ) || $sessions === [] ) {
			return [];
		}

		$now      = time();
		$filtered = [];

		foreach ( $sessions as $verifier => $session ) {
			if ( is_int( $session ) ) {
				$session = [ 'expiration' => $session ];
			}

			if ( ! is_array( $session ) ) {
				continue;
			}

			$expiration = isset( $session['expiration'] ) ? (int) $session['expiration'] : 0;

			if ( $expiration >= $now ) {
				$filtered[ (string) $verifier ] = $session;
			}
		}

		return $filtered;
	}

	private function ensure_session_tokens_loaded(): void {
		if ( class_exists( 'WP_Session_Tokens' ) ) {
			return;
		}

		require_once ABSPATH . WPINC . '/class-wp-session-tokens.php';
		require_once ABSPATH . WPINC . '/class-wp-user-meta-session-tokens.php';
	}
}
