<?php
/**
 * Central access decision maker.
 *
 * @package PrivexPasswordProtect
 */

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

/**
 * Class PPMSTR_Access_Checker
 */
class PPMSTR_Access_Checker {

	/**
	 * @var PPMSTR_Password_Validator
	 */
	private $validator;

	/**
	 * @var PPMSTR_Cookie_Manager
	 */
	private $cookies;

	/**
	 * Constructor.
	 *
	 * @param PPMSTR_Password_Validator $validator Password validator.
	 * @param PPMSTR_Cookie_Manager     $cookies   Cookie manager.
	 */
	public function __construct( PPMSTR_Password_Validator $validator, PPMSTR_Cookie_Manager $cookies ) {
		$this->validator = $validator;
		$this->cookies   = $cookies;
	}

	/* ================================================================
	 * Primary access checks
	 * ================================================================ */

	/**
	 * Check whether a specific post is currently accessible.
	 *
	 * @param int $post_id Post ID.
	 * @return bool
	 */
	public function can_access_post( $post_id ) {
		if ( ! $this->is_post_protected( $post_id ) ) {
			return true;
		}
		// IP blacklist always blocks.
		if ( $this->is_ip_blacklisted() ) {
			return false;
		}
		if ( $this->should_bypass() ) {
			return true;
		}
		if ( $this->cookies->has_master_cookie() ) {
			return true;
		}
		// IP whitelist bypasses.
		if ( $this->is_ip_whitelisted() ) {
			return true;
		}
		// Role-based bypass (global + per-post).
		if ( $this->has_bypass_role() || $this->has_post_role_access( $post_id ) ) {
			return true;
		}
		// User-based access.
		if ( $this->has_post_user_access( $post_id ) ) {
			return true;
		}
		// Time-based: outside protection window = free access.
		if ( $this->is_outside_schedule( $post_id ) ) {
			return true;
		}
		if ( $this->is_excluded( $post_id ) ) {
			return true;
		}
		if ( $this->cookies->has_post_cookie( $post_id ) ) {
			return true;
		}
		return false;
	}

	/**
	 * Check whether the site is currently accessible (site-wide protection).
	 *
	 * @return bool
	 */
	public function can_access_site() {
		$settings = get_option( 'ppmstr_settings', array() );

		if ( empty( $settings['site_protection_enabled'] ) ) {
			return true;
		}
		if ( empty( $settings['site_password'] ) ) {
			return true;
		}
		if ( $this->is_ip_blacklisted() ) {
			return false;
		}
		if ( $this->should_bypass() ) {
			return true;
		}
		if ( $this->cookies->has_site_cookie() ) {
			return true;
		}
		if ( $this->cookies->has_master_cookie() ) {
			return true;
		}
		if ( $this->is_ip_whitelisted() ) {
			return true;
		}
		if ( $this->has_bypass_role() ) {
			return true;
		}
		return false;
	}

	/**
	 * Check whether a post's category grants access.
	 *
	 * @param int $post_id Post ID.
	 * @return bool True if accessible.
	 */
	public function can_access_via_category( $post_id ) {
		$protected_cat = $this->get_protected_category_for_post( $post_id );

		if ( ! $protected_cat ) {
			return true;
		}
		if ( $this->is_ip_blacklisted() ) {
			return false;
		}
		if ( $this->should_bypass() ) {
			return true;
		}
		if ( $this->cookies->has_category_cookie( $protected_cat ) ) {
			return true;
		}
		if ( $this->cookies->has_master_cookie() ) {
			return true;
		}
		if ( $this->is_ip_whitelisted() || $this->has_bypass_role() ) {
			return true;
		}
		return false;
	}

	/**
	 * Check whether a post's post type grants access.
	 *
	 * @param int $post_id Post ID.
	 * @return bool True if accessible.
	 */
	public function can_access_via_post_type( $post_id ) {
		$post_type = get_post_type( $post_id );
		if ( ! $this->is_post_type_protected( $post_type ) ) {
			return true; // Not globally protected.
		}

		if ( $this->is_ip_blacklisted() ) {
			return false;
		}
		if ( $this->should_bypass() ) {
			return true;
		}
		if ( $this->cookies->has_post_type_cookie( $post_type ) ) {
			return true;
		}
		if ( $this->cookies->has_master_cookie() ) {
			return true;
		}
		if ( $this->is_ip_whitelisted() || $this->has_bypass_role() ) {
			return true;
		}
		return false;
	}

