use super::id::Id;
use serde::{Serialize, Deserialize};
use serde_valid::Validate;
use tritools::time::unix_epoch_timestamp;

#[derive(Clone, Debug, Serialize, Deserialize, Validate)]
pub struct PostContext {
    #[serde(default = "default_comments_enabled")]
    pub comments_enabled: bool,
    #[serde(default)]
    pub is_profile_pinned: bool,
    #[serde(default)]
    pub edited: u128,
    #[serde(default)]
    pub is_nsfw: bool,
    #[serde(default)]
    pub repost: Option<RepostContext>,
    #[serde(default = "default_reposts_enabled")]
    pub reposts_enabled: bool,
    /// The ID of the question this post is answering.
    #[serde(default)]
    pub answering: Option<Id>,
    #[serde(default = "default_reactions_enabled")]
    pub reactions_enabled: bool,
    #[serde(default)]
    #[validate(max_length = 1024)]
    pub content_warning: String,
    #[serde(default)]
    #[validate(max_items = 12)]
    pub tags: Vec<String>,
}

fn default_comments_enabled() -> bool {
    true
}

fn default_reposts_enabled() -> bool {
    true
}

fn default_reactions_enabled() -> bool {
    true
}

impl Default for PostContext {
    fn default() -> Self {
        Self {
            comments_enabled: default_comments_enabled(),
            reposts_enabled: default_reposts_enabled(),
            is_profile_pinned: false,
            edited: 0,
            is_nsfw: false,
            repost: None,
            answering: None,
            reactions_enabled: default_reactions_enabled(),
            content_warning: String::new(),
            tags: Vec::new(),
        }
    }
}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct RepostContext {
    /// Should be `false` is `reposting` is `Some`.
    ///
    /// Declares the post to be a repost of another post.
    pub is_repost: bool,
    /// Should be `None` if `is_repost` is true.
    ///
    /// Sets the ID of the other post to load.
    pub reposting: Option<Id>,
}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Post {
    pub id: Id,
    pub created: u128,
    pub content: String,
    /// The ID of the owner of this post.
    pub owner: Id,
    /// Extra information about the post.
    pub context: PostContext,
    /// The ID of the post this post is a comment on.
    pub replying_to: Option<Id>,
    pub likes: isize,
    pub dislikes: isize,
    pub comment_count: usize,
    /// IDs of all uploads linked to this post.
    pub uploads: Vec<usize>,
    /// If the post was deleted.
    pub is_deleted: bool,
    /// The ID of the poll associated with this post.
    pub poll_id: Option<Id>,
    pub views: usize,
    pub group: Option<Id>,
    pub limited_reach: bool,
}

impl Post {
    /// Create a new [`Post`].
    pub fn new(
        content: String,
        replying_to: Option<Id>,
        owner: Id,
        poll_id: Option<Id>,
        group: Option<Id>,
    ) -> Self {
        Self {
            id: Id::new(),
            created: unix_epoch_timestamp(),
            content,
            owner,
            context: PostContext::default(),
            replying_to,
            likes: 0,
            dislikes: 0,
            comment_count: 0,
            uploads: Vec::new(),
            is_deleted: false,
            poll_id,
            views: 0,
            group,
            limited_reach: false,
        }
    }

    /// Create a new [`Post`] (as a repost of the given `post_id`).
    pub fn repost(content: String, owner: Id, post_id: Id) -> Self {
        let mut post = Self::new(content, None, owner, None, None);

        post.context.repost = Some(RepostContext {
            is_repost: false,
            reposting: Some(post_id),
        });

        post
    }

    /// Make the given post a reposted post.
    pub fn mark_as_repost(&mut self) {
        self.context.repost = Some(RepostContext {
            is_repost: true,
            reposting: None,
        });
    }

