<?php


namespace Fliegerlogin;


use Fliegerlogin\Premium\CircuitBreaker;
use Fliegerlogin\Premium\GeoBlocker;
use Fliegerlogin\Premium\GeoDatabase;
use Fliegerlogin\Premium\Guards;
use Fliegerlogin\Premium\Hardening;
use Fliegerlogin\Premium\NegativeCache;
use Fliegerlogin\Premium\Settings;
use Fliegerlogin\Premium\Pages\DebugLog;
use Fliegerlogin\Premium\Pages\RateLimits;


/**
 * The seam between the free single sign-on core and the paid safeguards.
 *
 * The core calls this class instead of the safeguards themselves, so it stays
 * compilable when they are not installed. In the package published on
 * WordPress.org the whole Fliegerlogin\Premium namespace is absent and every
 * method here answers "no objection" - that is the free plugin genuinely not
 * having the feature, not the feature being switched off.
 *
 * Basic IP rate limiting is deliberately not routed through here. It guards the
 * Vereinsflieger request quota the whole plugin depends on, so it belongs to the
 * free core and lives in {@see RateLimiter}.
 */
class Guard {

	/**
	 * Whether the add-on with the extra safeguards is installed.
	 *
	 * This asks nothing about entitlement - it only reports whether that code is
	 * present. Each safeguard decides for itself whether it may run. Deliberately
	 * not cached: the answer can only change between requests, and caching a
	 * too-early answer would keep the rest of the request wrong.
	 */
	public static function isAvailable(): bool {
		return class_exists(Guards::class);
	}

	// -- login path ----------------------------------------------------------

	/**
	 * Error codes from an anti-bot plugin that mean "do not spend a
	 * Vereinsflieger request on this attempt".
	 *
	 * @param \WP_Error $error
	 * @return array<int,string> Empty when nothing should be skipped.
	 */
	public static function captchaBlockedCodes(\WP_Error $error): array {
		return self::isAvailable() ? Guards::captchaBlockedCodes($error) : [];
	}

	/**
	 * Whether the username can never authenticate against Vereinsflieger and the
	 * API request should be saved.
	 */
	public static function rejectsUsername(string $username): bool {
		return self::isAvailable() && Guards::rejectsUsername($username);
	}

	/**
	 * Whether this XML-RPC request must not reach the Vereinsflieger API.
	 */
	public static function blocksXmlRpc(): bool {
		return self::isAvailable() && Guards::blocksXmlRpc();
	}

	public static function isGeoBlocked(): bool {
		return self::isAvailable() && GeoBlocker::isBlocked();
	}

	public static function geoCountry(): ?string {
		return self::isAvailable() ? GeoBlocker::getCountry() : null;
	}

	public static function isCircuitOpen(): bool {
		return self::isAvailable() && CircuitBreaker::isOpen();
	}

	public static function circuitReason(): ?string {
		return self::isAvailable() ? CircuitBreaker::getReason() : null;
	}

	public static function circuitRetryAfterSeconds(): int {
		return self::isAvailable() ? CircuitBreaker::getRetryAfterSeconds() : 0;
	}

	public static function isKnownBadCredential(string $username, string $hashedPassword): bool {
		return self::isAvailable() && NegativeCache::has($username, $hashedPassword);
	}

	public static function rememberBadCredential(string $username, string $hashedPassword): void {
		if (self::isAvailable()) {
			NegativeCache::record($username, $hashedPassword);
		}
	}

	public static function forgetBadCredential(string $username, string $hashedPassword): void {
		if (self::isAvailable()) {
			NegativeCache::forget($username, $hashedPassword);
		}
	}

	/**
	 * Counts one outbound Vereinsflieger request towards the burst window.
	 */
	public static function recordApiRequest(): void {
		if (self::isAvailable()) {
			CircuitBreaker::recordRequest();
		}
	}

	// -- setup ---------------------------------------------------------------

	public static function registerHardening(): void {
		if (self::isAvailable()) {
			(new Hardening())->register();
		}
	}

	/**
	 * Registers the add-on's admin pages.
	 *
	 * @return bool True when the add-on provided its own pages, so the core
	 *              knows not to register the built-in security page as well.
	 */
	public static function registerAdminPages(): bool {
		if (!self::isAvailable() || !RateLimits::isReady()) {
			return false;
		}

		RateLimits::setup();
		DebugLog::setup();

		return true;
	}

	public static function registerGeoDatabaseHooks(): void {
		if (self::isAvailable()) {
			GeoDatabase::registerHooks();
		}
	}

	public static function unscheduleGeoDatabase(): void {
		// Runs on deactivation, so clear the cron entry regardless of entitlement -
		// otherwise a dead schedule stays behind.
		if (class_exists(GeoDatabase::class)) {
			GeoDatabase::unschedule();
		}
	}

	/**
	 * Removes the settings the add-on owns, so uninstalling leaves no rows behind.
	 */
	public static function uninstallAddOn(): void {
		if (class_exists(Settings::class)) {
			Settings::uninstall();
		}
	}

}
