//! Database models for chattyness.
//!
//! These structs mirror the database schema and are used for SQLx queries.

use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;

#[cfg(feature = "ssr")]
use chattyness_error::AppError;
#[cfg(feature = "ssr")]
use chattyness_shared::validation;

// =============================================================================
// Enums (matching PostgreSQL ENUMs)
// =============================================================================

/// Realm privacy setting.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "ssr", derive(sqlx::Type))]
#[cfg_attr(feature = "ssr", sqlx(type_name = "realm_privacy", rename_all = "lowercase"))]
#[serde(rename_all = "lowercase")]
pub enum RealmPrivacy {
    #[default]
    Public,
    Unlisted,
    Private,
}

impl std::fmt::Display for RealmPrivacy {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            RealmPrivacy::Public => write!(f, "public"),
            RealmPrivacy::Unlisted => write!(f, "unlisted"),
            RealmPrivacy::Private => write!(f, "private"),
        }
    }
}

impl std::str::FromStr for RealmPrivacy {
    type Err = String;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s.to_lowercase().as_str() {
            "public" => Ok(RealmPrivacy::Public),
            "unlisted" => Ok(RealmPrivacy::Unlisted),
            "private" => Ok(RealmPrivacy::Private),
            _ => Err(format!("Invalid privacy setting: {}", s)),
        }
    }
}

impl RealmPrivacy {
    /// Get the string representation for database storage.
    pub fn as_str(&self) -> &'static str {
        match self {
            RealmPrivacy::Public => "public",
            RealmPrivacy::Unlisted => "unlisted",
            RealmPrivacy::Private => "private",
        }
    }
}

/// Server-wide reputation tier.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "ssr", derive(sqlx::Type))]
#[cfg_attr(feature = "ssr", sqlx(type_name = "reputation_tier", rename_all = "lowercase"))]
#[serde(rename_all = "lowercase")]
pub enum ReputationTier {
    Guest,
    #[default]
    Member,
    Established,
    Trusted,
    Elder,
}

/// User account status.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "ssr", derive(sqlx::Type))]
#[cfg_attr(feature = "ssr", sqlx(type_name = "account_status", rename_all = "lowercase"))]
#[serde(rename_all = "lowercase")]
pub enum AccountStatus {
    #[default]
    Active,
    Suspended,
    Banned,
    Deleted,
}

/// Authentication provider.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "ssr", derive(sqlx::Type))]
#[cfg_attr(feature = "ssr", sqlx(type_name = "auth_provider", rename_all = "snake_case"))]
#[serde(rename_all = "snake_case")]
pub enum AuthProvider {
    #[default]
    Local,
    OauthGoogle,
    OauthDiscord,
    OauthGithub,
}

/// Server-level staff role.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "ssr", derive(sqlx::Type))]
#[cfg_attr(feature = "ssr", sqlx(type_name = "server_role", rename_all = "lowercase"))]
#[serde(rename_all = "lowercase")]
pub enum ServerRole {
    #[default]
    Moderator,
    Admin,
    Owner,
}

impl std::fmt::Display for ServerRole {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            ServerRole::Moderator => write!(f, "moderator"),
            ServerRole::Admin => write!(f, "admin"),
            ServerRole::Owner => write!(f, "owner"),
        }
    }
}

impl std::str::FromStr for ServerRole {
    type Err = String;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s.to_lowercase().as_str() {
            "moderator" => Ok(ServerRole::Moderator),
            "admin" => Ok(ServerRole::Admin),
            "owner" => Ok(ServerRole::Owner),
            _ => Err(format!("Invalid server role: {}", s)),
        }
    }
}

/// Realm membership role.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "ssr", derive(sqlx::Type))]
#[cfg_attr(feature = "ssr", sqlx(type_name = "realm_role", rename_all = "lowercase"))]
#[serde(rename_all = "lowercase")]
pub enum RealmRole {
    #[default]
    Member,
    Builder,
    Moderator,
    Owner,
}

impl std::fmt::Display for RealmRole {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            RealmRole::Member => write!(f, "member"),
            RealmRole::Builder => write!(f, "builder"),
            RealmRole::Moderator => write!(f, "moderator"),
            RealmRole::Owner => write!(f, "owner"),
        }
    }
}

impl std::str::FromStr for RealmRole {
    type Err = String;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s.to_lowercase().as_str() {
            "member" => Ok(RealmRole::Member),
            "builder" => Ok(RealmRole::Builder),
            "moderator" => Ok(RealmRole::Moderator),
            "owner" => Ok(RealmRole::Owner),
            _ => Err(format!("Invalid realm role: {}", s)),
        }
    }
}

/// Scene dimension mode.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "ssr", derive(sqlx::Type))]
#[cfg_attr(feature = "ssr", sqlx(type_name = "dimension_mode", rename_all = "lowercase"))]
#[serde(rename_all = "lowercase")]
pub enum DimensionMode {
    #[default]
    Fixed,
    Viewport,
}

impl std::fmt::Display for DimensionMode {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            DimensionMode::Fixed => write!(f, "fixed"),
            DimensionMode::Viewport => write!(f, "viewport"),
        }
    }
}

impl std::str::FromStr for DimensionMode {
    type Err = String;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s.to_lowercase().as_str() {
            "fixed" => Ok(DimensionMode::Fixed),
            "viewport" => Ok(DimensionMode::Viewport),
            _ => Err(format!("Invalid dimension mode: {}", s)),
        }
    }
}

/// Interactive spot type.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "ssr", derive(sqlx::Type))]
#[cfg_attr(feature = "ssr", sqlx(type_name = "spot_type", rename_all = "lowercase"))]
#[serde(rename_all = "lowercase")]
pub enum SpotType {
    #[default]
    Normal,
    Door,
    Trigger,
}

impl std::fmt::Display for SpotType {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            SpotType::Normal => write!(f, "normal"),
            SpotType::Door => write!(f, "door"),
            SpotType::Trigger => write!(f, "trigger"),
        }
    }
}

impl std::str::FromStr for SpotType {
    type Err = String;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s.to_lowercase().as_str() {
            "normal" => Ok(SpotType::Normal),
            "door" => Ok(SpotType::Door),
            "trigger" => Ok(SpotType::Trigger),
            _ => Err(format!("Invalid spot type: {}", s)),
        }
    }
}

/// Avatar layer for prop positioning (z-depth).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "ssr", derive(sqlx::Type))]
#[cfg_attr(feature = "ssr", sqlx(type_name = "avatar_layer", rename_all = "lowercase"))]
#[serde(rename_all = "lowercase")]
pub enum AvatarLayer {
    Skin,
    #[default]
    Clothes,
    Accessories,
}

impl std::fmt::Display for AvatarLayer {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            AvatarLayer::Skin => write!(f, "skin"),
            AvatarLayer::Clothes => write!(f, "clothes"),
            AvatarLayer::Accessories => write!(f, "accessories"),
        }
    }
}

impl std::str::FromStr for AvatarLayer {
    type Err = String;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s.to_lowercase().as_str() {
            "skin" => Ok(AvatarLayer::Skin),
            "clothes" => Ok(AvatarLayer::Clothes),
            "accessories" => Ok(AvatarLayer::Accessories),
            _ => Err(format!("Invalid avatar layer: {}", s)),
        }
    }
}

