use std::net::SocketAddr;

use serde::{Deserialize, Serialize};

/// A post identifier — BLAKE3 hash of the canonical serialized post
pub type PostId = [u8; 32];

/// A node identifier — ed25519 public key bytes (same as iroh EndpointId)
pub type NodeId = [u8; 32];

/// A public post on the network
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct Post {
    /// Author's node ID (ed25519 public key bytes)
    pub author: NodeId,
    /// Post text content
    pub content: String,
    /// Media attachments (empty for text-only posts)
    #[serde(default)]
    pub attachments: Vec<Attachment>,
    /// Unix timestamp in milliseconds
    pub timestamp_ms: u64,
}

/// A reference to a media blob attached to a post
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct Attachment {
    /// BLAKE3 hash of the blob data
    pub cid: [u8; 32],
    /// MIME type (e.g. "image/jpeg")
    pub mime_type: String,
    /// Size of the blob in bytes
    pub size_bytes: u64,
}

/// Public profile — plaintext, synced to all peers
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct PublicProfile {
    pub node_id: NodeId,
    pub display_name: String,
    /// Short bio text (can be empty)
    #[serde(default)]
    pub bio: String,
    /// Unix timestamp in milliseconds of last update
    pub updated_at: u64,
    /// Anchor nodes this peer advertises for reachability
    #[serde(default)]
    pub anchors: Vec<NodeId>,
    /// Up to 10 currently-connected peer NodeIds (for 11-needle worm search)
    #[serde(default)]
    pub recent_peers: Vec<NodeId>,
    /// Bilateral preferred peer NodeIds (stable relay hubs)
    #[serde(default)]
    pub preferred_peers: Vec<NodeId>,
    /// Whether display_name/bio are visible to non-circle peers
    #[serde(default = "default_true")]
    pub public_visible: bool,
    /// Avatar blob CID (BLAKE3 hash)
    #[serde(default)]
    pub avatar_cid: Option<[u8; 32]>,
}

fn default_true() -> bool {
    true
}

/// Per-circle profile variant — encrypted with the circle's group key
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct CircleProfile {
    pub author: NodeId,
    pub circle_name: String,
    pub display_name: String,
    pub bio: String,
    pub avatar_cid: Option<[u8; 32]>,
    pub updated_at: u64,
}

/// Visibility of a follow relationship
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub enum FollowVisibility {
    /// Shared in gossip with all peers
    Public,
    /// Never shared unless encrypted for a circle (future)
    Private,
}

impl Default for FollowVisibility {
    fn default() -> Self {
        Self::Public
    }
}

/// Full peer record stored in the database
#[derive(Debug, Clone)]
pub struct PeerRecord {
    pub node_id: NodeId,
    pub addresses: Vec<SocketAddr>,
    pub last_seen: u64,
    pub introduced_by: Option<NodeId>,
    pub is_anchor: bool,
    pub first_seen: u64,
}

/// Compact peer info exchanged during gossip (address-free since sync/6)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GossipPeerInfo {
    pub node_id: NodeId,
    pub is_anchor: bool,
}

/// Worm identifier for deduplication
pub type WormId = [u8; 16];

/// Result of a worm lookup for a single target
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WormResult {
    pub node_id: NodeId,
    pub addresses: Vec<String>,
    pub reporter: NodeId,
    pub freshness_ms: u64,
    pub post_holder: Option<NodeId>,
    pub blob_holder: Option<NodeId>,
}

/// Audience relationship direction
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum AudienceDirection {
    /// They are in our audience (we push to them)
    Inbound,
    /// We are in their audience (they push to us)
    Outbound,
}

/// Audience membership status
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum AudienceStatus {
    Pending,
    Approved,
    Denied,
}

/// An audience membership record
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AudienceRecord {
    pub node_id: NodeId,
    pub direction: AudienceDirection,
    pub status: AudienceStatus,
    pub requested_at: u64,
    pub approved_at: Option<u64>,
}

/// Audience approval mode setting
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum AudienceApprovalMode {
    /// Auto-accept all audience join requests
    PublicApprove,
    /// Queue requests for manual review
    ApprovalQueue,
}

