//! Capability framework types — shared vocabulary for non-gameplay engine
//! extensions (annotations, journal, audio, …).
//!
//! Types that reference `wire::ServerFrame` (`FrameContext`) and the
//! `Capability` trait stay in `maat-sdk` because sdk-types has no
//! wire module. Everything else — identifiers, scope enums, persistence
//! primitives, settings schema, annotation data — lives here.

use std::collections::HashMap;
use std::sync::Arc;

// ---------------------------------------------------------------------------
// WireFrameHash
// ---------------------------------------------------------------------------

/// Compile-time stable schema hash for a wire frame type.
///
/// A `#[derive(WireFrameHash)]` proc-macro (from `maat-sdk-derive`) walks
/// the type's AST in declaration order and produces a `blake3` hash truncated
/// to `u64`. Manual impls are also fine.
pub trait WireFrameHash {
    /// Stable hash of the frame's serde-relevant shape.
    const SCHEMA_HASH: u64;
}

// ---------------------------------------------------------------------------
// CapabilityId
// ---------------------------------------------------------------------------

/// Wire-stable identifier for a registered capability.
///
/// Format: `"<plugin_namespace>:<local_name>"`, e.g. `"core:annotations"`.
/// Equality and hashing are by string value.
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
pub struct CapabilityId(pub String);

impl CapabilityId {
    pub fn new(s: impl Into<String>) -> Self {
        Self(s.into())
    }

    pub fn as_str(&self) -> &str {
        &self.0
    }
}

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

impl From<&str> for CapabilityId {
    fn from(s: &str) -> Self {
        Self(s.to_owned())
    }
}

impl From<String> for CapabilityId {
    fn from(s: String) -> Self {
        Self(s)
    }
}

// ---------------------------------------------------------------------------
// CapabilityScope + CapabilityLifecycleState
// ---------------------------------------------------------------------------

/// Where a capability's state lives. Determines persistence path routing.
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
pub enum CapabilityScope {
    Canvas,
    Session,
    Campaign,
    Installation,
    User,
}

/// Installation/enable status of a capability in a particular running engine.
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
pub enum CapabilityLifecycleState {
    NotInstalled,
    InstalledDisabled,
    InstalledEnabled,
}

// ---------------------------------------------------------------------------
// SeatInfo / UserInfo / BroadcastRecipients
// ---------------------------------------------------------------------------

/// Identity of one seat at the table, as seen by capability handlers.
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Debug)]
pub struct SeatInfo {
    pub seat_id: crate::types::SeatId,
    pub user_id: Option<crate::identity::UserId>,
    pub role:    crate::config::SeatRole,
}

/// Identity of one connected user, as seen by capability handlers.
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Debug)]
pub struct UserInfo {
    pub user_id: crate::identity::UserId,
    pub seat_id: Option<crate::types::SeatId>,
    pub role:    crate::config::SeatRole,
}

/// Recipient narrowing for `FrameContext::emit`.
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Debug)]
pub enum BroadcastRecipients {
    All,
    Public,
    Role(crate::config::SeatRole),
    Roles(Vec<crate::config::SeatRole>),
    Seat(crate::types::SeatId),
    Seats(Vec<crate::types::SeatId>),
    AllExcept(crate::types::SeatId),
    User(crate::identity::UserId),
    Users(Vec<crate::identity::UserId>),
}

// ---------------------------------------------------------------------------
// EventLogTail / EventEmitter
// ---------------------------------------------------------------------------

/// Read-only tail of the gameplay event log, exposed to capabilities that
/// opted in via `Capability::observe_gameplay`.
#[derive(Debug, Default)]
pub struct EventLogTail {
    _stub: (),
}

impl EventLogTail {
    pub fn empty() -> Self {
        Self { _stub: () }
    }
}

/// Outbound channel for capabilities that emit gameplay events.
#[derive(Debug, Default)]
pub struct EventEmitter {
    _stub: (),
}