/// Emotion state for avatar emotion overlays.
///
/// Maps to emotion slots 0-11 in the avatar grid:
/// - e0: neutral, e1: happy, e2: sad, e3: angry, e4: surprised
/// - e5: thinking, e6: laughing, e7: crying, e8: love, e9: confused
/// - e10: sleeping, e11: wink
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "ssr", derive(sqlx::Type))]
#[cfg_attr(feature = "ssr", sqlx(type_name = "emotion_state", rename_all = "lowercase"))]
#[serde(rename_all = "lowercase")]
pub enum EmotionState {
    #[default]
    Neutral,
    Happy,
    Sad,
    Angry,
    Surprised,
    Thinking,
    Laughing,
    Crying,
    Love,
    Confused,
    Sleeping,
    Wink,
}

impl std::fmt::Display for EmotionState {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            EmotionState::Neutral => write!(f, "neutral"),
            EmotionState::Happy => write!(f, "happy"),
            EmotionState::Sad => write!(f, "sad"),
            EmotionState::Angry => write!(f, "angry"),
            EmotionState::Surprised => write!(f, "surprised"),
            EmotionState::Thinking => write!(f, "thinking"),
            EmotionState::Laughing => write!(f, "laughing"),
            EmotionState::Crying => write!(f, "crying"),
            EmotionState::Love => write!(f, "love"),
            EmotionState::Confused => write!(f, "confused"),
            EmotionState::Sleeping => write!(f, "sleeping"),
            EmotionState::Wink => write!(f, "wink"),
        }
    }
}

impl std::str::FromStr for EmotionState {
    type Err = String;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s.to_lowercase().as_str() {
            "neutral" => Ok(EmotionState::Neutral),
            "happy" => Ok(EmotionState::Happy),
            "sad" => Ok(EmotionState::Sad),
            "angry" => Ok(EmotionState::Angry),
            "surprised" => Ok(EmotionState::Surprised),
            "thinking" => Ok(EmotionState::Thinking),
            "laughing" => Ok(EmotionState::Laughing),
            "crying" => Ok(EmotionState::Crying),
            "love" => Ok(EmotionState::Love),
            "confused" => Ok(EmotionState::Confused),
            "sleeping" => Ok(EmotionState::Sleeping),
            "wink" => Ok(EmotionState::Wink),
            _ => Err(format!("Invalid emotion state: {}", s)),
        }
    }
}

impl EmotionState {
    /// Convert a keybinding index (0-11) to an emotion state.
    pub fn from_index(i: u8) -> Option<Self> {
        match i {
            0 => Some(Self::Neutral),
            1 => Some(Self::Happy),
            2 => Some(Self::Sad),
            3 => Some(Self::Angry),
            4 => Some(Self::Surprised),
            5 => Some(Self::Thinking),
            6 => Some(Self::Laughing),
            7 => Some(Self::Crying),
            8 => Some(Self::Love),
            9 => Some(Self::Confused),
            10 => Some(Self::Sleeping),
            11 => Some(Self::Wink),
            _ => None,
        }
    }

    /// Convert emotion state to its index (0-11).
    pub fn to_index(&self) -> u8 {
        match self {
            Self::Neutral => 0,
            Self::Happy => 1,
            Self::Sad => 2,
            Self::Angry => 3,
            Self::Surprised => 4,
            Self::Thinking => 5,
            Self::Laughing => 6,
            Self::Crying => 7,
            Self::Love => 8,
            Self::Confused => 9,
            Self::Sleeping => 10,
            Self::Wink => 11,
        }
    }
}

// =============================================================================
// User Models
// =============================================================================

/// A user account.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "ssr", derive(sqlx::FromRow))]
pub struct User {
    pub id: Uuid,
    pub username: String,
    pub email: Option<String>,
    pub display_name: String,
    pub bio: Option<String>,
    pub avatar_url: Option<String>,
    pub reputation_tier: ReputationTier,
    pub status: AccountStatus,
    pub email_verified: bool,
    pub created_at: DateTime<Utc>,
    pub updated_at: DateTime<Utc>,
}

/// Minimal user info for display purposes.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserSummary {
    pub id: Uuid,
    pub username: String,
    pub display_name: String,
    pub avatar_url: Option<String>,
}

// =============================================================================
// Realm Models
// =============================================================================

/// A realm (themed virtual space).
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "ssr", derive(sqlx::FromRow))]
pub struct Realm {
    pub id: Uuid,
    pub name: String,
    pub slug: String,
    pub description: Option<String>,
    pub tagline: Option<String>,
    pub owner_id: Uuid,
    pub privacy: RealmPrivacy,
    pub is_nsfw: bool,
    pub min_reputation_tier: ReputationTier,
    pub theme_color: Option<String>,
    pub banner_image_path: Option<String>,
    pub thumbnail_path: Option<String>,
    pub max_users: i32,
    pub allow_guest_access: bool,
    pub default_scene_id: Option<Uuid>,
    pub member_count: i32,
    pub current_user_count: i32,
    pub created_at: DateTime<Utc>,
    pub updated_at: DateTime<Utc>,
}

/// Realm with the current user's role (if authenticated and a member).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RealmWithUserRole {
    #[serde(flatten)]
    pub realm: Realm,
    /// The current user's role in this realm, if they are a member.
    pub user_role: Option<RealmRole>,
}

/// Request to create a new realm.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreateRealmRequest {
    pub name: String,
    pub slug: String,
    pub description: Option<String>,
    pub tagline: Option<String>,
    pub privacy: RealmPrivacy,
    pub is_nsfw: bool,
    pub max_users: i32,
    pub allow_guest_access: bool,
    pub theme_color: Option<String>,
}

#[cfg(feature = "ssr")]
impl CreateRealmRequest {
    /// Validate the create realm request.
    pub fn validate(&self) -> Result<(), AppError> {
        validation::validate_non_empty(&self.name, "Realm name")?;
        validation::validate_slug(&self.slug)?;
        validation::validate_range(self.max_users, "Max users", 1, 10000)?;
        validation::validate_optional_hex_color(self.theme_color.as_deref())?;
        Ok(())
    }
}

/// Minimal realm info for listings.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "ssr", derive(sqlx::FromRow))]
pub struct RealmSummary {
    pub id: Uuid,
    pub name: String,
    pub slug: String,
    pub tagline: Option<String>,
    pub privacy: RealmPrivacy,
    pub is_nsfw: bool,
    pub thumbnail_path: Option<String>,
    pub member_count: i32,
    pub current_user_count: i32,
}

/// Response after creating a realm.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreateRealmResponse {
    pub id: Uuid,
    pub slug: String,
    pub redirect_url: String,
}

// =============================================================================
// Scene Models
// =============================================================================

/// A scene (room within a realm).
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "ssr", derive(sqlx::FromRow))]
pub struct Scene {
    pub id: Uuid,
    pub realm_id: Uuid,
    pub name: String,
    pub slug: String,
    pub description: Option<String>,
    pub background_image_path: Option<String>,
    pub background_color: Option<String>,
    /// Bounds as WKT string (e.g., "POLYGON((0 0, 800 0, 800 600, 0 600, 0 0))")
    pub bounds_wkt: String,
    pub dimension_mode: DimensionMode,
    pub ambient_audio_id: Option<Uuid>,
    pub ambient_volume: Option<f32>,
    pub sort_order: i32,
    pub is_entry_point: bool,
    pub is_hidden: bool,
    pub created_at: DateTime<Utc>,
    pub updated_at: DateTime<Utc>,
    /// Default public channel ID for this scene.
    pub default_channel_id: Option<Uuid>,
}

