<?php
/**
 * Abstract base class shared by FENSE_BPVT_RUN and FENSE_BPVT_RUN_SAVE_MODE.
 *
 * BUG 11 FIX: eliminates ~300 lines of near-identical code that was duplicated
 * between the two concrete engine classes.
 *
 * @package FENSE_BPVT
 */

if ( !defined( 'ABSPATH' ) ) exit; // Exit if accessed directly

abstract class FENSE_BPVT_RUN_BASE
{
	
	private const DB_KEY = FENSE_BPVT_PLUGIN::DB_KEY;
	private const DAILY_LIMIT = self::DB_KEY . 'is_daily_limit_reached';
	private const DAILY_LIMIT_TTL = 600;
	protected FENSE_BPVT_FUN $fun;
	protected string $appKey = '';
	protected string $ip = '';
	protected stdClass $settings;
	protected stdClass $ipData;
	protected array $template = []; // 10 minutes
	
	public function __construct()
	{
		if ( !defined( 'DONOTCACHEPAGE' ) ) {
			define( 'DONOTCACHEPAGE', true );
		}
		$this->ipData = new stdClass();
		$this->settings = new stdClass();
		$this->fun = new FENSE_BPVT_FUN();
		
		$this->loadUserIp();
	}
	
	// -----------------------------------------------------------------------
	// Loaders
	// -----------------------------------------------------------------------
	
	public function loadAppKey(): string
	{
		$this->appKey = (string)get_option( self::DB_KEY . 'licence_key', '' );
		return $this->appKey;
	}
	
	public function loadSettings(): stdClass
	{
		$this->settings = FENSE_BPVT_plugin_settings( $this->fun );
		
		$this->appKey = $this->settings->appKey;
		
		if ( empty( $this->settings->protection_mode ) ) {
			$this->settings->protection_mode = 'message';
		}
		$this->template = [
			'English',
			$this->settings->message,
			$this->settings->disable_link,
			$this->settings->link,
			$this->settings->protection_mode,
			$this->settings->url,
		];
		return $this->settings;
	}
	
	public function loadUserIp(): string
	{
		$this->ip = $this->fun->get_the_user_ip();
//		$this->ip = '8.8.8.10';
		return $this->ip;
	}
	
	public function loadData(): object
	{
		$this->ipData = $this->fun->Fense_data( $this->ip, $this->appKey );
		$this->saveDailyLimitIfReached();
		return $this->ipData;
	}
	
	// -----------------------------------------------------------------------
	// Daily-limit helpers
	// -----------------------------------------------------------------------
	
	public function isDailyLimitReached(): bool
	{
		$t = FENSE_BPVT_Cache::wp_get( self::DAILY_LIMIT );
		return is_array( $t ) && isset( $t['time'] ) && time() < (int)$t['time'];
	}
	
	protected function saveDailyLimitIfReached(): void
	{
		if ( !in_array( $this->ipData->status ?? '', [ 'Error', 'error' ], true ) ) {
			return;
		}
		
		if ( ( $this->ipData->message ?? '' ) === FENSE_BPVT_PLUGIN::RATE_LIMIT_MSG ||
			( $this->ipData->code ?? '' ) === FENSE_BPVT_PLUGIN::RATE_LIMIT_CODE ||
			( $this->fun->statusCode ?? '' ) == 429
		) {
			$t = FENSE_BPVT_Cache::wp_get( self::DAILY_LIMIT );
			if ( is_array( $t ) && isset( $t['time'] ) && time() < (int)$t['time'] ) {
				return;
			}
			
			FENSE_BPVT_Cache::wp_set( self::DAILY_LIMIT, [ 'time' => time() + self::DAILY_LIMIT_TTL ], self::DAILY_LIMIT_TTL );
		}
	}
	
	// -----------------------------------------------------------------------
	// Shared detection helpers
	// -----------------------------------------------------------------------
	
	/**
	 * BUG 2 FIX: original used function_exists('DOING_AJAX') which always returns
	 * false because DOING_AJAX is a constant, not a function.
	 * wp_doing_ajax() is the correct, future-proof WordPress API.
	 */
	public function isAjaxCall(): bool
	{
		return wp_doing_ajax();
	}
	
	public function isAdminOrLoggedIn(): bool
	{
		return is_admin() || is_user_logged_in();
	}
	
	/** Returns true if the current page type should be skipped per custom-page settings. */
	public function shouldSkipPageType(): bool
	{
		if ( $this->settings->enable_on !== 'custom' ) {
			return false;
		}
		if ( !$this->settings->enable_on_logged && is_user_logged_in() ) {
			return true;
		}
		if ( !$this->settings->enable_on_posts && is_single() ) {
			return true;
		}
		if ( !$this->settings->enable_on_pages && is_page() ) {
			return true;
		}
		if ( !$this->settings->enable_on_login
			&& ( $GLOBALS['pagenow'] ?? '' ) === 'wp-login.php' ) {
			return true;
		}
		return false;
	}
	
