use std::collections::HashMap;
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::Instant;

use rusqlite::Connection;
use tokio::sync::{Mutex, RwLock, Semaphore};
use uuid::Uuid;

use cruciverb_core::clue::{ClueDatabase, CrypticClueDb};
use crate::activitypub::ApState;
use cruciverb_core::dict::Dictionary;
use cruciverb_core::puzzle::Puzzle;

const STALENESS_WINDOW: u64 = 50;

const PUZZLE_CACHE_MAX: usize = 500;
const PUZZLE_CACHE_EXPIRY_SECS: u64 = 3600;

pub struct AppStateInner {
    pub db: Connection,
}

pub struct AppState {
    pub dict: Arc<Dictionary>,
    pub clue_db: Arc<ClueDatabase>,
    pub cryptic_db: Option<Arc<CrypticClueDb>>,
    pub word_diffs: Arc<HashMap<String, f64>>,
    pub puzzles: RwLock<HashMap<Uuid, (Puzzle, Instant)>>,
    pub db: Mutex<AppStateInner>,
    pub db_path: String,
    pub word_quality: RwLock<HashMap<String, f64>>,
    pub clue_quality: RwLock<HashMap<(String, String), f64>>,
    pub rp_id: String,
    pub rp_origin: String,
    // maps challenge -> (session_token, timestamp)
    pub reg_challenges: Mutex<HashMap<String, (String, i64)>>,
    // maps challenge -> timestamp (no session needed for auth)
    pub auth_challenges: Mutex<HashMap<String, i64>>,
    pub generation_semaphore: Semaphore,
    pub ap: Option<Arc<ApState>>,
    pub daily_secret: String,
    pub rooms: crate::rooms::Rooms,
    pub blocklist: std::sync::RwLock<cruciverb_core::dict::blocklist::Blocklist>,
    pub definitions: Arc<HashMap<String, String>>,
    pub wotd_words: Arc<Vec<String>>,
    /// Per-(date, preset) locks so the pre-generator and the API cannot both
    /// generate the same daily puzzle at once.
    pub daily_gen_locks: Mutex<HashMap<String, Arc<tokio::sync::Mutex<()>>>>,
    /// Tracks the generation number when each word was last used in a puzzle.
    pub word_staleness: RwLock<HashMap<String, u64>>,
    /// Monotonic counter incremented after each puzzle generation.
    pub generation_counter: AtomicU64,
}

impl AppState {
    pub async fn cache_puzzle(&self, id: Uuid, puzzle: Puzzle) {
        let mut cache = self.puzzles.write().await;
        cache.insert(id, (puzzle, Instant::now()));
        if cache.len() > PUZZLE_CACHE_MAX {
            let cutoff = Instant::now() - std::time::Duration::from_secs(PUZZLE_CACHE_EXPIRY_SECS);
            cache.retain(|_, (_, ts)| *ts > cutoff);
        }
        if cache.len() > PUZZLE_CACHE_MAX {
            let mut entries: Vec<(Uuid, Instant)> = cache.iter().map(|(k, (_, ts))| (*k, *ts)).collect();
            entries.sort_by_key(|(_, ts)| *ts);
            for (k, _) in entries.iter().take(100) {
                cache.remove(k);
            }
        }
    }

    /// Build a staleness penalty map for the solver.
    /// Returns word -> penalty where penalty is 0.3 (just used) to 1.0 (old/unused).
    pub async fn build_staleness_penalties(&self) -> Option<Arc<HashMap<String, f64>>> {
        let gen = self.generation_counter.load(Ordering::Relaxed);
        let staleness = self.word_staleness.read().await;
        if staleness.is_empty() {
            return None;
        }
        let mut penalties: HashMap<String, f64> = HashMap::new();
        for (word, &last_gen) in staleness.iter() {
            let age = gen.saturating_sub(last_gen);
            if age < STALENESS_WINDOW {
                // 0.3 for just-used words, linearly decaying to 1.0
                let decay = age as f64 / STALENESS_WINDOW as f64;
                penalties.insert(word.clone(), 0.3 + 0.7 * decay);
            }
        }
        if penalties.is_empty() { None } else { Some(Arc::new(penalties)) }
    }

    /// Record words from a generated puzzle so future puzzles deprioritize them.
    pub async fn record_puzzle_words(&self, words: &[String]) {
        let gen = self.generation_counter.fetch_add(1, Ordering::Relaxed);
        let mut staleness = self.word_staleness.write().await;
        for word in words {
            staleness.insert(word.clone(), gen);
        }
        // prune old entries periodically
        if staleness.len() > 10_000 {
            let cutoff = gen.saturating_sub(STALENESS_WINDOW);
            staleness.retain(|_, v| *v >= cutoff);
        }
    }

    /// Returns a per-(date, preset) async lock that serializes daily
    /// generation between the pre-generator and on-demand API requests.
    pub async fn daily_gen_lock(&self, date: &str, preset: &str) -> Arc<tokio::sync::Mutex<()>> {
        let mut locks = self.daily_gen_locks.lock().await;
        locks
            .entry(format!("{}:{}", date, preset))
            .or_insert_with(|| Arc::new(tokio::sync::Mutex::new(())))
            .clone()
    }

    pub fn new(
        dict: Dictionary,
        clue_db: ClueDatabase,
        cryptic_db: Option<CrypticClueDb>,
        word_diffs: HashMap<String, f64>,
        db: Connection,
        db_path: String,
        rp_id: String,
        rp_origin: String,
        ap: Option<ApState>,
        daily_secret: String,
        blocklist: cruciverb_core::dict::blocklist::Blocklist,
        definitions: HashMap<String, String>,
        wotd_words: Vec<String>,
    ) -> Arc<Self> {
        Arc::new(Self {
            dict: Arc::new(dict),
            clue_db: Arc::new(clue_db),
            cryptic_db: cryptic_db.map(Arc::new),
            word_diffs: Arc::new(word_diffs),
            puzzles: RwLock::new(HashMap::new()),
            db: Mutex::new(AppStateInner { db }),
            db_path,
            word_quality: RwLock::new(HashMap::new()),
            clue_quality: RwLock::new(HashMap::new()),
            rp_id,
            rp_origin,
            reg_challenges: Mutex::new(HashMap::new()),
            auth_challenges: Mutex::new(HashMap::new()),
            generation_semaphore: Semaphore::new(3),
            ap: ap.map(Arc::new),
            daily_secret,
            rooms: crate::rooms::new_rooms(),
            blocklist: std::sync::RwLock::new(blocklist),
            definitions: Arc::new(definitions),
            wotd_words: Arc::new(wotd_words),
            daily_gen_locks: Mutex::new(HashMap::new()),
            word_staleness: RwLock::new(HashMap::new()),
            generation_counter: AtomicU64::new(0),
        })
    }
}
