//! Persistent user identity and campaign-membership primitives.
//!
//! Types live in the SDK so plugins and clients can depend on them
//! without pulling in server-side storage code.

/// Stable opaque identifier for a User. Monotonic within a UserStore;
/// survives username/email changes. Display format is `"u<n>"`.
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
pub struct UserId(pub u64);

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

/// Game-system-agnostic roles a user may hold on a campaign.
/// Sysadmin has delete/transfer authority; GameRunner has every
/// operational permission except those; Player participates; Observer
/// is read-only.
#[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 CampaignRole {
    Sysadmin,
    GameRunner,
    Player,
    Observer,
}

impl CampaignRole {
    pub fn can_delete_campaign(self) -> bool    { matches!(self, Self::Sysadmin) }
    pub fn can_transfer_sysadmin(self) -> bool  { matches!(self, Self::Sysadmin) }
    pub fn can_manage_members(self) -> bool {
        matches!(self, Self::Sysadmin | Self::GameRunner)
    }
    pub fn can_run_sessions(self) -> bool {
        matches!(self, Self::Sysadmin | Self::GameRunner)
    }
    pub fn can_edit_content(self) -> bool {
        matches!(self, Self::Sysadmin | Self::GameRunner)
    }
    pub fn can_act_in_session(self) -> bool {
        matches!(self, Self::Sysadmin | Self::GameRunner | Self::Player)
    }
}

/// Ties a `UserId` to a `CampaignRole` with a join timestamp. A campaign's
/// membership list includes the Sysadmin themselves.
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CampaignMember {
    pub user_id:   UserId,
    pub role:      CampaignRole,
    /// ms since Unix epoch.
    pub joined_at: u64,
}

/// Supported external identity providers. v1 ships Google + Discord;
/// marked `#[non_exhaustive]` so adding providers later is additive-only
/// at the SemVer level.
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum OAuthProvider { Google, Discord }

/// Records that a User has linked an external OAuth identity. `subject_id`
/// is the provider's stable per-user id (OIDC `sub` claim for Google;
/// Discord user id for Discord). `email_at_link` is informational — we
/// capture it when the link is first made but do NOT use it for lookup
/// or trust decisions (users can change their provider-side email
/// without re-linking, and we shouldn't re-authorise based on it).
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OAuthLink {
    pub provider:      OAuthProvider,
    pub subject_id:    String,
    pub email_at_link: String,
    /// ms since Unix epoch.
    pub linked_at:     u64,
}

/// Full user record. `username_lower` and `email_lower` are indexes for
/// case-insensitive uniqueness + lookup; display fields preserve case.
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, PartialEq, Eq)]
pub struct User {
    pub id:                UserId,
    pub username:          String,
    pub username_lower:    String,
    pub email:             Option<String>,
    pub email_lower:       Option<String>,
    pub display_name:      String,
    /// ms since Unix epoch.
    pub created_at:        u64,
    pub password_hash:     Option<String>,
    #[cfg_attr(feature = "serde", serde(default))]
    pub oauth_links:       Vec<OAuthLink>,
    #[cfg_attr(feature = "serde", serde(default))]
    pub is_guest:          bool,
    /// ms since Unix epoch.
    pub last_activity_at:  u64,
}

impl std::fmt::Debug for User {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("User")
            .field("id",                &self.id)
            .field("username",          &self.username)
            .field("username_lower",    &self.username_lower)
            .field("email",             &self.email.as_ref().map(|_| "<redacted>"))
            .field("email_lower",       &self.email_lower.as_ref().map(|_| "<redacted>"))
            .field("display_name",      &self.display_name)
            .field("created_at",        &self.created_at)
            .field("password_hash",     &self.password_hash.as_ref().map(|_| "<redacted>"))
            .field("oauth_links",       &format!("{} link(s)", self.oauth_links.len()))
            .field("is_guest",          &self.is_guest)
            .field("last_activity_at",  &self.last_activity_at)
            .finish()
    }
}