// --- Encryption / Circles ---

/// Circle name (unique per node)
pub type CircleId = String;

/// Group identifier — BLAKE3 hash of the initial group public key
pub type GroupId = [u8; 32];

/// Group key epoch — incremented on each key rotation
pub type GroupEpoch = u64;

/// A group's private key wrapped for a specific member
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct GroupMemberKey {
    pub member: NodeId,
    pub epoch: GroupEpoch,
    /// 60 bytes: nonce(12) || encrypted_group_seed(32) || tag(16)
    pub wrapped_group_key: Vec<u8>,
}

/// A group key record (circle ↔ group key binding)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GroupKeyRecord {
    pub group_id: GroupId,
    pub circle_name: String,
    pub epoch: GroupEpoch,
    pub group_public_key: [u8; 32],
    pub admin: NodeId,
    pub created_at: u64,
}

/// Visibility of a post — separate from Post struct so it doesn't affect PostId
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum PostVisibility {
    Public,
    Encrypted { recipients: Vec<WrappedKey> },
    GroupEncrypted {
        group_id: GroupId,
        epoch: GroupEpoch,
        /// 60 bytes: nonce(12) || encrypted_cek(32) || tag(16)
        wrapped_cek: Vec<u8>,
    },
}

impl Default for PostVisibility {
    fn default() -> Self {
        Self::Public
    }
}

/// A CEK wrapped for a specific recipient via X25519 DH
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct WrappedKey {
    pub recipient: NodeId,
    /// 60 bytes: nonce(12) || encrypted_cek(32) || tag(16)
    pub wrapped_cek: Vec<u8>,
}

/// User-facing intent for post visibility (resolved to recipients before encryption)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum VisibilityIntent {
    Public,
    /// All public follows
    Friends,
    /// Named circle
    Circle(String),
    /// Specific recipients
    Direct(Vec<NodeId>),
}

/// A named group of recipients
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Circle {
    pub name: String,
    pub members: Vec<NodeId>,
    pub created_at: u64,
}

// --- Delete / Revocation ---

/// A signed record attesting that an author deleted one of their posts
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DeleteRecord {
    pub post_id: PostId,
    pub author: NodeId,
    pub timestamp_ms: u64,
    /// 64-byte ed25519 signature over post_id bytes
    pub signature: Vec<u8>,
}

/// An update to a post's visibility (new wrapped keys after revocation)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VisibilityUpdate {
    pub post_id: PostId,
    pub author: NodeId,
    pub visibility: PostVisibility,
}

/// How to handle revoking a recipient's access to past encrypted posts
#[derive(Debug, Clone, Copy)]
pub enum RevocationMode {
    /// Re-wrap the CEK for remaining recipients (fast, but revoked user still has ciphertext)
    SyncAccessList,
    /// Re-encrypt the post with a new CEK (slower, but revoked user can't decrypt even cached copy)
    ReEncrypt,
}

// --- Discovery Protocol v3 (N1/N2/N3 architecture) ---

/// A node reachable through one of our peers (stored in reachable_n2 or reachable_n3)
#[derive(Debug, Clone)]
pub struct ReachableEntry {
    pub reachable_node_id: NodeId,
    pub reporter_node_id: NodeId,
    pub updated_at: u64,
}

/// NAT type classification from STUN probing
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum NatType {
    /// Observed addr matches local, or UPnP mapped — fully reachable
    Public,
    /// Same mapped port from multiple STUN servers — cone NAT, hole punch works
    Easy,
    /// Different mapped ports — symmetric NAT, hole punch unlikely to work
    Hard,
    /// Detection failed or not yet run
    Unknown,
}

impl std::fmt::Display for NatType {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            NatType::Public => write!(f, "public"),
            NatType::Easy => write!(f, "easy"),
            NatType::Hard => write!(f, "hard"),
            NatType::Unknown => write!(f, "unknown"),
        }
    }
}