impl EventEmitter {
    pub fn new() -> Self {
        Self { _stub: () }
    }
}

/// Result type used across the Capability trait surface.
pub type CapabilityResult<T> = Result<T, Box<dyn std::error::Error + Send + Sync>>;

// ---------------------------------------------------------------------------
// SemverVersion / CapabilityManifest / ManifestBundle
// ---------------------------------------------------------------------------

/// Minimal semver triple for capability versioning.
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
pub struct SemverVersion {
    pub major: u32,
    pub minor: u32,
    pub patch: u32,
}

impl SemverVersion {
    pub const fn new(major: u32, minor: u32, patch: u32) -> Self {
        Self { major, minor, patch }
    }
}

impl std::fmt::Display for SemverVersion {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
    }
}

/// A single capability's serialised configuration.
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
pub struct CapabilityManifest {
    pub id: CapabilityId,
    pub version: SemverVersion,
    pub scope: CapabilityScope,
    pub state: CapabilityLifecycleState,
    pub schema_hash: u64,
    #[cfg(feature = "serde")]
    pub settings: serde_json::Value,
}

/// A group of [`CapabilityManifest`]s with optional metadata.
#[derive(Clone, Debug, PartialEq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
pub struct ManifestBundle {
    pub manifests: Vec<CapabilityManifest>,
    pub label: Option<String>,
    pub description: Option<String>,
}

// ---------------------------------------------------------------------------
// Scope identifiers + scope_path
// ---------------------------------------------------------------------------

/// Per-scene routing id used for the `CapabilityScope::Canvas` persistence path.
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
pub struct SceneId(pub String);

impl SceneId {
    pub fn new(s: impl Into<String>) -> Self {
        Self(s.into())
    }
    pub fn as_str(&self) -> &str {
        &self.0
    }
}

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

pub use crate::identity::CampaignId;

/// Routing handle for the `CapabilityScope::Session` persistence path.
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
pub struct ScopeSessionId(pub u64);

impl ScopeSessionId {
    pub fn new(n: u64) -> Self {
        Self(n)
    }
}

impl std::fmt::Display for ScopeSessionId {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.0)
    }
}

/// Routing handle for the `CapabilityScope::User` persistence path.
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
pub struct ScopeUserId(pub u64);

impl ScopeUserId {
    pub fn new(n: u64) -> Self {
        Self(n)
    }
}

impl std::fmt::Display for ScopeUserId {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.0)
    }
}

/// Concrete identifier for a capability's persistence scope.
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
pub enum ScopeId {
    Canvas(SceneId),
    Session(ScopeSessionId),
    Campaign(CampaignId),
    Installation,
    User(ScopeUserId),
}

impl ScopeId {
    pub fn scope(&self) -> CapabilityScope {
        match self {
            ScopeId::Canvas(_) => CapabilityScope::Canvas,
            ScopeId::Session(_) => CapabilityScope::Session,
            ScopeId::Campaign(_) => CapabilityScope::Campaign,
            ScopeId::Installation => CapabilityScope::Installation,
            ScopeId::User(_) => CapabilityScope::User,
        }
    }
}

/// Compute the relative storage path prefix for a given `(scope, scope_id)` pair.
pub fn scope_path(scope_id: &ScopeId) -> String {
    match scope_id {
        ScopeId::Canvas(scene) => format!("scenes/{}", scene),
        ScopeId::Session(session) => format!("sessions/{}", session),
        ScopeId::Campaign(campaign) => format!("campaigns/{}", campaign),
        ScopeId::Installation => "system".to_owned(),
        ScopeId::User(user) => format!("users/{}", user),
    }
}

// ---------------------------------------------------------------------------
// BackendError / KeyValueBackend / PersistenceContext / PersistContext
// ---------------------------------------------------------------------------

/// Errors a `KeyValueBackend` can surface.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum BackendError {
    NotFound,
    Io(String),
    Serialization(String),
}

