use serde::{Deserialize, Serialize};
use std::fmt;
use std::str::FromStr;

/// Error returned when constructing a `SessionKey` with invalid components.
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum SessionKeyError {
    /// A component (channel_name or kind) contains a colon, which would break parsing.
    #[error("session key component '{component}' contains a colon in value '{value}'")]
    ColonInComponent {
        /// Which component is invalid.
        component: &'static str,
        /// The invalid value.
        value: String,
    },
    /// The string does not have the expected "channel:kind:peer" structure.
    #[error("invalid session key: expected 'channel:kind:peer', got '{0}'")]
    InvalidFormat(String),
}

/// Session key encoding channel + conversation identity.
/// Format: "{channel_name}:{kind}:{peer_id}"
/// Examples: "debug-http:local:dev", "telegram:direct:alice"
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct SessionKey(String);

impl SessionKey {
    /// Create a new session key from channel name, conversation kind, and peer identifier.
    ///
    /// # Panics
    ///
    /// Panics if `channel_name` or `kind` contain colons. Use [`try_new`](Self::try_new)
    /// for fallible construction.
    pub fn new(channel_name: &str, kind: &str, peer_id: &str) -> Self {
        Self::try_new(channel_name, kind, peer_id)
            .unwrap_or_else(|e| panic!("SessionKey::new() called with invalid components: {e}"))
    }

    /// Fallible constructor that validates components do not contain colons.
    ///
    /// `channel_name` and `kind` must not contain colons (they are used as delimiters).
    /// `peer_id` may contain colons — it occupies the final segment and is parsed
    /// via `splitn(3, ':')`.
    pub fn try_new(channel_name: &str, kind: &str, peer_id: &str) -> Result<Self, SessionKeyError> {
        if channel_name.contains(':') {
            return Err(SessionKeyError::ColonInComponent {
                component: "channel_name",
                value: channel_name.to_owned(),
            });
        }
        if kind.contains(':') {
            return Err(SessionKeyError::ColonInComponent {
                component: "kind",
                value: kind.to_owned(),
            });
        }
        Ok(Self(format!("{channel_name}:{kind}:{peer_id}")))
    }

    /// Extract the channel name (first colon-delimited segment) from the key.
    pub fn channel_name(&self) -> &str {
        self.0.split(':').next().unwrap_or("")
    }

    /// Extract the kind (second colon-delimited segment) from the key.
    pub fn kind(&self) -> &str {
        self.0.split(':').nth(1).unwrap_or("")
    }

    /// Extract the peer ID (everything after the second colon) from the key.
    pub fn peer_id(&self) -> &str {
        self.0.splitn(3, ':').nth(2).unwrap_or("")
    }
}

impl fmt::Display for SessionKey {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(&self.0)
    }
}

impl FromStr for SessionKey {
    type Err = SessionKeyError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        // Validate structure: must have at least two colons (channel:kind:peer).
        let mut parts = s.splitn(3, ':');
        let _channel = parts.next().unwrap_or("");
        let kind = parts.next();
        let peer = parts.next();

        if kind.is_none() || peer.is_none() {
            return Err(SessionKeyError::InvalidFormat(s.to_owned()));
        }

        Ok(Self(s.to_owned()))
    }
}

impl From<String> for SessionKey {
    fn from(s: String) -> Self {
        s.parse().unwrap_or_else(|e: SessionKeyError| {
            panic!("SessionKey::from(String) called with invalid value '{s}': {e}")
        })
    }
}

impl From<&str> for SessionKey {
    fn from(s: &str) -> Self {
        s.parse().unwrap_or_else(|e: SessionKeyError| {
            panic!("SessionKey::from(&str) called with invalid value '{s}': {e}")
        })
    }
}