/// Minimal scene info for listings.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "ssr", derive(sqlx::FromRow))]
pub struct SceneSummary {
    pub id: Uuid,
    pub name: String,
    pub slug: String,
    pub sort_order: i32,
    pub is_entry_point: bool,
    pub is_hidden: bool,
    pub background_color: Option<String>,
    pub background_image_path: Option<String>,
}

/// A spot (interactive region within a scene).
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "ssr", derive(sqlx::FromRow))]
pub struct Spot {
    pub id: Uuid,
    pub scene_id: Uuid,
    pub name: Option<String>,
    pub slug: Option<String>,
    /// Region as WKT string (e.g., "POLYGON((100 100, 200 100, 200 200, 100 200, 100 100))")
    pub region_wkt: String,
    pub spot_type: SpotType,
    pub destination_scene_id: Option<Uuid>,
    /// Destination position as WKT string (e.g., "POINT(400 300)")
    pub destination_position_wkt: Option<String>,
    pub current_state: i16,
    pub sort_order: i32,
    pub is_visible: bool,
    pub is_active: bool,
    pub created_at: DateTime<Utc>,
    pub updated_at: DateTime<Utc>,
}

/// Minimal spot info for listings.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "ssr", derive(sqlx::FromRow))]
pub struct SpotSummary {
    pub id: Uuid,
    pub name: Option<String>,
    pub slug: Option<String>,
    pub spot_type: SpotType,
    pub region_wkt: String,
    pub sort_order: i32,
    pub is_visible: bool,
    pub is_active: bool,
}

// =============================================================================
// Props Models
// =============================================================================

/// Origin source for a prop in inventory.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "ssr", derive(sqlx::Type))]
#[cfg_attr(feature = "ssr", sqlx(type_name = "prop_origin", rename_all = "snake_case"))]
#[serde(rename_all = "snake_case")]
pub enum PropOrigin {
    #[default]
    ServerLibrary,
    RealmLibrary,
    UserUpload,
}

impl std::fmt::Display for PropOrigin {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            PropOrigin::ServerLibrary => write!(f, "server_library"),
            PropOrigin::RealmLibrary => write!(f, "realm_library"),
            PropOrigin::UserUpload => write!(f, "user_upload"),
        }
    }
}

/// An inventory item (user-owned prop).
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "ssr", derive(sqlx::FromRow))]
pub struct InventoryItem {
    pub id: Uuid,
    pub prop_name: String,
    pub prop_asset_path: String,
    pub layer: Option<AvatarLayer>,
    pub is_transferable: bool,
    pub is_portable: bool,
    pub origin: PropOrigin,
    pub acquired_at: DateTime<Utc>,
}

/// Response for inventory list.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InventoryResponse {
    pub items: Vec<InventoryItem>,
}

/// A prop dropped in a channel, available for pickup.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "ssr", derive(sqlx::FromRow))]
pub struct LooseProp {
    pub id: Uuid,
    pub channel_id: Uuid,
    pub server_prop_id: Option<Uuid>,
    pub realm_prop_id: Option<Uuid>,
    pub position_x: f64,
    pub position_y: f64,
    pub dropped_by: Option<Uuid>,
    pub expires_at: Option<DateTime<Utc>>,
    pub created_at: DateTime<Utc>,
    /// Prop name (JOINed from source prop).
    pub prop_name: String,
    /// Asset path for rendering (JOINed from source prop).
    pub prop_asset_path: String,
}

/// A server-wide prop (global library).
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "ssr", derive(sqlx::FromRow))]
pub struct ServerProp {
    pub id: Uuid,
    pub name: String,
    pub slug: String,
    pub description: Option<String>,
    pub tags: Vec<String>,
    pub asset_path: String,
    pub thumbnail_path: Option<String>,
    /// Default content layer (skin/clothes/accessories). Mutually exclusive with default_emotion.
    pub default_layer: Option<AvatarLayer>,
    /// Default emotion layer (neutral/happy/sad/etc). Mutually exclusive with default_layer.
    pub default_emotion: Option<EmotionState>,
    /// Grid position (0-8): top row 0,1,2 / middle 3,4,5 / bottom 6,7,8
    pub default_position: Option<i16>,
    pub is_unique: bool,
    pub is_transferable: bool,
    pub is_portable: bool,
    pub is_active: bool,
    pub available_from: Option<DateTime<Utc>>,
    pub available_until: Option<DateTime<Utc>>,
    pub created_by: Option<Uuid>,
    pub created_at: DateTime<Utc>,
    pub updated_at: DateTime<Utc>,
}

/// Minimal server prop info for listings.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "ssr", derive(sqlx::FromRow))]
pub struct ServerPropSummary {
    pub id: Uuid,
    pub name: String,
    pub slug: String,
    pub asset_path: String,
    pub default_layer: Option<AvatarLayer>,
    pub is_active: bool,
    pub created_at: DateTime<Utc>,
}

/// Request to create a server prop.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreateServerPropRequest {
    pub name: String,
    #[serde(default)]
    pub slug: Option<String>,
    #[serde(default)]
    pub description: Option<String>,
    #[serde(default)]
    pub tags: Vec<String>,
    /// Default content layer (skin/clothes/accessories). Mutually exclusive with default_emotion.
    #[serde(default)]
    pub default_layer: Option<AvatarLayer>,
    /// Default emotion layer (neutral/happy/sad/etc). Mutually exclusive with default_layer.
    #[serde(default)]
    pub default_emotion: Option<EmotionState>,
    /// Grid position (0-8): top row 0,1,2 / middle 3,4,5 / bottom 6,7,8
    #[serde(default)]
    pub default_position: Option<i16>,
}

#[cfg(feature = "ssr")]
impl CreateServerPropRequest {
    /// Validate the create prop request.
    pub fn validate(&self) -> Result<(), AppError> {
        validation::validate_non_empty(&self.name, "Prop name")?;
        if let Some(ref slug) = self.slug {
            validation::validate_slug(slug)?;
        }
        // Validate grid position is 0-8
        if let Some(pos) = self.default_position {
            if !(0..=8).contains(&pos) {
                return Err(AppError::Validation(
                    "default_position must be between 0 and 8".to_string(),
                ));
            }
        }
        // Validate mutual exclusivity: can't have both default_layer and default_emotion
        if self.default_layer.is_some() && self.default_emotion.is_some() {
            return Err(AppError::Validation(
                "Cannot specify both default_layer and default_emotion - they are mutually exclusive".to_string(),
            ));
        }
        // If either layer or emotion is set, position must also be set
        if (self.default_layer.is_some() || self.default_emotion.is_some())
            && self.default_position.is_none()
        {
            return Err(AppError::Validation(
                "default_position is required when default_layer or default_emotion is set".to_string(),
            ));
        }
        Ok(())
    }

    /// Generate a slug from the name if not provided.
    pub fn slug_or_generate(&self) -> String {
        self.slug.clone().unwrap_or_else(|| {
            self.name
                .to_lowercase()
                .chars()
                .map(|c| if c.is_alphanumeric() { c } else { '-' })
                .collect::<String>()
                .trim_matches('-')
                .to_string()
        })
    }
}