impl std::fmt::Display for BackendError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::NotFound => f.write_str("not found"),
            Self::Io(s) => write!(f, "io: {s}"),
            Self::Serialization(s) => write!(f, "serialization: {s}"),
        }
    }
}

impl std::error::Error for BackendError {}

/// Small string read/write/delete surface used by capability persistence.
pub trait KeyValueBackend: std::fmt::Debug + Send + Sync {
    fn read_string(&self, key: &str) -> Result<Option<String>, BackendError>;
    fn write_string(&self, key: &str, value: &str) -> Result<(), BackendError>;
    fn delete(&self, key: &str) -> Result<(), BackendError>;
}

/// Context handed to a capability during `load` / `save`. Borrows the
/// storage backend so capabilities can read/write without owning storage internals.
#[derive(Debug)]
pub struct PersistenceContext<'a> {
    pub backend:  &'a dyn KeyValueBackend,
    pub scope:    CapabilityScope,
    pub scope_id: ScopeId,
}

impl<'a> PersistenceContext<'a> {
    pub fn scope_path(&self) -> String {
        scope_path(&self.scope_id)
    }

    pub fn capability_state_key(&self, capability_id: &CapabilityId) -> String {
        let safe = capability_id.as_str().replace(':', "_");
        format!("{}/{safe}.json", self.scope_path())
    }

    pub fn read_capability_state(
        &self,
        capability_id: &CapabilityId,
    ) -> Result<Option<String>, BackendError> {
        let key = self.capability_state_key(capability_id);
        self.backend.read_string(&key)
    }

    pub fn write_capability_state(
        &self,
        capability_id: &CapabilityId,
        value: &str,
    ) -> Result<(), BackendError> {
        let key = self.capability_state_key(capability_id);
        self.backend.write_string(&key, value)
    }

    pub fn delete_capability_state(
        &self,
        capability_id: &CapabilityId,
    ) -> Result<(), BackendError> {
        let key = self.capability_state_key(capability_id);
        self.backend.delete(&key)
    }
}

/// Async, Store-routed persistence context handed to a capability's
/// `load` / `save` methods.
#[derive(Debug, Clone)]
pub struct PersistContext {
    pub store: Arc<dyn crate::store::Store>,
    pub scope: CapabilityScope,
}

impl PersistContext {
    pub fn new(
        store: Arc<dyn crate::store::Store>,
        scope: CapabilityScope,
    ) -> Self {
        Self { store, scope }
    }
}

// ---------------------------------------------------------------------------
// Settings schema
// ---------------------------------------------------------------------------

/// A single user-facing capability setting.
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
pub struct SettingDescriptor {
    pub key: String,
    pub label: String,
    pub description: Option<String>,
    pub hover_help: Option<String>,
    pub kind: SettingKind,
}

/// Input kind for a `SettingDescriptor`.
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
pub enum SettingKind {
    Bool { default: bool },
    Int { default: i64, min: Option<i64>, max: Option<i64> },
    String { default: String, max_len: Option<usize> },
    Enum { default: String, options: Vec<EnumOption> },
    Color { default: String },
}

/// One choice in a `SettingKind::Enum`.
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
pub struct EnumOption {
    pub value: String,
    pub label: String,
    pub description: Option<String>,
}

// ---------------------------------------------------------------------------
// Annotation data types
// ---------------------------------------------------------------------------

/// Stable, scene-local identifier for one annotation. Server-allocated.
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Default, PartialOrd, Ord)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
pub struct AnnotationId(pub u64);

impl AnnotationId {
    pub const fn new(n: u64) -> Self {
        Self(n)
    }
    pub const fn raw(self) -> u64 {
        self.0
    }
}

impl std::fmt::Display for AnnotationId {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.0)
    }
}

/// Wall-clock millisecond timestamp at create / update time.
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Default, PartialOrd, Ord)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
pub struct TimestampMs(pub u64);

