use std::collections::HashMap;

use serde::ser::{Serialize, SerializeMap, Serializer};
use serde_json::{Map, Value};

use super::interface::SessionState;

const SESSION_STATE_FORMAT_VERSION: u8 = 1;

#[derive(Debug)]
struct StoredSessionStateRef<'a> {
    state: &'a SessionState,
}

impl Serialize for StoredSessionStateRef<'_> {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        let mut map = serializer.serialize_map(Some(2))?;
        map.serialize_entry("v", &SESSION_STATE_FORMAT_VERSION)?;
        map.serialize_entry("state", self.state)?;
        map.end()
    }
}

pub fn serialize_session_state(
    session_state: &SessionState,
) -> Result<String, anyhow::Error> {
    let stored = StoredSessionStateRef {
        state: session_state,
    };

    serde_json::to_string(&stored).map_err(anyhow::Error::new)
}

pub fn deserialize_session_state(
    value: &str,
) -> Result<SessionState, anyhow::Error> {
    let value = serde_json::from_str::<Value>(value)?;

    let Value::Object(mut obj) = value else {
        anyhow::bail!("Session state is not a JSON object");
    };

    if matches!(obj.get("state"), Some(Value::Object(_)))
        && let Some(Value::Number(v)) = obj.get("v")
    {
        let v = v.as_u64().ok_or_else(|| {
            anyhow::anyhow!("Invalid session state format version")
        })?;
        let v = u8::try_from(v).map_err(|_| {
            anyhow::anyhow!("Invalid session state format version")
        })?;
        anyhow::ensure!(
            v == SESSION_STATE_FORMAT_VERSION,
            "Unsupported session state format version: {}",
            v
        );

        let Some(Value::Object(state)) = obj.remove("state") else {
            unreachable!("`state` was checked to be an object above");
        };
        return Ok(state);
    }

    if obj.values().all(Value::is_string) {
        let legacy: HashMap<String, String> =
            serde_json::from_value(Value::Object(obj))?;
        let mut migrated: Map<String, Value> = Map::new();
        for (key, json_encoded) in legacy {
            migrated.insert(key, serde_json::from_str::<Value>(&json_encoded)?);
        }

        return Ok(migrated);
    }

    Ok(obj)
}
