<?php
/**
 * TrackSharp Events Log
 * 
 * Handles database logging, table creation, and event pruning.
 *
 * @package TrackSharp
 */

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

class TrackSharp_Events_Log
{
    public const TABLE_NAME = 'ws_events';

    /**
     * Get the events table name.
     *
     * @return string Table name with prefix.
     */
    public function table_name(): string
    {
        global $wpdb;
        return $wpdb->prefix . self::TABLE_NAME;
    }

    /**
     * Create the events table on plugin activation.
     *
     * @return void
     */
    public static function create_table()
    {
        global $wpdb;
        $table = $wpdb->prefix . self::TABLE_NAME;
        $charset = $wpdb->get_charset_collate();

        $sql = "CREATE TABLE IF NOT EXISTS {$table} (
            id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
            ts DATETIME NOT NULL,
            channel VARCHAR(16) NOT NULL,
            source VARCHAR(16) NOT NULL,
            event_name VARCHAR(64) NOT NULL,
            event_id VARCHAR(128) NULL,
            txid VARCHAR(128) NULL,
            ok TINYINT(1) NOT NULL DEFAULT 0,
            http_status INT NULL,
            ua VARCHAR(255) NULL,
            consent VARCHAR(16) NULL,
            INDEX (channel), INDEX (source), INDEX (event_name),
            INDEX (txid), INDEX (ts)
        ) $charset;";

        require_once ABSPATH . 'wp-admin/includes/upgrade.php';
        dbDelta($sql);
    }

    /**
     * Log an event to the database.
     *
     * @param array $row Event data.
     * @return void
     */
    public function log_event(array $row)
    {
        global $wpdb;
        $table = $this->table_name();

        $wpdb->insert($table, [
            'ts' => current_time('mysql'),
            'channel' => sanitize_text_field($row['channel'] ?? ''),
            'source' => sanitize_text_field($row['source'] ?? ''),
            'event_name' => sanitize_text_field($row['event_name'] ?? ''),
            'event_id' => isset($row['event_id']) ? sanitize_text_field($row['event_id']) : null,
            'txid' => isset($row['txid']) ? sanitize_text_field($row['txid']) : null,
            'ok' => !empty($row['ok']) ? 1 : 0,
            'http_status' => isset($row['http_status']) ? intval($row['http_status']) : null,
            'ua' => isset($_SERVER['HTTP_USER_AGENT']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_USER_AGENT'])) : null,
            'consent' => isset($row['consent']) ? sanitize_text_field($row['consent']) : null,
        ]);
    }

    /**
     * Check if an event should be logged based on blocked events filter.
     *
     * @param string $channel    Channel name.
     * @param string $source     Source (server/browser).
     * @param string $event_name Event name.
     * @return bool True if event should be logged.
     */
    public function should_log_event(string $channel, string $source, string $event_name): bool
    {
        $blocked = apply_filters('tracksharp_blocked_log_events', ['view_item', 'viewcontent', 'pageview']);
        return !in_array(strtolower($event_name), array_map('strtolower', $blocked), true);
    }

    /**
     * Parse a User-Agent string into a friendly browser name.
     *
     * @param string|null $ua Raw User-Agent string.
     * @return string Friendly browser name (e.g. "Chrome 131", "Safari (Mobile)").
     */
    public static function parse_browser($ua)
    {
        if (empty($ua)) {
            return '—';
        }

        // Known non-browser clients (payment gateways, bots, etc.)
        $known_clients = [
            'Mollie'     => '/Mollie\b/',
            'Googlebot'  => '/Googlebot/',
            'Bingbot'    => '/bingbot/',
            'cURL'       => '/curl\//i',
            'WordPress'  => '/WordPress\//i',
            'Python'     => '/python-requests|Python-urllib/i',
        ];

        foreach ($known_clients as $name => $pattern) {
            if (preg_match($pattern, $ua)) {
                return $name;
            }
        }

        $mobile = preg_match('/Mobile|iPhone|Android/', $ua) ? ' (Mobile)' : '';

        // Order matters: check most specific tokens first.
        $browsers = [
            'Edge'             => '/Edg(?:e|A|iOS)?\/(\d+)/',
            'Opera'            => '/(?:OPR|Opera)\/(\d+)/',
            'Samsung Internet' => '/SamsungBrowser\/(\d+)/',
            'Firefox'          => '/Firefox\/(\d+)/',
            'Chrome'           => '/Chrome\/(\d+)/',
            'Safari'           => '/Version\/(\d+).*Safari/',
        ];

        foreach ($browsers as $name => $pattern) {
            if (preg_match($pattern, $ua, $m)) {
                return $name . ' ' . $m[1] . $mobile;
            }
        }

        // Safari fallback (no Version/ token)
        if (strpos($ua, 'Safari/') !== false && strpos($ua, 'Chrome') === false) {
            return 'Safari' . $mobile;
        }

        // Unknown: return truncated raw string
        return strlen($ua) > 40 ? substr($ua, 0, 40) . '…' : $ua;
    }

    /**
     * Cron handler for pruning old events.
     *
     * @return void
     */
    public function cron_prune_events()
    {
        $days = (int) get_option('tracksharp_log_retention_days', 180);

        // Hard floor/ceiling
        if ($days < 7) {
            $days = 7;
        }
        if ($days > 3650) {
            $days = 3650;
        }

        $deleted = $this->prune_events_older_than_days($days, 2000, 25);

        if ($deleted > 0) {
            // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Intentional production logging for cron job
            error_log('[TrackSharp] Pruned ' . $deleted . ' old rows from ' . $this->table_name());
        }
    }

    /**
     * Delete old rows in batches to avoid long locks.
     *
     * @param int $days_to_keep Keep rows newer than this many days.
     * @param int $batch_size   How many to delete per loop.
     * @param int $max_seconds  Safety time budget for the loop.
     * @return int Total rows deleted.
     */
    public function prune_events_older_than_days($days_to_keep, $batch_size = 2000, $max_seconds = 20)
    {
        global $wpdb;
        $table = esc_sql($this->table_name());

        $cutoff = gmdate('Y-m-d H:i:s', time() - $days_to_keep * DAY_IN_SECONDS);
        $start = microtime(true);
        $total = 0;

        do {
            if ((microtime(true) - $start) >= $max_seconds) {
                break;
            }

            // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Identifiers cannot be prepared; table name is fixed and escaped.
            $sql = $wpdb->prepare("DELETE FROM {$table} WHERE ts < %s ORDER BY id LIMIT %d", $cutoff, (int) $batch_size);

            // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Query is prepared above with $wpdb->prepare().
            $aff = $wpdb->query($sql);

            if ($aff === false) {
                break;
            }

            $total += (int) $aff;

            if ($aff < $batch_size) {
                break;
            }

            usleep(150000); // 150ms breather
        } while (true);

        return $total;
    }
}