    /// Check if a particular user can comment on this post.
    pub fn can_comment(&self) -> bool {
        self.context.repost.is_none() || !self.content.is_empty()
    }
}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Question {
    pub id: Id,
    pub created: u128,
    pub owner: Id,
    pub receiver: Id,
    pub content: String,
    /// The `is_global` flag allows any (authenticated) user to respond
    /// to the question. Normally, only the `receiver` can do so.
    ///
    /// If `is_global` is true, `receiver` should be 0 (and vice versa).
    pub is_global: bool,
    /// The number of answers the question has. Should never really be changed
    /// unless the question has `is_global` set to true.
    pub answer_count: usize,
    // likes
    #[serde(default)]
    pub likes: isize,
    #[serde(default)]
    pub dislikes: isize,
    // ...
    #[serde(default)]
    pub context: QuestionContext,
    /// The IP of the question creator for IP blocking and identifying anonymous users.
    #[serde(default)]
    pub ip: String,
    /// The IDs of all uploads which hold this question's drawings.
    #[serde(default)]
    pub drawings: Vec<usize>,
}

impl Question {
    /// Create a new [`Question`].
    pub fn new(owner: Id, receiver: Id, content: String, is_global: bool, ip: String) -> Self {
        Self {
            id: Id::new(),
            created: unix_epoch_timestamp(),
            owner,
            receiver,
            content,
            is_global,
            answer_count: 0,
            likes: 0,
            dislikes: 0,
            context: QuestionContext::default(),
            ip,
            drawings: Vec::new(),
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct QuestionContext {
    #[serde(default)]
    pub is_nsfw: bool,
    /// If the owner is shown as anonymous in the UI.
    #[serde(default)]
    pub mask_owner: bool,
    /// The POST this question is asking about.
    #[serde(default)]
    pub asking_about: Option<Id>,
}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct PostDraft {
    pub id: Id,
    pub created: u128,
    pub content: String,
    pub owner: Id,
}

impl PostDraft {
    /// Create a new [`PostDraft`].
    pub fn new(content: String, owner: Id) -> Self {
        Self {
            id: Id::new(),
            created: unix_epoch_timestamp(),
            content,
            owner,
        }
    }
}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Poll {
    pub id: Id,
    pub owner: Id,
    pub created: u128,
    /// The number of milliseconds until this poll can no longer receive votes.
    pub expires: u128,
    // options
    pub option_a: String,
    pub option_b: String,
    pub option_c: String,
    pub option_d: String,
    // votes
    pub votes_a: usize,
    pub votes_b: usize,
    pub votes_c: usize,
    pub votes_d: usize,
}

impl Poll {
    /// Create a new [`Poll`].
    pub fn new(
        owner: Id,
        expires: u128,
        option_a: String,
        option_b: String,
        option_c: String,
        option_d: String,
    ) -> Self {
        Self {
            id: Id::new(),
            owner,
            created: unix_epoch_timestamp(),
            expires,
            // options
            option_a,
            option_b,
            option_c,
            option_d,
            // votes
            votes_a: 0,
            votes_b: 0,
            votes_c: 0,
            votes_d: 0,
        }
    }
}

/// Poll option (selectors) are stored in the database as numbers 0 to 3.
///
/// This enum allows us to convert from these numbers into letters.
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub enum PollOption {
    A,
    B,
    C,
    D,
}

impl From<u8> for PollOption {
    fn from(value: u8) -> Self {
        match value {
            0 => Self::A,
            1 => Self::B,
            2 => Self::C,
            3 => Self::D,
            _ => Self::A,
        }
    }
}

impl From<PollOption> for u8 {
    fn from(val: PollOption) -> Self {
        match val {
            PollOption::A => 0,
            PollOption::B => 1,
            PollOption::C => 2,
            PollOption::D => 3,
        }
    }
}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct PollVote {
    pub id: Id,
    pub owner: Id,
    pub created: u128,
    pub poll_id: Id,
    pub vote: PollOption,
}

impl PollVote {
    /// Create a new [`PollVote`].
    pub fn new(owner: Id, poll_id: Id, vote: PollOption) -> Self {
        Self {
            id: Id::new(),
            owner,
            created: unix_epoch_timestamp(),
            poll_id,
            vote,
        }
    }
}