impl NatType {
    pub fn from_str_label(s: &str) -> Self {
        match s {
            "public" => NatType::Public,
            "easy" => NatType::Easy,
            "hard" => NatType::Hard,
            _ => NatType::Unknown,
        }
    }
}

/// NAT mapping behavior — whether port allocation depends on destination
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum NatMapping {
    /// Endpoint-Independent Mapping: same port for all destinations (cone NAT)
    EndpointIndependent,
    /// Endpoint-Dependent Mapping: different port per destination (symmetric NAT)
    EndpointDependent,
    /// Not yet determined
    Unknown,
}

impl std::fmt::Display for NatMapping {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            NatMapping::EndpointIndependent => write!(f, "eim"),
            NatMapping::EndpointDependent => write!(f, "edm"),
            NatMapping::Unknown => write!(f, "unknown"),
        }
    }
}

impl NatMapping {
    pub fn from_str_label(s: &str) -> Self {
        match s {
            "eim" => NatMapping::EndpointIndependent,
            "edm" => NatMapping::EndpointDependent,
            _ => NatMapping::Unknown,
        }
    }
}

/// NAT filtering behavior — what inbound packets are allowed through
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum NatFiltering {
    /// Full cone or address-restricted — no scanning needed
    Open,
    /// Port-restricted — only exact IP:port gets through, needs scanning
    PortRestricted,
    /// Not yet determined
    Unknown,
}

impl std::fmt::Display for NatFiltering {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            NatFiltering::Open => write!(f, "open"),
            NatFiltering::PortRestricted => write!(f, "port_restricted"),
            NatFiltering::Unknown => write!(f, "unknown"),
        }
    }
}

impl NatFiltering {
    pub fn from_str_label(s: &str) -> Self {
        match s {
            "open" => NatFiltering::Open,
            "port_restricted" => NatFiltering::PortRestricted,
            _ => NatFiltering::Unknown,
        }
    }
}

/// Combined NAT profile: mapping + filtering behavior
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct NatProfile {
    pub mapping: NatMapping,
    pub filtering: NatFiltering,
}

impl NatProfile {
    pub fn new(mapping: NatMapping, filtering: NatFiltering) -> Self {
        Self { mapping, filtering }
    }

    pub fn unknown() -> Self {
        Self { mapping: NatMapping::Unknown, filtering: NatFiltering::Unknown }
    }

    /// Whether this NAT profile requires port scanning for hole punch
    pub fn needs_scanning(&self) -> bool {
        self.mapping == NatMapping::EndpointDependent
            && self.filtering == NatFiltering::PortRestricted
    }

    /// Convert from legacy NatType (filtering is always Unknown —
    /// "Public" may only be public on IPv6, not IPv4. Use anchor filter probe
    /// to determine filtering reliably.)
    pub fn from_nat_type(nat: NatType) -> Self {
        match nat {
            NatType::Public => Self::new(NatMapping::EndpointIndependent, NatFiltering::Unknown),
            NatType::Easy => Self::new(NatMapping::EndpointIndependent, NatFiltering::Unknown),
            NatType::Hard => Self::new(NatMapping::EndpointDependent, NatFiltering::Unknown),
            NatType::Unknown => Self::unknown(),
        }
    }

    /// Whether standard hole punch should work (no scanning needed)
    pub fn standard_punch_likely(&self, peer: &NatProfile) -> bool {
        // If either side is EIM or Public, standard punch works
        if self.mapping == NatMapping::EndpointIndependent
            || peer.mapping == NatMapping::EndpointIndependent
        {
            return true;
        }
        // Both EDM — only works if at least one has Open filtering
        if self.filtering == NatFiltering::Open || peer.filtering == NatFiltering::Open {
            return true;
        }
        false
    }

