<?php
/**
 * AuctionForge Site Statistics - Initialization
 * Loads all stats classes, registers hooks, enqueues tracker script.
 */

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

class AF_Stats_Init {

    private static $initialized = false;

    public static function init() {
        if (self::$initialized) return;
        self::$initialized = true;

        $dir = plugin_dir_path(__FILE__);
        require_once $dir . 'class-stats-database.php';
        // Before class-stats-ajax.php and class-stats-database.php run: both
        // resolve table names through AF_Stats_Archive.
        require_once $dir . 'class-stats-archive.php';
        require_once $dir . 'class-stats-tracker.php';
        require_once $dir . 'class-country-names.php';
        require_once $dir . 'class-stats-ajax.php';
        require_once $dir . 'class-visitor-email-map.php';
        require_once $dir . 'class-keyword-extractor.php';
        require_once $dir . 'class-user-interests.php';

        // Marketing-data sync handlers (Phase 2 of the cross-site data lake).
        $sync_dir = dirname($dir) . '/sync/';
        require_once $sync_dir . 'class-sync-base.php';
        require_once $sync_dir . 'class-profile-sync.php';
        require_once $sync_dir . 'class-interests-sync.php';
        require_once $sync_dir . 'class-activity-sync.php';
        require_once $sync_dir . 'class-lots-sync.php';
        require_once $sync_dir . 'class-bids-sync.php';

        // 3.7.2.3: add the bid/alert provenance column to pre-existing
        // af_bid_events tables. Option-guarded, so this is a no-op after the
        // first request following the upgrade.
        AF_Bids_Sync::ensure_source_column();

        // Retention purge cron.
        require_once dirname($dir) . '/maintenance/class-data-purge.php';
        AF_Data_Purge::register();

        // Nightly archiver. Moves aged event rows into the *_archive tables
        // rather than deleting them, keeping the hot tables bounded while
        // history is retained indefinitely.
        AF_Stats_Archive::register();

        // One-shot migration: sweep duplicate per-hostname cron events left
        // over from pre-3.7.0.28 scheduling, plus orphan hooks from removed
        // plugin code. Idempotent via auctionforge_cron_cleanup_v1 option.
        require_once dirname($dir) . '/maintenance/class-cron-cleanup.php';
        AF_Cron_Cleanup::register();

        // Register all sync crons (idempotent — wp_schedule_event guards).
        // Deferred to `init`: AF_Lots_Sync uses the custom `bp_ten_minutely`
        // schedule, which auctionforge.php only adds to the cron_schedules
        // filter further down the file — i.e. AFTER this init() runs at plugin
        // load. Scheduling the lots hook here directly calls wp_schedule_event()
        // before that schedule exists, which fails silently and leaves
        // auctionforge_sync_lots permanently unscheduled (its wpdeploy push then
        // never runs). Running on `init` guarantees every custom schedule is
        // registered first. The other hooks use core's `hourly` and were fine.
        if (!empty(get_option('auctionforge_update_server_url'))
            && !empty(get_option('auctionforge_update_api_key'))) {
            add_action('init', function () {
                (new AF_Profile_Sync())->register();
                (new AF_Interests_Sync())->register();
                (new AF_Activity_Sync())->register();
                (new AF_Lots_Sync())->register();
                (new AF_Bids_Sync())->register();
            }, 20);
        }

        // Self-install the keepalive mu-plugin so failed updates can be
        // recovered without filesystem access. Idempotent: only writes when
        // missing or on a version bump.
        require_once dirname($dir) . '/keepalive/class-keepalive-installer.php';
        AF_Keepalive_Installer::maybe_install();

        // Sync Status admin diagnostic panel — registers AJAX handlers and
        // is rendered inline at the bottom of admin/view/sync-status.php.
        require_once dirname($dir) . '/admin/class-sync-status-page.php';
        AF_Sync_Status_Page::init();

        // Pre-extract keywords whenever a lot is created/updated.
        add_action('save_post_lot', array('AF_Keyword_Extractor', 'on_save_post'), 20, 3);

        // Refresh the user's profile row on wpdeploy whenever they log in or
        // edit their profile. Bypasses the watermark so empty/stale rows
        // pick up the latest first_name/last_name/phone/postcode immediately
        // — instead of waiting for the next hourly walk (which only re-pulls
        // newly-registered users).
        add_action('wp_login', function ($user_login, $user) {
            if ($user && class_exists('AF_Profile_Sync')) {
                AF_Profile_Sync::push_for_user_id($user->ID);
            }
        }, 10, 2);
        add_action('profile_update', function ($user_id) {
            if (class_exists('AF_Profile_Sync')) {
                AF_Profile_Sync::push_for_user_id((int) $user_id);
            }
        }, 10, 1);
        add_action('user_register', function ($user_id) {
            if (class_exists('AF_Profile_Sync')) {
                AF_Profile_Sync::push_for_user_id((int) $user_id);
            }
        }, 10, 1);

        // After AF_Stats_Ajax records the visitor→email pair, also tally
        // a lot view in user_interests if the URL belongs to a lot.
        add_action('af_stats_pageview_recorded', array(__CLASS__, 'maybe_record_lot_view'), 10, 3);

        // Wire the existing favourite/share AJAX into UserInterests.
        add_action('af_stats_lot_favorited', array(__CLASS__, 'record_lot_favorite'), 10, 2);

        // Register AJAX handlers
        AF_Stats_Ajax::init();

        // Enqueue frontend tracker on non-admin pages
        add_action('wp_enqueue_scripts', array(__CLASS__, 'enqueue_tracker'));

        // Track 404 errors server-side
        add_action('template_redirect', array(__CLASS__, 'track_404'));

        // Admin menu
        if (is_admin()) {
            add_action('admin_menu', array(__CLASS__, 'register_admin_menu'));
        }

        // Auto-create tables if they don't exist (handles updates, not just activation)
        add_action('plugins_loaded', array(__CLASS__, 'maybe_create_tables'), 20);

        // Data retention cron
        add_action('af_stats_daily_purge', array('AF_Stats_Database', 'purge_old_data'));
        if (!wp_next_scheduled('af_stats_daily_purge')) {
            wp_schedule_event(time(), 'daily', 'af_stats_daily_purge');
        }

        // Weekly email report (opt-in; daily cron that only sends on the chosen day).
        require_once $dir . 'class-stats-report.php';
        AF_Stats_Report::register();
    }

