<?php
/**
 * Plugin Name: Kantan Analytics
 * Plugin URI: https://trykantan.com
 * Update URI: https://wordpress.org/plugins/kantan-analytics/
 * Description: Lightweight, privacy-friendly analytics for WordPress that keeps all data in your database. Track visits, clicks, scroll depth, user journeys, and traffic sources with a beautiful Tailwind dashboard. No external services, no cookies - just simple, self-hosted visitor insights.
 * Version: 2.28.1
 * Author: Try Kantan
 * Requires at least: 6.9
 * Requires PHP: 7.0
 * License: GPL v2 or later
 * License URI: https://www.gnu.org/licenses/gpl-2.0.html
 * Text Domain: kantan-analytics
 * Domain Path: /languages
 */

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

// Define plugin constants
define('KANTAN_ANALYTICS_VERSION', '2.28.1');
// Version of the downloadable Claude Code agent *template*. Independent of the
// plugin version — bump this only when agent-templates.php changes, so deployed
// agents can detect a new template via GET /agent/latest and self-update.
define('KANTAN_AGENT_VERSION', '2.0.1');
define('KANTAN_ANALYTICS_PLUGIN_DIR', plugin_dir_path(__FILE__));
define('KANTAN_ANALYTICS_PLUGIN_URL', plugin_dir_url(__FILE__));

/**
 * Load plugin modules
 */
require_once KANTAN_ANALYTICS_PLUGIN_DIR . 'includes/database.php';
require_once KANTAN_ANALYTICS_PLUGIN_DIR . 'includes/sqlite-storage.php';
require_once KANTAN_ANALYTICS_PLUGIN_DIR . 'includes/long-range-queries.php';
require_once KANTAN_ANALYTICS_PLUGIN_DIR . 'includes/cron-aggregation.php';
require_once KANTAN_ANALYTICS_PLUGIN_DIR . 'includes/geo.php';
require_once KANTAN_ANALYTICS_PLUGIN_DIR . 'includes/api.php';
require_once KANTAN_ANALYTICS_PLUGIN_DIR . 'includes/analytics.php';
require_once KANTAN_ANALYTICS_PLUGIN_DIR . 'includes/admin.php';
require_once KANTAN_ANALYTICS_PLUGIN_DIR . 'includes/form-integrations.php';
require_once KANTAN_ANALYTICS_PLUGIN_DIR . 'includes/rest-api.php';
require_once KANTAN_ANALYTICS_PLUGIN_DIR . 'includes/abilities.php';
require_once KANTAN_ANALYTICS_PLUGIN_DIR . 'includes/recommendations.php';
require_once KANTAN_ANALYTICS_PLUGIN_DIR . 'includes/agent-templates.php';
require_once KANTAN_ANALYTICS_PLUGIN_DIR . 'includes/seo-upsell.php';
require_once KANTAN_ANALYTICS_PLUGIN_DIR . 'includes/conversions-tab.php';
require_once KANTAN_ANALYTICS_PLUGIN_DIR . 'includes/ai-crawlers-tab.php';
require_once KANTAN_ANALYTICS_PLUGIN_DIR . 'includes/video-analytics.php';
require_once KANTAN_ANALYTICS_PLUGIN_DIR . 'includes/video-tab.php';
require_once KANTAN_ANALYTICS_PLUGIN_DIR . 'includes/ai-prompts.php';
require_once KANTAN_ANALYTICS_PLUGIN_DIR . 'includes/attribution-webhook.php';
require_once KANTAN_ANALYTICS_PLUGIN_DIR . 'includes/site-profile.php';

/**
 * Load translations.
 *
 * Bundled .mo/.po files live in /languages (required for the paid/self-hosted
 * distribution, which WordPress.org's auto-loader never sees). On WordPress.org
 * installs, community translations from translate.wordpress.org also load
 * automatically once the plugin is hosted there.
 */
function kantan_analytics_load_textdomain() {
	load_plugin_textdomain('kantan-analytics', false, dirname(plugin_basename(__FILE__)) . '/languages');
}
add_action('init', 'kantan_analytics_load_textdomain');

/**
 * Distribution channel.
 *
 * As of 2.21.2 every install updates from WordPress.org, whatever ZIP it was
 * first installed from — the self-hosted updater is gone and the Update URI
 * header points at wordpress.org. Since a wp.org update overwrites the whole
 * plugin directory, a site installed from trykantan.com converges on the wp.org
 * build at its next update, so 'wporg' is the only channel that can persist.
 *
 * The constant survives because promotional intensity still reads it (see
 * includes/seo-upsell.php and includes/conversions-tab.php) and a build may
 * still define it ahead of time.
 */
