<?php
if ( ! defined( 'ABSPATH' ) ) exit;

/**
 * Shared utility helpers used across the plugin.
 */
class VLENS_Helpers {

    /**
     * Get full table name with WP prefix.
     */
    public static function table( string $name ): string {
        global $wpdb;
        return $wpdb->prefix . VLENS_TABLE_PREFIX . $name;
    }

    /**
     * Get the real visitor IP address.
     */
    public static function get_real_ip(): string {
        $headers = [
            'HTTP_CF_CONNECTING_IP',
            'HTTP_X_FORWARDED_FOR',
            'HTTP_X_REAL_IP',
            'REMOTE_ADDR',
        ];
        foreach ( $headers as $key ) {
            if ( ! empty( $_SERVER[ $key ] ) ) {
                $ip = trim( explode( ',', sanitize_text_field( wp_unslash( $_SERVER[ $key ] ) ) )[0] );
                if ( filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE ) ) {
                    return $ip;
                }
            }
        }
        return isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : '0.0.0.0';
    }

    /**
     * Hash an IP for GDPR-compliant storage.
     */
    public static function hash_ip( string $ip ): string {
        $salt = get_option( 'vlens_ip_salt', 'default-salt' );
        return hash_hmac( 'sha256', $ip, $salt );
    }

    /**
     * Sanitize a string to a max length.
     */
    public static function sanitize_str( string $value, int $max ): string {
        return mb_substr( sanitize_text_field( $value ), 0, $max );
    }

    /**
     * Whether a request's Origin/Referer (if any) points back at this site.
     * Missing headers, and the literal "null" origin browsers send for
     * sandboxed iframes/some redirect flows, are allowed through — this is a
     * forgery deterrent for blind cross-site abuse, not an auth check.
     */
    public static function is_same_site_request( WP_REST_Request $request ): bool {
        $check = $request->get_header( 'origin' ) ?: $request->get_header( 'referer' );
        if ( ! $check || strtolower( $check ) === 'null' ) {
            return true;
        }
        $host      = strtolower( (string) wp_parse_url( $check, PHP_URL_HOST ) );
        $site_host = strtolower( (string) wp_parse_url( home_url(), PHP_URL_HOST ) );
        return $host && $host === $site_host;
    }

    /**
     * Per-IP fixed-window rate limit, best-effort (not atomic — under heavy
     * concurrency a few requests past the limit may slip through, which is
     * fine for an abuse deterrent rather than a hard security boundary).
     *
     * All IPs for a given $bucket share a single transient (a small pruned
     * array) instead of one transient per IP, so a flood of distinct visitor
     * IPs can't balloon the number of rows added to wp_options.
     *
     * Returns true once the caller has made $max_requests or more requests
     * to $bucket within $window_seconds.
     */
    public static function rate_limit_exceeded( string $bucket, int $max_requests, int $window_seconds ): bool {
        $key     = 'vlens_rl_' . $bucket;
        $now     = time();
        $buckets = get_transient( $key );
        if ( ! is_array( $buckets ) ) {
            $buckets = [];
        }
        // Prune expired entries so this stays small regardless of unique-visitor volume.
        foreach ( $buckets as $hash => $entry ) {
            if ( ! is_array( $entry ) || $entry['reset'] <= $now ) {
                unset( $buckets[ $hash ] );
            }
        }

        $ip_hash = self::hash_ip( self::get_real_ip() );
        if ( ! isset( $buckets[ $ip_hash ] ) ) {
            $buckets[ $ip_hash ] = [ 'count' => 0, 'reset' => $now + $window_seconds ];
        }
        $buckets[ $ip_hash ]['count']++;
        $exceeded = $buckets[ $ip_hash ]['count'] > $max_requests;

        set_transient( $key, $buckets, $window_seconds );

        return $exceeded;
    }

    /**
     * Permission callback for intentionally-public write endpoints
     * (anonymous visitor tracking beacons). Stays open to unauthenticated
     * visitors — that's the point of these endpoints — but rejects
     * cross-origin forgery attempts and throttles per-IP flooding.
     *
     * The limit is deliberately generous (300 requests / 10s from one IP):
     * many real visitors can share one IP behind corporate NAT or mobile
     * carrier CGNAT, and this is meant to stop scripted flooding/forgery,
     * not throttle ordinary shared-IP traffic.
     */
    public static function public_write_permission( WP_REST_Request $request, string $bucket ) {
        if ( ! self::is_same_site_request( $request ) ) {
            return new WP_Error( 'vlens_forbidden_origin', 'Invalid request origin.', [ 'status' => 403 ] );
        }
        if ( self::rate_limit_exceeded( $bucket, 300, 10 ) ) {
            return new WP_Error( 'vlens_rate_limited', 'Too many requests.', [ 'status' => 429 ] );
        }
        return true;
    }

    /**
     * Parse browser name and version from User-Agent.
     */
    public static function parse_browser( string $ua ): array {
        $browsers = [
            'Edg'     => 'Edge',
            'OPR'     => 'Opera',
            'Opera'   => 'Opera',
            'Chrome'  => 'Chrome',
            'Safari'  => 'Safari',
            'Firefox' => 'Firefox',
            'MSIE'    => 'Internet Explorer',
            'Trident' => 'Internet Explorer',
        ];
        foreach ( $browsers as $key => $name ) {
            if ( stripos( $ua, $key ) !== false ) {
                preg_match( '/' . preg_quote( $key, '/' ) . '[\/\s]?([\d.]+)/i', $ua, $m );
                return [ $name, $m[1] ?? '' ];
            }
        }
        return [ 'Other', '' ];
    }

    /**
     * Parse OS / platform from User-Agent.
     */
    public static function parse_platform( string $ua ): string {
        $map = [
            'Windows NT 10' => 'Windows 10/11',
            'Windows NT 6.3' => 'Windows 8.1',
            'Windows NT 6.1' => 'Windows 7',
            'Windows'       => 'Windows',
            'iPhone'        => 'iOS',
            'iPad'          => 'iOS',
            'Android'       => 'Android',
            'Macintosh'     => 'macOS',
            'Linux'         => 'Linux',
            'CrOS'          => 'ChromeOS',
        ];
        foreach ( $map as $needle => $os ) {
            if ( stripos( $ua, $needle ) !== false ) {
                return $os;
            }
        }
        return 'Unknown';
    }

    /**
     * Parse referrer into [host, type, search_engine, search_query].
     */
    public static function parse_referrer( string $referrer ): array {
        if ( ! $referrer ) {
            return [ '', 'direct', '', '' ];
        }

        $parsed = wp_parse_url( $referrer );
        $host   = strtolower( $parsed['host'] ?? '' );

        // Internal?
        $site_host = strtolower( wp_parse_url( home_url(), PHP_URL_HOST ) ?? '' );
        if ( $host === $site_host ) {
            return [ $host, 'internal', '', '' ];
        }

        // Search engines
        $engines = [
            'google'     => 'Google',
            'bing'       => 'Bing',
            'yahoo'      => 'Yahoo',
            'duckduckgo' => 'DuckDuckGo',
            'baidu'      => 'Baidu',
            'yandex'     => 'Yandex',
        ];
        foreach ( $engines as $pattern => $name ) {
            if ( stripos( $host, $pattern ) !== false ) {
                parse_str( $parsed['query'] ?? '', $qs );
                $query = $qs['q'] ?? $qs['query'] ?? $qs['p'] ?? '';
                return [ $host, 'search', $name, self::sanitize_str( $query, 500 ) ];
            }
        }

        // Social
        $social = [ 'facebook', 'twitter', 'instagram', 'linkedin', 'youtube', 'tiktok', 'reddit', 'pinterest' ];
        foreach ( $social as $s ) {
            if ( stripos( $host, $s ) !== false ) {
                return [ $host, 'social', '', '' ];
            }
        }

        return [ $host, 'other', '', '' ];
    }

    /**
     * Simple GeoIP lookup via ip-api.com (free tier).
     */
    public static function geoip_lookup( string $ip ): array {
        if ( get_option( 'vlens_geoip_enabled' ) !== '1' ) {
            return [];
        }

        $url = "http://ip-api.com/json/{$ip}?fields=status,country,countryCode,regionName,city,lat,lon";
        $response = wp_remote_get( $url, [ 'timeout' => 2 ] );

        if ( is_wp_error( $response ) ) {
            return [];
        }

        $body = json_decode( wp_remote_retrieve_body( $response ), true );
        if ( ( $body['status'] ?? '' ) !== 'success' ) {
            return [];
        }

        return [
            'country_code' => $body['countryCode'] ?? '--',
            'country_name' => $body['country']     ?? '',
            'city'         => $body['city']         ?? '',
            'region'       => $body['regionName']   ?? '',
            'latitude'     => $body['lat']          ?? null,
            'longitude'    => $body['lon']          ?? null,
        ];
    }

    /**
     * Check if current user should be excluded from tracking.
     */
    public static function should_exclude(): bool {
        // Don't track in admin
        if ( is_admin() ) {
            return true;
        }

        // Don't track REST API requests
        if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) {
            return true;
        }

        // Don't track AJAX requests
        if ( wp_doing_ajax() ) {
            return true;
        }

        // Don't track cron
        if ( wp_doing_cron() ) {
            return true;
        }

        // Check if recording is enabled
        if ( get_option( 'vlens_recording_enabled' ) !== '1' && get_option( 'vlens_tracking_enabled' ) !== '1' ) {
            return true;
        }

        // Exclude logged-in admins if configured
        global $wpdb;
        $config_table = self::table( 'recording_config' );
        $config = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM %i WHERE id = 1", $config_table ), ARRAY_A );

        if ( $config && $config['exclude_logged_admins'] && current_user_can( 'manage_options' ) ) {
            return true;
        }

        return false;
    }
}
