use anyhow::{Context, Result};
use reqwest::Client;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::RwLock;

use super::nats_bridge::NatsBridge;

/// Steam App ID for the Rust Dedicated Server.
pub const RUST_SERVER_APP_ID: u32 = 258550;

/// Default polling interval: 60 seconds.
const DEFAULT_POLL_INTERVAL: Duration = Duration::from_secs(60);

#[derive(Debug, Serialize)]
struct SteamUpdateEvent {
    app_id: u32,
    old_build_id: Option<String>,
    new_build_id: String,
    detected_at: String,
}

#[derive(Debug, Deserialize)]
struct SteamApiResponse {
    response: SteamUpToDateCheck,
}

#[derive(Debug, Deserialize)]
struct SteamUpToDateCheck {
    success: bool,
    #[serde(default)]
    up_to_date: bool,
    #[serde(default)]
    version_is_listable: bool,
    #[serde(default)]
    required_version: Option<u64>,
}

/// Steam build ID polling service.
///
/// Periodically checks the Steam Web API for new Rust Dedicated Server
/// builds. When a new build is detected (build ID changes), it publishes
/// an event to NATS so that servers configured for auto-update-on-force-wipe
/// can be notified and trigger their wipe schedules.
pub struct SteamUpdateWatcher {
    http: Client,
    nats: Arc<NatsBridge>,
    db: sqlx::PgPool,
    steam_api_key: String,
    poll_interval: Duration,
    last_known_build_id: Arc<RwLock<Option<String>>>,
}

impl SteamUpdateWatcher {
    pub fn new(
        nats: Arc<NatsBridge>,
        db: sqlx::PgPool,
        steam_api_key: String,
    ) -> Self {
        Self {
            http: Client::new(),
            nats,
            db,
            steam_api_key,
            poll_interval: DEFAULT_POLL_INTERVAL,
            last_known_build_id: Arc::new(RwLock::new(None)),
        }
    }

    /// Start the polling loop. Runs indefinitely, checking for updates
    /// at the configured interval.
    pub async fn start_polling(&self) -> Result<()> {
        tracing::info!(
            "Steam Update Watcher started — polling every {}s for app {}",
            self.poll_interval.as_secs(),
            RUST_SERVER_APP_ID
        );

        loop {
            match self.check_for_update().await {
                Ok(Some(new_build_id)) => {
                    tracing::info!("Steam update detected! New build ID: {}", new_build_id);
                    if let Err(e) = self.handle_update_detected(&new_build_id).await {
                        tracing::error!("Failed to handle Steam update event: {e}");
                    }
                }
                Ok(None) => {
                    tracing::trace!("No Steam update detected");
                }
                Err(e) => {
                    tracing::warn!("Steam API check failed: {e}");
                }
            }

            tokio::time::sleep(self.poll_interval).await;
        }
    }

    /// Check Steam Web API for the current build ID of the Rust server app.
    ///
    /// Returns the build ID string if an update is detected (differs from
    /// last known), or None if unchanged.
    pub async fn check_for_update(&self) -> Result<Option<String>> {
        // Use ISteamApps/UpToDateCheck to detect version changes
        let url = format!(
            "https://api.steampowered.com/ISteamApps/UpToDateCheck/v1/?appid={}&version=0",
            RUST_SERVER_APP_ID
        );

        let response = self
            .http
            .get(&url)
            .send()
            .await
            .context("Failed to query Steam API")?;

        let body: SteamApiResponse = response
            .json()
            .await
            .context("Failed to parse Steam API response")?;

        if !body.response.success {
            anyhow::bail!("Steam API returned success=false");
        }

        // Use the required_version as our build ID proxy
        let current_version = body
            .response
            .required_version
            .map(|v| v.to_string())
            .unwrap_or_default();

        if current_version.is_empty() {
            return Ok(None);
        }

        let mut last = self.last_known_build_id.write().await;
        match last.as_ref() {
            Some(known) if known == &current_version => Ok(None),
            _ => {
                *last = Some(current_version.clone());
                // Don't fire on first poll (we're just learning the current version)
                if last.is_some() {
                    Ok(Some(current_version))
                } else {
                    Ok(None)
                }
            }
        }
    }

    /// Handle a detected Steam update: persist the new build ID,
    /// publish NATS event for downstream consumers.
    pub async fn handle_update_detected(&self, new_build_id: &str) -> Result<()> {
        let old = self.last_known_build_id.read().await.clone();

        let event = SteamUpdateEvent {
            app_id: RUST_SERVER_APP_ID,
            old_build_id: old,
            new_build_id: new_build_id.to_string(),
            detected_at: chrono::Utc::now().to_rfc3339(),
        };

        // Publish to NATS for all interested consumers
        self.nats
            .publish_json_jetstream("corrosion.steam.update_detected", &event)
            .await
            .context("Failed to publish Steam update event")?;

        tracing::info!(
            "Published Steam update event: build {} -> {}",
            event.old_build_id.as_deref().unwrap_or("unknown"),
            new_build_id
        );

        // Query all licenses with auto_update_on_force_wipe = true
        // and notify them to check their wipe schedules
        let eligible_licenses: Vec<(uuid::Uuid,)> = sqlx::query_as(
            "SELECT sc.license_id FROM server_config sc
             JOIN licenses l ON l.id = sc.license_id
             WHERE sc.force_wipe_eligible = true
             AND sc.auto_update_on_force_wipe = true
             AND l.status = 'active'"
        )
        .fetch_all(&self.db)
        .await
        .context("Failed to query eligible licenses for force wipe")?;

        for (license_id,) in &eligible_licenses {
            let subject = format!("corrosion.{}.wipe.force_wipe_detected", license_id);
            if let Err(e) = self.nats.publish_json(&subject, &event).await {
                tracing::error!("Failed to notify license {} of force wipe: {e}", license_id);
            }
        }

        tracing::info!(
            "Notified {} licenses of potential force wipe",
            eligible_licenses.len()
        );

        Ok(())
    }
}