impl TimestampMs {
    pub const fn new(n: u64) -> Self {
        Self(n)
    }
}

/// Geometry of an annotation.
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
pub enum AnnotationKind {
    Freehand { points: Vec<(f64, f64)> },
    Line { from: (f64, f64), to: (f64, f64) },
    Rectangle { top_left: (f64, f64), size: (f64, f64) },
    Ellipse { center: (f64, f64), radii: (f64, f64) },
    Arrow { from: (f64, f64), to: (f64, f64) },
    Text { position: (f64, f64), content: String },
}

/// Visual style for an annotation.
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
pub struct AnnotationStyle {
    pub color: String,
    pub stroke_width: f32,
    pub fill: Option<String>,
    pub font_size: Option<u32>,
}

impl Default for AnnotationStyle {
    fn default() -> Self {
        Self {
            color:        "#3a7ed8".into(),
            stroke_width: 2.0,
            fill:         None,
            font_size:    None,
        }
    }
}

/// Visibility scope for annotations.
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
pub enum AnnotationScope {
    #[default]
    Public,
    GMOnly,
}

/// One annotation.
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Annotation {
    pub id:         AnnotationId,
    pub author:     crate::types::SeatId,
    pub created_at: TimestampMs,
    pub kind:       AnnotationKind,
    pub style:      AnnotationStyle,
    pub scope:      AnnotationScope,
}

/// Aggregate annotations state, keyed by scene.
#[derive(Clone, Debug, PartialEq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct AnnotationsState {
    pub by_scene: HashMap<SceneId, Vec<Annotation>>,
}

impl AnnotationsState {
    pub fn new() -> Self {
        Self::default()
    }

    pub fn total_count(&self) -> usize {
        self.by_scene.values().map(|v| v.len()).sum()
    }

    pub fn for_scene(&self, scene: &SceneId) -> &[Annotation] {
        self.by_scene
            .get(scene)
            .map(|v| v.as_slice())
            .unwrap_or(&[])
    }
}

#[cfg(feature = "serde")]
impl crate::artifact::Artifact for AnnotationsState {
    const SCHEMA_HASH: &'static str = "capability/annotations/v1";
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

#[cfg(test)]
mod tests {
    use super::*;
    use std::collections::HashMap;
    use std::sync::Mutex;

    // WireFrameHash manual impl test
    #[derive(Debug)]
    struct ManuallyHashedFrame;
    impl WireFrameHash for ManuallyHashedFrame {
        const SCHEMA_HASH: u64 = 0xCAFEBABE_DEADBEEF;
    }

    #[test]
    fn wire_frame_hash_manual_impl() {
        assert_eq!(ManuallyHashedFrame::SCHEMA_HASH, 0xCAFEBABE_DEADBEEF);
        fn read<T: WireFrameHash>() -> u64 { T::SCHEMA_HASH }
        assert_eq!(read::<ManuallyHashedFrame>(), 0xCAFEBABE_DEADBEEF);
    }

    #[test]
    fn capability_id_round_trip() {
        let id = CapabilityId::new("core:annotations");
        assert_eq!(id.as_str(), "core:annotations");
        assert_eq!(id.to_string(), "core:annotations");
    }

    #[test]
    fn capability_id_from_conversions() {
        let a: CapabilityId = "core:journal".into();
        let b: CapabilityId = String::from("core:journal").into();
        assert_eq!(a, b);
    }

    // KeyValueBackend mock for persistence tests
    #[derive(Debug, Default)]
    struct MockBackend(Mutex<HashMap<String, String>>);

    impl KeyValueBackend for MockBackend {
        fn read_string(&self, key: &str) -> Result<Option<String>, BackendError> {
            Ok(self.0.lock().unwrap().get(key).cloned())
        }
        fn write_string(&self, key: &str, value: &str) -> Result<(), BackendError> {
            self.0.lock().unwrap().insert(key.to_owned(), value.to_owned());
            Ok(())
        }
        fn delete(&self, key: &str) -> Result<(), BackendError> {
            self.0.lock().unwrap().remove(key);
            Ok(())
        }
    }