/// A saved avatar configuration (up to 10 per user).
///
/// Contains 117 prop slot references:
/// - 27 content layer slots (3 layers × 9 positions)
/// - 90 emotion layer slots (10 emotions × 9 positions)
///
/// Grid positions (0-8):
/// ```text
/// ┌───┬───┬───┐
/// │ 0 │ 1 │ 2 │  top row
/// ├───┼───┼───┤
/// │ 3 │ 4 │ 5 │  middle row
/// ├───┼───┼───┤
/// │ 6 │ 7 │ 8 │  bottom row
/// └───┴───┴───┘
/// ```
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "ssr", derive(sqlx::FromRow))]
pub struct Avatar {
    pub id: Uuid,
    pub user_id: Uuid,
    pub name: String,
    /// Slot number (0-9, keyboard: a0-a9)
    pub slot_number: i16,
    /// Last used emotion slot (0-9, NULL if none)
    pub last_emotion: Option<i16>,

    // Content Layer: Skin (behind user, body/face)
    pub l_skin_0: Option<Uuid>,
    pub l_skin_1: Option<Uuid>,
    pub l_skin_2: Option<Uuid>,
    pub l_skin_3: Option<Uuid>,
    pub l_skin_4: Option<Uuid>,
    pub l_skin_5: Option<Uuid>,
    pub l_skin_6: Option<Uuid>,
    pub l_skin_7: Option<Uuid>,
    pub l_skin_8: Option<Uuid>,

    // Content Layer: Clothes (with user, worn items)
    pub l_clothes_0: Option<Uuid>,
    pub l_clothes_1: Option<Uuid>,
    pub l_clothes_2: Option<Uuid>,
    pub l_clothes_3: Option<Uuid>,
    pub l_clothes_4: Option<Uuid>,
    pub l_clothes_5: Option<Uuid>,
    pub l_clothes_6: Option<Uuid>,
    pub l_clothes_7: Option<Uuid>,
    pub l_clothes_8: Option<Uuid>,

    // Content Layer: Accessories (in front of user, held/attached items)
    pub l_accessories_0: Option<Uuid>,
    pub l_accessories_1: Option<Uuid>,
    pub l_accessories_2: Option<Uuid>,
    pub l_accessories_3: Option<Uuid>,
    pub l_accessories_4: Option<Uuid>,
    pub l_accessories_5: Option<Uuid>,
    pub l_accessories_6: Option<Uuid>,
    pub l_accessories_7: Option<Uuid>,
    pub l_accessories_8: Option<Uuid>,

    // Emotion: Neutral (e0)
    pub e_neutral_0: Option<Uuid>,
    pub e_neutral_1: Option<Uuid>,
    pub e_neutral_2: Option<Uuid>,
    pub e_neutral_3: Option<Uuid>,
    pub e_neutral_4: Option<Uuid>,
    pub e_neutral_5: Option<Uuid>,
    pub e_neutral_6: Option<Uuid>,
    pub e_neutral_7: Option<Uuid>,
    pub e_neutral_8: Option<Uuid>,

    // Emotion: Happy (e1)
    pub e_happy_0: Option<Uuid>,
    pub e_happy_1: Option<Uuid>,
    pub e_happy_2: Option<Uuid>,
    pub e_happy_3: Option<Uuid>,
    pub e_happy_4: Option<Uuid>,
    pub e_happy_5: Option<Uuid>,
    pub e_happy_6: Option<Uuid>,
    pub e_happy_7: Option<Uuid>,
    pub e_happy_8: Option<Uuid>,

    // Emotion: Sad (e2)
    pub e_sad_0: Option<Uuid>,
    pub e_sad_1: Option<Uuid>,
    pub e_sad_2: Option<Uuid>,
    pub e_sad_3: Option<Uuid>,
    pub e_sad_4: Option<Uuid>,
    pub e_sad_5: Option<Uuid>,
    pub e_sad_6: Option<Uuid>,
    pub e_sad_7: Option<Uuid>,
    pub e_sad_8: Option<Uuid>,

    // Emotion: Angry (e3)
    pub e_angry_0: Option<Uuid>,
    pub e_angry_1: Option<Uuid>,
    pub e_angry_2: Option<Uuid>,
    pub e_angry_3: Option<Uuid>,
    pub e_angry_4: Option<Uuid>,
    pub e_angry_5: Option<Uuid>,
    pub e_angry_6: Option<Uuid>,
    pub e_angry_7: Option<Uuid>,
    pub e_angry_8: Option<Uuid>,

    // Emotion: Surprised (e4)
    pub e_surprised_0: Option<Uuid>,
    pub e_surprised_1: Option<Uuid>,
    pub e_surprised_2: Option<Uuid>,
    pub e_surprised_3: Option<Uuid>,
    pub e_surprised_4: Option<Uuid>,
    pub e_surprised_5: Option<Uuid>,
    pub e_surprised_6: Option<Uuid>,
    pub e_surprised_7: Option<Uuid>,
    pub e_surprised_8: Option<Uuid>,

    // Emotion: Thinking (e5)
    pub e_thinking_0: Option<Uuid>,
    pub e_thinking_1: Option<Uuid>,
    pub e_thinking_2: Option<Uuid>,
    pub e_thinking_3: Option<Uuid>,
    pub e_thinking_4: Option<Uuid>,
    pub e_thinking_5: Option<Uuid>,
    pub e_thinking_6: Option<Uuid>,
    pub e_thinking_7: Option<Uuid>,
    pub e_thinking_8: Option<Uuid>,

    // Emotion: Laughing (e6)
    pub e_laughing_0: Option<Uuid>,
    pub e_laughing_1: Option<Uuid>,
    pub e_laughing_2: Option<Uuid>,
    pub e_laughing_3: Option<Uuid>,
    pub e_laughing_4: Option<Uuid>,
    pub e_laughing_5: Option<Uuid>,
    pub e_laughing_6: Option<Uuid>,
    pub e_laughing_7: Option<Uuid>,
    pub e_laughing_8: Option<Uuid>,

    // Emotion: Crying (e7)
    pub e_crying_0: Option<Uuid>,
    pub e_crying_1: Option<Uuid>,
    pub e_crying_2: Option<Uuid>,
    pub e_crying_3: Option<Uuid>,
    pub e_crying_4: Option<Uuid>,
    pub e_crying_5: Option<Uuid>,
    pub e_crying_6: Option<Uuid>,
    pub e_crying_7: Option<Uuid>,
    pub e_crying_8: Option<Uuid>,

    // Emotion: Love (e8)
    pub e_love_0: Option<Uuid>,
    pub e_love_1: Option<Uuid>,
    pub e_love_2: Option<Uuid>,
    pub e_love_3: Option<Uuid>,
    pub e_love_4: Option<Uuid>,
    pub e_love_5: Option<Uuid>,
    pub e_love_6: Option<Uuid>,
    pub e_love_7: Option<Uuid>,
    pub e_love_8: Option<Uuid>,

    // Emotion: Confused (e9)
    pub e_confused_0: Option<Uuid>,
    pub e_confused_1: Option<Uuid>,
    pub e_confused_2: Option<Uuid>,
    pub e_confused_3: Option<Uuid>,
    pub e_confused_4: Option<Uuid>,
    pub e_confused_5: Option<Uuid>,
    pub e_confused_6: Option<Uuid>,
    pub e_confused_7: Option<Uuid>,
    pub e_confused_8: Option<Uuid>,