if (!defined('KANTAN_ANALYTICS_CHANNEL')) {
    define('KANTAN_ANALYTICS_CHANNEL', 'wporg');
}

/**
 * Auto-create/update tables when plugin version changes
 * Handles cases where files are uploaded without deactivate/reactivate
 */
function kantan_analytics_check_db_version() {
    $installed_version = get_option('kantan_analytics_db_version', '0');
    if (version_compare($installed_version, KANTAN_ANALYTICS_VERSION, '<')) {
        kantan_analytics_create_tables();
        update_option('kantan_analytics_db_version', KANTAN_ANALYTICS_VERSION);
        kantan_analytics_schedule_crons();
    }
}
add_action('admin_init', 'kantan_analytics_check_db_version');

/**
 * One-time purge of any historical IP addresses.
 *
 * Kantan's privacy promise is "no IP stored", and as of 2.27.0 the tracker no
 * longer persists the client IP. Earlier versions did write it, so this clears the
 * column for any rows still holding an address, making the claim true immediately
 * rather than only for new traffic. Guarded by an option so it runs once; batched
 * to avoid a long lock on a large table. Exclusion and rate-limiting are unaffected
 * — they read the live request IP, never this column.
 */
function kantan_analytics_purge_stored_ips() {
    if (get_option('kantan_analytics_ips_purged')) {
        return;
    }
    global $wpdb;
    $table = $wpdb->prefix . 'kantan_analytics';

    // Clear in bounded batches so a big table doesn't stall the request.
    // last_seen = last_seen pins the timestamp — last_seen has ON UPDATE
    // CURRENT_TIMESTAMP, so without this every purged row's last_seen would jump
    // to now and the rows would flood the "active in the last 5 min" realtime view.
    do {
        $affected = $wpdb->query(
            "UPDATE $table SET ip_address = NULL, last_seen = last_seen
             WHERE ip_address IS NOT NULL AND ip_address <> '' LIMIT 5000"
        );
    } while ($affected === 5000);

    update_option('kantan_analytics_ips_purged', 1);
}
add_action('admin_init', 'kantan_analytics_purge_stored_ips');

/**
 * One-time reclassification of historical visits whose referrer is an AI
 * assistant into the dedicated 'ai' traffic source, so existing AI traffic
 * appears in the dashboard right after upgrade. Guarded by an option so it
 * runs once. New visits are classified live in api.php.
 */
function kantan_analytics_backfill_ai_source() {
    if (get_option('kantan_analytics_ai_source_backfilled')) {
        return;
    }

    $hosts = function_exists('kantan_ai_referrer_hosts') ? kantan_ai_referrer_hosts() : array();
    if (!empty($hosts)) {
        global $wpdb;
        $table = $wpdb->prefix . 'kantan_analytics';
        $likes = array();
        $args  = array();
        foreach ($hosts as $h) {
            $likes[] = 'referrer LIKE %s';
            $args[]  = '%' . $wpdb->esc_like($h) . '%';
        }
        // last_seen = last_seen pins the timestamp (ON UPDATE CURRENT_TIMESTAMP)
        // so reclassifying historical rows doesn't surface them in Realtime.
        $wpdb->query($wpdb->prepare(
            "UPDATE $table SET traffic_source = 'ai', last_seen = last_seen
             WHERE traffic_source <> 'ai' AND (" . implode(' OR ', $likes) . ")",
            $args
        ));
    }

    update_option('kantan_analytics_ai_source_backfilled', 1);
}
add_action('admin_init', 'kantan_analytics_backfill_ai_source');

/**
 * Ensure the ai_assistant column exists on existing installs. Hooked early on
 * 'init' (every request) so the column is present before any front-end visit
 * insert references it — otherwise those inserts would fail. Cheap after the
 * first run (a single option read). No version bump required.
 */
function kantan_analytics_ensure_ai_assistant_column() {
    if (get_option('kantan_analytics_ai_assistant_col')) {
        return;
    }
    global $wpdb;
    $table = $wpdb->prefix . 'kantan_analytics';
    $has_col = (int) $wpdb->get_var($wpdb->prepare(
        "SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
         WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = %s AND COLUMN_NAME = 'ai_assistant'",
        $table
    ));
    if (!$has_col) {
        $wpdb->query("ALTER TABLE $table ADD COLUMN ai_assistant varchar(50) DEFAULT NULL");
        $wpdb->query("ALTER TABLE $table ADD KEY ai_assistant (ai_assistant)");
    }
    update_option('kantan_analytics_ai_assistant_col', 1);
}
add_action('init', 'kantan_analytics_ensure_ai_assistant_column', 1);