	/**
	 * Check whether a child page inherits protection from a parent.
	 *
	 * @param int $post_id Post ID.
	 * @return bool True if accessible.
	 */
	public function can_access_as_child( $post_id ) {
		$settings = get_option( 'ppmstr_settings', array() );

		if ( empty( $settings['protect_children'] ) ) {
			return true;
		}

		$parent_id = $this->get_protected_ancestor( $post_id );
		if ( ! $parent_id ) {
			return true;
		}
		if ( $this->should_bypass() ) {
			return true;
		}
		if ( $this->cookies->has_post_cookie( $parent_id ) ) {
			return true;
		}
		if ( $this->cookies->has_master_cookie() || $this->is_ip_whitelisted() || $this->has_bypass_role() ) {
			return true;
		}
		return false;
	}

	/**
	 * Full access check with Hierarchical Priority:
	 * 1. Individual Post (highest)
	 * 2. Post Type
	 * 3. Category
	 * 4. Child Inheritance
	 * (Site Lock is handled separately by the engine)
	 *
	 * @param int $post_id Post ID.
	 * @return bool
	 */
	public function can_fully_access_post( $post_id ) {
		// 1. Individual Post Password.
		if ( $this->is_post_protected( $post_id ) ) {
			return $this->can_access_post( $post_id );
		}

		// 2. Post Type Password.
		$post_type = get_post_type( $post_id );
		if ( $this->is_post_type_protected( $post_type ) ) {
			return $this->can_access_via_post_type( $post_id );
		}

		// 3. Category Password.
		if ( $this->is_in_protected_category( $post_id ) ) {
			return $this->can_access_via_category( $post_id );
		}

		// 4. Child Inheritance.
		if ( $this->has_protected_ancestor( $post_id ) ) {
			return $this->can_access_as_child( $post_id );
		}

		return true;
	}

	/* ================================================================
	 * Protection status helpers
	 * ================================================================ */

	/**
	 * Determine if a post is individually protected.
	 *
	 * @param int $post_id Post ID.
	 * @return bool
	 */
	public function is_post_protected( $post_id ) {
		$protected = get_post_meta( $post_id, '_ppmstr_protected', true );
		if ( '1' !== $protected ) {
			return false;
		}

		$passwords = get_post_meta( $post_id, '_ppmstr_passwords', true );
		$has_pass  = ( is_array( $passwords ) && ! empty( $passwords ) ) || ! empty( get_post_meta( $post_id, '_ppmstr_password', true ) );

		if ( ! $has_pass ) {
			return false;
		}

		// Scheduling (Auto-Unlock / Auto-Lock) phase 7.
		$sched_start = get_post_meta( $post_id, '_ppmstr_schedule_start', true );
		$sched_end   = get_post_meta( $post_id, '_ppmstr_schedule_end', true );
		$now         = current_time( 'timestamp' );

		if ( ! empty( $sched_end ) && $now > strtotime( $sched_end ) ) {
			return true; // Auto-locked.
		}

		if ( ! empty( $sched_start ) && $now > strtotime( $sched_start ) ) {
			return false; // Auto-unlocked.
		}

		return true;
	}

	public function is_post_type_protected( $post_type ) {
		$settings = get_option( 'ppmstr_settings', array() );
		$protected = $settings['protected_post_types'] ?? array();
		$password  = $settings['post_type_password'] ?? '';

		return in_array( $post_type, $protected ) && ! empty( $password );
	}

	/**
	 * Check if a post is in a protected category.
	 *
	 * @param int $post_id Post ID.
	 * @return bool
	 */
	public function is_in_protected_category( $post_id ) {
		return (bool) $this->get_protected_category_for_post( $post_id );
	}

	/**
	 * Check if a post has a protected ancestor.
	 *
	 * @param int $post_id Post ID.
	 * @return bool
	 */
	public function has_protected_ancestor( $post_id ) {
		$settings = get_option( 'ppmstr_settings', array() );
		if ( empty( $settings['protect_children'] ) ) {
			return false;
		}
		return (bool) $this->get_protected_ancestor( $post_id );
	}