    /**
     * Enqueue the lightweight tracker JS on the frontend
     */
    public static function enqueue_tracker() {
        if (get_option('af_stats_enabled', 'on') !== 'on') return;

        // Check if should skip (excluded roles, IPs)
        if (AF_Stats_Tracker::should_skip()) return;

        // Check bot
        $ua = isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : '';
        if (AF_Stats_Tracker::is_bot($ua)) return;

        $plugin_url = plugin_dir_url(dirname(dirname(__FILE__)));
        $version = defined('AUCTIONFORGE_VERSION') ? AUCTIONFORGE_VERSION : '1.0';

        wp_enqueue_script(
            'af-stats-tracker',
            $plugin_url . 'assets/js/af-stats-tracker.js',
            array(),
            $version,
            true
        );

        wp_add_inline_script('af-stats-tracker', 'window.af_stats_config = ' . wp_json_encode(array(
            'ajax_url' => admin_url('admin-ajax.php'),
            'nonce' => wp_create_nonce('af_stats_nonce'),
        )) . ';', 'before');
    }

    /**
     * Track 404 errors server-side
     */
    public static function track_404() {
        if (!is_404()) return;
        if (get_option('af_stats_enabled', 'on') !== 'on') return;

        $ua = isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : '';
        if (AF_Stats_Tracker::is_bot($ua)) return;

        $url = (is_ssl() ? 'https' : 'http') . '://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
        $referrer = isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : '';
        $ip = AF_Stats_Tracker::get_client_ip();
        $ip = AF_Stats_Tracker::maybe_anonymize_ip($ip);

        // We don't have visitor_hash server-side, use IP hash
        $visitor_hash = md5($ip . $ua);

        AF_Stats_Tracker::record_404($url, $referrer, $visitor_hash, $ip);
    }

    /**
     * Register admin menu page
     */
    public static function register_admin_menu() {
        add_submenu_page(
            'edit.php?post_type=lot',
            __('Site Statistics', 'auctionforge'),
            __('Site Statistics', 'auctionforge'),
            'manage_options',
            'auctionforge-stats',
            array(__CLASS__, 'render_admin_page')
        );
    }

    /**
     * Render admin dashboard
     */
    public static function render_admin_page() {
        require_once plugin_dir_path(dirname(dirname(__FILE__))) . 'admin/view/stats/dashboard.php';
    }

    /**
     * Hook handler — fires after AF_Stats_Ajax records a pageview.
     * Tallies a `view` into user_interests when we can resolve the visitor
     * to an email.
     *
     * On these auction sites the bidder is rarely a `wp_user` — they
     * authenticate via BidSpirit, so `is_user_logged_in()` is false on the
     * tracker AJAX and `$email` arrives empty. Fall back to the
     * visitor→email stitch map (populated on login by
     * `auctionforge_ajax_log_ip`) so views by previously-stitched visitors
     * still tally.
     */
    public static function maybe_record_lot_view($url, $email, $visitor_hash) {
        if (empty($url)) return;

        if (empty($email) && !empty($visitor_hash)) {
            $email = AF_Stats_Visitor_Email_Map::lookup($visitor_hash);
        }
        if (empty($email)) return;

        $post_id = url_to_postid($url);
        if (!$post_id) return;
        $post = get_post($post_id);
        if (!$post || $post->post_type !== 'lot') return;

        AF_User_Interests::record_view($email, $post_id);
    }

    /**
     * Hook handler — fires when an existing favourite AJAX endpoint records
     * a watchlist add. Email is already known from that AJAX path.
     */
    public static function record_lot_favorite($email, $post_id) {
        if (empty($email) || empty($post_id)) return;
        AF_User_Interests::record_favorite($email, (int) $post_id);
    }

    /**
     * Auto-create / migrate tables when the schema version on disk
     * (AF_Stats_Database::DB_VERSION) is newer than what's installed in
     * the DB. Runs on every plugins_loaded — the version-check is a
     * single autoloaded option read, so when nothing has changed the
     * cost is effectively zero. Only on actual mismatch do we run dbDelta.
     *
     * Originally gated on is_admin() to keep frontend hits cheap, but
     * that left a hole for sites with no traffic + no admin logins +
     * external system cron hitting wp-cron.php (where is_admin() is
     * false). Removing the gate guarantees migration fires on any
     * request context.
     */
    public static function maybe_create_tables() {
        $installed = intval(get_option('af_stats_db_version', 0));
        if ($installed < AF_Stats_Database::DB_VERSION) {
            self::activate();
        }
    }

    /**
     * Run on plugin activation - create tables
     */
    public static function activate() {
        $dir = plugin_dir_path(__FILE__);
        $db_file = $dir . 'class-stats-database.php';
        if (!file_exists($db_file)) return;
        require_once $db_file;
        AF_Stats_Database::create_tables();
    }
}