    /// Whether scanning should be attempted after standard punch fails.
    /// If both sides have Open filtering, scanning is never needed — standard punch
    /// works regardless of mapping. Scanning is only needed when at least one side
    /// is PortRestricted (or Unknown, conservatively).
    pub fn should_try_scanning(&self, peer: &NatProfile) -> bool {
        // If both sides have confirmed Open filtering, no scanning needed
        if self.filtering == NatFiltering::Open && peer.filtering == NatFiltering::Open {
            return false;
        }
        // Need EDM or Unknown mapping on at least one side, AND at least one side
        // with PortRestricted or Unknown filtering
        let has_edm_or_unknown = self.mapping == NatMapping::EndpointDependent
            || peer.mapping == NatMapping::EndpointDependent
            || self.mapping == NatMapping::Unknown
            || peer.mapping == NatMapping::Unknown;
        let has_restricted_filtering = self.filtering != NatFiltering::Open
            || peer.filtering != NatFiltering::Open;
        has_edm_or_unknown && has_restricted_filtering
    }
}

/// Device profile — determines connection slot budget
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DeviceProfile {
    /// Desktop: 81 local + 20 wide = 101 mesh connections
    Desktop,
    /// Mobile: 10 local + 5 wide = 15 mesh connections
    Mobile,
}

impl DeviceProfile {
    pub fn preferred_slots(&self) -> usize {
        match self {
            DeviceProfile::Desktop => 10,
            DeviceProfile::Mobile => 3,
        }
    }

    pub fn local_slots(&self) -> usize {
        match self {
            DeviceProfile::Desktop => 71,
            DeviceProfile::Mobile => 7,
        }
    }

    pub fn wide_slots(&self) -> usize {
        match self {
            DeviceProfile::Desktop => 20,
            DeviceProfile::Mobile => 5,
        }
    }

    pub fn session_slots(&self) -> usize {
        match self {
            DeviceProfile::Desktop => 20,
            DeviceProfile::Mobile => 5,
        }
    }

    pub fn max_relay_pipes(&self) -> usize {
        match self {
            DeviceProfile::Desktop => 10,
            DeviceProfile::Mobile => 2,
        }
    }
}

/// How a session connection was established
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum SessionReachMethod {
    /// Direct QUIC (address from relay introduction)
    Direct,
    /// Coordinated hole punch via relay introduction
    HolePunch,
    /// Piped through intermediary relay
    Relayed,
}

impl std::fmt::Display for SessionReachMethod {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            SessionReachMethod::Direct => write!(f, "direct"),
            SessionReachMethod::HolePunch => write!(f, "holepunch"),
            SessionReachMethod::Relayed => write!(f, "relayed"),
        }
    }
}

/// Slot kind for mesh connections
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum PeerSlotKind {
    /// Bilateral preferred connections (Desktop: 10, Mobile: 3)
    Preferred,
    /// Diverse local connections (Desktop: 71, Mobile: 7)
    Local,
    /// Bloom-sourced random distant connections (20 slots)
    Wide,
}

impl std::fmt::Display for PeerSlotKind {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            PeerSlotKind::Preferred => write!(f, "preferred"),
            PeerSlotKind::Local => write!(f, "local"),
            PeerSlotKind::Wide => write!(f, "wide"),
        }
    }
}

impl std::str::FromStr for PeerSlotKind {
    type Err = anyhow::Error;
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s {
            "preferred" => Ok(PeerSlotKind::Preferred),
            "local" | "social" => Ok(PeerSlotKind::Local),
            "wide" => Ok(PeerSlotKind::Wide),
            _ => Err(anyhow::anyhow!("unknown slot kind: {}", s)),
        }
    }
}

// --- Social Routing Cache ---

/// How we last reached a social contact
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ReachMethod {
    Direct,
    Referral,
    Worm,
    Inbound,
}

impl std::fmt::Display for ReachMethod {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            ReachMethod::Direct => write!(f, "direct"),
            ReachMethod::Referral => write!(f, "referral"),
            ReachMethod::Worm => write!(f, "worm"),
            ReachMethod::Inbound => write!(f, "inbound"),
        }
    }
}

impl std::str::FromStr for ReachMethod {
    type Err = anyhow::Error;
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s {
            "direct" => Ok(ReachMethod::Direct),
            "referral" => Ok(ReachMethod::Referral),
            "worm" => Ok(ReachMethod::Worm),
            "inbound" => Ok(ReachMethod::Inbound),
            _ => Err(anyhow::anyhow!("unknown reach method: {}", s)),
        }
    }
}