    // Emotion: Sleeping (e10)
    pub e_sleeping_0: Option<Uuid>,
    pub e_sleeping_1: Option<Uuid>,
    pub e_sleeping_2: Option<Uuid>,
    pub e_sleeping_3: Option<Uuid>,
    pub e_sleeping_4: Option<Uuid>,
    pub e_sleeping_5: Option<Uuid>,
    pub e_sleeping_6: Option<Uuid>,
    pub e_sleeping_7: Option<Uuid>,
    pub e_sleeping_8: Option<Uuid>,

    // Emotion: Wink (e11)
    pub e_wink_0: Option<Uuid>,
    pub e_wink_1: Option<Uuid>,
    pub e_wink_2: Option<Uuid>,
    pub e_wink_3: Option<Uuid>,
    pub e_wink_4: Option<Uuid>,
    pub e_wink_5: Option<Uuid>,
    pub e_wink_6: Option<Uuid>,
    pub e_wink_7: Option<Uuid>,
    pub e_wink_8: Option<Uuid>,

    pub created_at: DateTime<Utc>,
    pub updated_at: DateTime<Utc>,
}

/// Currently active avatar for a user in a realm.
/// Users can have different active avatars per realm.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "ssr", derive(sqlx::FromRow))]
pub struct ActiveAvatar {
    pub user_id: Uuid,
    pub realm_id: Uuid,
    pub avatar_id: Uuid,
    /// Current emotion slot (0-9, keyboard: e0-e9)
    pub current_emotion: i16,
    pub updated_at: DateTime<Utc>,
}

// =============================================================================
// Server Config Models
// =============================================================================

/// Server-wide configuration (singleton).
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "ssr", derive(sqlx::FromRow))]
pub struct ServerConfig {
    pub id: Uuid,
    pub name: String,
    pub description: Option<String>,
    pub welcome_message: Option<String>,
    pub max_users_per_channel: i32,
    pub message_rate_limit: i32,
    pub message_rate_window_seconds: i32,
    pub allow_guest_access: bool,
    pub allow_user_uploads: bool,
    pub require_email_verification: bool,
    pub created_at: DateTime<Utc>,
    pub updated_at: DateTime<Utc>,
}

/// Request to update server configuration.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UpdateServerConfigRequest {
    pub name: String,
    pub description: Option<String>,
    pub welcome_message: Option<String>,
    pub max_users_per_channel: i32,
    pub message_rate_limit: i32,
    pub message_rate_window_seconds: i32,
    pub allow_guest_access: bool,
    pub allow_user_uploads: bool,
    pub require_email_verification: bool,
}

#[cfg(feature = "ssr")]
impl UpdateServerConfigRequest {
    /// Validate the update request.
    pub fn validate(&self) -> Result<(), AppError> {
        validation::validate_non_empty(&self.name, "Server name")?;
        validation::validate_range(self.max_users_per_channel, "Max users per channel", 1, 1000)?;
        validation::validate_range(self.message_rate_limit, "Message rate limit", 1, i32::MAX)?;
        validation::validate_range(
            self.message_rate_window_seconds,
            "Message rate window",
            1,
            i32::MAX,
        )?;
        Ok(())
    }
}

// =============================================================================
// Staff Models
// =============================================================================

/// A server staff member (joined with user info).
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "ssr", derive(sqlx::FromRow))]
pub struct StaffMember {
    pub user_id: Uuid,
    pub username: String,
    pub display_name: String,
    pub email: Option<String>,
    pub role: ServerRole,
    pub appointed_by: Option<Uuid>,
    pub appointed_at: DateTime<Utc>,
}

// =============================================================================
// User Management Models
// =============================================================================

/// User listing item (for tables).
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "ssr", derive(sqlx::FromRow))]
pub struct UserListItem {
    pub id: Uuid,
    pub username: String,
    pub display_name: String,
    pub email: Option<String>,
    pub status: AccountStatus,
    pub reputation_tier: ReputationTier,
    pub staff_role: Option<ServerRole>,
    pub created_at: DateTime<Utc>,
    pub last_seen_at: Option<DateTime<Utc>>,
}

/// Full user detail (for user detail page).
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "ssr", derive(sqlx::FromRow))]
pub struct UserDetail {
    pub id: Uuid,
    pub username: String,
    pub email: Option<String>,
    pub display_name: String,
    pub bio: Option<String>,
    pub avatar_url: Option<String>,
    pub reputation_tier: ReputationTier,
    pub status: AccountStatus,
    pub email_verified: bool,
    pub staff_role: Option<ServerRole>,
    pub created_at: DateTime<Utc>,
    pub updated_at: DateTime<Utc>,
    pub last_seen_at: Option<DateTime<Utc>>,
}

/// User's realm membership (for user detail page).
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "ssr", derive(sqlx::FromRow))]
pub struct UserRealmMembership {
    pub realm_id: Uuid,
    pub realm_name: String,
    pub realm_slug: String,
    pub role: RealmRole,
    pub nickname: Option<String>,
    pub joined_at: DateTime<Utc>,
    pub last_visited_at: Option<DateTime<Utc>>,
}

// =============================================================================
// Staff Request Models
// =============================================================================

/// Request to create a staff member.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreateStaffRequest {
    /// Existing user ID to promote to staff.
    pub user_id: Option<Uuid>,
    /// Or create a new user.
    pub new_user: Option<NewUserData>,
    /// Role to assign.
    pub role: ServerRole,
}

#[cfg(feature = "ssr")]
impl CreateStaffRequest {
    /// Validate the create staff request.
    pub fn validate(&self) -> Result<(), AppError> {
        // Must have either user_id or new_user, not both
        match (&self.user_id, &self.new_user) {
            (None, None) => {
                return Err(AppError::Validation(
                    "Must provide either user_id or new_user".to_string(),
                ));
            }
            (Some(_), Some(_)) => {
                return Err(AppError::Validation(
                    "Cannot provide both user_id and new_user".to_string(),
                ));
            }
            _ => {}
        }

        // Validate new_user if provided
        if let Some(new_user) = &self.new_user {
            new_user.validate()?;
        }

        Ok(())
    }
}

/// Data for creating a new user (password is auto-generated).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NewUserData {
    pub username: String,
    pub email: String,
    pub display_name: String,
}

#[cfg(feature = "ssr")]
impl NewUserData {
    /// Validate new user data.
    pub fn validate(&self) -> Result<(), AppError> {
        validation::validate_non_empty(&self.username, "Username")?;
        validation::validate_length(&self.username, "Username", 3, 32)?;
        validation::validate_email(&self.email)?;
        validation::validate_non_empty(&self.display_name, "Display name")?;
        Ok(())
    }
}

/// Request to update a user's account status.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UpdateUserStatusRequest {
    pub status: AccountStatus,
}

/// Request to add a user to a realm.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AddUserToRealmRequest {
    pub realm_id: Uuid,
    pub role: RealmRole,
}

/// Request to create a standalone user (from owner interface).
/// Password is auto-generated as a random token.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreateUserRequest {
    pub username: String,
    pub email: String,
    pub display_name: String,
    /// Optional: make this user a staff member.
    pub staff_role: Option<ServerRole>,
}

