use std::collections::{BTreeMap, HashMap};
use std::hash::{Hash, Hasher};
use std::sync::{LazyLock, Mutex};
use std::time::{Duration, Instant};

use serde_json::{Map, Value};

use super::import_device_tools_enabled;

pub(super) const TOOL_CALL_DEDUPE_WINDOW_MS: u64 = 2500;
const TOOL_CALL_DEDUPE_CACHE_TTL_SECS: u64 = 20;

pub(super) static TOOL_CALL_DEDUPE_CACHE: LazyLock<Mutex<HashMap<String, ToolCallDedupeEntry>>> =
    LazyLock::new(|| Mutex::new(HashMap::new()));

#[derive(Clone, Debug)]
pub(super) struct ToolCallDedupeEntry {
    pub created_at: Instant,
    pub envelope: Value,
}

fn canonicalize_json(value: &Value) -> Value {
    match value {
        Value::Object(map) => {
            let mut sorted = BTreeMap::<String, Value>::new();
            for (key, entry) in map {
                sorted.insert(key.clone(), canonicalize_json(entry));
            }
            let mut out = Map::new();
            for (key, entry) in sorted {
                out.insert(key, entry);
            }
            Value::Object(out)
        }
        Value::Array(items) => Value::Array(items.iter().map(canonicalize_json).collect()),
        _ => value.clone(),
    }
}

pub(super) fn stable_dedupe_args_signature(args: &Map<String, Value>) -> String {
    let canonical = canonicalize_json(&Value::Object(args.clone()));
    let serialized = serde_json::to_string(&canonical).unwrap_or_else(|_| "{}".to_string());
    let mut hasher = std::collections::hash_map::DefaultHasher::new();
    serialized.hash(&mut hasher);
    format!("{:x}", hasher.finish())
}

pub(super) fn is_tool_call_dedupe_eligible(name: &str) -> bool {
    if matches!(
        name,
        "kura_import_job_get" | "kura_provider_connections_list"
    ) {
        return import_device_tools_enabled();
    }
    matches!(
        name,
        "kura_events_list"
            | "read_query"
            | "kura_projection_get"
            | "kura_projection_list"
            | "kura_system_manifest"
            | "kura_system_section_get"
            | "kura_semantic_resolve"
            | "kura_exercise_vocabulary_discover"
            | "kura_account_api_keys_list"
    )
}

pub(super) fn tool_call_dedupe_key(
    session_id: &str,
    name: &str,
    args: &Map<String, Value>,
) -> String {
    let sig = stable_dedupe_args_signature(args);
    format!("{session_id}|{name}|{sig}")
}

pub(super) fn get_tool_call_dedupe_entry(
    session_id: &str,
    name: &str,
    args: &Map<String, Value>,
) -> Option<(Value, u128)> {
    if !is_tool_call_dedupe_eligible(name) {
        return None;
    }

    let now = Instant::now();
    let mut cache = TOOL_CALL_DEDUPE_CACHE
        .lock()
        .unwrap_or_else(|e| e.into_inner());
    let cutoff = now - Duration::from_secs(TOOL_CALL_DEDUPE_CACHE_TTL_SECS);
    cache.retain(|_, entry| entry.created_at > cutoff);

    let key = tool_call_dedupe_key(session_id, name, args);
    let entry = cache.get(&key)?;
    let age_ms = now.duration_since(entry.created_at).as_millis();
    if age_ms > u128::from(TOOL_CALL_DEDUPE_WINDOW_MS) {
        return None;
    }
    Some((entry.envelope.clone(), age_ms))
}

pub(super) fn store_tool_call_dedupe_entry(
    session_id: &str,
    name: &str,
    args: &Map<String, Value>,
    envelope: &Value,
) {
    if !is_tool_call_dedupe_eligible(name) {
        return;
    }
    let mut cache = TOOL_CALL_DEDUPE_CACHE
        .lock()
        .unwrap_or_else(|e| e.into_inner());
    let key = tool_call_dedupe_key(session_id, name, args);
    cache.insert(
        key,
        ToolCallDedupeEntry {
            created_at: Instant::now(),
            envelope: envelope.clone(),
        },
    );
}
