<?php


namespace Fliegerlogin;


class RateLimiter {

	private const TRANSIENT_PREFIX = 'fliegerlogin_rl_';

	public static function isBlocked(): bool {
		if (!self::isEnabled()) {
			return false;
		}

		return self::getAttempts() >= self::getMaxAttempts();
	}

	public static function getAttempts(): int {
		return self::getAttemptsForIp(self::getIp());
	}

	public static function recordFailure(): void {
		if (!self::isEnabled()) {
			return;
		}

		$ip       = self::getIp();
		$key      = self::transientKey($ip);
		$attempts = (int) get_transient($key);
		$attempts++;
		set_transient($key, $attempts, self::getLockoutSeconds());
		self::trackFailure($ip, $attempts);
	}

	public static function refreshBlock(): void {
		if (!self::isEnabled()) {
			return;
		}

		$ip       = self::getIp();
		$attempts = self::getAttemptsForIp($ip);
		if ($attempts < self::getMaxAttempts()) {
			return;
		}

		set_transient(self::transientKey($ip), $attempts, self::getLockoutSeconds());
		self::trackFailure($ip, $attempts);
	}

	public static function clearFailures(): void {
		self::clearFailuresForIp(self::getIp());
	}

	public static function clearFailuresForIp(string $ip): void {
		delete_transient(self::transientKey($ip));

		$blockedIps = Options::getRateLimitBlockedIps();
		unset($blockedIps[self::transientKey($ip)]);
		Options::setRateLimitBlockedIps($blockedIps);
	}

	public static function getAttemptsForIp(string $ip): int {
		return (int) get_transient(self::transientKey($ip));
	}

	public static function isEnabled(): bool {
		return Options::isRateLimitEnabled();
	}

	public static function appliesToAllLogins(): bool {
		return Options::isRateLimitAllLoginsEnabled();
	}

	public static function getMaxAttempts(): int {
		return Options::getRateLimitMaxAttempts();
	}

	public static function getLockoutSeconds(): int {
		return Options::getRateLimitLockoutSeconds();
	}

	/**
	 * @return array<int, array{ip:string, attempts:int, first_failed_at:int, last_failed_at:int, blocked_until:int}>
	 */
	public static function getBlockedIps(): array {
		$blockedIps = self::getTrackedFailures();
		$active = [];

		foreach ($blockedIps as $key => $entry) {
			if (!is_array($entry) || empty($entry['ip'])) {
				continue;
			}

			$ip = (string) $entry['ip'];
			$attempts = self::getAttemptsForIp($ip);
			if ($attempts < self::getMaxAttempts()) {
				continue;
			}

			$entry['attempts'] = $attempts;
			$active[$key] = $entry;
		}

		return array_values($active);
	}

	/**
	 * @return array<int, array{ip:string, attempts:int, first_failed_at:int, last_failed_at:int, blocked_until:int|null}>
	 */
	public static function getActiveFailureIps(): array {
		$trackedFailures = self::getTrackedFailures();
		$active = [];

		foreach ($trackedFailures as $key => $entry) {
			if (!is_array($entry) || empty($entry['ip'])) {
				continue;
			}

			$ip = (string) $entry['ip'];
			$attempts = self::getAttemptsForIp($ip);
			if ($attempts <= 0 || $attempts >= self::getMaxAttempts()) {
				continue;
			}

			$entry['attempts'] = $attempts;
			$active[$key] = $entry;
		}

		return array_values($active);
	}

	/**
	 * @return array<int, array{ts:float, ip:string, attempts:int}>
	 */
	public static function getRecentRateLimitEvents(int $limit = 5): array {
		return array_slice(array_reverse(Options::getRateLimitEvents()), 0, $limit);
	}

	public static function recordRateLimitEvent(): void {
		if (!self::isEnabled()) {
			return;
		}

		Options::appendRateLimitEvent([
			'ts' => microtime(true),
			'ip' => self::getIp(),
			'attempts' => self::getAttempts(),
		]);
	}

	public static function getIp(): string {
		// Prefer Cloudflare's verified header, fall back to the direct connection
		// IP. Both keys are checked before use: behind no proxy the Cloudflare
		// header is absent, and on CLI or cron REMOTE_ADDR can be missing too.
		foreach (['HTTP_CF_CONNECTING_IP', 'REMOTE_ADDR'] as $key) {
			if (empty($_SERVER[$key])) {
				continue;
			}

			$ip = sanitize_text_field(wp_unslash($_SERVER[$key]));
			if ($ip !== '') {
				return $ip;
			}
		}

		return '';
	}

	private static function transientKey(string $ip): string {
		return self::TRANSIENT_PREFIX . md5($ip);
	}

	private static function getTrackedFailures(): array {
		$trackedFailures = Options::getRateLimitBlockedIps();
		$active = [];

		foreach ($trackedFailures as $key => $entry) {
			if (!is_array($entry) || empty($entry['ip'])) {
				continue;
			}

			$ip = (string) $entry['ip'];
			if (self::getAttemptsForIp($ip) <= 0) {
				continue;
			}

			$active[$key] = $entry;
		}

		if ($active !== $trackedFailures) {
			Options::setRateLimitBlockedIps($active);
		}

		return $active;
	}

	private static function trackFailure(string $ip, int $attempts): void {
		$key = self::transientKey($ip);
		$blockedIps = Options::getRateLimitBlockedIps();
		$now = time();
		$previous = $blockedIps[$key] ?? [];

		$blockedIps[$key] = [
			'ip' => $ip,
			'attempts' => $attempts,
			'first_failed_at' => isset($previous['first_failed_at']) ? (int) $previous['first_failed_at'] : $now,
			'last_failed_at' => $now,
			'blocked_until' => $attempts >= self::getMaxAttempts() ? $now + self::getLockoutSeconds() : null,
		];

		Options::setRateLimitBlockedIps($blockedIps);
	}

}
