<?php

declare( strict_types=1 );

namespace StrykeWP\Modules\Connector;

/**
 * Auto-Login Token Generator & Redeemer for the Hub Connector.
 *
 * Allows the STRYKE Hub to generate one-time-use login tokens that
 * redirect users straight into the WordPress admin (e.g. Elementor editor)
 * without requiring a separate browser login.
 *
 * Security model:
 * - Tokens are 32 random bytes (64 hex chars), stored as SHA-256 hash only.
 * - One-time-use: deleted on first redemption.
 * - 60-second TTL via WordPress transients.
 * - Rate limited to 10 tokens/minute per site.
 * - HTTPS-only in production.
 * - Raw token is NEVER logged — only hash prefix (8 chars) for debugging.
 *
 * @package StrykeWP\Modules\Connector
 */
class AutoLogin {

	// -----------------------------------------------------------------------
	// Constants
	// -----------------------------------------------------------------------

	/** Transient prefix for stored token hashes. */
	private const TRANSIENT_PREFIX = 'stryke_autologin_';

	/** Transient key for rate limit counter. */
	private const RATE_LIMIT_KEY = 'stryke_autologin_rate';

	/** Maximum tokens per minute. */
	private const RATE_LIMIT_MAX = 10;

	/** Rate limit window in seconds. */
	private const RATE_LIMIT_WINDOW = 60;

	/** Token TTL in seconds. */
	private const TOKEN_TTL = 60;

	/** Token byte length (produces 64 hex chars). */
	private const TOKEN_BYTES = 32;

	/** Hash algorithm — matches Auth class convention. */
	private const HASH_ALGO = 'sha256';

	/** Query parameter name used for token redemption. */
	private const QUERY_PARAM = 'stryke_autologin';

	// -----------------------------------------------------------------------
	// REST Callback — Token Creation
	// -----------------------------------------------------------------------

	/**
	 * POST /stryke/v1/auto-login
	 *
	 * Generates a one-time auto-login token and returns the login URL.
	 *
	 * @param \WP_REST_Request $request Incoming REST request.
	 * @return \WP_REST_Response
	 */
	public function handle_create_token( \WP_REST_Request $request ): \WP_REST_Response {

		// HTTPS enforcement (skip in local dev).
		if ( ! is_ssl() && 'local' !== wp_get_environment_type() ) {
			return new \WP_REST_Response(
				array( 'success' => false, 'message' => 'HTTPS is required for auto-login tokens.' ),
				403
			);
		}

		// Rate limit check.
		$current_count = (int) get_transient( self::RATE_LIMIT_KEY );
		if ( $current_count >= self::RATE_LIMIT_MAX ) {
			return new \WP_REST_Response(
				array( 'success' => false, 'message' => 'Rate limit exceeded. Max 10 tokens per minute.' ),
				429
			);
		}

		// Find the target user.
		$user_role = sanitize_key( (string) $request->get_param( 'user_role' ) ) ?: 'administrator';
		$user      = $this->find_user_by_role( $user_role );

		if ( null === $user ) {
			return new \WP_REST_Response(
				array( 'success' => false, 'message' => "No user with role '{$user_role}' found on this site." ),
				404
			);
		}

		// Validate redirect_to.
		$redirect_to = sanitize_text_field( (string) $request->get_param( 'redirect_to' ) );
		$validated   = wp_validate_redirect( $redirect_to, '/wp-admin/' );

		// Generate token.
		$raw_token  = bin2hex( random_bytes( self::TOKEN_BYTES ) );
		$token_hash = hash( self::HASH_ALGO, $raw_token );

		// Store hash as transient with payload.
		$payload = array(
			'user_id'     => $user->ID,
			'redirect_to' => $validated,
			'created_at'  => current_time( 'c' ),
		);

		set_transient( self::TRANSIENT_PREFIX . $token_hash, $payload, self::TOKEN_TTL );

		// Increment rate limit counter.
		if ( false === get_transient( self::RATE_LIMIT_KEY ) ) {
			set_transient( self::RATE_LIMIT_KEY, 1, self::RATE_LIMIT_WINDOW );
		} else {
			set_transient( self::RATE_LIMIT_KEY, $current_count + 1, self::RATE_LIMIT_WINDOW );
		}

		// Build the login URL.
		$login_url = add_query_arg( self::QUERY_PARAM, $raw_token, home_url( '/' ) );

		return new \WP_REST_Response(
			array(
				'success'    => true,
				'login_url'  => $login_url,
				'expires_in' => self::TOKEN_TTL,
			),
			200
		);
	}

