<?php
/**
 * IP access control for Security Pack.
 *
 * @package ArkHost_Security_Pack
 */

// Prevent direct access.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * IP control class for whitelist/blacklist management.
 */
class ARKSP_IP_Control {

	/**
	 * Cached whitelist IPs.
	 *
	 * @var array|null
	 */
	private $whitelist_cache = null;

	/**
	 * Cached blacklist IPs.
	 *
	 * @var array|null
	 */
	private $blacklist_cache = null;

	/**
	 * Constructor.
	 */
	public function __construct() {
		// Run IP check immediately (constructor runs during init).
		// This blocks blacklisted IPs from accessing the entire website.
		$this->check_ip_access();
	}

	/**
	 * Check IP access on every request.
	 */
	public function check_ip_access() {
		$ip = ARKSP_Helper::get_client_ip();

		if ( ! $ip ) {
			return;
		}

		// Always allow whitelisted IPs.
		if ( $this->is_whitelisted( $ip ) ) {
			return;
		}

		// Block blacklisted IPs.
		if ( $this->is_blacklisted( $ip ) ) {
			ARKSP_Activity_Log::log( ARKSP_Activity_Log::EVENT_IP_BLOCKED, $ip, null, __( 'IP blacklisted', 'arkhost-security-pack' ) );
			$this->block_access( __( 'Your IP address has been blocked.', 'arkhost-security-pack' ) );
		}

		// Check auto-blocked IPs.
		if ( $this->is_auto_blocked( $ip ) ) {
			ARKSP_Activity_Log::log( ARKSP_Activity_Log::EVENT_IP_BLOCKED, $ip, null, __( 'IP auto-blocked', 'arkhost-security-pack' ) );
			$this->block_access( __( 'Your IP address has been temporarily blocked due to suspicious activity.', 'arkhost-security-pack' ) );
		}
	}

	/**
	 * Check if IP is whitelisted.
	 *
	 * @param string $ip IP address to check.
	 * @return bool
	 */
	public function is_whitelisted( $ip ) {
		$whitelist = $this->get_whitelist();
		return ARKSP_Helper::ip_matches_rules( $ip, $whitelist );
	}

	/**
	 * Check if IP is blacklisted.
	 *
	 * @param string $ip IP address to check.
	 * @return bool
	 */
	public function is_blacklisted( $ip ) {
		$blacklist = $this->get_blacklist();
		return ARKSP_Helper::ip_matches_rules( $ip, $blacklist );
	}

	/**
	 * Check if IP is auto-blocked (temporary block from failed logins).
	 *
	 * @param string $ip IP address to check.
	 * @return bool
	 */
	public function is_auto_blocked( $ip ) {
		global $wpdb;

		$max_attempts    = (int) ARKSP_Plugin::get_setting( 'login_max_attempts', 5 );
		$lockout_minutes = (int) ARKSP_Plugin::get_setting( 'login_lockout_duration', 15 );

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Security data must be real-time.
		$lockout = $wpdb->get_row(
			$wpdb->prepare(
				"SELECT * FROM {$wpdb->prefix}arksp_lockouts WHERE ip_address = %s",
				$ip
			)
		);

		if ( ! $lockout ) {
			return false;
		}

		// Check if IP has reached max attempts and is still within lockout window.
		if ( (int) $lockout->failed_attempts >= $max_attempts ) {
			// Calculate if lockout is still active based on updated_at + duration.
			$updated_time    = strtotime( $lockout->updated_at );
			$lockout_expires = $updated_time + ( $lockout_minutes * 60 );
			$current_time    = time();

			if ( $current_time < $lockout_expires ) {
				return true; // Still locked out.
			}
		}

		// Also check lockout_until for honeypot/manual blocks (stored as Unix timestamp).
		if ( ! empty( $lockout->lockout_until ) && is_numeric( $lockout->lockout_until ) ) {
			if ( time() < (int) $lockout->lockout_until ) {
				return true; // Still locked out via lockout_until.
			}
		}

		return false;
	}

	/**
	 * Get whitelist IPs.
	 *
	 * @return array
	 */
	public function get_whitelist() {
		if ( null === $this->whitelist_cache ) {
			$whitelist_text        = ARKSP_Plugin::get_setting( 'ip_whitelist', '' );
			$this->whitelist_cache = ARKSP_Helper::parse_ip_list( $whitelist_text );
		}
		return $this->whitelist_cache;
	}

	/**
	 * Get blacklist IPs.
	 *
	 * @return array
	 */
	public function get_blacklist() {
		if ( null === $this->blacklist_cache ) {
			$blacklist_text        = ARKSP_Plugin::get_setting( 'ip_blacklist', '' );
			$this->blacklist_cache = ARKSP_Helper::parse_ip_list( $blacklist_text );
		}
		return $this->blacklist_cache;
	}

