//! Matomo analytics plugin — buffers game-play events and flushes them to
//! the configured Matomo instance in the background.
//!
//! Disabled by default (opt-in via Settings → Privacy). Only active when
//! `settings.analytics_enabled` is `true` AND `settings.matomo_url` is set.

use std::sync::Arc;

use bevy::prelude::*;
use bevy::tasks::AsyncComputeTaskPool;
use solitaire_core::game_state::GameMode;
use solitaire_data::{Settings, matomo_client::MatomoClient, settings::SyncBackend};

use crate::events::{AchievementUnlockedEvent, ForfeitEvent, GameWonEvent, NewGameRequestEvent};
use crate::resources::{GameStateResource, TokioRuntimeResource};
use crate::settings_plugin::{SettingsChangedEvent, SettingsResource};

// ---------------------------------------------------------------------------
// Resource
// ---------------------------------------------------------------------------

/// Holds the active Matomo client. `None` when the feature is disabled.
#[derive(Resource)]
pub struct AnalyticsResource {
    pub client: Option<Arc<MatomoClient>>,
    flush_timer: Timer,
}

impl Default for AnalyticsResource {
    fn default() -> Self {
        Self {
            client: None,
            flush_timer: Timer::from_seconds(60.0, TimerMode::Repeating),
        }
    }
}

// ---------------------------------------------------------------------------
// Plugin
// ---------------------------------------------------------------------------

/// Registers analytics systems. Add after `SettingsPlugin` in the app.
pub struct AnalyticsPlugin;

impl Plugin for AnalyticsPlugin {
    fn build(&self, app: &mut App) {
        app.init_resource::<AnalyticsResource>()
            .add_systems(Startup, init_analytics)
            .add_systems(
                Update,
                (
                    react_to_settings_change,
                    on_new_game,
                    on_achievement_unlocked,
                ),
            );

        // Build the shared Tokio runtime; skip network flush systems if the OS
        // refuses to create threads (resource-limited / sandboxed environments).
        match TokioRuntimeResource::new() {
            Ok(rt) => {
                app.insert_resource(rt)
                    .add_systems(Update, (on_game_won, on_forfeit, tick_flush_timer));
            }
            Err(e) => {
                bevy::log::warn!(
                    "analytics_plugin: Tokio runtime unavailable — analytics flush disabled: {e}"
                );
            }
        }
    }
}

// ---------------------------------------------------------------------------
// Systems
// ---------------------------------------------------------------------------

fn init_analytics(settings: Res<SettingsResource>, mut analytics: ResMut<AnalyticsResource>) {
    analytics.client = client_for(&settings.0);
}

fn react_to_settings_change(
    mut events: MessageReader<SettingsChangedEvent>,
    mut analytics: ResMut<AnalyticsResource>,
) {
    for ev in events.read() {
        analytics.client = client_for(&ev.0);
    }
}

fn on_game_won(
    mut wins: MessageReader<GameWonEvent>,
    analytics: Res<AnalyticsResource>,
    rt: Res<TokioRuntimeResource>,
) {
    let Some(client) = analytics.client.clone() else {
        return;
    };
    let mut any = false;
    for ev in wins.read() {
        client.event("Game", "Won", None, Some(ev.score as f64));
        any = true;
    }
    if any {
        fire_flush(client, rt.0.clone());
    }
}

fn on_forfeit(
    mut forfeits: MessageReader<ForfeitEvent>,
    analytics: Res<AnalyticsResource>,
    rt: Res<TokioRuntimeResource>,
) {
    let Some(client) = analytics.client.clone() else {
        return;
    };
    let mut any = false;
    for _ev in forfeits.read() {
        client.event("Game", "Forfeit", None, None);
        any = true;
    }
    if any {
        fire_flush(client, rt.0.clone());
    }
}

fn on_new_game(
    mut requests: MessageReader<NewGameRequestEvent>,
    analytics: Res<AnalyticsResource>,
    game: Res<GameStateResource>,
) {
    let Some(client) = analytics.client.clone() else {
        return;
    };
    for ev in requests.read() {
        if !ev.confirmed {
            continue;
        }
        let mode = ev.mode.unwrap_or(game.0.mode);
        client.event("Game", "Start", Some(mode_str(mode)), None);
    }
}

fn on_achievement_unlocked(
    mut achievements: MessageReader<AchievementUnlockedEvent>,
    analytics: Res<AnalyticsResource>,
) {
    let Some(client) = analytics.client.clone() else {
        return;
    };
    for ev in achievements.read() {
        client.event("Achievement", "Unlocked", Some(&ev.0.id), None);
    }
}

fn tick_flush_timer(
    time: Res<Time>,
    mut analytics: ResMut<AnalyticsResource>,
    rt: Res<TokioRuntimeResource>,
) {
    analytics.flush_timer.tick(time.delta());
    if !analytics.flush_timer.just_finished() {
        return;
    }
    if let Some(client) = analytics.client.clone() {
        fire_flush(client, rt.0.clone());
    }
}

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

fn client_for(settings: &Settings) -> Option<Arc<MatomoClient>> {
    if !settings.analytics_enabled {
        return None;
    }
    let url = settings.matomo_url.as_deref()?;
    let uid = match &settings.sync_backend {
        SyncBackend::SolitaireServer { username, .. } => Some(username.clone()),
        SyncBackend::Local => None,
    };
    Some(Arc::new(MatomoClient::new(
        url,
        settings.matomo_site_id,
        uid,
    )))
}

fn fire_flush(client: Arc<MatomoClient>, rt: Arc<tokio::runtime::Runtime>) {
    AsyncComputeTaskPool::get()
        .spawn(async move {
            rt.block_on(client.flush());
        })
        .detach();
}

fn mode_str(mode: GameMode) -> &'static str {
    match mode {
        GameMode::Classic => "classic",
        GameMode::Zen => "zen",
        GameMode::Challenge => "challenge",
        GameMode::TimeAttack => "time_attack",
        GameMode::Difficulty(_) => "difficulty",
    }
}

#[cfg(test)]
mod tests {
    use solitaire_core::game_state::DifficultyLevel;

    use super::*;

    #[test]
    fn client_for_requires_analytics_opt_in() {
        let settings = Settings {
            analytics_enabled: false,
            matomo_url: Some("https://analytics.example.com".into()),
            ..Settings::default()
        };

        assert!(client_for(&settings).is_none());
    }

    #[test]
    fn client_for_requires_matomo_url() {
        let settings = Settings {
            analytics_enabled: true,
            matomo_url: None,
            ..Settings::default()
        };

        assert!(client_for(&settings).is_none());
    }

    #[test]
    fn client_for_creates_client_when_enabled_and_configured() {
        let settings = Settings {
            analytics_enabled: true,
            matomo_url: Some("https://analytics.example.com".into()),
            matomo_site_id: 2,
            sync_backend: SyncBackend::SolitaireServer {
                url: "https://solitaire.example.com".into(),
                username: "alice".into(),
                avatar_url: None,
            },
            ..Settings::default()
        };

        assert!(client_for(&settings).is_some());
    }

    #[test]
    fn mode_labels_match_analytics_payload_contract() {
        assert_eq!(mode_str(GameMode::Classic), "classic");
        assert_eq!(mode_str(GameMode::Zen), "zen");
        assert_eq!(mode_str(GameMode::Challenge), "challenge");
        assert_eq!(mode_str(GameMode::TimeAttack), "time_attack");
        assert_eq!(
            mode_str(GameMode::Difficulty(DifficultyLevel::Grandmaster)),
            "difficulty"
        );
    }
}