impl User {
    /// Construct a User with `username_lower` and `email_lower` derived
    /// from the display forms. Use this in preference to struct literal
    /// syntax so the twin-field invariant (`*_lower == *.to_ascii_lowercase()`)
    /// can't drift.
    pub fn new(
        id: UserId,
        username: impl Into<String>,
        display_name: impl Into<String>,
        now_ms: u64,
    ) -> Self {
        let username: String = username.into();
        let username_lower = username.to_ascii_lowercase();
        Self {
            id,
            username,
            username_lower,
            email: None,
            email_lower: None,
            display_name: display_name.into(),
            created_at: now_ms,
            password_hash: None,
            oauth_links: Vec::new(),
            is_guest: false,
            last_activity_at: now_ms,
        }
    }

    /// Replace the email; keeps `email_lower` in sync.
    pub fn with_email(mut self, email: impl Into<String>) -> Self {
        let email: String = email.into();
        self.email_lower = Some(email.to_ascii_lowercase());
        self.email = Some(email);
        self
    }

    /// Replace the password hash (already argon2id-hashed).
    pub fn with_password_hash(mut self, hash: impl Into<String>) -> Self {
        self.password_hash = Some(hash.into());
        self
    }

    /// Flip the guest flag. Use for guest auto-creation.
    pub fn mark_as_guest(mut self) -> Self {
        self.is_guest = true;
        self
    }
}

// ---------------------------------------------------------------------------
// CampaignId
//
// Stable handle for the per-campaign persistence scope. Used by:
// - capability persistence (CapabilityScope::Campaign)
// - PostInstallContext to surface the active campaign
// - CampaignState / CampaignStore in engine (saves)
//
// Promoted to sdk-types in CC-2a so a single canonical type backs both
// the sdk-side capability scope and the engine-side campaign state. The
// engine and sdk facades retain back-compat re-exports.
// ---------------------------------------------------------------------------

/// Per-campaign routing id.
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
pub struct CampaignId(pub String);

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

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

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

    #[test]
    fn role_permissions_matrix() {
        // Matrix-style assertion across every (role, permission) pair so
        // regressions get caught wholesale.
        let cases: &[(CampaignRole, [bool; 6])] = &[
            // (role, [delete, transfer, manage, run, edit, act])
            (CampaignRole::Sysadmin,   [true,  true,  true,  true,  true,  true]),
            (CampaignRole::GameRunner, [false, false, true,  true,  true,  true]),
            (CampaignRole::Player,     [false, false, false, false, false, true]),
            (CampaignRole::Observer,   [false, false, false, false, false, false]),
        ];
        for (role, expect) in cases {
            assert_eq!(role.can_delete_campaign(),   expect[0], "{role:?} delete");
            assert_eq!(role.can_transfer_sysadmin(), expect[1], "{role:?} transfer");
            assert_eq!(role.can_manage_members(),    expect[2], "{role:?} manage");
            assert_eq!(role.can_run_sessions(),      expect[3], "{role:?} run");
            assert_eq!(role.can_edit_content(),      expect[4], "{role:?} edit");
            assert_eq!(role.can_act_in_session(),    expect[5], "{role:?} act");
        }
    }

    #[test]
    fn user_id_display() {
        assert_eq!(UserId(42).to_string(), "u42");
    }

    #[test]
    fn user_new_derives_lower_case_fields() {
        let u = User::new(UserId(1), "Alice", "Alice the Wise", 1_000)
            .with_email("Alice@Example.com");
        assert_eq!(u.username, "Alice");
        assert_eq!(u.username_lower, "alice");
        assert_eq!(u.email.as_deref(), Some("Alice@Example.com"));
        assert_eq!(u.email_lower.as_deref(), Some("alice@example.com"));
        assert_eq!(u.id, UserId(1));
        assert_eq!(u.created_at, 1_000);
        assert_eq!(u.last_activity_at, 1_000);
        assert!(u.password_hash.is_none());
        assert!(!u.is_guest);
    }

    #[test]
    fn user_debug_redacts_sensitive_fields() {
        let u = User::new(UserId(2), "bob", "Bob", 0)
            .with_email("bob@example.com")
            .with_password_hash("$argon2id$…");
        let s = format!("{u:?}");
        assert!(s.contains("username: \"bob\""));
        assert!(!s.contains("bob@example.com"), "email must be redacted: {s}");
        assert!(!s.contains("$argon2id"),       "password_hash must be redacted: {s}");
        assert!(s.contains("<redacted>"));
    }
}
