//! User-content storage primitives.
//!
//! A [`LoadedMap`](crate::map::LoadedMap) is the old special case — a map
//! uploaded for one session. This module generalises: any user-hostable
//! asset (token art, portraits, icons, backgrounds, SFX, long-form audio)
//! flows through the same shape.
//!
//! The engine side ships an **interface** for asset lookup (the
//! `CatalogFixture` in `maat-engine`); the actual on-disk format is a
//! server concern and lives there. Game plugins that want to resolve "give
//! me an icon matching this item name" call into the fixture through
//! `ctx.fixtures()` — no direct filesystem access in plugins.
//!
//! Wire visibility: clients browse the library via
//! `ClientFrame::QueryAssets` → `ServerFrame::AssetList`, and upload new
//! content with `ClientFrame::UploadAsset`. Both are typed.

/// Who can see this asset in the library. Applied at pack granularity —
/// every asset in a pack shares the pack's visibility. Plugins that want
/// per-asset gating publish sub-packs with stricter scope.
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
#[non_exhaustive]
pub enum PackVisibility {
    /// Every connected player can see and request assets from this pack.
    #[default]
    Public,
    /// Only the session director sees these assets in catalog queries
    /// and can reference them in Director-authored content (maps,
    /// scenes, journal notes).
    Director,
}

impl PackVisibility {
    pub fn parse(s: &str) -> Option<Self> {
        match s.to_ascii_lowercase().as_str() {
            "public"   => Some(Self::Public),
            "director" | "gm" | "host" => Some(Self::Director),
            _          => None,
        }
    }
    pub fn as_str(self) -> &'static str {
        match self {
            Self::Public   => "public",
            Self::Director => "director",
        }
    }
}

/// Broad category for library assets. The kind determines UI placement
/// (which sidebar list shows it) and serves as a filter on catalog
/// queries.
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum AssetKind {
    /// Token art — a creature / piece / pawn image shown on the board.
    Token,
    /// Actor portrait — used in sidebar cards, chat portraits.
    Portrait,
    /// Generic icon — spells, items, abilities, inline.
    Icon,
    /// Specific item art (weapons, armour, treasure).
    Item,
    /// Full-scene backdrops — image-only, not a UVTT scene.
    Background,
    /// Short sound effects (click, hit, roll).
    Sfx,
    /// Long-form audio (music, ambience). Streamed, not bundled.
    Audio,
    /// A full scene bundle (UVTT or image+metadata). Maps live here.
    Map,
    /// Anything else. Use sparingly — specific kinds get better UI.
    Other,
}

impl AssetKind {
    /// Case-insensitive parse. Used by URL query params.
    pub fn parse(s: &str) -> Option<Self> {
        Some(match s.to_ascii_lowercase().as_str() {
            "token"       => Self::Token,
            "portrait"    => Self::Portrait,
            "icon"        => Self::Icon,
            "item"        => Self::Item,
            "background"  | "bg" => Self::Background,
            "sfx"         => Self::Sfx,
            "audio"       => Self::Audio,
            "map"         => Self::Map,
            "other"       => Self::Other,
            _             => return None,
        })
    }

    pub fn as_str(self) -> &'static str {
        match self {
            Self::Token => "token",
            Self::Portrait => "portrait",
            Self::Icon => "icon",
            Self::Item => "item",
            Self::Background => "background",
            Self::Sfx => "sfx",
            Self::Audio => "audio",
            Self::Map => "map",
            Self::Other => "other",
        }
    }
}

/// Wire-safe metadata for a library asset. The `url` is relative to the
/// server origin (`/storage/assets/tokens/goblin_01.png`); clients resolve
/// it against the WebSocket's host.
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
#[derive(Debug, Clone)]
pub struct AssetInfo {
    pub id:       u64,
    pub kind:     AssetKind,
    pub name:     String,
    pub url:      String,
    pub mime:     String,
    pub bytes:    u64,
    /// Image dimensions (width, height). `None` for audio / non-image.
    pub dims:     Option<(u32, u32)>,
    /// Duration of audio in milliseconds. `None` for images.
    pub duration_ms: Option<u32>,
    pub tags:     Vec<String>,
    /// SHA-256 of file contents. Used for dedup and (future) pack
    /// integrity checks.
    #[cfg_attr(feature = "serde", serde(with = "hex_array"))]
    pub hash:     [u8; 32],
    /// `Some(pack_id)` if installed from a signed content pack, `None` if
    /// a direct upload or bundled demo content.
    pub pack_id:  Option<String>,
    /// Audience gate inherited from the pack. Defaults to `Public` if
    /// the serialised payload omits the field (older servers, local
    /// uploads).
    #[cfg_attr(feature = "serde", serde(default))]
    pub visibility: PackVisibility,
    /// Optional subfolder organisation under the asset's kind directory:
    /// `packs/<slug>/assets/<kind>/<subfolder>/<file>`. `None` for assets
    /// stored directly under the kind directory (the legacy / default
    /// flat layout). Wire-default for older servers/serialisations is
    /// `None`.
    #[cfg_attr(feature = "serde", serde(default))]
    pub subfolder: Option<String>,
}

/// Criteria for a library query. Empty values mean "any".
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Default)]
pub struct AssetQuery {
    pub kind:    Option<AssetKind>,
    /// AND-combined: asset must carry every tag listed. Empty = no filter.
    pub tags:    Vec<String>,
    /// Free-text search against `name`. Case-insensitive substring.
    pub name_contains: Option<String>,
    /// Maximum results. Server enforces a ceiling (typically 200).
    pub limit:   u32,
}

impl AssetQuery {
    pub fn of_kind(kind: AssetKind) -> Self {
        Self { kind: Some(kind), limit: 50, ..Default::default() }
    }
}

/// Hex ser/de for the `hash` field. Postcard will happily take raw bytes;
/// we use hex for JSON compatibility when the query API is exposed via
/// REST (e.g. for uploader tooling).
#[cfg(feature = "serde")]
mod hex_array {
    use serde::{Deserialize, Deserializer, Serialize, Serializer};

    pub fn serialize<S: Serializer>(bytes: &[u8; 32], s: S) -> Result<S::Ok, S::Error> {
        let mut hex = String::with_capacity(64);
        for b in bytes {
            hex.push_str(&format!("{:02x}", b));
        }
        hex.serialize(s)
    }

    #[allow(
        clippy::indexing_slicing,
        reason = "the `hex.len() != 64` early return above guarantees \
                  hex[i*2..i*2+2] is in range for every i in 0..32"
    )]
    pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<[u8; 32], D::Error> {
        let hex = String::deserialize(d)?;
        if hex.len() != 64 {
            return Err(serde::de::Error::custom("hash must be 64 hex chars"));
        }
        let mut out = [0u8; 32];
        for i in 0..32 {
            out[i] = u8::from_str_radix(&hex[i * 2..i * 2 + 2], 16)
                .map_err(serde::de::Error::custom)?;
        }
        Ok(out)
    }
}