/**
 * One-time backfill: label existing 'ai' sessions by assistant (referrer pass)
 * and reclassify the empty-referrer-but-UTM-tagged sessions the old classifier
 * mislabelled 'direct' (utm pass). Runs once on an admin load, after the column
 * is guaranteed by the 'init' hook above. The guard never overwrites a set label.
 */
function kantan_analytics_ai_assistant_backfill() {
    if (get_option('kantan_analytics_ai_assistant_v2') || !function_exists('kantan_ai_assistants')) {
        return;
    }
    global $wpdb;
    $table = $wpdb->prefix . 'kantan_analytics';
    $utm   = $wpdb->prefix . 'kantan_utm';

    // NOTE: last_seen = last_seen is intentional — last_seen has ON UPDATE
    // CURRENT_TIMESTAMP, so without pinning it, reclassifying historical rows
    // would bump last_seen to "now" and make old sessions appear active in the
    // Realtime tab. Explicitly assigning it preserves the original value.
    foreach (kantan_ai_assistants() as $needle => $label) {
        $like = '%' . $wpdb->esc_like(strtolower($needle)) . '%';
        $wpdb->query($wpdb->prepare(
            "UPDATE $table SET traffic_source = 'ai', ai_assistant = %s, last_seen = last_seen
             WHERE LOWER(referrer) LIKE %s AND (ai_assistant IS NULL OR ai_assistant = '')",
            $label, $like
        ));
        $wpdb->query($wpdb->prepare(
            "UPDATE $table a JOIN $utm u ON a.session_id = u.session_id
             SET a.traffic_source = 'ai', a.ai_assistant = %s, a.last_seen = a.last_seen
             WHERE LOWER(u.utm_source) LIKE %s AND (a.ai_assistant IS NULL OR a.ai_assistant = '')",
            $label, $like
        ));
    }

    update_option('kantan_analytics_ai_assistant_v2', 1);
}
add_action('admin_init', 'kantan_analytics_ai_assistant_backfill');

/**
 * Schedule the two recurring events this plugin owns.
 * Aggregation runs 1h before cleanup so the SQLite archive is up to date
 * before any MySQL rows get purged.
 */
function kantan_analytics_schedule_crons() {
    if (!wp_next_scheduled('kantan_analytics_daily_aggregate')) {
        wp_schedule_event(strtotime('tomorrow 02:00'), 'daily', 'kantan_analytics_daily_aggregate');
    }
    if (!wp_next_scheduled('kantan_analytics_daily_cleanup')) {
        wp_schedule_event(strtotime('tomorrow 03:00'), 'daily', 'kantan_analytics_daily_cleanup');
    }
}

/**
 * Activation hook - create database tables and schedule cleanup
 */
function kantan_analytics_activate() {
    kantan_analytics_create_tables();
    kantan_analytics_schedule_crons();
}
register_activation_hook(__FILE__, 'kantan_analytics_activate');

/**
 * Deactivation hook - clear scheduled events
 */
function kantan_analytics_deactivate() {
    wp_clear_scheduled_hook('kantan_analytics_daily_cleanup');
    wp_clear_scheduled_hook('kantan_analytics_daily_aggregate');
}
register_deactivation_hook(__FILE__, 'kantan_analytics_deactivate');

/**
 * Plugin-row links: a Settings shortcut (left) and a Support link (right meta).
 */
function kantan_analytics_plugin_action_links($links) {
    array_unshift($links, '<a href="' . esc_url(admin_url('admin.php?page=kantan-analytics&tab=settings')) . '">' . esc_html__('Settings', 'kantan-analytics') . '</a>');
    return $links;
}
add_filter('plugin_action_links_' . plugin_basename(__FILE__), 'kantan_analytics_plugin_action_links');

function kantan_analytics_plugin_row_meta($links, $file) {
    if ($file === plugin_basename(__FILE__)) {
        $links[] = '<a href="mailto:hello@trykantan.com">' . esc_html__('Support', 'kantan-analytics') . '</a>';
    }
    return $links;
}
add_filter('plugin_row_meta', 'kantan_analytics_plugin_row_meta', 10, 2);

/**
 * Daily cleanup - remove old analytics data based on retention settings.
 *
 * Safety net: before deleting anything, run aggregation to guarantee that
 * anything about to be purged has been archived to SQLite. Aggregation is
 * idempotent via last_aggregated_date, so calling it here is cheap if the
 * dedicated aggregation cron already ran an hour ago.
 */