#[cfg(feature = "ssr")]
impl CreateUserRequest {
    /// Validate the create user request.
    pub fn validate(&self) -> Result<(), AppError> {
        validation::validate_non_empty(&self.username, "Username")?;
        validation::validate_length(&self.username, "Username", 3, 32)?;
        validation::validate_email(&self.email)?;
        validation::validate_non_empty(&self.display_name, "Display name")?;
        Ok(())
    }
}

/// Request to create a realm from the owner interface.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OwnerCreateRealmRequest {
    /// Realm name.
    pub name: String,
    /// Realm slug (URL-friendly identifier).
    pub slug: String,
    /// Optional description.
    pub description: Option<String>,
    /// Optional tagline.
    pub tagline: Option<String>,
    /// Privacy setting.
    pub privacy: RealmPrivacy,
    /// Is this realm NSFW?
    pub is_nsfw: bool,
    /// Maximum concurrent users.
    pub max_users: i32,
    /// Allow guest access?
    pub allow_guest_access: bool,
    /// Optional theme color.
    pub theme_color: Option<String>,
    /// Existing user ID to make owner.
    pub owner_id: Option<Uuid>,
    /// Or create a new user as owner.
    pub new_owner: Option<NewUserData>,
}

#[cfg(feature = "ssr")]
impl OwnerCreateRealmRequest {
    /// Validate the create realm request.
    pub fn validate(&self) -> Result<(), AppError> {
        validation::validate_non_empty(&self.name, "Realm name")?;
        validation::validate_slug(&self.slug)?;
        validation::validate_range(self.max_users, "Max users", 1, 10000)?;
        validation::validate_optional_hex_color(self.theme_color.as_deref())?;

        // Must have either owner_id or new_owner
        match (&self.owner_id, &self.new_owner) {
            (None, None) => {
                return Err(AppError::Validation(
                    "Must provide either owner_id or new_owner".to_string(),
                ));
            }
            (Some(_), Some(_)) => {
                return Err(AppError::Validation(
                    "Cannot provide both owner_id and new_owner".to_string(),
                ));
            }
            (None, Some(new_owner)) => {
                new_owner.validate()?;
            }
            _ => {}
        }

        Ok(())
    }
}

/// Realm listing item for owner interface.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "ssr", derive(sqlx::FromRow))]
pub struct RealmListItem {
    pub id: Uuid,
    pub name: String,
    pub slug: String,
    pub tagline: Option<String>,
    pub privacy: RealmPrivacy,
    pub is_nsfw: bool,
    pub owner_id: Uuid,
    pub owner_username: String,
    pub member_count: i32,
    pub current_user_count: i32,
    pub created_at: DateTime<Utc>,
}

/// Full realm detail for owner interface.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "ssr", derive(sqlx::FromRow))]
pub struct RealmDetail {
    pub id: Uuid,
    pub name: String,
    pub slug: String,
    pub description: Option<String>,
    pub tagline: Option<String>,
    pub owner_id: Uuid,
    pub owner_username: String,
    pub owner_display_name: String,
    pub privacy: RealmPrivacy,
    pub is_nsfw: bool,
    pub min_reputation_tier: ReputationTier,
    pub theme_color: Option<String>,
    pub banner_image_path: Option<String>,
    pub thumbnail_path: Option<String>,
    pub max_users: i32,
    pub allow_guest_access: bool,
    pub member_count: i32,
    pub current_user_count: i32,
    pub created_at: DateTime<Utc>,
    pub updated_at: DateTime<Utc>,
}

/// Request to update a realm.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UpdateRealmRequest {
    pub name: String,
    pub description: Option<String>,
    pub tagline: Option<String>,
    pub privacy: RealmPrivacy,
    pub is_nsfw: bool,
    pub max_users: i32,
    pub allow_guest_access: bool,
    pub theme_color: Option<String>,
}

#[cfg(feature = "ssr")]
impl UpdateRealmRequest {
    /// Validate the update realm request.
    pub fn validate(&self) -> Result<(), AppError> {
        validation::validate_non_empty(&self.name, "Realm name")?;
        validation::validate_range(self.max_users, "Max users", 1, 10000)?;
        validation::validate_optional_hex_color(self.theme_color.as_deref())?;
        Ok(())
    }
}

// =============================================================================
// Authentication Models
// =============================================================================

/// Login type for authentication.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum LoginType {
    Staff,
    Realm,
}

/// User with authentication fields for login verification.
#[derive(Debug, Clone)]
#[cfg_attr(feature = "ssr", derive(sqlx::FromRow))]
pub struct UserWithAuth {
    pub id: Uuid,
    pub username: String,
    pub email: Option<String>,
    pub display_name: String,
    pub avatar_url: Option<String>,
    pub status: AccountStatus,
    pub force_pw_reset: bool,
    pub password_hash: Option<String>,
}

/// Request to login.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LoginRequest {
    pub username: String,
    pub password: String,
    pub login_type: LoginType,
    /// Required if login_type is Realm.
    pub realm_slug: Option<String>,
}

#[cfg(feature = "ssr")]
impl LoginRequest {
    /// Validate the login request.
    pub fn validate(&self) -> Result<(), AppError> {
        validation::validate_non_empty(&self.username, "Username")?;
        validation::validate_non_empty(&self.password, "Password")?;
        if self.login_type == LoginType::Realm && self.realm_slug.is_none() {
            return Err(AppError::Validation(
                "Realm slug is required for realm login".to_string(),
            ));
        }
        Ok(())
    }
}

/// Response after successful login.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LoginResponse {
    pub user: UserSummary,
    pub redirect_url: String,
    pub requires_pw_reset: bool,
    /// For realm login: whether user is already a member.
    pub is_member: Option<bool>,
    /// Original destination for redirect after password reset.
    pub original_destination: Option<String>,
    /// Staff role if logging in as staff.
    pub staff_role: Option<ServerRole>,
    /// Realm info if logging into a realm (for join confirmation).
    pub realm: Option<RealmSummary>,
}

/// Request to reset password.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PasswordResetRequest {
    pub new_password: String,
    pub confirm_password: String,
}

#[cfg(feature = "ssr")]
impl PasswordResetRequest {
    /// Validate the password reset request.
    pub fn validate(&self) -> Result<(), AppError> {
        validation::validate_password(&self.new_password)?;
        validation::validate_passwords_match(&self.new_password, &self.confirm_password)?;
        Ok(())
    }
}

/// Response after password reset.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PasswordResetResponse {
    pub success: bool,
    pub redirect_url: String,
}

/// Request to sign up (create a new account and join a realm).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SignupRequest {
    pub username: String,
    pub email: Option<String>,
    pub display_name: String,
    pub password: String,
    pub confirm_password: String,
    pub realm_slug: String,
}

#[cfg(feature = "ssr")]
impl SignupRequest {
    /// Validate the signup request.
    pub fn validate(&self) -> Result<(), AppError> {
        validation::validate_username(&self.username)?;
        validation::validate_non_empty(&self.display_name, "Display name")?;
        validation::validate_length(self.display_name.trim(), "Display name", 1, 50)?;

        // Email: basic format if provided and non-empty
        if let Some(ref email) = self.email {
            let email_trimmed = email.trim();
            if !email_trimmed.is_empty() && !validation::is_valid_email(email_trimmed) {
                return Err(AppError::Validation("Invalid email address".to_string()));
            }
        }

        validation::validate_password(&self.password)?;
        validation::validate_passwords_match(&self.password, &self.confirm_password)?;

        // Realm slug: required
        if self.realm_slug.trim().is_empty() {
            return Err(AppError::Validation(
                "Please select a realm to join".to_string(),
            ));
        }

        Ok(())
    }
}

