<?php

namespace WPProofShield\Security;

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


use WPProofShield\Support\RequestInput;
use WPProofShield\Support\Settings;

final class RateLimiter {

	private const RESET_GENERATION_OPTION = 'wpsh_rate_limit_reset_generation';

	public function __construct(
		private Settings $settings
	) {
	}

	public static function resetAllRateLimitState(): void {
		$current = (int) get_option( self::RESET_GENERATION_OPTION, 1 );
		update_option( self::RESET_GENERATION_OPTION, max( 1, $current ) + 1, false );
	}

	public function inCooldown( string $context, string $ip ): bool {
		if ( $this->shouldResetLocalLoginCooldownOnce( $context, $ip ) ) {
			$this->clearRateLimitState( $context, $ip );
			return false;
		}

		$key = $this->key( 'cool', $context, $ip );
		return (bool) get_transient( $key );
	}

	public function recordFailure( string $context, string $ip ): void {
		if ( $context === 'login' ) {
			return;
		}

		$this->incrementFailureCounter( $context, $ip );
	}

	public function recordLoginCredentialFailure( string $ip ): void {
		if ( ! $this->isCredentialLoginPost() ) {
			return;
		}

		$this->incrementFailureCounter( 'login', $ip, 'login_credential_failure' );
	}

	private function incrementFailureCounter( string $context, string $ip, string $trigger = 'failure_threshold' ): void {
		$failKey = $this->key( 'fail', $context, $ip );
		$count   = (int) get_transient( $failKey );
		++$count;
		set_transient( $failKey, $count, $this->failureWindowSeconds( $context ) );

		if ( $count >= $this->failureThreshold( $context ) ) {
			$cooldown = $this->cooldownSeconds( $context );
			if ( $context === 'login' ) {
				// After a login lockout expires, start a fresh 5-attempt cycle.
				set_transient( $failKey, $count, $cooldown );
			}
			$this->setCooldown( $context, $ip, $cooldown, $trigger, $count );
		}
	}

	public function recordSuccess( string $context, string $ip ): void {
		$this->clearRateLimitState( $context, $ip );
	}

	public function forceCooldown( string $context, string $ip, ?int $seconds = null, string $trigger = 'force_cooldown' ): void {
		$cooldown        = $seconds ?? max( $this->cooldownSeconds( $context ), 20 * MINUTE_IN_SECONDS );
		$failureWindow   = $this->failureWindowSeconds( $context );
		$threshold       = $this->failureThreshold( $context );
		$currentFailures = (int) get_transient( $this->key( 'fail', $context, $ip ) );
		$failValue       = max( $threshold, $currentFailures );
		$failTtl         = $context === 'login' ? $cooldown : $failureWindow;
		set_transient( $this->key( 'fail', $context, $ip ), $failValue, $failTtl );
		$this->setCooldown( $context, $ip, $cooldown, $trigger, $failValue );
	}

	public function failureCount( string $context, string $ip ): int {
		return (int) get_transient( $this->key( 'fail', $context, $ip ) );
	}

	public function recordChallengeAttempt( string $context, string $ip ): int {
		$key   = $this->key( 'att', $context, $ip );
		$count = (int) get_transient( $key );
		++$count;
		set_transient( $key, $count, 5 * MINUTE_IN_SECONDS );
		return $count;
	}

	public function consumeRetryRelief( string $context, string $ip ): bool {
		$key   = $this->key( 'relief', $context, $ip );
		$count = (int) get_transient( $key );
		if ( $count >= 2 ) {
			return false;
		}
		++$count;
		set_transient( $key, $count, 10 * MINUTE_IN_SECONDS );
		return true;
	}

	public function activateRetryGrace( string $context, string $ip, int $seconds = 300 ): void {
		set_transient( $this->key( 'grace', $context, $ip ), 1, max( 60, $seconds ) );
	}

	public function hasRetryGrace( string $context, string $ip ): bool {
		return (bool) get_transient( $this->key( 'grace', $context, $ip ) );
	}

	public function cooldownDebugInfo( string $context, string $ip ): ?array {
		if ( ! $this->isLocalTestEnvironment( $ip ) ) {
			return null;
		}

		if ( ! $this->inCooldown( $context, $ip ) ) {
			return null;
		}

		$coolKey   = $this->key( 'cool', $context, $ip );
		$meta      = get_transient( $this->key( 'coolmeta', $context, $ip ) );
		$timeoutTs = (int) get_option( '_transient_timeout_' . $coolKey, 0 );
		$remaining = $timeoutTs > 0 ? max( 0, $timeoutTs - time() ) : null;

		if ( ! is_array( $meta ) ) {
			$meta = array();
		}

		return array(
			'trigger'           => (string) ( $meta['trigger'] ?? 'unknown' ),
			'set_at'            => isset( $meta['set_at'] ) ? (int) $meta['set_at'] : null,
			'until'             => $timeoutTs > 0 ? $timeoutTs : ( isset( $meta['until'] ) ? (int) $meta['until'] : null ),
			'remaining_seconds' => $remaining,
			'failure_count'     => isset( $meta['failure_count'] ) ? (int) $meta['failure_count'] : $this->failureCount( $context, $ip ),
			'context'           => $context,
		);
	}