	/**
	 * Clear internal caches (call after modifying whitelist/blacklist).
	 */
	public function clear_cache() {
		$this->whitelist_cache = null;
		$this->blacklist_cache = null;
	}

	/**
	 * Add IP to auto-block list (temporary lockout).
	 *
	 * @param string $ip       IP address.
	 * @param int    $duration Duration in minutes.
	 * @param string $reason   Reason for blocking.
	 * @return bool
	 */
	public function auto_block_ip( $ip, $duration = 15, $reason = '' ) {
		global $wpdb;

		// Store as Unix timestamp for consistency with lockout_ip().
		$lockout_until = time() + ( $duration * 60 );

		// Check if already exists.
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Security data must be real-time.
		$existing = $wpdb->get_var(
			$wpdb->prepare(
				"SELECT id FROM {$wpdb->prefix}arksp_lockouts WHERE ip_address = %s",
				$ip
			)
		);

		if ( $existing ) {
			// Update existing record.
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Security data must be real-time.
			return false !== $wpdb->update(
				$wpdb->prefix . 'arksp_lockouts',
				array(
					'lockout_until' => $lockout_until,
					'updated_at'    => current_time( 'mysql' ),
				),
				array( 'ip_address' => $ip ),
				array( '%d', '%s' ),
				array( '%s' )
			);
		}

		// Insert new record.
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Security data must be real-time.
		return false !== $wpdb->insert(
			$wpdb->prefix . 'arksp_lockouts',
			array(
				'ip_address'     => $ip,
				'failed_attempts' => 0,
				'lockout_until'  => $lockout_until,
				'created_at'     => current_time( 'mysql' ),
				'updated_at'     => current_time( 'mysql' ),
			),
			array( '%s', '%d', '%d', '%s', '%s' )
		);
	}

	/**
	 * Remove IP from auto-block list.
	 *
	 * @param string $ip IP address.
	 * @return bool
	 */
	public function unblock_ip( $ip ) {
		global $wpdb;

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Security data must be real-time.
		return false !== $wpdb->delete(
			$wpdb->prefix . 'arksp_lockouts',
			array( 'ip_address' => $ip ),
			array( '%s' )
		);
	}

	/**
	 * Get all currently blocked IPs.
	 *
	 * @return array
	 */
	public function get_blocked_ips() {
		global $wpdb;

		$max_attempts    = (int) ARKSP_Plugin::get_setting( 'login_max_attempts', 5 );
		$lockout_minutes = (int) ARKSP_Plugin::get_setting( 'login_lockout_duration', 15 );
		$current_time    = time();

		// Get all lockout records.
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Security data must be real-time.
		$lockouts = $wpdb->get_results( "SELECT * FROM {$wpdb->prefix}arksp_lockouts ORDER BY updated_at DESC" );

		$blocked = array();
		foreach ( $lockouts as $lockout ) {
			$is_blocked      = false;
			$lockout_expires = 0;

			// Check login-based lockout (failed_attempts >= max and within time window).
			if ( (int) $lockout->failed_attempts >= $max_attempts ) {
				$updated_time    = strtotime( $lockout->updated_at );
				$lockout_expires = $updated_time + ( $lockout_minutes * 60 );
				if ( $current_time < $lockout_expires ) {
					$is_blocked = true;
				}
			}

			// Check honeypot/manual lockout (lockout_until as Unix timestamp).
			if ( ! empty( $lockout->lockout_until ) && is_numeric( $lockout->lockout_until ) ) {
				$lockout_until_ts = (int) $lockout->lockout_until;
				if ( $current_time < $lockout_until_ts ) {
					$is_blocked = true;
					// Use the later expiry time.
					if ( $lockout_until_ts > $lockout_expires ) {
						$lockout_expires = $lockout_until_ts;
					}
				}
			}

			if ( $is_blocked ) {
				// Add computed lockout_until field for display.
				$lockout->lockout_until = gmdate( 'Y-m-d H:i:s', $lockout_expires );
				$blocked[]              = $lockout;
			}
		}

		return $blocked;
	}

	/**
	 * Block access and exit.
	 *
	 * @param string $message Error message.
	 */
	private function block_access( $message ) {
		status_header( 403 );
		nocache_headers();

		if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) {
			wp_send_json_error( array( 'message' => $message ), 403 );
		}

		// Simple blocked page.
		wp_die(
			esc_html( $message ),
			esc_html__( 'Access Denied', 'arkhost-security-pack' ),
			array(
				'response'  => 403,
				'back_link' => false,
			)
		);
	}
}