	/** Returns true if the user-agent matches a known crawler/bot pattern. */
	public function isCrawlerBot(): bool
	{
		
		$verifier = new FENSE_BPVT_Bot_Verifier();
		$ua = isset( $_SERVER['HTTP_USER_AGENT'] )
			? sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) )
			: '';
		
		if ( $verifier->is_verified_bot( $this->ip, $ua ) ) {
			return true;
		}
		
		$ua = $_SERVER['HTTP_USER_AGENT'] ?? '';
		if ( $ua === '' || ( $this->settings->crawlers_bots ?? '' ) !== 'yes' ) {
			return false;
		}
		$raw = $this->settings->crawlers_bots_data ?? '';
		if ( $raw === '' ) {
			return false;
		}
		
		// Escape each bot token individually before building the pattern.
		$tokens = array_filter( array_map( 'trim', explode( ',', $raw ) ) );
		$pattern = '/(' . implode( '|', array_map( 'preg_quote', $tokens ) ) . ')/i';
		return (bool)preg_match( $pattern, $ua );
	}
	
	/** Extracts the root domain (e.g. "spam.example.com" → "example.com"). */
	protected function extractRootDomain( string $raw ): string
	{
		if ( $raw === '' ) {
			return 'direct';
		}
		$host = parse_url( trim( $raw ), PHP_URL_HOST );
		if ( !$host ) {
			return 'direct';
		}
		$host = strtolower( preg_replace( '/^www\./i', '', $host ) );
		$segments = explode( '.', $host );
		$c = count( $segments );
		return $c >= 2 ? $segments[ $c - 2 ] . '.' . $segments[ $c - 1 ] : $host;
	}
	
	/** Returns true if HTTP_REFERER matches a blocked domain. */
	public function isReferrerBlocked(): bool
	{
		$host = $this->extractRootDomain( $_SERVER['HTTP_REFERER'] ?? '' );
		$blocked = is_array( $this->settings->block_domains ?? null ) ? $this->settings->block_domains : [];
		return in_array( $host, $blocked, true );
	}
	
	/** Returns true if the visitor IP is in the whitelist (individual or range). */
	public function isWhitelisted(): bool
	{
		if ( empty( $this->settings->ip_whitelisting_enable ) ) {
			return false;
		}
		$list = array_map( 'trim', explode( ',', (string)( $this->settings->ip_whitelisting ?? '' ) ) );
		if ( in_array( $this->ip, $list, true ) ) {
			return true;
		}
		return $this->ipInRange( $this->settings->ip_whitelisting_range ?? [] );
	}
	
	/** Returns true if the visitor IP is in the blacklist. Also sets proxy='yes'. */
	public function isBlacklisted(): bool
	{
		if ( empty( $this->settings->ip_blacklisting_enable ) ) {
			return false;
		}
		$list = array_map( 'trim', explode( ',', (string)( $this->settings->ip_blacklisting ?? '' ) ) );
		$hit = in_array( $this->ip, $list, true ) || $this->ipInRange( $this->settings->ip_blacklisting_range ?? [] );
		if ( $hit ) {
			$this->ipData->proxy = 'yes';
		}
		return $hit;
	}
	
	/** Returns true if a cached (non-expired) result exists for this IP. */
	public function hasCachedResult(): bool
	{
		$cached = FENSE_BPVT_Cache::wp_get( 'bpvt_ip_' . md5( $this->appKey . '_' . $this->ip ) );
		$expires = is_array( $cached ) && isset( $cached['time'] ) ? (int)$cached['time'] : 0;
		if ( time() < $expires ) {
			$this->ipData = $cached['data'];
			return true;
		}
		return false;
	}
	
	/**
	 * Evaluates country filter rules.
	 * Returns an array: ['block' => bool, 'show_message' => bool]
	 */
	public function evaluateCountry(): array
	{
		$country = $this->ipData->country ?? '';
		if ( $country === '' ) {
			return [ 'ignoreProxy' => false, 'exclude' => false ];
		}
		
		// Block + show message.
		if ( !empty( $this->settings->re_disable_country )
			&& in_array( $country, (array)$this->settings->re_disable_country, true ) ) {
			return [ 'ignoreProxy' => true, 'exclude' => false ];
		}
		
		if ( !empty( $this->settings->re_enable_country )
			&& !in_array( $country, (array)$this->settings->re_enable_country, true ) ) {
			return [ 'ignoreProxy' => true, 'exclude' => false ];
		}
		
		if ( !empty( $this->settings->re_enable_country )
			&& in_array( $country, (array)$this->settings->re_enable_country, true ) ) {
			return [ 'ignoreProxy' => false, 'exclude' => true ];
		}
		
		// Silent block.
		if ( !empty( $this->settings->disable_country )
			&& in_array( $country, (array)$this->settings->disable_country, true ) ) {
			return [ 'ignoreProxy' => true, 'exclude' => true ];
		}
		
		// Silent allow-only.
		if ( !empty( $this->settings->enable_country )
			&& !in_array( $country, (array)$this->settings->enable_country, true ) ) {
			return [ 'ignoreProxy' => true, 'exclude' => true ];
		}
		
		return [ 'ignoreProxy' => false, 'exclude' => false ];
	}
	
	public function isProxy(): bool
	{
		return ( $this->ipData->proxy ?? '' ) === 'yes';
	}
	
	// -----------------------------------------------------------------------
	// Proxy status shorthand
	// -----------------------------------------------------------------------
	
	/**
	 * Returns true if detection should run on the current URL.
	 * Checks excluded_urls first (always skip), then applies url_mode
	 * with url_prefixes and specific_pages.
	 */
	public function isUrlAllowed(): bool
	{
		// --- 1. Excluded URLs (always skip detection on these) ---
		$excluded_csv = $this->settings->excluded_urls ?? '';
		if ( $excluded_csv !== '' && $this->matchesUrlRules( $excluded_csv ) ) {
			return false;
		}
		
		// --- 2. No include/exclude rules configured → allow everything ---
		$mode = $this->settings->url_mode ?? 'exclude';
		$prefixes = $this->parseCsv( $this->settings->url_prefixes ?? '' );
		$page_ids = array_map( 'absint', (array)( $this->settings->specific_pages ?? [] ) );
		
		if ( empty( $prefixes ) && empty( $page_ids ) ) {
			return true;
		}
		
		// --- 3. Match against WordPress query + URL helpers ---
		$matched = $this->matchesPageIds( $page_ids )
			|| $this->matchesUrlPrefixes( $prefixes );
		
		return $mode === 'include' ? $matched : !$matched;
	}