/// Response after successful signup.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SignupResponse {
    pub user: UserSummary,
    pub redirect_url: String,
    pub membership_id: Uuid,
}

/// Request to login as a guest.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GuestLoginRequest {
    pub realm_slug: String,
}

#[cfg(feature = "ssr")]
impl GuestLoginRequest {
    /// Validate the guest login request.
    pub fn validate(&self) -> Result<(), AppError> {
        validation::validate_non_empty(&self.realm_slug, "Realm")?;
        Ok(())
    }
}

/// Response after successful guest login.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GuestLoginResponse {
    pub guest_name: String,
    pub guest_id: Uuid,
    pub redirect_url: String,
    pub realm: RealmSummary,
}

/// Request to join a realm.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JoinRealmRequest {
    pub realm_id: Uuid,
}

/// Response after joining a realm.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JoinRealmResponse {
    pub success: bool,
    pub membership_id: Uuid,
    pub redirect_url: String,
}

/// Current user info (for /api/auth/me endpoint).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CurrentUserResponse {
    pub user: Option<AuthenticatedUser>,
}

/// Authenticated user info.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuthenticatedUser {
    pub id: Uuid,
    pub username: String,
    pub display_name: String,
    pub avatar_url: Option<String>,
    pub staff_role: Option<ServerRole>,
}

// =============================================================================
// Membership Models
// =============================================================================

/// A realm membership record.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "ssr", derive(sqlx::FromRow))]
pub struct Membership {
    pub id: Uuid,
    pub realm_id: Uuid,
    pub user_id: Uuid,
    pub role: RealmRole,
    pub nickname: Option<String>,
    pub joined_at: DateTime<Utc>,
    pub last_visited_at: Option<DateTime<Utc>>,
}

/// Membership with realm info for user's realm list.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "ssr", derive(sqlx::FromRow))]
pub struct MembershipWithRealm {
    pub membership_id: Uuid,
    pub realm_id: Uuid,
    pub realm_name: String,
    pub realm_slug: String,
    pub realm_privacy: RealmPrivacy,
    pub role: RealmRole,
    pub nickname: Option<String>,
    pub last_visited_at: Option<DateTime<Utc>>,
}

// =============================================================================
// Scene Request/Response Models
// =============================================================================

/// Request to create a new scene.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreateSceneRequest {
    pub name: String,
    pub slug: String,
    pub description: Option<String>,
    /// URL to download background image from. Server stores locally and sets background_image_path.
    pub background_image_url: Option<String>,
    /// If true and background_image_url is provided, extract dimensions from the image.
    #[serde(default)]
    pub infer_dimensions_from_image: bool,
    /// Set directly only if not using background_image_url.
    pub background_image_path: Option<String>,
    pub background_color: Option<String>,
    /// Bounds as WKT string (e.g., "POLYGON((0 0, 800 0, 800 600, 0 600, 0 0))")
    pub bounds_wkt: Option<String>,
    pub dimension_mode: Option<DimensionMode>,
    pub sort_order: Option<i32>,
    pub is_entry_point: Option<bool>,
    pub is_hidden: Option<bool>,
}

#[cfg(feature = "ssr")]
impl CreateSceneRequest {
    /// Validate the create scene request.
    pub fn validate(&self) -> Result<(), AppError> {
        validation::validate_non_empty(&self.name, "Scene name")?;
        validation::validate_slug(&self.slug)?;
        validation::validate_optional_hex_color(self.background_color.as_deref())?;
        Ok(())
    }
}

/// Request to update a scene.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UpdateSceneRequest {
    pub name: Option<String>,
    pub description: Option<String>,
    /// URL to download background image from. Server stores locally and sets background_image_path.
    pub background_image_url: Option<String>,
    /// If true and background_image_url is provided, extract dimensions from the image.
    #[serde(default)]
    pub infer_dimensions_from_image: bool,
    /// Set to true to clear the existing background image.
    #[serde(default)]
    pub clear_background_image: bool,
    /// Set directly only if not using background_image_url.
    pub background_image_path: Option<String>,
    pub background_color: Option<String>,
    pub bounds_wkt: Option<String>,
    pub dimension_mode: Option<DimensionMode>,
    pub sort_order: Option<i32>,
    pub is_entry_point: Option<bool>,
    pub is_hidden: Option<bool>,
}

#[cfg(feature = "ssr")]
impl UpdateSceneRequest {
    /// Validate the update scene request.
    pub fn validate(&self) -> Result<(), AppError> {
        // Validate name if provided
        if let Some(ref name) = self.name {
            validation::validate_non_empty(name, "Scene name")?;
        }

        validation::validate_optional_hex_color(self.background_color.as_deref())?;

        Ok(())
    }
}

/// Response after creating a scene.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreateSceneResponse {
    pub id: Uuid,
    pub slug: String,
}

/// Response for scene list.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SceneListResponse {
    pub scenes: Vec<SceneSummary>,
}

// =============================================================================
// Spot Request/Response Models
// =============================================================================

/// Request to create a new spot.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreateSpotRequest {
    pub name: Option<String>,
    pub slug: Option<String>,
    /// Region as WKT string (e.g., "POLYGON((100 100, 200 100, 200 200, 100 200, 100 100))")
    pub region_wkt: String,
    pub spot_type: Option<SpotType>,
    pub destination_scene_id: Option<Uuid>,
    /// Destination position as WKT string (e.g., "POINT(400 300)")
    pub destination_position_wkt: Option<String>,
    pub sort_order: Option<i32>,
    pub is_visible: Option<bool>,
    pub is_active: Option<bool>,
}

#[cfg(feature = "ssr")]
impl CreateSpotRequest {
    /// Validate the create spot request.
    pub fn validate(&self) -> Result<(), AppError> {
        // Validate slug if provided
        if let Some(ref slug) = self.slug {
            validation::validate_slug(slug)?;
        }

        validation::validate_non_empty(&self.region_wkt, "Region WKT")?;

        // Validate door type has destination
        if self.spot_type == Some(SpotType::Door) && self.destination_scene_id.is_none() {
            return Err(AppError::Validation(
                "Door spots must have a destination scene".to_string(),
            ));
        }

        Ok(())
    }
}

/// Request to update a spot.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UpdateSpotRequest {
    pub name: Option<String>,
    pub slug: Option<String>,
    pub region_wkt: Option<String>,
    pub spot_type: Option<SpotType>,
    pub destination_scene_id: Option<Uuid>,
    pub destination_position_wkt: Option<String>,
    pub current_state: Option<i16>,
    pub sort_order: Option<i32>,
    pub is_visible: Option<bool>,
    pub is_active: Option<bool>,
}

