<?php
/**
 * Daily-stats aggregator — runs hourly, refreshes today's row + yesterday's
 * once, idempotent. Lets the dashboard read pre-computed numbers instead of
 * scanning the full visits table.
 *
 * @package Brainwerk SEO Suite
 */

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

class Seoforge_Aggregator {

    public static function register( Seoforge_Loader $loader ) {
        $loader->add_action( 'seoforge_aggregate_sessions', __CLASS__, 'tick', 20 );
        $loader->add_action( 'seoforge_cleanup_visits', __CLASS__, 'cleanup' );
    }

    /**
     * Refresh aggregate rows for today + yesterday (covers cron drift across
     * midnight UTC). Per-blog when on Multisite.
     */
    public static function tick() {
        self::rebuild_range( 2 );
    }

    /**
     * Rebuild the last N days for every blog. Used by the seeder + WP-CLI
     * `wp seoforge aggregate --days=N`.
     */
    public static function rebuild_range( $days = 2 ) {
        $sites = is_multisite()
            ? array_map( 'intval', get_sites( array( 'number' => 0, 'fields' => 'ids' ) ) )
            : array( 0 );
        $days  = max( 1, (int) $days );
        foreach ( $sites as $blog_id ) {
            for ( $d = 0; $d < $days; $d++ ) {
                $day = gmdate( 'Y-m-d', time() - DAY_IN_SECONDS * $d );
                self::rebuild_day( $day, $blog_id );
            }
        }
    }

    /**
     * Recomputes one (blog_id, day) row.
     */
    public static function rebuild_day( $day, $blog_id = 0 ) {
        global $wpdb;
        $visits_t   = Seoforge_Utils::table( 'visits' );
        $sessions_t = Seoforge_Utils::table( 'sessions' );
        $daily_t    = Seoforge_Utils::table( 'daily' );

        $start = $day . ' 00:00:00';
        $end   = gmdate( 'Y-m-d H:i:s', strtotime( $day . ' 00:00:00' ) + DAY_IN_SECONDS );

        $visits = (int) $wpdb->get_var( $wpdb->prepare(
            "SELECT COUNT(*) FROM %i WHERE blog_id = %d AND is_bot = 0 AND created_at >= %s AND created_at < %s",
            $visits_t, $blog_id, $start, $end
        ) );
        $uniques = (int) $wpdb->get_var( $wpdb->prepare(
            "SELECT COUNT(DISTINCT visitor_hash) FROM %i WHERE blog_id = %d AND is_bot = 0 AND created_at >= %s AND created_at < %s",
            $visits_t, $blog_id, $start, $end
        ) );
        $sessions = (int) $wpdb->get_var( $wpdb->prepare(
            "SELECT COUNT(*) FROM %i WHERE blog_id = %d AND started_at >= %s AND started_at < %s",
            $sessions_t, $blog_id, $start, $end
        ) );
        $bounces = (int) $wpdb->get_var( $wpdb->prepare(
            "SELECT COUNT(*) FROM %i WHERE blog_id = %d AND started_at >= %s AND started_at < %s AND is_bounce = 1",
            $sessions_t, $blog_id, $start, $end
        ) );
        $duration = (int) $wpdb->get_var( $wpdb->prepare(
            "SELECT COALESCE(SUM(duration_sec),0) FROM %i WHERE blog_id = %d AND started_at >= %s AND started_at < %s",
            $sessions_t, $blog_id, $start, $end
        ) );
        // 'ai' counts as a genuine access source (GEO/AEO answer-engine
        // traffic); 'outbound'/'download'/'conversion' are click-outs, not
        // where visitors came from, so they stay excluded.
        $top_source = (string) $wpdb->get_var( $wpdb->prepare(
            "SELECT source_name FROM %i WHERE blog_id = %d AND is_bot = 0 AND created_at >= %s AND created_at < %s
             AND source_type IN ('search','social','referral','ai')
             GROUP BY source_name ORDER BY COUNT(*) DESC LIMIT 1",
            $visits_t, $blog_id, $start, $end
        ) );

        $row = array(
            'blog_id'      => $blog_id,
            'day'          => $day,
            'visits'       => $visits,
            'uniques'      => $uniques,
            'sessions'     => $sessions,
            'bounces'      => $bounces,
            'duration_sum' => $duration,
            'top_source'   => mb_substr( $top_source, 0, 60 ),
            'updated_at'   => current_time( 'mysql', 1 ),
        );
        $existing_id = (int) $wpdb->get_var( $wpdb->prepare(
            "SELECT id FROM %i WHERE blog_id = %d AND day = %s LIMIT 1",
            $daily_t, $blog_id, $day
        ) );
        if ( $existing_id ) {
            $wpdb->update( $daily_t, $row, array( 'id' => $existing_id ) );
        } else {
            $wpdb->insert( $daily_t, $row );
        }
    }

    /**
     * Cleanup cron — drop pre-aggregated daily rows older than retention,
     * scoped to one blog (defaults to the current one) so a Multisite
     * cleanup never touches another site's rows — see
     * Seoforge_Tools::cleanup_now().
     */
    public static function cleanup( $blog_id = null ) {
        global $wpdb;
        $blog_id = null === $blog_id ? Seoforge_Utils::current_blog_id() : (int) $blog_id;
        $days   = max( 1, (int) Seoforge_Settings::get( 'daily_retention_days', 730 ) );
        $cutoff = gmdate( 'Y-m-d', time() - DAY_IN_SECONDS * $days );
        return $wpdb->query( $wpdb->prepare( "DELETE FROM %i WHERE blog_id = %d AND day < %s", Seoforge_Utils::table( 'daily' ), $blog_id, $cutoff ) );
    }
}