	private function key( string $prefix, string $context, string $ip ): string {
		return 'wpsh_' . $prefix . '_' . md5( $this->resetGeneration() . '|' . $context . '|' . $ip . '|' . wp_salt( 'auth' ) );
	}

	private function resetGeneration(): int {
		return max( 1, (int) get_option( self::RESET_GENERATION_OPTION, 1 ) );
	}

	private function failureThreshold( string $context ): int {
		if ( $context === 'login' ) {
			return max( 2, min( 20, (int) $this->settings->get( 'login_lockout_threshold', 5 ) ) );
		}

		if ( str_starts_with( $context, 'form:' ) ) {
			return 3;
		}

		return 5;
	}

	private function failureWindowSeconds( string $context ): int {
		if ( $context === 'login' ) {
			return 30 * MINUTE_IN_SECONDS;
		}

		if ( str_starts_with( $context, 'form:' ) ) {
			return 15 * MINUTE_IN_SECONDS;
		}

		return 10 * MINUTE_IN_SECONDS;
	}

	private function cooldownSeconds( string $context ): int {
		if ( $context === 'login' ) {
			$minutes = max( 1, min( 1440, (int) $this->settings->get( 'login_lockout_minutes', 10 ) ) );
			return $minutes * MINUTE_IN_SECONDS;
		}

		if ( str_starts_with( $context, 'form:' ) ) {
			return 10 * MINUTE_IN_SECONDS;
		}

		return 10 * MINUTE_IN_SECONDS;
	}

	private function clearRateLimitState( string $context, string $ip ): void {
		delete_transient( $this->key( 'fail', $context, $ip ) );
		delete_transient( $this->key( 'cool', $context, $ip ) );
		delete_transient( $this->key( 'coolmeta', $context, $ip ) );
		delete_transient( $this->key( 'att', $context, $ip ) );
		delete_transient( $this->key( 'relief', $context, $ip ) );
		delete_transient( $this->key( 'grace', $context, $ip ) );
	}

	private function shouldResetLocalLoginCooldownOnce( string $context, string $ip ): bool {
		if ( $context !== 'login' || ! $this->isLocalTestEnvironment( $ip ) ) {
			return false;
		}

		$hasCooldown = (bool) get_transient( $this->key( 'cool', $context, $ip ) );
		$hasFailures = ( (int) get_transient( $this->key( 'fail', $context, $ip ) ) ) > 0;
		if ( ! $hasCooldown && ! $hasFailures ) {
			return false;
		}

		$markerKey = 'wpsh_local_login_cooldown_reset_once_v3_' . md5( $ip );
		if ( (bool) get_transient( $markerKey ) ) {
			return false;
		}

		// One-time reset for local test sessions after changing cooldown behavior.
		set_transient( $markerKey, 1, 2 * HOUR_IN_SECONDS );
		return true;
	}

	private function isLocalHost(): bool {
		$host = (string) wp_parse_url( home_url( '/' ), PHP_URL_HOST );
		if ( in_array( $host, array( 'localhost', '127.0.0.1', '::1' ), true ) ) {
			return true;
		}

		$host = strtolower( $host );
		return str_ends_with( $host, '.test' )
			|| str_ends_with( $host, '.local' )
			|| str_ends_with( $host, '.localhost' );
	}

	private function isLocalTestEnvironment( string $ip ): bool {
		if ( $this->isLocalHost() ) {
			return true;
		}

		if ( $ip === '127.0.0.1' || $ip === '::1' ) {
			return true;
		}

		if ( ! filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 ) ) {
			return false;
		}

		return str_starts_with( $ip, '10.' )
			|| str_starts_with( $ip, '192.168.' )
			|| preg_match( '/^172\.(1[6-9]|2[0-9]|3[0-1])\./', $ip ) === 1;
	}

	private function isCredentialLoginPost(): bool {
		$method = isset( $_SERVER['REQUEST_METHOD'] ) ? strtoupper( sanitize_text_field( wp_unslash( (string) $_SERVER['REQUEST_METHOD'] ) ) ) : 'GET';
		if ( $method !== 'POST' ) {
			return false;
		}

		$user = $this->postText( 'log' );
		$pass = $this->postText( 'pwd' );

		return $user !== '' && $pass !== '';
	}

	private function postText( string $key ): string {
		return RequestInput::postText( $key );
	}

	private function setCooldown( string $context, string $ip, int $seconds, string $trigger, int $failureCount ): void {
		$seconds = max( 1, $seconds );
		$now     = time();
		set_transient( $this->key( 'cool', $context, $ip ), 1, $seconds );
		set_transient(
			$this->key( 'coolmeta', $context, $ip ),
			array(
				'trigger'       => $trigger,
				'set_at'        => $now,
				'until'         => $now + $seconds,
				'failure_count' => max( 0, $failureCount ),
			),
			$seconds
		);
	}
}