#[cfg(feature = "ssr")]
impl UpdateSpotRequest {
    /// Validate the update spot request.
    pub fn validate(&self) -> Result<(), AppError> {
        // Validate slug if provided
        if let Some(ref slug) = self.slug {
            validation::validate_slug(slug)?;
        }

        // Validate region_wkt if provided
        if let Some(ref wkt) = self.region_wkt {
            validation::validate_non_empty(wkt, "Region WKT")?;
        }

        Ok(())
    }
}

/// Response after creating a spot.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreateSpotResponse {
    pub id: Uuid,
}

/// Response for spot list.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SpotListResponse {
    pub spots: Vec<SpotSummary>,
}

// =============================================================================
// Channel Member Models
// =============================================================================

/// A user's presence in a channel.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "ssr", derive(sqlx::FromRow))]
pub struct ChannelMember {
    pub id: Uuid,
    pub channel_id: Uuid,
    pub user_id: Option<Uuid>,
    pub guest_session_id: Option<Uuid>,
    /// X coordinate in scene space
    pub position_x: f64,
    /// Y coordinate in scene space
    pub position_y: f64,
    /// Facing direction in degrees (0-359)
    pub facing_direction: i16,
    pub is_moving: bool,
    pub is_afk: bool,
    pub joined_at: DateTime<Utc>,
    pub last_moved_at: DateTime<Utc>,
}

/// Channel member with user info for display.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "ssr", derive(sqlx::FromRow))]
pub struct ChannelMemberInfo {
    pub id: Uuid,
    pub channel_id: Uuid,
    pub user_id: Option<Uuid>,
    pub guest_session_id: Option<Uuid>,
    /// Display name (user's display_name or guest's guest_name)
    pub display_name: String,
    /// X coordinate in scene space
    pub position_x: f64,
    /// Y coordinate in scene space
    pub position_y: f64,
    /// Facing direction in degrees (0-359)
    pub facing_direction: i16,
    pub is_moving: bool,
    pub is_afk: bool,
    /// Current emotion slot (0-9)
    pub current_emotion: i16,
    pub joined_at: DateTime<Utc>,
}

/// Request to update position in a channel.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UpdatePositionRequest {
    pub x: f64,
    pub y: f64,
}

/// Request to switch emotion.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UpdateEmotionRequest {
    /// Emotion slot 0-9 (e0-e9 hotkeys)
    pub emotion: u8,
}

// =============================================================================
// Avatar Render Data Models
// =============================================================================

/// Data needed to render an avatar's current appearance.
/// Contains the asset paths for all equipped props.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AvatarRenderData {
    pub avatar_id: Uuid,
    pub current_emotion: i16,
    /// Asset paths for skin layer positions 0-8 (None if slot empty)
    pub skin_layer: [Option<String>; 9],
    /// Asset paths for clothes layer positions 0-8
    pub clothes_layer: [Option<String>; 9],
    /// Asset paths for accessories layer positions 0-8
    pub accessories_layer: [Option<String>; 9],
    /// Asset paths for current emotion overlay positions 0-8
    pub emotion_layer: [Option<String>; 9],
}

impl Default for AvatarRenderData {
    fn default() -> Self {
        Self {
            avatar_id: Uuid::nil(),
            current_emotion: 0,
            skin_layer: Default::default(),
            clothes_layer: Default::default(),
            accessories_layer: Default::default(),
            emotion_layer: Default::default(),
        }
    }
}

/// Channel member with full avatar render data.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChannelMemberWithAvatar {
    pub member: ChannelMemberInfo,
    pub avatar: AvatarRenderData,
}

/// Response for channel members list.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChannelMembersResponse {
    pub members: Vec<ChannelMemberWithAvatar>,
}

/// Response after joining a channel.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JoinChannelResponse {
    pub member: ChannelMemberInfo,
    pub members: Vec<ChannelMemberWithAvatar>,
}

// =============================================================================
// Emotion Availability
// =============================================================================

/// Emotion availability data for the emote command UI.
///
/// Indicates which of the 12 emotions have assets configured for the user's avatar,
/// and provides preview paths (position 4/center) for rendering in the emotion picker.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EmotionAvailability {
    /// Which emotions have at least one non-null asset slot (positions 0-8).
    /// Index corresponds to emotion: 0=neutral, 1=happy, 2=sad, etc.
    pub available: [bool; 12],
    /// Center position (4) asset path for each emotion, used for preview rendering.
    /// None if that emotion has no center asset.
    pub preview_paths: [Option<String>; 12],
}

impl Default for EmotionAvailability {
    fn default() -> Self {
        Self {
            available: [false; 12],
            preview_paths: Default::default(),
        }
    }
}

// =============================================================================
// Full Avatar with Paths
// =============================================================================

/// Full avatar data with all inventory UUIDs resolved to asset paths.
///
/// This struct contains all 135 slots (27 content layer + 108 emotion layer)
/// with paths pre-resolved, enabling client-side emotion availability computation
/// and rendering without additional server queries.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AvatarWithPaths {
    pub avatar_id: Uuid,
    pub current_emotion: i16,
    /// Asset paths for skin layer positions 0-8
    pub skin_layer: [Option<String>; 9],
    /// Asset paths for clothes layer positions 0-8
    pub clothes_layer: [Option<String>; 9],
    /// Asset paths for accessories layer positions 0-8
    pub accessories_layer: [Option<String>; 9],
    /// Asset paths for all 12 emotions, each with 9 positions.
    /// Index: emotions[emotion_index][position] where emotion_index is 0-11
    /// (neutral, happy, sad, angry, surprised, thinking, laughing, crying, love, confused, sleeping, wink)
    pub emotions: [[Option<String>; 9]; 12],
    /// Whether each emotion has at least one slot populated (UUID exists, even if path lookup failed).
    /// This matches the old get_emotion_availability behavior.
    pub emotions_available: [bool; 12],
}

impl Default for AvatarWithPaths {
    fn default() -> Self {
        Self {
            avatar_id: Uuid::nil(),
            current_emotion: 0,
            skin_layer: Default::default(),
            clothes_layer: Default::default(),
            accessories_layer: Default::default(),
            emotions: Default::default(),
            emotions_available: [false; 12],
        }
    }
}

impl AvatarWithPaths {
    /// Compute emotion availability from the avatar data.
    /// Uses pre-computed emotions_available which checks if UUIDs exist (not just resolved paths).
    pub fn compute_emotion_availability(&self) -> EmotionAvailability {
        let mut preview_paths: [Option<String>; 12] = Default::default();

        for (i, emotion_layer) in self.emotions.iter().enumerate() {
            // Preview is position 4 (center)
            preview_paths[i] = emotion_layer[4].clone();
        }

        EmotionAvailability {
            available: self.emotions_available,
            preview_paths,
        }
    }

    /// Get the 9-path emotion layer for a specific emotion index (0-11).
    pub fn get_emotion_layer(&self, emotion: usize) -> [Option<String>; 9] {
        if emotion < 12 {
            self.emotions[emotion].clone()
        } else {
            Default::default()
        }
    }

    /// Convert to AvatarRenderData for the current emotion.
    pub fn to_render_data(&self) -> AvatarRenderData {
        AvatarRenderData {
            avatar_id: self.avatar_id,
            current_emotion: self.current_emotion,
            skin_layer: self.skin_layer.clone(),
            clothes_layer: self.clothes_layer.clone(),
            accessories_layer: self.accessories_layer.clone(),
            emotion_layer: self.get_emotion_layer(self.current_emotion as usize),
        }
    }
}