function kantan_analytics_cleanup_old_data() {
    global $wpdb;

    if (function_exists('kantan_analytics_run_aggregation')) {
        kantan_analytics_run_aggregation();
    }

    // Retention resolves in this order: filter > option > default 90.
    // The settings page (Settings tab) writes kantan_retention_raw_days.
    $retention_days = intval(get_option('kantan_retention_raw_days', 90));
    if ($retention_days < 7) {
        $retention_days = 90;
    }
    $retention_days = apply_filters('kantan_analytics_retention_days', $retention_days);
    $cutoff_date = gmdate('Y-m-d H:i:s', strtotime("-{$retention_days} days"));

    // Clean up main analytics table
    $analytics_table = $wpdb->prefix . 'kantan_analytics';
    $wpdb->query($wpdb->prepare(
        "DELETE FROM $analytics_table WHERE visit_date < %s",
        $cutoff_date
    ));

    // Clean up clicks table
    $clicks_table = $wpdb->prefix . 'kantan_clicks';
    $wpdb->query($wpdb->prepare(
        "DELETE FROM $clicks_table WHERE click_time < %s",
        $cutoff_date
    ));

    // Clean up pageviews table
    $pageviews_table = $wpdb->prefix . 'kantan_pageviews';
    $wpdb->query($wpdb->prepare(
        "DELETE FROM $pageviews_table WHERE entered_at < %s",
        $cutoff_date
    ));

    // Clean up UTM table
    $utm_table = $wpdb->prefix . 'kantan_utm';
    $wpdb->query($wpdb->prepare(
        "DELETE FROM $utm_table WHERE created_at < %s",
        $cutoff_date
    ));

    // Clean up conversions table
    $conversions_table = $wpdb->prefix . 'kantan_conversions';
    $wpdb->query($wpdb->prepare(
        "DELETE FROM $conversions_table WHERE converted_at < %s",
        $cutoff_date
    ));

    // Clean up AI-crawler hits table
    $crawler_table = $wpdb->prefix . 'kantan_crawler_hits';
    $wpdb->query($wpdb->prepare(
        "DELETE FROM $crawler_table WHERE hit_time < %s",
        $cutoff_date
    ));

    // Clean up raw video events. The durable per-video engagement aggregate
    // (wp_kantan_video_engagement) and registry (wp_kantan_videos) are the
    // long-term layer and are never purged.
    $video_events_table = $wpdb->prefix . 'kantan_video_events';
    $wpdb->query($wpdb->prepare(
        "DELETE FROM $video_events_table WHERE created_at < %s",
        $cutoff_date
    ));
}
add_action('kantan_analytics_daily_cleanup', 'kantan_analytics_cleanup_old_data');

/**
 * Suggest privacy-policy text when an admin generates a policy via WordPress'
 * built-in tool (Settings > Privacy). Mirrors the readme's Privacy section so
 * site owners can disclose what Kantan records without writing it themselves.
 */
function kantan_analytics_add_privacy_policy_content() {
    if (!function_exists('wp_add_privacy_policy_content')) {
        return;
    }

    $retention_days = intval(get_option('kantan_retention_raw_days', 90));
    if ($retention_days < 7) {
        $retention_days = 90;
    }

    $content = sprintf(
        '<p class="privacy-policy-tutorial">%s</p>',
        esc_html__('This site uses Kantan Analytics to measure visitor activity. All data is stored in this site\'s own database and is never sent to external services.', 'kantan-analytics')
    ) . '<p>' . sprintf(
        /* translators: %d: number of days raw session data is retained. */
        esc_html__('Kantan Analytics records page views, click positions, scroll depth, navigation paths, traffic sources, and approximate device/browser information for visitors. It does not use cookies and does not store visitor IP addresses — country and region are resolved from an offline database at the moment of the visit and the raw IP is discarded. Detailed session records are automatically deleted after %d days; only anonymous daily aggregate totals are kept beyond that.', 'kantan-analytics'),
        $retention_days
    ) . '</p><p>' . esc_html__('When video tracking is enabled, the plugin also measures engagement with videos embedded on this site (plays, watch time, and completion). On pages embedding YouTube or Vimeo videos, those providers\' player scripts are loaded to enable this measurement, which may transmit data to those third parties; YouTube embeds are switched to the privacy-enhanced youtube-nocookie.com player by default. Video engagement data is stored in this site\'s own database.', 'kantan-analytics') . '</p>';

    wp_add_privacy_policy_content('Kantan Analytics', wp_kses_post($content));
}
add_action('admin_init', 'kantan_analytics_add_privacy_policy_content');
