use sqlx::PgPool;
use uuid::Uuid;
use anyhow::{Context, Result};
use chrono::{DateTime, Utc, Duration};
use serde::{Deserialize, Serialize};

/// Player session record.
#[derive(Debug, Clone, sqlx::FromRow, Serialize)]
pub struct PlayerSession {
    pub id: Uuid,
    pub license_id: Uuid,
    pub steam_id: String,
    pub player_name: String,
    pub session_start: DateTime<Utc>,
    pub session_end: Option<DateTime<Utc>>,
    pub duration_seconds: Option<i32>,
    pub created_at: DateTime<Utc>,
}

/// Retention metrics for a specific wipe.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WipeRetentionMetrics {
    pub wipe_id: Uuid,
    pub wipe_date: DateTime<Utc>,
    pub total_players_before_wipe: i64,
    pub returned_24h: i64,
    pub returned_48h: i64,
    pub returned_72h: i64,
    pub retention_24h_percent: f64,
    pub retention_48h_percent: f64,
    pub retention_72h_percent: f64,
}

/// Summary metrics for a time period.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionSummary {
    pub unique_players: i64,
    pub total_sessions: i64,
    pub avg_session_duration_minutes: f64,
    pub new_players: i64,
    pub returning_players: i64,
    pub new_vs_returning_ratio: f64,
}

/// Track player join event — creates new session.
pub async fn track_player_join(
    pool: &PgPool,
    license_id: Uuid,
    steam_id: &str,
    player_name: &str,
) -> Result<Uuid> {
    let id = Uuid::new_v4();

    sqlx::query(
        "INSERT INTO player_sessions (id, license_id, steam_id, player_name, session_start)
         VALUES ($1, $2, $3, $4, NOW())",
    )
    .bind(id)
    .bind(license_id)
    .bind(steam_id)
    .bind(player_name)
    .execute(pool)
    .await
    .context("Failed to insert player session")?;

    Ok(id)
}

/// Track player leave event — updates the most recent open session for this steam_id.
/// Calculates duration_seconds and sets session_end.
pub async fn track_player_leave(
    pool: &PgPool,
    license_id: Uuid,
    steam_id: &str,
) -> Result<()> {
    // Find the most recent session without session_end
    sqlx::query(
        "UPDATE player_sessions
         SET session_end = NOW(),
             duration_seconds = EXTRACT(EPOCH FROM (NOW() - session_start))::INTEGER
         WHERE id = (
             SELECT id FROM player_sessions
             WHERE license_id = $1 AND steam_id = $2 AND session_end IS NULL
             ORDER BY session_start DESC
             LIMIT 1
         )",
    )
    .bind(license_id)
    .bind(steam_id)
    .execute(pool)
    .await
    .context("Failed to update player session on leave")?;

    Ok(())
}

/// Get count of unique players for a license in a time range.
pub async fn get_unique_player_count(
    pool: &PgPool,
    license_id: Uuid,
    start: DateTime<Utc>,
    end: DateTime<Utc>,
) -> Result<i64> {
    let result: (i64,) = sqlx::query_as(
        "SELECT COUNT(DISTINCT steam_id)
         FROM player_sessions
         WHERE license_id = $1 AND session_start >= $2 AND session_start < $3",
    )
    .bind(license_id)
    .bind(start)
    .bind(end)
    .fetch_one(pool)
    .await
    .context("Failed to count unique players")?;

    Ok(result.0)
}

/// Get average session duration (in minutes) for a time range.
pub async fn get_avg_session_duration(
    pool: &PgPool,
    license_id: Uuid,
    start: DateTime<Utc>,
    end: DateTime<Utc>,
) -> Result<f64> {
    let result: (Option<f64>,) = sqlx::query_as(
        "SELECT AVG(duration_seconds) / 60.0 as avg_minutes
         FROM player_sessions
         WHERE license_id = $1
           AND session_start >= $2
           AND session_start < $3
           AND duration_seconds IS NOT NULL",
    )
    .bind(license_id)
    .bind(start)
    .bind(end)
    .fetch_one(pool)
    .await
    .context("Failed to calculate avg session duration")?;

    Ok(result.0.unwrap_or(0.0))
}