    #[test]
    fn scope_path_canvas() {
        let id = ScopeId::Canvas(SceneId::new("dungeon-7"));
        assert_eq!(scope_path(&id), "scenes/dungeon-7");
        assert_eq!(id.scope(), CapabilityScope::Canvas);
    }

    #[test]
    fn scope_path_session() {
        let id = ScopeId::Session(ScopeSessionId(42));
        assert_eq!(scope_path(&id), "sessions/42");
    }

    #[test]
    fn scope_path_installation() {
        let id = ScopeId::Installation;
        assert_eq!(scope_path(&id), "system");
    }

    #[test]
    fn persistence_context_round_trip() {
        let backend = MockBackend::default();
        let ctx = PersistenceContext {
            backend:  &backend,
            scope:    CapabilityScope::Canvas,
            scope_id: ScopeId::Canvas(SceneId::new("alpha")),
        };
        assert_eq!(ctx.scope_path(), "scenes/alpha");

        let id = CapabilityId::new("core:annotations");
        assert_eq!(ctx.capability_state_key(&id), "scenes/alpha/core_annotations.json");

        ctx.write_capability_state(&id, "hello").unwrap();
        assert_eq!(ctx.read_capability_state(&id).unwrap(), Some("hello".to_owned()));

        ctx.delete_capability_state(&id).unwrap();
        assert_eq!(ctx.read_capability_state(&id).unwrap(), None);
    }

    #[test]
    fn backend_error_display() {
        assert_eq!(format!("{}", BackendError::NotFound), "not found");
        assert!(format!("{}", BackendError::Io("disk full".into())).contains("disk full"));
    }

    #[test]
    fn annotations_state_total_count() {
        let mut state = AnnotationsState::new();
        let scene = SceneId::new("room-1");
        assert_eq!(state.total_count(), 0);
        assert_eq!(state.for_scene(&scene), &[]);
        state.by_scene.insert(scene.clone(), vec![
            Annotation {
                id: AnnotationId::new(1),
                author: crate::types::SeatId::from_raw(1),
                created_at: TimestampMs::new(0),
                kind: AnnotationKind::Line { from: (0.0, 0.0), to: (1.0, 1.0) },
                style: AnnotationStyle::default(),
                scope: AnnotationScope::Public,
            },
        ]);
        assert_eq!(state.total_count(), 1);
    }

    #[cfg(all(test, feature = "serde"))]
    #[test]
    fn manifest_bundle_json_round_trip() {
        let bundle = ManifestBundle {
            label: Some("Theatre of the Mind".into()),
            description: None,
            manifests: vec![
                CapabilityManifest {
                    id: CapabilityId::new("core:annotations"),
                    version: SemverVersion::new(0, 1, 0),
                    scope: CapabilityScope::Canvas,
                    state: CapabilityLifecycleState::InstalledDisabled,
                    schema_hash: 0xDEAD_BEEF_CAFE_F00D,
                    settings: serde_json::json!({ "color": "#3a7ed8" }),
                },
            ],
        };
        let json = serde_json::to_string(&bundle).unwrap();
        let back: ManifestBundle = serde_json::from_str(&json).unwrap();
        assert_eq!(bundle, back);
    }

    #[cfg(feature = "wire")]
    #[test]
    fn scope_and_lifecycle_postcard_round_trip() {
        for scope in [
            CapabilityScope::Canvas,
            CapabilityScope::Session,
            CapabilityScope::Campaign,
            CapabilityScope::Installation,
            CapabilityScope::User,
        ] {
            let bytes = postcard::to_allocvec(&scope).unwrap();
            let back: CapabilityScope = postcard::from_bytes(&bytes).unwrap();
            assert_eq!(scope, back);
        }
    }
}