/// Social relationship type
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SocialRelation {
    Follow,
    Audience,
    Mutual,
}

impl std::fmt::Display for SocialRelation {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            SocialRelation::Follow => write!(f, "follow"),
            SocialRelation::Audience => write!(f, "audience"),
            SocialRelation::Mutual => write!(f, "mutual"),
        }
    }
}

impl std::str::FromStr for SocialRelation {
    type Err = anyhow::Error;
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s {
            "follow" => Ok(SocialRelation::Follow),
            "audience" => Ok(SocialRelation::Audience),
            "mutual" => Ok(SocialRelation::Mutual),
            _ => Err(anyhow::anyhow!("unknown social relation: {}", s)),
        }
    }
}

/// Online/disconnected status
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SocialStatus {
    Online,
    Disconnected,
}

impl std::fmt::Display for SocialStatus {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            SocialStatus::Online => write!(f, "online"),
            SocialStatus::Disconnected => write!(f, "disconnected"),
        }
    }
}

impl std::str::FromStr for SocialStatus {
    type Err = anyhow::Error;
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s {
            "online" => Ok(SocialStatus::Online),
            "disconnected" => Ok(SocialStatus::Disconnected),
            _ => Err(anyhow::anyhow!("unknown social status: {}", s)),
        }
    }
}

/// A peer with resolved addresses (for peer_addresses in social_routes)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PeerWithAddress {
    pub n: String,
    pub a: Vec<String>,
}

// --- CDN Manifest Layer ---

/// Lightweight post reference in a manifest
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ManifestEntry {
    pub post_id: PostId,
    pub timestamp_ms: u64,
    pub has_attachments: bool,
}

/// Author-signed manifest — cannot be forged by hosts. Max 256KB serialized.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuthorManifest {
    /// The post this manifest is anchored to
    pub post_id: PostId,
    pub author: NodeId,
    /// Author's N+10:A (ip:port strings)
    pub author_addresses: Vec<String>,
    /// Original post creation time (ms)
    pub created_at: u64,
    /// When manifest was last updated (ms)
    pub updated_at: u64,
    /// Up to 10 posts before this one
    pub previous_posts: Vec<ManifestEntry>,
    /// Up to 10 posts after (grows via updates)
    pub following_posts: Vec<ManifestEntry>,
    /// ed25519 over canonical digest
    pub signature: Vec<u8>,
}

/// CDN manifest traveling with blobs (author-signed part + host metadata)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CdnManifest {
    pub author_manifest: AuthorManifest,
    /// Serving host's NodeId
    pub host: NodeId,
    /// Serving host's N+10:A
    pub host_addresses: Vec<String>,
    /// Who the host got it from
    pub source: NodeId,
    /// Source's N+10:A
    pub source_addresses: Vec<String>,
    /// How many downstream this host has
    pub downstream_count: u32,
}

/// Cached routing info for a social contact
#[derive(Debug, Clone)]
pub struct SocialRouteEntry {
    pub node_id: NodeId,
    pub addresses: Vec<SocketAddr>,
    pub peer_addresses: Vec<PeerWithAddress>,
    pub relation: SocialRelation,
    pub status: SocialStatus,
    pub last_connected_ms: u64,
    pub last_seen_ms: u64,
    pub reach_method: ReachMethod,
    /// 2-layer preferred peer tree (~100 nodes) for fast relay candidate search
    pub preferred_tree: Vec<NodeId>,
}

// --- Engagement System ---

/// Maximum engagement header size before splitting oldest comments into a separate post
pub const BLOB_HEADER_ENGAGEMENT_THRESHOLD: usize = 16 * 1024;

