use anyhow::Result;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use sqlx::SqlitePool;
use std::collections::HashMap;

use crate::calc::league_stats::{
    calculate_batter_league_stats, calculate_pitcher_league_stats, BatterLeagueStats,
    PitcherLeagueStats,
};
use crate::calc::matchup::{
    get_batter_dist, get_batter_stat, get_pitcher_dist, get_pitcher_stat, standardize_value,
};
use crate::calc::weights::{BATTER_WEIGHTS, PITCHER_WEIGHTS};
use crate::db::models::{BatterCard, PitcherCard, StandardizedScoreCache};
use crate::db::queries::clear_score_cache;

/// Score details for a single stat.
#[derive(Debug, Serialize, Deserialize)]
pub struct StatScore {
    pub raw: f64,
    pub std: i32,
    pub weighted: f64,
}

/// Result of a full cache rebuild.
#[derive(Debug)]
pub struct CacheRebuildResult {
    pub batter_splits: i64,
    pub pitcher_splits: i64,
}

/// Generate a stable hash of the current weight configuration.
///
/// Uses SHA-256 of a sorted JSON representation of BATTER_WEIGHTS and PITCHER_WEIGHTS.
/// Returns the first 16 hex characters.
fn compute_weights_hash() -> String {
    let batter: std::collections::BTreeMap<&str, (i32, bool)> = BATTER_WEIGHTS
        .iter()
        .map(|(name, w)| (*name, (w.weight, w.high_is_better)))
        .collect();
    let pitcher: std::collections::BTreeMap<&str, (i32, bool)> = PITCHER_WEIGHTS
        .iter()
        .map(|(name, w)| (*name, (w.weight, w.high_is_better)))
        .collect();

    let data = serde_json::json!({
        "batter": batter,
        "pitcher": pitcher,
    });

    let mut hasher = Sha256::new();
    hasher.update(data.to_string().as_bytes());
    let result = hasher.finalize();
    let hex: String = result.iter().map(|b| format!("{:02x}", b)).collect();
    hex[..16].to_string()
}

/// Generate a hash of representative league stat values to detect significant changes.
///
/// Hashes 5 key distribution values; returns first 16 hex chars of SHA-256.
fn compute_league_stats_hash(batter: &BatterLeagueStats, pitcher: &PitcherLeagueStats) -> String {
    let key_values = [
        batter.hit_vrhp.avg,
        batter.hit_vrhp.stdev,
        batter.so_vrhp.avg,
        pitcher.hit_vrhb.avg,
        pitcher.so_vrhb.avg,
    ];
    let repr = format!("{:?}", key_values);

    let mut hasher = Sha256::new();
    hasher.update(repr.as_bytes());
    let result = hasher.finalize();
    let hex: String = result.iter().map(|b| format!("{:02x}", b)).collect();
    hex[..16].to_string()
}

/// Calculate standardized scores for all stats on a batter card split.
///
/// `split` is "vlhp" (vs left-handed pitchers) or "vrhp" (vs right-handed pitchers).
/// Returns (total_weighted_score, per-stat StatScore map).
fn calculate_batter_split_scores(
    card: &BatterCard,
    split: &str,
    stats: &BatterLeagueStats,
) -> (f64, HashMap<String, StatScore>) {
    let pitcher_hand = if split == "vlhp" { "L" } else { "R" };

    let mut total = 0.0;
    let mut stat_scores = HashMap::new();

    for (stat_name, weight) in BATTER_WEIGHTS.iter() {
        let raw = get_batter_stat(card, stat_name, pitcher_hand);
        let dist = get_batter_dist(stats, stat_name, pitcher_hand);
        let std = standardize_value(raw, dist, weight.high_is_better);
        let weighted = std as f64 * weight.weight as f64;
        total += weighted;
        stat_scores.insert(stat_name.to_string(), StatScore { raw, std, weighted });
    }

    (total, stat_scores)
}

