<?php
/**
 * Shared helper methods.
 *
 * @package Live_Traffic_Analytics
 */

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

/**
 * Helper methods for tracking and formatting.
 */
class WPLTA_Helpers {

	/**
	 * Resolve the visitor IP.
	 *
	 * @return string
	 */
	public static function get_client_ip() {
		$keys = array(
			'HTTP_CLIENT_IP',
			'HTTP_X_FORWARDED_FOR',
			'HTTP_X_FORWARDED',
			'HTTP_X_CLUSTER_CLIENT_IP',
			'HTTP_FORWARDED_FOR',
			'HTTP_FORWARDED',
			'REMOTE_ADDR',
		);

		foreach ( $keys as $key ) {
			if ( empty( $_SERVER[ $key ] ) ) {
				continue;
			}

			$raw_ips = sanitize_text_field( wp_unslash( $_SERVER[ $key ] ) );
			$ips     = explode( ',', $raw_ips );

			foreach ( $ips as $ip ) {
				$ip = trim( $ip );
				if ( filter_var( $ip, FILTER_VALIDATE_IP ) ) {
					return $ip;
				}
			}
		}

		return '';
	}

	/**
	 * Check if IP is private or reserved.
	 *
	 * @param string $ip IP address.
	 * @return bool
	 */
	public static function is_private_ip( $ip ) {
		if ( empty( $ip ) ) {
			return true;
		}

		return ! filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE );
	}

	/**
	 * Resolve country name from IP.
	 *
	 * @param string $ip IP address.
	 * @return string
	 */
	public static function get_country_by_ip( $ip ) {
		$geo_data = self::get_geo_data_by_ip( $ip );

		return ! empty( $geo_data['country'] ) ? $geo_data['country'] : 'Unknown';
	}

	/**
	 * Resolve geo data from IP.
	 *
	 * @param string $ip IP address.
	 * @return array
	 */
	public static function get_geo_data_by_ip( $ip ) {
		$empty_geo_data = self::get_empty_geo_data();

		if ( empty( $ip ) || self::is_private_ip( $ip ) ) {
			return $empty_geo_data;
		}

		$cache_key = 'wplta_geo_' . md5( $ip );
		$cached    = get_transient( $cache_key );

		if ( false !== $cached && is_array( $cached ) ) {
			return $cached;
		}

		$filtered_geo_data = apply_filters( 'wplta_resolve_geo_data', null, $ip );
		$geo_data          = is_array( $filtered_geo_data ) ? self::normalize_geo_data( $filtered_geo_data ) : self::get_geo_data_from_server_headers();

		set_transient( $cache_key, $geo_data, DAY_IN_SECONDS );

		return $geo_data;
	}

	/**
	 * Return an empty geo payload.
	 *
	 * @return array
	 */
	private static function get_empty_geo_data() {
		return array(
			'country'   => 'Unknown',
			'latitude'  => null,
			'longitude' => null,
		);
	}

	/**
	 * Normalize geo data from filters or headers.
	 *
	 * @param array $geo_data Geo payload.
	 * @return array
	 */
	private static function normalize_geo_data( $geo_data ) {
		$country = ! empty( $geo_data['country'] ) ? sanitize_text_field( $geo_data['country'] ) : 'Unknown';

		return array(
			'country'   => $country,
			'latitude'  => isset( $geo_data['latitude'] ) && is_numeric( $geo_data['latitude'] ) ? (float) $geo_data['latitude'] : null,
			'longitude' => isset( $geo_data['longitude'] ) && is_numeric( $geo_data['longitude'] ) ? (float) $geo_data['longitude'] : null,
		);
	}

	/**
	 * Resolve geo data from server-provided geo headers.
	 *
	 * @return array
	 */
	private static function get_geo_data_from_server_headers() {
		$country_code_headers = array(
			'HTTP_CF_IPCOUNTRY',
			'GEOIP_COUNTRY_CODE',
			'HTTP_X_COUNTRY_CODE',
		);
		$country_name_headers = array(
			'HTTP_CF_IPCOUNTRY_NAME',
			'GEOIP_COUNTRY_NAME',
			'HTTP_X_COUNTRY_NAME',
		);
		$country_code         = '';
		$country_name         = '';

		foreach ( $country_code_headers as $header ) {
			if ( empty( $_SERVER[ $header ] ) ) {
				continue;
			}

			$country_code = strtoupper( sanitize_text_field( wp_unslash( $_SERVER[ $header ] ) ) );
			break;
		}

		foreach ( $country_name_headers as $header ) {
			if ( empty( $_SERVER[ $header ] ) ) {
				continue;
			}

			$country_name = sanitize_text_field( wp_unslash( $_SERVER[ $header ] ) );
			break;
		}

		if ( empty( $country_name ) && ! empty( $country_code ) ) {
			$country_name = self::get_country_name_from_code( $country_code );
		}

		if ( empty( $country_name ) ) {
			return self::get_empty_geo_data();
		}

		$centroid = self::get_country_centroid( $country_name, $country_code );

		return array(
			'country'   => $country_name,
			'latitude'  => $centroid['latitude'],
			'longitude' => $centroid['longitude'],
		);
	}

	/**
	 * Map a country code to a display name.
	 *
	 * @param string $country_code Country code.
	 * @return string
	 */
	private static function get_country_name_from_code( $country_code ) {
		$country_names = array(
			'AU' => 'Australia',
			'BR' => 'Brazil',
			'CA' => 'Canada',
			'DE' => 'Germany',
			'ES' => 'Spain',
			'FR' => 'France',
			'GB' => 'United Kingdom',
			'IN' => 'India',
			'IT' => 'Italy',
			'JP' => 'Japan',
			'NL' => 'Netherlands',
			'PK' => 'Pakistan',
			'SA' => 'Saudi Arabia',
			'SG' => 'Singapore',
			'UA' => 'Ukraine',
			'US' => 'United States',
			'ZA' => 'South Africa',
		);

		return isset( $country_names[ $country_code ] ) ? $country_names[ $country_code ] : 'Unknown';
	}

	/**
	 * Return a rough country centroid for map plotting.
	 *
	 * @param string $country_name Country name.
	 * @param string $country_code Country code.
	 * @return array
	 */
	private static function get_country_centroid( $country_name, $country_code = '' ) {
		$centroids = array(
			'Australia'      => array( 'latitude' => -25.274400, 'longitude' => 133.775100 ),
			'Brazil'         => array( 'latitude' => -14.235000, 'longitude' => -51.925300 ),
			'Canada'         => array( 'latitude' => 56.130400, 'longitude' => -106.346800 ),
			'France'         => array( 'latitude' => 46.227600, 'longitude' => 2.213700 ),
			'Germany'        => array( 'latitude' => 51.165700, 'longitude' => 10.451500 ),
			'India'          => array( 'latitude' => 20.593700, 'longitude' => 78.962900 ),
			'Italy'          => array( 'latitude' => 41.871900, 'longitude' => 12.567400 ),
			'Japan'          => array( 'latitude' => 36.204800, 'longitude' => 138.252900 ),
			'Netherlands'    => array( 'latitude' => 52.132600, 'longitude' => 5.291300 ),
			'Pakistan'       => array( 'latitude' => 30.375300, 'longitude' => 69.345100 ),
			'Saudi Arabia'   => array( 'latitude' => 23.885900, 'longitude' => 45.079200 ),
			'Singapore'      => array( 'latitude' => 1.352100, 'longitude' => 103.819800 ),
			'South Africa'   => array( 'latitude' => -30.559500, 'longitude' => 22.937500 ),
			'Spain'          => array( 'latitude' => 40.463700, 'longitude' => -3.749200 ),
			'Ukraine'        => array( 'latitude' => 48.379400, 'longitude' => 31.165600 ),
			'United Kingdom' => array( 'latitude' => 55.378100, 'longitude' => -3.436000 ),
			'United States'  => array( 'latitude' => 39.828300, 'longitude' => -98.579500 ),
		);

		if ( isset( $centroids[ $country_name ] ) ) {
			return $centroids[ $country_name ];
		}

		if ( 'Unknown' === $country_name && ! empty( $country_code ) ) {
			$country_name = self::get_country_name_from_code( $country_code );
			if ( isset( $centroids[ $country_name ] ) ) {
				return $centroids[ $country_name ];
			}
		}

		return array(
			'latitude'  => null,
			'longitude' => null,
		);
	}

	/**
	 * Detect browser from user agent.
	 *
	 * @param string $user_agent Browser user agent.
	 * @return string
	 */
	public static function detect_browser( $user_agent ) {
		$map = array(
			'Edg/'     => 'Edge',
			'OPR/'     => 'Opera',
			'Chrome/'  => 'Chrome',
			'Firefox/' => 'Firefox',
			'Safari/'  => 'Safari',
			'MSIE'     => 'Internet Explorer',
			'Trident/' => 'Internet Explorer',
		);

		foreach ( $map as $needle => $browser ) {
			if ( false !== strpos( $user_agent, $needle ) ) {
				return $browser;
			}
		}

		return 'Unknown';
	}

	/**
	 * Detect device family from user agent.
	 *
	 * @param string $user_agent Browser user agent.
	 * @return string
	 */
	public static function detect_device_type( $user_agent ) {
		if ( preg_match( '/ipad|tablet|(android(?!.*mobile))/i', $user_agent ) ) {
			return 'tablet';
		}

		if ( preg_match( '/mobile|iphone|ipod|android|blackberry|opera mini|iemobile/i', $user_agent ) ) {
			return 'mobile';
		}

		return 'desktop';
	}

	/**
	 * Sanitize tracking payload values.
	 *
	 * @param array $payload Raw payload.
	 * @return array
	 */
	public static function sanitize_tracking_payload( $payload ) {
		$page_url = isset( $payload['page_url'] ) ? esc_url_raw( wp_unslash( $payload['page_url'] ) ) : '';
		$referrer = isset( $payload['referrer'] ) ? esc_url_raw( wp_unslash( $payload['referrer'] ) ) : '';
		$action   = isset( $payload['action_type'] ) ? sanitize_key( wp_unslash( $payload['action_type'] ) ) : 'heartbeat';

		return array(
			'session_id'    => isset( $payload['session_id'] ) ? sanitize_text_field( wp_unslash( $payload['session_id'] ) ) : '',
			'page_view_id'  => isset( $payload['page_view_id'] ) ? sanitize_text_field( wp_unslash( $payload['page_view_id'] ) ) : '',
			'page_url'      => $page_url,
			'page_title'    => isset( $payload['page_title'] ) ? sanitize_text_field( wp_unslash( $payload['page_title'] ) ) : '',
			'referrer'      => $referrer,
			'time_on_page'  => isset( $payload['time_on_page'] ) ? absint( $payload['time_on_page'] ) : 0,
			'page_entered'  => isset( $payload['page_entered_at'] ) ? sanitize_text_field( wp_unslash( $payload['page_entered_at'] ) ) : '',
			'action_type'   => in_array( $action, array( 'pageview', 'heartbeat', 'leave' ), true ) ? $action : 'heartbeat',
		);
	}

	/**
	 * Validate REST nonce for guest/admin requests.
	 *
	 * @param WP_REST_Request $request Request object.
	 * @return bool
	 */
	public static function verify_rest_nonce( $request ) {
		$nonce = $request->get_header( 'X-WP-Nonce' );

		if ( empty( $nonce ) ) {
			$nonce = $request->get_param( 'nonce' );
		}

		return ! empty( $nonce ) && wp_verify_nonce( sanitize_text_field( $nonce ), 'wp_rest' );
	}

	/**
	 * Check whether a request appears to come from the local site origin.
	 *
	 * @param WP_REST_Request $request Request object.
	 * @return bool
	 */
	public static function is_same_site_request( $request ) {
		$site_host = wp_parse_url( home_url(), PHP_URL_HOST );

		if ( empty( $site_host ) ) {
			return true;
		}

		$headers = array(
			$request->get_header( 'Origin' ),
			$request->get_header( 'Referer' ),
		);

		foreach ( $headers as $header_value ) {
			if ( empty( $header_value ) ) {
				continue;
			}

			$header_host = wp_parse_url( esc_url_raw( $header_value ), PHP_URL_HOST );

			if ( empty( $header_host ) ) {
				continue;
			}

			return strtolower( $header_host ) === strtolower( $site_host );
		}

		return true;
	}

	/**
	 * Format datetime in site timezone.
	 *
	 * @param string $datetime MySQL datetime.
	 * @return string
	 */
	public static function format_datetime( $datetime ) {
		if ( empty( $datetime ) ) {
			return '';
		}

		// Stored datetimes use current_time( 'mysql' ), i.e. they are already in the
		// site timezone. mysql2date() interprets the string in the site timezone and
		// formats it in the same timezone, so it must NOT be combined with wp_date()
		// (which would apply the timezone offset a second time and shift the result).
		$formatted = mysql2date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $datetime );

		return $formatted ? $formatted : '';
	}

	/**
	 * Return host from URL.
	 *
	 * @param string $url URL.
	 * @return string
	 */
	public static function get_host_from_url( $url ) {
		$host = wp_parse_url( $url, PHP_URL_HOST );

		return $host ? sanitize_text_field( $host ) : '';
	}

	/**
	 * Detect bot classification and score.
	 *
	 * @param string $user_agent User agent.
	 * @param array  $payload Tracking payload.
	 * @param array  $signals Aggregated signals.
	 * @return array
	 */
	public static function detect_bot( $user_agent, $payload, $signals ) {
		$bot_patterns = array(
			'Googlebot'   => 'googlebot',
			'Bingbot'     => 'bingbot',
			'DuckDuckBot' => 'duckduckbot',
			'YandexBot'   => 'yandexbot',
			'Baiduspider' => 'baiduspider',
			'AhrefsBot'   => 'ahrefsbot',
			'SemrushBot'  => 'semrushbot',
			'MJ12bot'     => 'mj12bot',
			'GPTBot'      => 'gptbot',
			'ClaudeBot'   => 'claudebot',
			'facebookexternalhit' => 'facebookexternalhit',
			'Twitterbot'  => 'twitterbot',
			'Applebot'    => 'applebot',
		);
		$score        = 0;
		$bot_name     = '';
		$user_agent_l = strtolower( $user_agent );

		foreach ( $bot_patterns as $label => $needle ) {
			if ( false !== strpos( $user_agent_l, $needle ) ) {
				$score    += 90;
				$bot_name = $label;
				break;
			}
		}

		if ( empty( $payload['referrer'] ) && 'pageview' === $payload['action_type'] ) {
			$score += 10;
		}

		if ( ! empty( $signals['recent_hits'] ) && $signals['recent_hits'] >= 6 ) {
			$score += 40;
		} elseif ( ! empty( $signals['recent_hits'] ) && $signals['recent_hits'] >= 4 ) {
			$score += 20;
		}

		if ( ! empty( $signals['recent_page_views'] ) && $signals['recent_page_views'] >= 3 ) {
			$score += 20;
		}

		if ( ! empty( $signals['unique_pages'] ) && $signals['unique_pages'] >= 4 ) {
			$score += 15;
		}

		if ( empty( $user_agent ) ) {
			$score += 20;
		}

		if ( ! $bot_name && $score >= 60 ) {
			$bot_name = 'Suspicious automation';
		}

		return array(
			'is_bot'   => $score >= 60,
			'bot_name' => $bot_name,
			'score'    => $score,
		);
	}
}
