<?php

/**
 * Analytics API Endpoint
 * Handles AJAX requests for tracking visitor data
 */

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

/**
 * Match an IP against a list of exact addresses / CIDR ranges (IPv4 and IPv6).
 *
 * Uses inet_pton so a v4 and a v6 address never cross-match. Each range may be a
 * bare IP ("1.2.3.4", "2001:db8::1") or CIDR ("10.0.0.0/8", "2400:cb00::/32").
 *
 * @param string   $ip     Address to test.
 * @param string[] $ranges Exact IPs and/or CIDR ranges.
 * @return bool
 */
function kantan_ip_in_ranges($ip, $ranges) {
    $ip_bin = @inet_pton($ip);
    if ($ip_bin === false) {
        return false;
    }
    $ip_len = strlen($ip_bin); // 4 for IPv4, 16 for IPv6

    foreach ((array) $ranges as $range) {
        $range = trim($range);
        if ($range === '') {
            continue;
        }
        if (strpos($range, '/') === false) {
            $r = @inet_pton($range);
            if ($r !== false && $r === $ip_bin) {
                return true;
            }
            continue;
        }
        list($subnet, $bits) = explode('/', $range, 2);
        $subnet_bin = @inet_pton(trim($subnet));
        if ($subnet_bin === false || strlen($subnet_bin) !== $ip_len) {
            continue; // malformed, or a v4/v6 mismatch
        }
        $bits  = max(0, min((int) $bits, $ip_len * 8));
        $bytes = intdiv($bits, 8);
        $rem   = $bits % 8;
        if ($bytes > 0 && strncmp($ip_bin, $subnet_bin, $bytes) !== 0) {
            continue;
        }
        if ($rem > 0) {
            $mask = 0xff << (8 - $rem) & 0xff;
            if ((ord($ip_bin[$bytes]) & $mask) !== (ord($subnet_bin[$bytes]) & $mask)) {
                continue;
            }
        }
        return true;
    }
    return false;
}

/**
 * Cloudflare's published edge IP ranges (v4 + v6). Only when the direct peer is
 * one of these do we trust the CF-Connecting-IP header. Filterable so the list
 * can be refreshed without a plugin update. Source: https://www.cloudflare.com/ips/
 *
 * @return string[]
 */
function kantan_cloudflare_ranges() {
    return apply_filters('kantan_cloudflare_ranges', array(
        '173.245.48.0/20', '103.21.244.0/22', '103.22.200.0/22', '103.31.4.0/22',
        '141.101.64.0/18', '108.162.192.0/18', '190.93.240.0/20', '188.114.96.0/20',
        '197.234.240.0/22', '198.41.128.0/17', '162.158.0.0/15', '104.16.0.0/13',
        '104.24.0.0/14', '172.64.0.0/13', '131.0.72.0/22',
        '2400:cb00::/32', '2606:4700::/32', '2803:f800::/32', '2405:b500::/32',
        '2405:8100::/32', '2a06:98c0::/29', '2c0f:f248::/32',
    ));
}

/**
 * Address ranges we trust to have set a forwarding header — i.e. reverse proxies
 * sitting in front of PHP. Defaults to loopback + RFC1918/ULA private space, which
 * covers the same-host proxy setups (nginx, Local's router, Docker). A site behind
 * a proxy on a public IP should add it via the filter.
 *
 * @return string[]
 */
function kantan_trusted_proxy_ranges() {
    $defaults = array(
        '127.0.0.0/8', '::1/128',
        '10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16', 'fc00::/7',
    );
    return apply_filters('kantan_trusted_proxies', $defaults);
}

/**
 * Resolve the real client IP — safely.
 *
 * REMOTE_ADDR is the only address PHP can trust: it is whoever actually connected.
 * Forwarding headers (X-Forwarded-For, CF-Connecting-IP, …) are just request text
 * and are trivially spoofable, so we consult them ONLY when REMOTE_ADDR is a proxy
 * we recognise. Otherwise a client could forge its own source IP — defeating IP
 * exclusion and poisoning the stored ip_address column.
 *
 *   - REMOTE_ADDR is a Cloudflare edge  -> trust CF-Connecting-IP (CF sets it).
 *   - REMOTE_ADDR is a trusted proxy    -> trust X-Real-IP, else the rightmost
 *                                          public entry the proxy appended to XFF.
 *   - otherwise                         -> REMOTE_ADDR is the client; headers ignored.
 *
 * @return string
 */