/// A reaction to a post
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct Reaction {
    /// Who reacted
    pub reactor: NodeId,
    /// Emoji string (e.g. "👍", "❤️")
    pub emoji: String,
    /// Which post
    pub post_id: PostId,
    /// When the reaction was created (ms)
    pub timestamp_ms: u64,
    /// If private: X25519-encrypted payload (only author can decrypt)
    #[serde(default)]
    pub encrypted_payload: Option<String>,
    /// Tombstone timestamp — if set, this reaction has been soft-deleted
    #[serde(default)]
    pub deleted_at: Option<u64>,
    /// ed25519 signature over BLAKE3(reactor || post_id || emoji || timestamp_ms)
    #[serde(default)]
    pub signature: Vec<u8>,
}

/// An inline comment on a post
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct InlineComment {
    /// Comment author
    pub author: NodeId,
    /// Which post this comment is on
    pub post_id: PostId,
    /// Comment text
    pub content: String,
    /// When the comment was created (ms)
    pub timestamp_ms: u64,
    /// ed25519 signature over BLAKE3(author || post_id || content || timestamp_ms)
    pub signature: Vec<u8>,
    /// Tombstone timestamp — if set, this comment has been soft-deleted
    #[serde(default)]
    pub deleted_at: Option<u64>,
}

/// Permission level for comments on a post
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub enum CommentPermission {
    /// Anyone can comment
    Public,
    /// Only people in author's audience can comment
    AudienceOnly,
    /// Comments disabled
    None,
}

impl Default for CommentPermission {
    fn default() -> Self {
        Self::Public
    }
}

/// Permission level for reactions on a post
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub enum ReactPermission {
    /// Public reactions visible to all
    Public,
    /// Private reactions (encrypted, only author sees)
    Private,
    /// Both public and private allowed
    Both,
    /// Reactions disabled
    None,
}

impl Default for ReactPermission {
    fn default() -> Self {
        Self::Both
    }
}

/// Moderation mode for comments
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub enum ModerationMode {
    /// Author maintains a blocklist of users
    AuthorBlocklist,
    /// Only audience members can engage
    AudienceOnly,
}

impl Default for ModerationMode {
    fn default() -> Self {
        Self::AuthorBlocklist
    }
}

/// Author-controlled engagement policy for a post
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct CommentPolicy {
    pub allow_comments: CommentPermission,
    pub allow_reacts: ReactPermission,
    pub moderation: ModerationMode,
    /// Blocked NodeIds — rejected from commenting/reacting
    #[serde(default)]
    pub blocklist: Vec<NodeId>,
}

impl Default for CommentPolicy {
    fn default() -> Self {
        Self {
            allow_comments: CommentPermission::default(),
            allow_reacts: ReactPermission::default(),
            moderation: ModerationMode::default(),
            blocklist: Vec::new(),
        }
    }
}

/// Incremental engagement operation (sent in BlobHeaderDiff)
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum BlobHeaderDiffOp {
    AddReaction(Reaction),
    RemoveReaction { reactor: NodeId, emoji: String, post_id: PostId },
    AddComment(InlineComment),
    EditComment { author: NodeId, post_id: PostId, timestamp_ms: u64, new_content: String },
    DeleteComment { author: NodeId, post_id: PostId, timestamp_ms: u64 },
    SetPolicy(CommentPolicy),
    ThreadSplit { new_post_id: PostId },
    /// Write an encrypted receipt slot (64 bytes encrypted data)
    WriteReceiptSlot { post_id: PostId, slot_index: u32, data: Vec<u8> },
    /// Write an encrypted comment slot (256 bytes encrypted data)
    WriteCommentSlot { post_id: PostId, slot_index: u32, data: Vec<u8> },
    /// Add new encrypted comment slots (each 256 bytes)
    AddCommentSlots { post_id: PostId, count: u32, slots: Vec<Vec<u8>> },
    /// Unknown ops from newer protocol versions — silently ignored
    #[serde(other)]
    Unknown,
}

/// Aggregated engagement header for a post (stored locally, propagated as diffs)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BlobHeader {
    pub post_id: PostId,
    pub author: NodeId,
    pub reactions: Vec<Reaction>,
    pub comments: Vec<InlineComment>,
    pub policy: CommentPolicy,
    pub updated_at: u64,
    /// PostIds of split-off comment overflow posts
    #[serde(default)]
    pub thread_splits: Vec<PostId>,
    /// Encrypted receipt slots (each 64 bytes) — only for encrypted posts
    #[serde(default)]
    pub receipt_slots: Vec<Vec<u8>>,
    /// Encrypted comment slots (each 256 bytes) — only for encrypted posts
    #[serde(default)]
    pub comment_slots: Vec<Vec<u8>>,
}