	/**
	 * Get the first protected category term ID for a post.
	 *
	 * @param int $post_id Post ID.
	 * @return int|false Term ID or false.
	 */
	public function get_protected_category_for_post( $post_id ) {
		$settings       = get_option( 'ppmstr_settings', array() );
		$protected_cats = isset( $settings['protected_categories'] ) ? $settings['protected_categories'] : array();
		$category_pass  = isset( $settings['category_password'] ) ? $settings['category_password'] : '';

		if ( empty( $protected_cats ) || empty( $category_pass ) ) {
			return false;
		}

		$post_cats = wp_get_post_categories( $post_id, array( 'fields' => 'ids' ) );
		if ( ! is_array( $post_cats ) ) {
			return false;
		}

		$intersect = array_intersect( $post_cats, $protected_cats );
		return ! empty( $intersect ) ? reset( $intersect ) : false;
	}

	/**
	 * Get the nearest protected ancestor page ID.
	 *
	 * @param int $post_id Post ID.
	 * @return int|false
	 */
	public function get_protected_ancestor( $post_id ) {
		$post = get_post( $post_id );
		if ( ! $post || empty( $post->post_parent ) ) {
			return false;
		}

		$parent_id = $post->post_parent;
		while ( $parent_id ) {
			if ( $this->is_post_protected( $parent_id ) ) {
				return $parent_id;
			}
			$parent    = get_post( $parent_id );
			$parent_id = $parent ? $parent->post_parent : 0;
		}

		return false;
	}

	/**
	 * Check if a post is excluded from site-wide protection.
	 *
	 * @param int $post_id Post ID.
	 * @return bool
	 */
	public function is_excluded( $post_id ) {
		$settings = get_option( 'ppmstr_settings', array() );
		$excluded = isset( $settings['excluded_posts'] ) ? $settings['excluded_posts'] : array();
		return in_array( $post_id, $excluded );
	}

	/* ================================================================
	 * Phase 4: Advanced access control helpers
	 * ================================================================ */

	/**
	 * Check if the current visitor's IP is blacklisted.
	 * PRO feature — always returns false in the free version.
	 *
	 * @return bool
	 */
	public function is_ip_blacklisted() {
		/* fs_start_pro */
		if ( function_exists( 'ppp_fs' ) && ppp_fs()->can_use_premium_code__premium_only() ) {
			$settings  = get_option( 'ppmstr_settings', array() );
			$blacklist = isset( $settings['ip_blacklist'] ) ? trim( $settings['ip_blacklist'] ) : '';
			if ( empty( $blacklist ) ) {
				return false;
			}
			return $this->ip_in_list( $this->get_visitor_ip(), $blacklist );
		}
		/* fs_end_pro */
		return false; // Free version always returns false.
	}

	/**
	 * Check if the current visitor's IP is whitelisted.
	 * PRO feature — always returns false in the free version.
	 *
	 * @return bool
	 */
	public function is_ip_whitelisted() {
		/* fs_start_pro */
		if ( function_exists( 'ppp_fs' ) && ppp_fs()->can_use_premium_code__premium_only() ) {
			$settings  = get_option( 'ppmstr_settings', array() );
			$whitelist = isset( $settings['ip_whitelist'] ) ? trim( $settings['ip_whitelist'] ) : '';
			if ( empty( $whitelist ) ) {
				return false;
			}
			return $this->ip_in_list( $this->get_visitor_ip(), $whitelist );
		}
		/* fs_end_pro */
		return false; // Free version always returns false.
	}

	/**
	 * Check if the current user has a global bypass role.
	 * PRO feature — always returns false in the free version.
	 *
	 * @return bool
	 */
	public function has_bypass_role() {
		/* fs_start_pro */
		if ( function_exists( 'ppp_fs' ) && ppp_fs()->can_use_premium_code__premium_only() ) {
			if ( ! is_user_logged_in() ) {
				return false;
			}
			$settings     = get_option( 'ppmstr_settings', array() );
			$bypass_roles = isset( $settings['bypass_roles'] ) ? $settings['bypass_roles'] : array();
			if ( empty( $bypass_roles ) ) {
				return false;
			}
			$user = wp_get_current_user();
			return ! empty( array_intersect( $user->roles, $bypass_roles ) );
		}
		/* fs_end_pro */
		return false; // Free version always returns false.
	}

	/**
	 * Check if the current user has a per-post allowed role.
	 *
	 * @param int $post_id Post ID.
	 * @return bool
	 */
	public function has_post_role_access( $post_id ) {
		if ( ! is_user_logged_in() ) {
			return false;
		}
		$allowed_roles = get_post_meta( $post_id, '_ppmstr_allowed_roles', true );
		if ( ! is_array( $allowed_roles ) || empty( $allowed_roles ) ) {
			return false;
		}
		$user = wp_get_current_user();
		return ! empty( array_intersect( $user->roles, $allowed_roles ) );
	}