function kantan_get_client_ip() {
    $remote = isset($_SERVER['REMOTE_ADDR'])
        ? trim(sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR'])))
        : '';
    if ($remote === '' || !filter_var($remote, FILTER_VALIDATE_IP)) {
        return $remote;
    }

    // Cloudflare: CF-Connecting-IP is authoritative, but only from a CF edge.
    if (kantan_ip_in_ranges($remote, kantan_cloudflare_ranges())) {
        $cf = isset($_SERVER['HTTP_CF_CONNECTING_IP'])
            ? trim(sanitize_text_field(wp_unslash($_SERVER['HTTP_CF_CONNECTING_IP'])))
            : '';
        if ($cf !== '' && filter_var($cf, FILTER_VALIDATE_IP)) {
            return $cf;
        }
    }

    // Other reverse proxies (nginx, Local, Docker): trust their forwarding header.
    if (kantan_ip_in_ranges($remote, kantan_trusted_proxy_ranges())) {
        $real = isset($_SERVER['HTTP_X_REAL_IP'])
            ? trim(sanitize_text_field(wp_unslash($_SERVER['HTTP_X_REAL_IP'])))
            : '';
        if ($real !== '' && filter_var($real, FILTER_VALIDATE_IP)) {
            return $real;
        }
        if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
            // "client, proxy1, proxy2" — a well-behaved proxy appends, so the
            // rightmost public entry is the one it vouches for. The client-supplied
            // left side is ignored.
            $parts = array_map('trim', explode(',', sanitize_text_field(wp_unslash($_SERVER['HTTP_X_FORWARDED_FOR']))));
            foreach (array_reverse($parts) as $candidate) {
                if (filter_var($candidate, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
                    return $candidate;
                }
            }
        }
    }

    // Not behind a recognised proxy (or no usable header): REMOTE_ADDR is the client.
    return $remote;
}

/**
 * The property this install reports as.
 *
 * Every tracked row is stamped with this id so a Kantan Hub can partition many
 * sites' data by property (the 1:Many / property-switcher model). On a
 * standalone site it is a stable per-install id defaulting to the site host,
 * and it is harmless: all rows simply share one value. When the site is later
 * connected to a Hub, the collector overrides it with the Hub-assigned id via
 * the 'kantan_property_id' filter. Stored as an autoloaded option so the
 * tracking hot-path needs no extra query.
 */
function kantan_get_property_id() {
    $stored = get_option('kantan_property_id', '');
    if ($stored === '') {
        $host   = wp_parse_url(home_url(), PHP_URL_HOST);
        $stored = $host ? $host : 'local';
        update_option('kantan_property_id', $stored);
    }
    return apply_filters('kantan_property_id', $stored);
}

/**
 * Check if the current request IP is in the exclusion list (exact IPs + CIDR,
 * IPv4 and IPv6). Now that kantan_get_client_ip() no longer trusts spoofable
 * forwarding headers, this exclusion can't be bypassed by a forged X-Forwarded-For.
 */
function kantan_is_excluded_ip() {
    $excluded = get_option('kantan_excluded_ips', array());
    if (empty($excluded)) return false;

    $ip = sanitize_text_field(kantan_get_client_ip());
    if (empty($ip)) return false;

    return kantan_ip_in_ranges($ip, $excluded);
}

/**
 * @deprecated Use kantan_ip_in_ranges() (IPv4+IPv6). Kept as a thin shim in case
 * a companion plugin or theme still calls it.
 */
function kantan_ip_in_cidr($ip, $cidr) {
    return kantan_ip_in_ranges($ip, array($cidr));
}

/**
 * Per-IP flood backstop for NEW sessions.
 *
 * This is a volume cap, not the bot discriminator — the read-time heuristic
 * (kantan_human_filter_sql) does the actual filtering. Its only job is to stop an
 * unauthenticated flood from ballooning the table, so the default is deliberately
 * generous: a real visitor (even behind office NAT) never approaches it, while a
 * forging script hammering admin-ajax.php trips it fast. Relies on
 * kantan_get_client_ip() no longer being spoofable (see that function).
 *
 * Counts only NEW sessions; a session's later pageview/duration beacons don't
 * consume the budget. Returns true when the request may proceed.
 *
 * @param string $ip Resolved client IP.
 * @return bool
 */
function kantan_track_rate_ok($ip) {
    $max    = (int) apply_filters('kantan_rate_limit_max_sessions', 100);
    $window = (int) apply_filters('kantan_rate_limit_window', 300); // seconds
    if ($max <= 0 || $ip === '') {
        return true; // disabled, or no IP to key on
    }
    $key   = 'kantan_rl_' . md5($ip);
    $count = (int) get_transient($key);
    if ($count >= $max) {
        return false;
    }
    set_transient($key, $count + 1, $window);
    return true;
}

/* Handle analytics tracking via AJAX */
function kantan_track_visit() {
    // Filter bots and excluded IPs
    if (kantan_is_bot() || kantan_is_excluded_ip()) {
        wp_send_json_success();
        return;
    }

    global $wpdb;
    $table_name = $wpdb->prefix . 'kantan_analytics';
    $pageviews_table = $wpdb->prefix . 'kantan_pageviews';

    // Get POST data
    $session_id = sanitize_text_field(wp_unslash($_POST['session_id'] ?? ''));
    $page_url = esc_url_raw(wp_unslash($_POST['page_url'] ?? ''));
    $page_title = sanitize_text_field(wp_unslash($_POST['page_title'] ?? ''));
    $referrer = esc_url_raw(wp_unslash($_POST['referrer'] ?? ''));
    $traffic_source = sanitize_text_field(wp_unslash($_POST['traffic_source'] ?? 'direct'));

    // Classify AI-assistant traffic from the referrer. The empty-referrer-but-
    // UTM-tagged case is resolved on insert (utm lookup, below) and when the
    // separate kantan_track_utm() call lands.
    $ai_assistant = function_exists('kantan_classify_ai_assistant') ? kantan_classify_ai_assistant($referrer) : '';
    if ($ai_assistant !== '') {
        $traffic_source = 'ai';
    }
    $is_bounce = intval($_POST['is_bounce'] ?? 1);
    $visit_duration = intval($_POST['visit_duration'] ?? 0);
    $screen_width = intval($_POST['screen_width'] ?? 0);
    $screen_height = intval($_POST['screen_height'] ?? 0);
    $sequence_num = intval($_POST['sequence_num'] ?? 1);
    $scroll_depth = intval($_POST['scroll_depth'] ?? 0);
    $visitor_id = sanitize_text_field(wp_unslash($_POST['visitor_id'] ?? ''));

    // Get server data.
    $user_agent = sanitize_text_field(wp_unslash($_SERVER['HTTP_USER_AGENT'] ?? ''));
    // The client IP is resolved for the exclusion + rate-limit checks only — it is
    // NEVER written to the database. Kantan's privacy promise is "no IP stored", so
    // the raw address lives only for the duration of this request. (See the insert
    // below: the ip_address column is intentionally left at its NULL default.)
    $ip_address = sanitize_text_field(kantan_get_client_ip());

    // Coarse geo is resolved from that live IP now, before it is discarded, and
    // only the country/region result is kept — never the address itself.
    $geo = function_exists('kantan_resolve_geo') ? kantan_resolve_geo($ip_address) : array('country' => '', 'region' => '');

    // Check if this session already has a record in main analytics table
    $existing = $wpdb->get_row($wpdb->prepare(
        "SELECT id FROM $table_name WHERE session_id = %s ORDER BY id DESC LIMIT 1",
        $session_id
    ));

    // Cap duration at 30 minutes to prevent inflated averages from idle tabs
    $visit_duration = min($visit_duration, 1800);

    if ($existing) {
        // Update existing record
        $update_data = array(
            'is_bounce' => $is_bounce,
            'visit_duration' => $visit_duration,
            'page_url' => $page_url,
            'page_title' => $page_title
        );
        // Upgrade to ai if a later page in the session reveals an AI referrer.
        // Never downgrade an already-classified source.
        if ($ai_assistant !== '') {
            $update_data['traffic_source'] = 'ai';
            $update_data['ai_assistant']   = $ai_assistant;
        }
        $result = $wpdb->update(
            $table_name,
            $update_data,
            array('id' => $existing->id)
        );
        if ($result === false) {
            error_log('[Kantan Analytics] Failed to UPDATE analytics row: ' . $wpdb->last_error);
        }
    } else {
        // New session — apply the per-IP flood backstop before creating a row.
        // Silent success (mirrors the bot/excluded path) so a flooder gets no signal.
        if (!kantan_track_rate_ok($ip_address)) {
            wp_send_json_success();
            return;
        }

        // If the referrer didn't reveal an AI assistant, the UTM
        // call may already have landed (utm_source=chatgpt.com with an empty
        // referrer) — check the session's utm row before deciding the source.
        if ($ai_assistant === '' && !empty($session_id) && function_exists('kantan_classify_ai_assistant')) {
            $utm_src = $wpdb->get_var($wpdb->prepare(
                "SELECT utm_source FROM {$wpdb->prefix}kantan_utm WHERE session_id = %s LIMIT 1",
                $session_id
            ));
            if (!empty($utm_src)) {
                $maybe = kantan_classify_ai_assistant('', $utm_src);
                if ($maybe !== '') {
                    $ai_assistant   = $maybe;
                    $traffic_source = 'ai';
                }
            }
        }

        // Insert new record
        $result = $wpdb->insert(
            $table_name,
            array(
                'property_id' => kantan_get_property_id(),
                'session_id' => $session_id,
                'page_url' => $page_url,
                'page_title' => $page_title,
                'referrer' => $referrer,
                'traffic_source' => $traffic_source,
                'ai_assistant' => ($ai_assistant !== '' ? $ai_assistant : null),
                'is_bounce' => $is_bounce,
                'visit_duration' => $visit_duration,
                'screen_width' => $screen_width,
                'screen_height' => $screen_height,
                'user_agent' => $user_agent,
                // ip_address intentionally omitted — see note above; "no IP stored".
                'country' => ($geo['country'] !== '' ? $geo['country'] : null),
                'region' => ($geo['region'] !== '' ? $geo['region'] : null),
                'visitor_id' => $visitor_id
            )
        );
        if ($result === false) {
            error_log('[Kantan Analytics] Failed to INSERT analytics row: ' . $wpdb->last_error . ' | session_id=' . $session_id . ' | page_url=' . $page_url);
        }
    }

    // Track pageview in pageviews table for journey tracking
    $existing_pageview = $wpdb->get_row($wpdb->prepare(
        "SELECT id, time_on_page FROM $pageviews_table WHERE session_id = %s AND sequence_num = %d",
        $session_id,
        $sequence_num
    ));

    if ($existing_pageview) {
        // Update existing pageview with duration and scroll depth
        $wpdb->update(
            $pageviews_table,
            array(
                'time_on_page' => $visit_duration,
                'scroll_depth' => $scroll_depth,
                'exited_at' => current_time('mysql')
            ),
            array('id' => $existing_pageview->id)
        );
    } else {
        // Insert new pageview record
        $pageview_data = array(
            'property_id' => kantan_get_property_id(),
            'session_id' => $session_id,
            'page_url' => $page_url,
            'page_title' => $page_title,
            'sequence_num' => $sequence_num,
            'scroll_depth' => $scroll_depth,
            'time_on_page' => $visit_duration
        );

        // Store split test data if present (Kantan Split Testing integration)
        $split_test_id = intval($_POST['split_test_id'] ?? 0);
        $split_variant_id = intval($_POST['split_variant_id'] ?? 0);
        if ($split_test_id > 0) {
            $pageview_data['test_id'] = $split_test_id;
        }
        if ($split_variant_id > 0) {
            $pageview_data['variant_id'] = $split_variant_id;
        }

        $wpdb->insert(
            $pageviews_table,
            $pageview_data
        );

        // Update previous page's exit time if this is not the first page
        if ($sequence_num > 1) {
            $wpdb->query($wpdb->prepare(
                "UPDATE $pageviews_table SET exited_at = %s WHERE session_id = %s AND sequence_num = %d AND exited_at IS NULL",
                current_time('mysql'),
                $session_id,
                $sequence_num - 1
            ));
        }
    }

    // Pass a sanitized context array (not the raw $_POST superglobal) to listeners.
    do_action('kantan_pageview_tracked', $session_id, $sequence_num, array(
        'page_url'   => $page_url,
        'page_title' => $page_title,
        'visitor_id' => $visitor_id,
    ));

    wp_send_json_success();
}

/* Handle click tracking via AJAX */
function kantan_track_clicks() {
    if (kantan_is_bot() || kantan_is_excluded_ip()) {
        wp_send_json_success();
        return;
    }

    global $wpdb;
    $table_name = $wpdb->prefix . 'kantan_clicks';

    // Get POST data
    $session_id = sanitize_text_field(wp_unslash($_POST['session_id'] ?? ''));
    $page_url = esc_url_raw(wp_unslash($_POST['page_url'] ?? ''));

    // Decode clicks array. wp_unslash() reverses WordPress' magic-quotes slashing
    // before json_decode; every field is sanitized individually on insert below.
    $clicks = json_decode(wp_unslash($_POST['clicks'] ?? '[]'), true);

    if (!is_array($clicks) || empty($clicks)) {
        wp_send_json_success();
        return;
    }

    // Insert each click
    $property_id = kantan_get_property_id();
    foreach ($clicks as $click) {
        $wpdb->insert(
            $table_name,
            array(
                'property_id' => $property_id,
                'session_id' => $session_id,
                'page_url' => $page_url,
                'click_x_pct' => intval($click['x'] ?? 0),
                'click_y_pct' => intval($click['y'] ?? 0),
                'click_region' => intval($click['region'] ?? 5),
                'element_tag' => sanitize_text_field($click['tag'] ?? ''),
                'element_text' => sanitize_text_field(substr($click['text'] ?? '', 0, 255))
            )
        );
    }

    wp_send_json_success();
}

/* Handle UTM parameter tracking via AJAX */
function kantan_track_utm() {
    global $wpdb;
    $table_name = $wpdb->prefix . 'kantan_utm';

    $session_id = sanitize_text_field(wp_unslash($_POST['session_id'] ?? ''));
    if (empty($session_id)) {
        wp_send_json_error('Missing session_id');
        return;
    }

    // Don't insert duplicate UTM records for same session
    $existing = $wpdb->get_var($wpdb->prepare(
        "SELECT id FROM $table_name WHERE session_id = %s",
        $session_id
    ));

    if ($existing) {
        wp_send_json_success();
        return;
    }

    $utm_source = sanitize_text_field(wp_unslash($_POST['utm_source'] ?? ''));

    // Passthrough params (CRM/ad click & contact ids) — keep only allowlisted
    // keys, sanitize values, cap count/length, store as JSON. Lets email/CRM
    // tools join Kantan attribution to a contact.
    $passthrough_json = null;
    if (!empty($_POST['passthrough']) && function_exists('kantan_attribution_passthrough_params')) {
        $decoded = json_decode(wp_unslash($_POST['passthrough']), true);
        if (is_array($decoded)) {
            $allow = array_flip(kantan_attribution_passthrough_params());
            $clean = array();
            $i = 0;
            foreach ($decoded as $k => $v) {
                $k = strtolower(sanitize_text_field($k));
                if (!isset($allow[$k])) {
                    continue;
                }
                $clean[$k] = sanitize_text_field(substr((string) $v, 0, 255));
                if (++$i >= 25) {
                    break;
                }
            }
            if (!empty($clean)) {
                $passthrough_json = wp_json_encode($clean);
            }
        }
    }

    $wpdb->insert(
        $table_name,
        array(
            'property_id'  => kantan_get_property_id(),
            'session_id'   => $session_id,
            'utm_source'   => $utm_source,
            'utm_medium'   => sanitize_text_field(wp_unslash($_POST['utm_medium'] ?? '')),
            'utm_campaign' => sanitize_text_field(wp_unslash($_POST['utm_campaign'] ?? '')),
            'utm_term'     => sanitize_text_field(wp_unslash($_POST['utm_term'] ?? '')),
            'utm_content'  => sanitize_text_field(wp_unslash($_POST['utm_content'] ?? '')),
            'passthrough'  => $passthrough_json,
            'landing_url'  => esc_url_raw(wp_unslash($_POST['landing_url'] ?? '')),
        )
    );

    // An AI-assistant utm_source paired with an empty referrer is exactly what the
    // visit handler can't see — reclassify the session now. Never downgrades an
    // already-labelled session.
    if (function_exists('kantan_classify_ai_assistant')) {
        $assistant = kantan_classify_ai_assistant('', $utm_source);
        if ($assistant !== '') {
            $analytics_table = $wpdb->prefix . 'kantan_analytics';
            $wpdb->query($wpdb->prepare(
                "UPDATE $analytics_table SET traffic_source = 'ai', ai_assistant = %s
                 WHERE session_id = %s AND (ai_assistant IS NULL OR ai_assistant = '')",
                $assistant, $session_id
            ));
        }
    }

    wp_send_json_success();
}

/* Handle conversion tracking via AJAX */
function kantan_track_conversion() {
    if (kantan_is_excluded_ip()) {
        wp_send_json_success();
        return;
    }

    global $wpdb;
    $table_name = $wpdb->prefix . 'kantan_conversions';

    $session_id = sanitize_text_field(wp_unslash($_POST['session_id'] ?? ''));
    $visitor_id = sanitize_text_field(wp_unslash($_POST['visitor_id'] ?? ''));
    $conversion_type = sanitize_text_field(wp_unslash($_POST['conversion_type'] ?? ''));

    if (empty($session_id) || empty($conversion_type)) {
        wp_send_json_error('Missing required fields');
        return;
    }

    $conversion_label = sanitize_text_field(wp_unslash($_POST['conversion_label'] ?? ''));
    $conversion_value = sanitize_text_field(wp_unslash($_POST['conversion_value'] ?? ''));
    $page_url         = esc_url_raw(wp_unslash($_POST['page_url'] ?? ''));

    $wpdb->insert(
        $table_name,
        array(
            'property_id'      => kantan_get_property_id(),
            'session_id'       => $session_id,
            'visitor_id'       => $visitor_id,
            'conversion_type'  => $conversion_type,
            'conversion_label' => $conversion_label,
            'conversion_value' => $conversion_value,
            'page_url'         => $page_url,
        )
    );

    // Pass a sanitized context array (not the raw $_POST superglobal) to listeners.
    do_action('kantan_conversion_tracked', $session_id, $conversion_type, array(
        'visitor_id'       => $visitor_id,
        'conversion_label' => $conversion_label,
        'conversion_value' => $conversion_value,
        'page_url'         => $page_url,
    ));

    wp_send_json_success();
}

/* Register AJAX handlers for both logged in and logged out users */
add_action('wp_ajax_kantan_track_visit', 'kantan_track_visit');
add_action('wp_ajax_nopriv_kantan_track_visit', 'kantan_track_visit');
add_action('wp_ajax_kantan_track_clicks', 'kantan_track_clicks');
add_action('wp_ajax_nopriv_kantan_track_clicks', 'kantan_track_clicks');
add_action('wp_ajax_kantan_track_utm', 'kantan_track_utm');
add_action('wp_ajax_nopriv_kantan_track_utm', 'kantan_track_utm');
add_action('wp_ajax_kantan_track_conversion', 'kantan_track_conversion');
add_action('wp_ajax_nopriv_kantan_track_conversion', 'kantan_track_conversion');

/* Enqueue tracking JavaScript — skip logged-in admins/editors */
function kantan_enqueue_tracking_script() {
    if (current_user_can('edit_posts')) {
        return;
    }

    wp_enqueue_script(
        'kantan-analytics',
        KANTAN_ANALYTICS_PLUGIN_URL . 'assets/tracking.js',
        array(),
        KANTAN_ANALYTICS_VERSION,
        true
    );

    // Get conversion links config
    $conversion_links = get_option('kantan_conversion_links', array());

    wp_localize_script('kantan-analytics', 'kantanAnalytics', array(
        'ajaxUrl' => admin_url('admin-ajax.php'),
        'nonce' => wp_create_nonce('kantan_analytics_nonce'),
        'conversionLinks' => $conversion_links,
        'siteUrl' => home_url(),
        'passthroughParams' => function_exists('kantan_attribution_passthrough_params') ? kantan_attribution_passthrough_params() : array(),
    ));
}
add_action('wp_enqueue_scripts', 'kantan_enqueue_tracking_script');

/**
 * Check if request is from a known bot
 */
function kantan_is_bot() {
    $ua = isset($_SERVER['HTTP_USER_AGENT']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_USER_AGENT'])) : '';
    if (empty($ua)) return true;

    $bots = array(
        'bot', 'crawl', 'spider', 'slurp', 'mediapartners',
        'googlebot', 'bingbot', 'yandex', 'baidu', 'duckduckbot',
        'facebookexternalhit', 'twitterbot', 'linkedinbot',
        'semrush', 'ahrefs', 'mj12bot', 'dotbot', 'rogerbot',
        'screaming frog', 'uptimerobot', 'pingdom', 'gtmetrix',
        'pagespeed', 'lighthouse', 'headlesschrome', 'phantomjs',
        'wget', 'curl', 'python-requests', 'go-http-client',
        'java/', 'libwww', 'httpunit', 'nutch', 'biglotron',
        'teoma', 'convera', 'gigablast', 'ia_archiver',
    );

    $ua_lower = strtolower($ua);
    foreach ($bots as $bot) {
        if (strpos($ua_lower, $bot) !== false) {
            return true;
        }
    }

    return false;
}

/**
 * Log a server-side hit from a known AI crawler.
 *
 * AI crawlers (GPTBot, ClaudeBot, PerplexityBot, CCBot, …) fetch pages without
 * running JavaScript, so the tracking beacon never sees them and kantan_is_bot()
 * keeps them out of human metrics. This runs on every front-end page load,
 * cheaply ignores the ~all requests whose UA is not a known AI crawler, and
 * records the rest to a SEPARATE table (wp_kantan_crawler_hits) — making AI
 * crawling visible without ever polluting human visitor stats.
 */
function kantan_log_ai_crawler_hit() {
    // Front-end HTML page loads only — not admin, AJAX, REST, cron, feeds, or robots.txt.
    if (is_admin() || wp_doing_ajax() || wp_doing_cron() || is_feed() || is_robots()) {
        return;
    }
    if (defined('REST_REQUEST') && REST_REQUEST) {
        return;
    }
    if (!function_exists('kantan_classify_ai_crawler')) {
        return;
    }

    $ua = isset($_SERVER['HTTP_USER_AGENT']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_USER_AGENT'])) : '';
    $match = kantan_classify_ai_crawler($ua);
    if ($match === null) {
        return;
    }

    // Reconstruct the requested URL (path + query) against the site host, capped
    // to the column width to survive crawlers hitting very long query strings.
    $req_uri  = isset($_SERVER['REQUEST_URI']) ? wp_unslash($_SERVER['REQUEST_URI']) : '/';
    $page_url = substr(esc_url_raw(home_url($req_uri)), 0, 500);

    global $wpdb;
    $wpdb->insert(
        $wpdb->prefix . 'kantan_crawler_hits',
        array(
            'property_id'  => function_exists('kantan_get_property_id') ? kantan_get_property_id() : null,
            'crawler'      => $match['crawler'],
            'crawler_type' => $match['type'],
            'page_url'     => $page_url,
            'user_agent'   => substr($ua, 0, 500),
            'ip_address'   => kantan_get_client_ip(),
            // hit_time left to the column DEFAULT CURRENT_TIMESTAMP, matching
            // visit_date/converted_at so the gmdate()-based breakdown lines up.
        )
    );
}
add_action('template_redirect', 'kantan_log_ai_crawler_hit');