/// Calculate new vs returning players for a time range.
/// New = first session ever in this range.
/// Returning = had sessions before this range.
pub async fn get_new_vs_returning_ratio(
    pool: &PgPool,
    license_id: Uuid,
    start: DateTime<Utc>,
    end: DateTime<Utc>,
) -> Result<(i64, i64)> {
    // Get unique players in this range
    let players_in_range: Vec<(String,)> = sqlx::query_as(
        "SELECT DISTINCT steam_id
         FROM player_sessions
         WHERE license_id = $1 AND session_start >= $2 AND session_start < $3",
    )
    .bind(license_id)
    .bind(start)
    .bind(end)
    .fetch_all(pool)
    .await
    .context("Failed to get players in range")?;

    let mut new_players = 0i64;
    let mut returning_players = 0i64;

    for (steam_id,) in players_in_range {
        // Check if player had any sessions before this range
        let has_prior: (i64,) = sqlx::query_as(
            "SELECT COUNT(*) FROM player_sessions
             WHERE license_id = $1 AND steam_id = $2 AND session_start < $3",
        )
        .bind(license_id)
        .bind(&steam_id)
        .bind(start)
        .fetch_one(pool)
        .await
        .context("Failed to check prior sessions")?;

        if has_prior.0 > 0 {
            returning_players += 1;
        } else {
            new_players += 1;
        }
    }

    Ok((new_players, returning_players))
}

/// Get session summary for a time range.
pub async fn get_session_summary(
    pool: &PgPool,
    license_id: Uuid,
    start: DateTime<Utc>,
    end: DateTime<Utc>,
) -> Result<SessionSummary> {
    let unique_players = get_unique_player_count(pool, license_id, start, end).await?;

    let total_sessions: (i64,) = sqlx::query_as(
        "SELECT COUNT(*) FROM player_sessions
         WHERE license_id = $1 AND session_start >= $2 AND session_start < $3",
    )
    .bind(license_id)
    .bind(start)
    .bind(end)
    .fetch_one(pool)
    .await
    .context("Failed to count total sessions")?;

    let avg_session_duration_minutes = get_avg_session_duration(pool, license_id, start, end).await?;
    let (new_players, returning_players) = get_new_vs_returning_ratio(pool, license_id, start, end).await?;

    let new_vs_returning_ratio = if returning_players > 0 {
        new_players as f64 / returning_players as f64
    } else {
        new_players as f64
    };

    Ok(SessionSummary {
        unique_players,
        total_sessions: total_sessions.0,
        avg_session_duration_minutes,
        new_players,
        returning_players,
        new_vs_returning_ratio,
    })
}