// ─── Helpers ──────────────────────────────────────────────────────────────────
	
	/**
	 * True if the current request matches any rule in a CSV of URL prefixes/paths.
	 */
	private function matchesUrlRules( string $csv ): bool
	{
		return $this->matchesUrlPrefixes( $this->parseCsv( $csv ) );
	}
	
	/**
	 * Uses WordPress's own URL utilities instead of manually parsing REQUEST_URI.
	 *
	 * home_url() gives us the correctly normalised base.
	 * wp_make_link_relative() strips scheme+host so we compare paths only.
	 * trailingslashit() normalises trailing slashes for prefix checks.
	 */
	private function matchesUrlPrefixes( array $prefixes ): bool
	{
		if ( empty( $prefixes ) ) {
			return false;
		}
		
		// Build a clean, relative path for the current request.
		$current = wp_make_link_relative(
			home_url( add_query_arg( [] ) )   // preserves query string; no manual $_SERVER
		);
		$current_path = trailingslashit( strtok( $current, '?' ) );  // path only, trailing-slash normalised
		
		foreach ( $prefixes as $rule ) {
			$rule_path = trailingslashit( $rule );
			
			// Exact match or the current path starts with this prefix.
			if ( $current_path === $rule_path
				|| str_starts_with( $current_path, $rule_path ) ) {
				return true;
			}
		}
		
		return false;
	}
	
	/**
	 * WordPress's is_page() understands IDs, slugs, titles, and paths —
	 * far more robust than a manual get_the_ID() comparison.
	 */
	private function matchesPageIds( array $page_ids ): bool
	{
		if ( empty( $page_ids ) ) {
			return false;
		}
		
		$url = isset( $_REQUEST['url'] )
			? sanitize_url( wp_unslash( $_REQUEST['url'] ) )
			: '';
		
		if ( $url === '' ) {
			return false;
		}
		
		// Strip scheme+host+query to get a clean path, then resolve to a post ID.
		$path = parse_url( $url, PHP_URL_PATH );
		$post_id = $path ? url_to_postid( $path ) : 0;
		
		return $post_id > 0 && in_array( $post_id, $page_ids, true );
	}
	
	/** @return string[] Trimmed, non-empty tokens from a comma-separated string. */
	private function parseCsv( string $csv ): array
	{
		if ( $csv === '' ) {
			return [];
		}
		return array_values(
			array_filter(
				array_map( static fn( string $v ) => trim( $v, "/ \t" ), explode( ',', $csv ) )
			)
		);
	}
	
	/** Numeric IPv4 range comparison helper. */
	private function ipInRange( $ranges ): bool
	{
		if ( empty( $ranges ) || !is_array( $ranges ) ) {
			return false;
		}
		$long = ip2long( $this->ip );
		if ( $long === false ) {
			return false;
		}
		foreach ( $ranges as $range ) {
			$from = ip2long( $range['from'] ?? '' );
			$to = ip2long( $range['to'] ?? '' );
			if ( $from !== false && $to !== false && $long >= $from && $long <= $to ) {
				return true;
			}
		}
		return false;
	}
}