impl AsRef<str> for SessionKey {
    fn as_ref(&self) -> &str {
        &self.0
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use rstest::rstest;

    #[test]
    fn when_session_key_created_then_formatted_as_channel_slash_peer() {
        let key = SessionKey::new("debug-http", "local", "dev");
        assert_eq!(key.to_string(), "debug-http:local:dev");
    }

    #[test]
    fn when_channel_name_extracted_from_session_key_then_returns_first_segment() {
        let key = SessionKey::new("telegram", "direct", "alice");
        assert_eq!(key.channel_name(), "telegram");
    }

    #[test]
    fn when_kind_extracted_from_session_key_then_returns_second_segment() {
        let key = SessionKey::new("telegram", "direct", "alice");
        assert_eq!(key.kind(), "direct");
    }

    #[test]
    fn when_peer_id_extracted_from_session_key_then_returns_third_segment() {
        let key = SessionKey::new("telegram", "direct", "alice");
        assert_eq!(key.peer_id(), "alice");
    }

    #[test]
    fn when_session_key_round_tripped_through_display_and_from_str_then_equal() {
        let key = SessionKey::new("slack", "group", "general");
        let s = key.to_string();
        let parsed: SessionKey = s.parse().unwrap();
        assert_eq!(key, parsed);
    }

    #[test]
    fn when_two_equal_session_keys_compared_then_hash_and_eq_consistent() {
        let a = SessionKey::new("debug-http", "local", "dev");
        let b = SessionKey::new("debug-http", "local", "dev");
        assert_eq!(a, b);

        let mut set = std::collections::HashSet::new();
        set.insert(a);
        assert!(set.contains(&b));
    }

    #[rstest]
    fn when_session_key_serde_round_trips_then_identical() {
        let original = SessionKey::new("telegram", "direct", "alice");
        let json = serde_json::to_value(&original).unwrap();
        let restored: SessionKey = serde_json::from_value(json).unwrap();
        assert_eq!(original, restored);
    }

    #[rstest]
    fn when_session_key_display_from_str_round_trips_then_identical() {
        let original = SessionKey::new("slack", "group", "general");
        let displayed = original.to_string();
        let parsed: SessionKey = displayed.parse().unwrap();
        assert_eq!(original, parsed);
    }

    #[rstest]
    fn when_channel_name_contains_colon_then_try_new_returns_error() {
        let result = SessionKey::try_new("tele:gram", "direct", "alice");
        assert!(result.is_err());
        assert!(matches!(
            result.unwrap_err(),
            SessionKeyError::ColonInComponent {
                component: "channel_name",
                ..
            }
        ));
    }

    #[rstest]
    fn when_kind_contains_colon_then_try_new_returns_error() {
        let result = SessionKey::try_new("telegram", "di:rect", "alice");
        assert!(result.is_err());
        assert!(matches!(
            result.unwrap_err(),
            SessionKeyError::ColonInComponent {
                component: "kind",
                ..
            }
        ));
    }

    #[rstest]
    fn when_peer_id_contains_colon_then_try_new_succeeds() {
        let key = SessionKey::try_new("telegram", "direct", "peer:with:colons").unwrap();
        assert_eq!(key.channel_name(), "telegram");
        assert_eq!(key.kind(), "direct");
        assert_eq!(key.peer_id(), "peer:with:colons");
        assert_eq!(key.to_string(), "telegram:direct:peer:with:colons");
    }

    #[rstest]
    fn when_peer_id_has_colons_then_round_trip_preserves_them() {
        let key = SessionKey::new("telegram", "direct", "peer:with:colons");
        let s = key.to_string();
        let parsed: SessionKey = s.parse().unwrap();
        assert_eq!(parsed.peer_id(), "peer:with:colons");
        assert_eq!(key, parsed);
    }

    #[test]
    #[should_panic(expected = "invalid components")]
    fn when_channel_name_contains_colon_then_new_panics() {
        SessionKey::new("tele:gram", "direct", "alice");
    }

    #[test]
    fn when_from_str_with_insufficient_colons_then_error() {
        let result: Result<SessionKey, _> = "no-colons".parse();
        assert!(result.is_err());
    }

    #[test]
    fn when_from_str_with_valid_format_then_succeeds() {
        let key: SessionKey = "telegram:direct:alice".parse().unwrap();
        assert_eq!(key.channel_name(), "telegram");
        assert_eq!(key.kind(), "direct");
        assert_eq!(key.peer_id(), "alice");
    }

    #[test]
    fn when_from_str_with_colons_in_peer_then_succeeds() {
        let key: SessionKey = "system:tool-approval:agent:name".parse().unwrap();
        assert_eq!(key.channel_name(), "system");
        assert_eq!(key.kind(), "tool-approval");
        assert_eq!(key.peer_id(), "agent:name");
    }
}