	/**
	 * Check if the current user has per-post user-level access.
	 *
	 * @param int $post_id Post ID.
	 * @return bool
	 */
	public function has_post_user_access( $post_id ) {
		if ( ! is_user_logged_in() ) {
			return false;
		}
		$allowed_users = get_post_meta( $post_id, '_ppmstr_allowed_users', true );
		if ( ! is_array( $allowed_users ) || empty( $allowed_users ) ) {
			return false;
		}
		return in_array( get_current_user_id(), $allowed_users, true );
	}

	/**
	 * Check if the current time is outside the post's protection schedule.
	 * If outside the schedule, the content is freely accessible.
	 *
	 * @param int $post_id Post ID.
	 * @return bool True if outside the protection window (no protection needed).
	 */
	public function is_outside_schedule( $post_id ) {
		$start = get_post_meta( $post_id, '_ppmstr_schedule_start', true );
		$end   = get_post_meta( $post_id, '_ppmstr_schedule_end', true );

		if ( empty( $start ) && empty( $end ) ) {
			return false; // No schedule = always protected.
		}

		$now = current_time( 'timestamp' );

		// If start is set and we haven't reached it yet, content is free.
		if ( ! empty( $start ) && $now < strtotime( $start ) ) {
			return true;
		}

		// If end is set and we've passed it, content is free.
		if ( ! empty( $end ) && $now > strtotime( $end ) ) {
			return true;
		}

		return false; // Within the scheduled protection window.
	}

	/* ================================================================
	 * Private helpers
	 * ================================================================ */

	/**
	 * Check whether the current logged-in user should bypass protection.
	 *
	 * @return bool
	 */
	private function should_bypass() {
		$settings = get_option( 'ppmstr_settings', array() );
		if ( empty( $settings['admin_bypass'] ) ) {
			return false;
		}
		return current_user_can( 'manage_options' );
	}

	/**
	 * Get the visitor's real IP address.
	 *
	 * @return string
	 */
	private function get_visitor_ip() {
		$headers = array(
			'HTTP_CF_CONNECTING_IP', // Cloudflare.
			'HTTP_X_FORWARDED_FOR',
			'HTTP_X_REAL_IP',
			'REMOTE_ADDR',
		);
		foreach ( $headers as $header ) {
			if ( ! empty( $_SERVER[ $header ] ) ) {
				$ip = sanitize_text_field( wp_unslash( $_SERVER[ $header ] ) );
				// X-Forwarded-For may be comma-separated; take the first.
				if ( strpos( $ip, ',' ) !== false ) {
					$ip = trim( explode( ',', $ip )[0] );
				}
				if ( filter_var( $ip, FILTER_VALIDATE_IP ) ) {
					return $ip;
				}
			}
		}
		return '0.0.0.0';
	}

	/**
	 * Check if an IP is in a newline-separated list (supports CIDR).
	 *
	 * @param string $ip   IP address to check.
	 * @param string $list Newline-separated IP/CIDR list.
	 * @return bool
	 */
	private function ip_in_list( $ip, $list ) {
		$entries = array_filter( array_map( 'trim', explode( "\n", $list ) ) );
		foreach ( $entries as $entry ) {
			if ( $entry === $ip ) {
				return true;
			}
			// CIDR check.
			if ( strpos( $entry, '/' ) !== false && $this->ip_in_cidr( $ip, $entry ) ) {
				return true;
			}
		}
		return false;
	}

	/**
	 * Check if an IP address falls within a CIDR range.
	 *
	 * @param string $ip   IP address.
	 * @param string $cidr CIDR notation (e.g. 192.168.1.0/24).
	 * @return bool
	 */
	private function ip_in_cidr( $ip, $cidr ) {
		list( $subnet, $mask ) = explode( '/', $cidr );
		$mask = (int) $mask;
		if ( $mask < 0 || $mask > 32 ) {
			return false;
		}
		$ip_long     = ip2long( $ip );
		$subnet_long = ip2long( $subnet );
		if ( false === $ip_long || false === $subnet_long ) {
			return false;
		}
		$wildcard = pow( 2, 32 - $mask ) - 1;
		$netmask  = ~$wildcard;
		return ( $ip_long & $netmask ) === ( $subnet_long & $netmask );
	}
}