	// -----------------------------------------------------------------------
	// Init Hook — Token Redemption
	// -----------------------------------------------------------------------

	/**
	 * Hooked on `init` (priority 1).
	 *
	 * Checks for the `stryke_autologin` query parameter, validates the token,
	 * logs the user in, and redirects to the stored target URL.
	 *
	 * @return void
	 */
	public function maybe_process_autologin(): void {

		// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- one-time token, not a form.
		if ( empty( $_GET[ self::QUERY_PARAM ] ) ) {
			return;
		}

		// phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
		$raw_token = sanitize_text_field( wp_unslash( $_GET[ self::QUERY_PARAM ] ) );

		// Validate format (must be exactly 64 hex chars).
		if ( 1 !== preg_match( '/^[a-f0-9]{64}$/', $raw_token ) ) {
			wp_die(
				esc_html__( 'Invalid auto-login token format.', 'stryke-wp' ),
				esc_html__( 'Login Error', 'stryke-wp' ),
				array( 'response' => 400 )
			);
		}

		$token_hash = hash( self::HASH_ALGO, $raw_token );
		$payload    = get_transient( self::TRANSIENT_PREFIX . $token_hash );

		// Token not found or expired.
		if ( false === $payload || ! is_array( $payload ) ) {
			wp_die(
				esc_html__( 'Auto-login token expired or already used.', 'stryke-wp' ),
				esc_html__( 'Login Error', 'stryke-wp' ),
				array( 'response' => 403 )
			);
		}

		// One-time-use: delete immediately BEFORE processing.
		delete_transient( self::TRANSIENT_PREFIX . $token_hash );

		$user_id     = absint( $payload['user_id'] ?? 0 );
		$redirect_to = sanitize_text_field( $payload['redirect_to'] ?? '/wp-admin/' );

		// Validate user still exists and has admin capabilities.
		$user = get_userdata( $user_id );
		if ( false === $user || ! $user->has_cap( 'manage_options' ) ) {
			wp_die(
				esc_html__( 'Target user no longer exists or lacks admin privileges.', 'stryke-wp' ),
				esc_html__( 'Login Error', 'stryke-wp' ),
				array( 'response' => 403 )
			);
		}

		// Set auth cookie and redirect.
		wp_set_auth_cookie( $user_id, false );
		wp_set_current_user( $user_id );

		// Validate redirect target (internal paths only).
		$safe_redirect = wp_validate_redirect( $redirect_to, admin_url() );
		wp_safe_redirect( $safe_redirect );
		exit;
	}

	// -----------------------------------------------------------------------
	// Helpers
	// -----------------------------------------------------------------------

	/**
	 * Finds the first user with the given role that has `manage_options`.
	 *
	 * @param string $role WordPress role slug.
	 * @return \WP_User|null
	 */
	private function find_user_by_role( string $role ): ?\WP_User {

		$users = get_users(
			array(
				'role'    => $role,
				'number'  => 1,
				'orderby' => 'ID',
				'order'   => 'ASC',
			)
		);

		if ( empty( $users ) ) {
			return null;
		}

		$user = $users[0];

		// Safety: only return users who can manage options.
		if ( ! $user->has_cap( 'manage_options' ) ) {
			return null;
		}

		return $user;
	}
}