/// Calculate retention metrics for a specific wipe.
///
/// Algorithm:
/// 1. Get all unique players who joined in the 7 days BEFORE the wipe
/// 2. For each player, check if they returned within 24h/48h/72h AFTER the wipe
/// 3. Calculate percentage retention
pub async fn calculate_retention_after_wipe(
    pool: &PgPool,
    license_id: Uuid,
    wipe_id: Uuid,
) -> Result<WipeRetentionMetrics> {
    // Get wipe start time
    let wipe: (DateTime<Utc>,) = sqlx::query_as(
        "SELECT started_at FROM wipe_history WHERE id = $1 AND license_id = $2",
    )
    .bind(wipe_id)
    .bind(license_id)
    .fetch_one(pool)
    .await
    .context("Failed to fetch wipe date")?;

    let wipe_date = wipe.0;
    let pre_wipe_start = wipe_date - Duration::days(7);

    // Get unique players who played in 7 days before wipe
    let players_before_wipe: Vec<(String,)> = sqlx::query_as(
        "SELECT DISTINCT steam_id
         FROM player_sessions
         WHERE license_id = $1
           AND session_start >= $2
           AND session_start < $3",
    )
    .bind(license_id)
    .bind(pre_wipe_start)
    .bind(wipe_date)
    .fetch_all(pool)
    .await
    .context("Failed to get pre-wipe players")?;

    let total_players_before_wipe = players_before_wipe.len() as i64;

    if total_players_before_wipe == 0 {
        return Ok(WipeRetentionMetrics {
            wipe_id,
            wipe_date,
            total_players_before_wipe: 0,
            returned_24h: 0,
            returned_48h: 0,
            returned_72h: 0,
            retention_24h_percent: 0.0,
            retention_48h_percent: 0.0,
            retention_72h_percent: 0.0,
        });
    }

    let mut returned_24h = 0i64;
    let mut returned_48h = 0i64;
    let mut returned_72h = 0i64;

    for (steam_id,) in players_before_wipe {
        // Check if player returned within 24h
        let returned_24h_check: (i64,) = sqlx::query_as(
            "SELECT COUNT(*) FROM player_sessions
             WHERE license_id = $1
               AND steam_id = $2
               AND session_start >= $3
               AND session_start < $4",
        )
        .bind(license_id)
        .bind(&steam_id)
        .bind(wipe_date)
        .bind(wipe_date + Duration::hours(24))
        .fetch_one(pool)
        .await
        .context("Failed to check 24h retention")?;

        if returned_24h_check.0 > 0 {
            returned_24h += 1;
        }

        // Check if player returned within 48h
        let returned_48h_check: (i64,) = sqlx::query_as(
            "SELECT COUNT(*) FROM player_sessions
             WHERE license_id = $1
               AND steam_id = $2
               AND session_start >= $3
               AND session_start < $4",
        )
        .bind(license_id)
        .bind(&steam_id)
        .bind(wipe_date)
        .bind(wipe_date + Duration::hours(48))
        .fetch_one(pool)
        .await
        .context("Failed to check 48h retention")?;

        if returned_48h_check.0 > 0 {
            returned_48h += 1;
        }

        // Check if player returned within 72h
        let returned_72h_check: (i64,) = sqlx::query_as(
            "SELECT COUNT(*) FROM player_sessions
             WHERE license_id = $1
               AND steam_id = $2
               AND session_start >= $3
               AND session_start < $4",
        )
        .bind(license_id)
        .bind(&steam_id)
        .bind(wipe_date)
        .bind(wipe_date + Duration::hours(72))
        .fetch_one(pool)
        .await
        .context("Failed to check 72h retention")?;

        if returned_72h_check.0 > 0 {
            returned_72h += 1;
        }
    }

    Ok(WipeRetentionMetrics {
        wipe_id,
        wipe_date,
        total_players_before_wipe,
        returned_24h,
        returned_48h,
        returned_72h,
        retention_24h_percent: (returned_24h as f64 / total_players_before_wipe as f64) * 100.0,
        retention_48h_percent: (returned_48h as f64 / total_players_before_wipe as f64) * 100.0,
        retention_72h_percent: (returned_72h as f64 / total_players_before_wipe as f64) * 100.0,
    })
}

/// Get retention metrics for the last N wipes.
pub async fn get_recent_wipe_retention_metrics(
    pool: &PgPool,
    license_id: Uuid,
    limit: i64,
) -> Result<Vec<WipeRetentionMetrics>> {
    // Get recent wipes with started_at timestamp
    let wipes: Vec<(Uuid, DateTime<Utc>)> = sqlx::query_as(
        "SELECT id, started_at
         FROM wipe_history
         WHERE license_id = $1
           AND started_at IS NOT NULL
           AND status = 'success'
         ORDER BY started_at DESC
         LIMIT $2",
    )
    .bind(license_id)
    .bind(limit)
    .fetch_all(pool)
    .await
    .context("Failed to fetch recent wipes")?;

    let mut metrics = Vec::new();
    for (wipe_id, _wipe_date) in wipes {
        match calculate_retention_after_wipe(pool, license_id, wipe_id).await {
            Ok(metric) => metrics.push(metric),
            Err(e) => {
                tracing::warn!("Failed to calculate retention for wipe {}: {}", wipe_id, e);
                // Continue with other wipes
            }
        }
    }

    Ok(metrics)
}

/// Cleanup old player sessions beyond retention period (default 90 days).
pub async fn cleanup_old_player_sessions(pool: &PgPool, retention_days: i64) -> Result<u64> {
    let result = sqlx::query(
        "DELETE FROM player_sessions
         WHERE session_start < NOW() - ($1 || ' days')::INTERVAL",
    )
    .bind(retention_days)
    .execute(pool)
    .await
    .context("Failed to delete old player sessions")?;

    Ok(result.rows_affected())
}