/// Receipt slot state byte values
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[repr(u8)]
pub enum ReceiptState {
    Empty = 0,
    Delivered = 1,
    Seen = 2,
    Reacted = 3,
}

impl ReceiptState {
    pub fn from_u8(v: u8) -> Self {
        match v {
            1 => ReceiptState::Delivered,
            2 => ReceiptState::Seen,
            3 => ReceiptState::Reacted,
            _ => ReceiptState::Empty,
        }
    }

    pub fn from_str_label(s: &str) -> Self {
        match s {
            "delivered" => ReceiptState::Delivered,
            "seen" => ReceiptState::Seen,
            "reacted" => ReceiptState::Reacted,
            _ => ReceiptState::Empty,
        }
    }

    pub fn as_str(&self) -> &'static str {
        match self {
            ReceiptState::Empty => "empty",
            ReceiptState::Delivered => "delivered",
            ReceiptState::Seen => "seen",
            ReceiptState::Reacted => "reacted",
        }
    }
}

/// Decrypted receipt slot data
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReceiptSlotData {
    pub slot_index: u32,
    pub node_id: Option<NodeId>,
    pub state: ReceiptState,
    pub timestamp_ms: u64,
    /// Emoji string (only set when state == Reacted)
    pub emoji: Option<String>,
}

/// Decrypted comment slot data
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CommentSlotData {
    pub slot_index: u32,
    pub author: NodeId,
    pub timestamp_ms: u64,
    pub content: String,
}

/// Links a split-off comment post back to its original parent
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ThreadMeta {
    /// The split-off post's ID
    pub post_id: PostId,
    /// The original parent post this was split from
    pub parent_post_id: PostId,
}

// --- Active CDN Replication ---

/// Device role for CDN replication budgets and pull ordering.
/// Determines how aggressively a device replicates content and how much
/// delivery bandwidth it reserves.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum DeviceRole {
    /// Phone/tablet — high consumer, intermittent availability
    Intermittent,
    /// Desktop/laptop — moderate provider, usually available during sessions
    Available,
    /// Anchor/server — persistent availability, reserved for web serving
    Persistent,
}

impl DeviceRole {
    /// Hourly replication budget in bytes (content we pull to cache for others).
    pub fn replication_limit(&self) -> u64 {
        match self {
            DeviceRole::Intermittent => 100 * 1024 * 1024,  // 100 MB/hr — phones rarely get replication requests
            DeviceRole::Available => 200 * 1024 * 1024,     // 200 MB/hr — desktops are workhorses
            DeviceRole::Persistent => 200 * 1024 * 1024,    // 200 MB/hr — anchors save capacity for web serving
        }
    }

    /// Hourly delivery budget in bytes (content we serve when peers ask).
    pub fn delivery_limit(&self) -> u64 {
        match self {
            DeviceRole::Intermittent => 1024 * 1024 * 1024, // 1 GB/hr — phones serve heavily when online (OS data limits apply)
            DeviceRole::Available => 2048 * 1024 * 1024,    // 2 GB/hr — desktops on cheap home bandwidth
            DeviceRole::Persistent => 1024 * 1024 * 1024,   // 1 GB/hr — anchors reserved for web/browser serving
        }
    }

    pub fn as_str(&self) -> &'static str {
        match self {
            DeviceRole::Intermittent => "intermittent",
            DeviceRole::Available => "available",
            DeviceRole::Persistent => "persistent",
        }
    }

    pub fn from_str_label(s: &str) -> Self {
        match s {
            "intermittent" => DeviceRole::Intermittent,
            "available" => DeviceRole::Available,
            "persistent" => DeviceRole::Persistent,
            _ => DeviceRole::Available,
        }
    }
}

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