/// Calculate standardized scores for all stats on a pitcher card split.
///
/// `split` is "vlhb" (vs left-handed batters) or "vrhb" (vs right-handed batters).
/// Returns (total_weighted_score, per-stat StatScore map).
fn calculate_pitcher_split_scores(
    card: &PitcherCard,
    split: &str,
    stats: &PitcherLeagueStats,
) -> (f64, HashMap<String, StatScore>) {
    let batter_hand = if split == "vlhb" { "L" } else { "R" };

    let mut total = 0.0;
    let mut stat_scores = HashMap::new();

    for (stat_name, weight) in PITCHER_WEIGHTS.iter() {
        let raw = get_pitcher_stat(card, stat_name, batter_hand);
        let dist = get_pitcher_dist(stats, stat_name, batter_hand);
        let std = standardize_value(raw, dist, weight.high_is_better);
        let weighted = std as f64 * weight.weight as f64;
        total += weighted;
        stat_scores.insert(stat_name.to_string(), StatScore { raw, std, weighted });
    }

    (total, stat_scores)
}

/// Rebuild the entire standardized score cache.
///
/// Clears all existing entries and recalculates scores for every batter and pitcher
/// card using current league statistics and weight configuration.
pub async fn rebuild_score_cache(pool: &SqlitePool) -> Result<CacheRebuildResult> {
    let batter_stats = calculate_batter_league_stats(pool).await?;
    let pitcher_stats = calculate_pitcher_league_stats(pool).await?;

    let weights_hash = compute_weights_hash();
    let league_hash = compute_league_stats_hash(&batter_stats, &pitcher_stats);

    clear_score_cache(pool).await?;

    let batter_cards: Vec<BatterCard> =
        sqlx::query_as("SELECT * FROM batter_cards").fetch_all(pool).await?;
    let pitcher_cards: Vec<PitcherCard> =
        sqlx::query_as("SELECT * FROM pitcher_cards").fetch_all(pool).await?;

    let computed_at = chrono::Utc::now().naive_utc();
    let mut batter_count: i64 = 0;
    let mut pitcher_count: i64 = 0;

    for card in &batter_cards {
        for split in ["vlhp", "vrhp"] {
            let (total, stat_scores) = calculate_batter_split_scores(card, split, &batter_stats);
            let stat_scores_json = serde_json::to_string(&stat_scores)?;
            sqlx::query(
                "INSERT INTO standardized_score_cache \
                 (batter_card_id, pitcher_card_id, split, total_score, stat_scores, computed_at, \
                  weights_hash, league_stats_hash) \
                 VALUES (?, NULL, ?, ?, ?, ?, ?, ?)",
            )
            .bind(card.id)
            .bind(split)
            .bind(total)
            .bind(&stat_scores_json)
            .bind(computed_at)
            .bind(&weights_hash)
            .bind(&league_hash)
            .execute(pool)
            .await?;
            batter_count += 1;
        }
    }

    for card in &pitcher_cards {
        for split in ["vlhb", "vrhb"] {
            let (total, stat_scores) =
                calculate_pitcher_split_scores(card, split, &pitcher_stats);
            let stat_scores_json = serde_json::to_string(&stat_scores)?;
            sqlx::query(
                "INSERT INTO standardized_score_cache \
                 (batter_card_id, pitcher_card_id, split, total_score, stat_scores, computed_at, \
                  weights_hash, league_stats_hash) \
                 VALUES (NULL, ?, ?, ?, ?, ?, ?, ?)",
            )
            .bind(card.id)
            .bind(split)
            .bind(total)
            .bind(&stat_scores_json)
            .bind(computed_at)
            .bind(&weights_hash)
            .bind(&league_hash)
            .execute(pool)
            .await?;
            pitcher_count += 1;
        }
    }

    Ok(CacheRebuildResult { batter_splits: batter_count, pitcher_splits: pitcher_count })
}

/// Check if the score cache is valid (non-empty and matches current weight config).
///
/// Returns false if the cache is empty or if weights have changed since last rebuild.
pub async fn is_cache_valid(pool: &SqlitePool) -> Result<bool> {
    let entry: Option<StandardizedScoreCache> =
        sqlx::query_as("SELECT * FROM standardized_score_cache LIMIT 1")
            .fetch_optional(pool)
            .await?;

    let entry = match entry {
        Some(e) => e,
        None => return Ok(false),
    };

    let current_weights_hash = compute_weights_hash();
    Ok(entry.weights_hash.as_deref() == Some(current_weights_hash.as_str()))
}

/// Ensure the score cache exists, rebuilding if necessary.
pub async fn ensure_cache_exists(pool: &SqlitePool) -> Result<()> {
    if !is_cache_valid(pool).await? {
        rebuild_score_cache(pool).await?;
    }
    Ok(())
}
