use std::collections::HashMap;
use super::{oauth::AuthGrant, permissions::PermissionsContainer, Error, Result, id::Id};
use serde::{Deserialize, Serialize};
use totp_rs::TOTP;
use tritools::{
    encoding::{hash, hash_salted, salt},
    time::unix_epoch_timestamp,
};
use serde_valid::Validate;

/// `(ip, token, creation timestamp)`
pub type Token = (String, String, u128);

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct User {
    pub id: Id,
    pub created: u128,
    pub username: String,
    pub password: String,
    pub salt: String,
    pub settings: UserSettings,
    pub tokens: Vec<Token>,
    pub legacy_permissions: i32,
    pub is_verified: bool,
    pub notification_count: usize,
    pub follower_count: usize,
    pub following_count: usize,
    pub last_seen: u128,
    /// The TOTP secret for this profile. An empty value means the user has TOTP disabled.
    #[serde(default)]
    pub totp: String,
    /// The TOTP recovery codes for this profile.
    #[serde(default)]
    pub recovery_codes: Vec<String>,
    #[serde(default)]
    pub post_count: usize,
    #[serde(default)]
    pub request_count: usize,
    /// External service connection details.
    #[serde(default)]
    pub connections: UserConnections,
    /// The user's Stripe customer ID.
    #[serde(default)]
    pub stripe_id: String,
    /// The grants associated with the user's account.
    #[serde(default)]
    pub grants: Vec<AuthGrant>,
    /// A list of the IDs of all accounts the user has signed into through the UI.
    #[serde(default)]
    pub associated: Vec<Id>,
    /// Users collect achievements through little actions across the site.
    #[serde(default)]
    pub achievements: Vec<Achievement>,
    /// The reason the user was banned.
    #[serde(default)]
    pub ban_reason: String,
    /// If the user is deactivated. Deactivated users act almost like deleted
    /// users, but their data is not wiped.
    #[serde(default)]
    pub is_deactivated: bool,
    /// The time at which the user's ban will automatically expire.
    #[serde(default)]
    pub ban_expire: u128,
    /// The IDs of Stripe checkout sessions that this user has successfully completed.
    ///
    /// This should be checked BEFORE applying purchases to ensure that the user hasn't
    /// already applied this purchase.
    #[serde(default)]
    pub checkouts: Vec<String>,
    /// The time in which the user last consented to the site's policies.
    #[serde(default)]
    pub last_policy_consent: u128,
    /// The number of messages this user has missed.
    #[serde(default)]
    pub missed_messages_count: usize,
    /// The number of unique authenticated users who have viewed this user's profile.
    #[serde(default)]
    pub views: usize,
    /// The ID of the Shrimpcamp account the user is linked to.
    #[serde(default)]
    pub shrimpcamp_link: usize,
    pub permissions: PermissionsContainer,
}

pub type UserConnections =
    HashMap<ConnectionService, (ExternalConnectionInfo, ExternalConnectionData)>;

#[derive(Clone, Debug, Serialize, Deserialize, Default)]
pub enum ThemePreference {
    #[default]
    Auto,
    Dark,
    Light,
}

#[derive(Clone, Debug, Serialize, Deserialize, Default, Validate)]
pub struct UserSettings {
    #[serde(default)]
    #[validate(max_length = 32)]
    pub display_name: String,
    #[serde(default)]
    #[validate(max_length = 4096)]
    pub biography: String,
    #[serde(default)]
    #[validate(max_length = 2048)]
    pub warning: String,
    #[serde(default)]
    pub private_profile: bool,
    /// The theme shown to the user.
    #[serde(default)]
    pub theme_preference: ThemePreference,
    /// The theme used on the user's profile. Setting this to `Auto` will use
    /// the viewing user's `theme_preference` setting.
    #[serde(default)]
    pub profile_theme: ThemePreference,
    #[serde(default)]
    pub private_last_seen: bool,
    /// Page background.
    #[serde(default)]
    pub theme_color_surface: String,
    /// Text on elements with the surface backgrounds.
    #[serde(default)]
    pub theme_color_text: String,
    /// Links on all elements.
    #[serde(default)]
    pub theme_color_text_link: String,
    /// Box shadow color.
    #[serde(default)]
    pub theme_color_shadow: String,
    /// Some cards, buttons, or anything else with a darker background color than the surface.
    #[serde(default)]
    pub theme_color_lowered: String,
    /// Text on elements with the lowered backgrounds.
    #[serde(default)]
    pub theme_color_text_lowered: String,
    /// Borders.
    #[serde(default)]
    pub theme_color_super_lowered: String,
    /// Some cards, buttons, or anything else with a lighter background color than the surface.
    #[serde(default)]
    pub theme_color_raised: String,
    /// Text on elements with the raised backgrounds.
    #[serde(default)]
    pub theme_color_text_raised: String,
    /// Some borders.
    #[serde(default)]
    pub theme_color_super_raised: String,
    /// Primary color; navigation bar, some buttons, etc.
    #[serde(default)]
    pub theme_color_primary: String,
    /// Text on elements with the primary backgrounds.
    #[serde(default)]
    pub theme_color_text_primary: String,
    /// Hover state for primary buttons.
    #[serde(default)]
    pub theme_color_primary_lowered: String,
    /// Secondary color.
    #[serde(default)]
    pub theme_color_secondary: String,
    /// Text on elements with the secondary backgrounds.
    #[serde(default)]
    pub theme_color_text_secondary: String,
    /// Hover state for secondary buttons.
    #[serde(default)]
    pub theme_color_secondary_lowered: String,
    /// Custom CSS input.
    #[serde(default)]
    pub theme_custom_css: String,
    /// The color of an online online indicator.
    #[serde(default)]
    pub theme_color_online: String,
    /// The color of an idle online indicator.
    #[serde(default)]
    pub theme_color_idle: String,
    /// The color of an offline online indicator.
    #[serde(default)]
    pub theme_color_offline: String,
    #[serde(default)]
    pub disable_other_themes: bool,
    #[serde(default)]
    pub enable_questions: bool,
    /// A header shown in the place of "Ask question" if `enable_questions` is true.
    #[serde(default)]
    pub motivational_header: String,
    /// If questions from anonymous users are allowed. Requires `enable_questions`.
    #[serde(default)]
    pub allow_anonymous_questions: bool,
    /// The username used for anonymous users.
    #[serde(default)]
    pub anonymous_username: String,
    /// The URL of the avatar used for anonymous users.
    #[serde(default)]
    pub anonymous_avatar_url: String,
    /// If dislikes are hidden for the user.
    #[serde(default)]
    pub hide_dislikes: bool,
    /// Require an account to view the user's profile.
    #[serde(default)]
    pub require_account: bool,
    /// If NSFW content should be shown.
    #[serde(default)]
    pub show_nsfw: bool,
    /// Make your inbox your homepage.
    #[serde(default)]
    pub inbox_homepage: bool,
    /// A list of strings the user has muted.
    #[serde(default)]
    pub muted: Vec<String>,
    /// If drawings are enabled for questions sent to the user.
    #[serde(default)]
    pub enable_drawings: bool,
    /// Hide posts that are answering a question on the "All" timeline.
    #[serde(default)]
    pub all_timeline_hide_answers: bool,
    /// Increase the text size of buttons and paragraphs.
    #[serde(default)]
    pub large_text: bool,
    /// Disable achievements.
    #[serde(default)]
    pub disable_achievements: bool,
    /// If the user is hidden from followers/following tabs.
    ///
    /// The user will still impact the followers/following numbers, but will not
    /// be shown in the UI (or API).
    #[serde(default)]
    pub hide_from_social_lists: bool,
    /// Biography shown on `profile/private.lisp` page.
    #[serde(default)]
    pub private_biography: String,
    /// If the followers/following links are hidden from the user's profile.
    /// Will also revoke access to their respective pages.
    #[serde(default)]
    pub hide_social_follows: bool,
    /// The user's location. This isn't actually verified or anything, so it can really
    /// be whatever the user wants.
    #[serde(default)]
    #[validate(max_length = 128)]
    pub location: String,
    /// External links for the user's other profiles on other websites.
    #[serde(default)]
    #[validate(max_items = 15)]
    #[validate(unique_items)]
    pub links: Vec<(String, String)>,
    /// Automatically mark new posts as NSFW.
    #[serde(default)]
    pub auto_nsfw: bool,
    /// Automatically limit the reach of new posts (don't show them on public timelines).
    #[serde(default)]
    pub auto_limit_posts: bool,
}

impl UserSettings {
    pub fn verify_values(&self) -> Result<()> {
        if let Err(e) = self.validate() {
            return Err(Error::MiscError(e.to_string()));
        }

        Ok(())
    }
}

impl Default for User {
    fn default() -> Self {
        Self::new("<unknown>".to_string(), String::new())
    }
}

impl User {
    /// Create a new [`User`].
    pub fn new(username: String, password: String) -> Self {
        let salt = salt();
        let password = hash_salted(password, salt.clone());
        let created = unix_epoch_timestamp();

        Self {
            id: Id::new(),
            created,
            username,
            password,
            salt,
            settings: UserSettings::default(),
            tokens: Vec::new(),
            legacy_permissions: 0,
            is_verified: false,
            notification_count: 0,
            follower_count: 0,
            following_count: 0,
            last_seen: created,
            totp: String::new(),
            recovery_codes: Vec::new(),
            post_count: 0,
            request_count: 0,
            connections: HashMap::new(),
            stripe_id: String::new(),
            grants: Vec::new(),
            associated: Vec::new(),
            achievements: Vec::new(),
            ban_reason: String::new(),
            is_deactivated: false,
            ban_expire: 0,
            checkouts: Vec::new(),
            last_policy_consent: created,
            missed_messages_count: 0,
            views: 0,
            shrimpcamp_link: 0,
            permissions: PermissionsContainer::new(),
        }
    }

    /// Deleted user profile.
    pub fn deleted() -> Self {
        Self {
            username: "<deleted>".to_string(),
            id: Id::Legacy(0),
            ..Default::default()
        }
    }

    /// Banned user profile.
    pub fn banned() -> Self {
        Self {
            username: "<banned>".to_string(),
            id: Id::Legacy(0),
            ..Default::default()
        }
    }

    /// Anonymous user profile.
    pub fn anonymous() -> Self {
        Self {
            username: "anonymous".to_string(),
            id: Id::Legacy(0),
            ..Default::default()
        }
    }

    /// Create a new token
    ///
    /// # Returns
    /// `(unhashed id, token)`
    pub fn create_token(ip: &str) -> (String, Token) {
        let unhashed = Id::new().printable();
        (
            unhashed.clone(),
            (ip.to_string(), hash(unhashed), unix_epoch_timestamp()),
        )
    }

    /// Check if the given password is correct for the user.
    pub fn check_password(&self, against: String) -> bool {
        self.password == hash_salted(against, self.salt.clone())
    }

    /// Parse user mentions in a given `input`.
    pub fn parse_mentions(input: &str) -> Vec<String> {
        // state
        let mut escape: bool = false;
        let mut at: bool = false;
        let mut buffer: String = String::new();
        let mut out = Vec::new();

        // parse
        for char in input.chars() {
            if ((char == '\\') | (char == '/')) && !escape {
                escape = true;
                continue;
            }

            if (char == '@') && !escape {
                at = true;
                continue; // don't push @
            }

            if at {
                if char == ' ' {
                    // reached space, end @
                    at = false;

                    if !out.contains(&buffer) {
                        out.push(buffer);
                    }

                    buffer = String::new();
                    continue;
                }

                // push mention text
                buffer.push(char);
            }

            escape = false;
        }

        if !buffer.is_empty() {
            out.push(buffer);
        }

        if out.len() > 5 {
            // if we're trying to mention more than 5 people, mention nobody (we're a spammer)
            return Vec::new();
        }

        // return
        out
    }

    /// Get a [`TOTP`] from the profile's `totp` secret value.
    pub fn totp(&self, issuer: Option<String>) -> Option<TOTP> {
        if self.totp.is_empty() {
            return None;
        }

        TOTP::new(
            totp_rs::Algorithm::SHA1,
            6,
            1,
            30,
            self.totp.as_bytes().to_owned(),
            Some(issuer.unwrap_or("tetratto!".to_string())),
            self.username.clone(),
        )
        .ok()
    }

    /// Clean the struct for public viewing.
    pub fn clean(&mut self) {
        self.password = String::new();
        self.salt = String::new();

        self.tokens = Vec::new();
        self.grants = Vec::new();

        self.recovery_codes = Vec::new();
        self.totp = String::new();

        self.settings = UserSettings::default();
        self.stripe_id = String::new();
        self.connections = HashMap::new();
    }

    /// Get a grant from the user given the grant's `app` ID.
    ///
    /// Should be used **before** adding another grant (to ensure the app doesn't
    /// already have a grant for this user).
    pub fn get_grant_by_app_id(&self, id: &crate::model::id::Id) -> Option<&AuthGrant> {
        self.grants.iter().find(|x| x.app == *id)
    }
}

#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum ConnectionService {
    /// A connection to a last.fm account.
    LastFm,
    #[serde(other)]
    Unknown,
}

#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub enum ConnectionType {
    /// A connection through a token which never expires.
    Token,
    /// <https://www.rfc-editor.org/rfc/rfc7636>
    PKCE,
    /// A connection with no stored authentication.
    None,
}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ExternalConnectionInfo {
    pub con_type: ConnectionType,
    pub data: HashMap<String, String>,
    pub show_on_profile: bool,
}

#[derive(Clone, Debug, Serialize, Deserialize, Default)]
pub struct ExternalConnectionData {
    pub external_urls: HashMap<String, String>,
    pub data: HashMap<String, String>,
}

/// The total number of achievements needed to 100% Tetratto!
pub const ACHIEVEMENTS: usize = 29;
/// "self-serve" achievements can be granted by the user through the API.
pub const SELF_SERVE_ACHIEVEMENTS: &[AchievementName] = &[
    AchievementName::AcceptProfileWarning,
    AchievementName::OpenSessionSettings,
];

#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub enum AchievementName {
    CreatePost,
    FollowUser,
    Create50Posts,
    Create100Posts,
    Create1000Posts,
    CreateQuestion,
    EditSettings,
    FollowedByStaff,
    CreateDrawing,
    OpenAchievements,
    Get1Like,
    Get10Likes,
    Get50Likes,
    Get100Likes,
    Get25Dislikes,
    Get1Follower,
    Get10Followers,
    Get50Followers,
    Get100Followers,
    Follow10Users,
    CreateDraft,
    EditPost,
    Enable2fa,
    CreateRepost,
    GetAllOtherAchievements,
    AcceptProfileWarning,
    OpenSessionSettings,
    #[serde(other)]
    Removed,
}

#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub enum AchievementRarity {
    Common,
    Uncommon,
    Rare,
}

impl AchievementName {
    pub fn title(&self) -> &str {
        match self {
            Self::CreatePost => "Dear friends,",
            Self::FollowUser => "Virtual connections...",
            Self::Create50Posts => "Hello, world!",
            Self::Create100Posts => "It's my world",
            Self::Create1000Posts => "Timeline domination",
            Self::CreateQuestion => "Big questions...",
            Self::EditSettings => "Just how I like it!",
            Self::FollowedByStaff => "Big Shrimpin'",
            Self::CreateDrawing => "Modern art",
            Self::OpenAchievements => "Welcome!",
            Self::Get1Like => "Baby steps!",
            Self::Get10Likes => "WOW! 10 LIKES!",
            Self::Get50Likes => "banger post follow for more",
            Self::Get100Likes => "everyone liked that",
            Self::Get25Dislikes => "Sorry...",
            Self::Get1Follower => "Friends?",
            Self::Get10Followers => "Friends!",
            Self::Get50Followers => "50 WHOLE FOLLOWERS??",
            Self::Get100Followers => "Everyone is my friend!",
            Self::Follow10Users => "Big fan",
            Self::CreateDraft => "Maybe later!",
            Self::EditPost => "Grammar police?",
            Self::Enable2fa => "Locked in",
            Self::CreateRepost => "More than a like or comment...",
            Self::GetAllOtherAchievements => "The final performance",
            Self::AcceptProfileWarning => "I accept the risks!",
            Self::OpenSessionSettings => "Am I alone in here?",
            Self::Removed => "Removed achievement",
        }
    }

    pub fn description(&self) -> &str {
        match self {
            Self::CreatePost => "Create your first post!",
            Self::FollowUser => "Follow somebody!",
            Self::Create50Posts => "Create your 50th post.",
            Self::Create100Posts => "Create your 100th post.",
            Self::Create1000Posts => "Create your 1000th post.",
            Self::CreateQuestion => "Ask your first question!",
            Self::EditSettings => "Edit your settings.",
            Self::FollowedByStaff => "Get followed by a staff member!",
            Self::CreateDrawing => "Include a drawing in a question.",
            Self::OpenAchievements => "Open the achievements page.",
            Self::Get1Like => "Get 1 like on a post! Good job!",
            Self::Get10Likes => "Get 10 likes on one post.",
            Self::Get50Likes => "Get 50 likes on one post.",
            Self::Get100Likes => "Get 100 likes on one post.",
            Self::Get25Dislikes => "Get 25 dislikes on one post... :(",
            Self::Get1Follower => "Get 1 follower. Cool!",
            Self::Get10Followers => "Get 10 followers. You're getting popular!",
            Self::Get50Followers => "Get 50 followers. Okay, you're fairly popular!",
            Self::Get100Followers => "Get 100 followers. You might be famous..?",
            Self::Follow10Users => "Follow 10 other users. I'm sure people appreciate it!",
            Self::CreateDraft => "Save a post as a draft.",
            Self::EditPost => "Edit a post.",
            Self::Enable2fa => "Enable TOTP 2FA.",
            Self::CreateRepost => "Create a repost or quote.",
            Self::GetAllOtherAchievements => "Get every other achievement.",
            Self::AcceptProfileWarning => "Accept a profile warning.",
            Self::OpenSessionSettings => "Open your session settings.",
            Self::Removed => "An achievement that was removed.",
        }
    }

    pub fn rarity(&self) -> AchievementRarity {
        // i don't want to write that long ass type name everywhere
        use AchievementRarity::*;
        match self {
            Self::CreatePost => Common,
            Self::FollowUser => Common,
            Self::Create50Posts => Uncommon,
            Self::Create100Posts => Uncommon,
            Self::Create1000Posts => Rare,
            Self::CreateQuestion => Common,
            Self::EditSettings => Common,
            Self::FollowedByStaff => Rare,
            Self::CreateDrawing => Common,
            Self::OpenAchievements => Common,
            Self::Get1Like => Common,
            Self::Get10Likes => Common,
            Self::Get50Likes => Uncommon,
            Self::Get100Likes => Rare,
            Self::Get25Dislikes => Uncommon,
            Self::Get1Follower => Common,
            Self::Get10Followers => Common,
            Self::Get50Followers => Uncommon,
            Self::Get100Followers => Rare,
            Self::Follow10Users => Common,
            Self::CreateDraft => Common,
            Self::EditPost => Common,
            Self::Enable2fa => Rare,
            Self::CreateRepost => Common,
            Self::GetAllOtherAchievements => Rare,
            Self::AcceptProfileWarning => Common,
            Self::OpenSessionSettings => Common,
            Self::Removed => Rare,
        }
    }
}

impl From<AchievementName> for Achievement {
    fn from(val: AchievementName) -> Self {
        Achievement {
            name: val,
            unlocked: unix_epoch_timestamp(),
        }
    }
}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Achievement {
    pub name: AchievementName,
    pub unlocked: u128,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct Notification {
    pub id: Id,
    pub created: u128,
    pub title: String,
    pub content: String,
    pub owner: Id,
    pub read: bool,
    pub tag: String,
}

impl Notification {
    /// Returns a new [`Notification`].
    pub fn new(title: String, content: String, owner: Id) -> Self {
        Self {
            id: Id::new(),
            created: unix_epoch_timestamp(),
            title,
            content,
            owner,
            read: false,
            tag: String::new(),
        }
    }
}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct UserFollow {
    pub id: Id,
    pub created: u128,
    pub initiator: Id,
    pub receiver: Id,
}

impl UserFollow {
    /// Create a new [`UserFollow`].
    pub fn new(initiator: Id, receiver: Id) -> Self {
        Self {
            id: Id::new(),
            created: unix_epoch_timestamp(),
            initiator,
            receiver,
        }
    }
}

#[derive(Serialize, Deserialize)]
pub struct UserBlock {
    pub id: Id,
    pub created: u128,
    pub initiator: Id,
    pub receiver: Id,
}

impl UserBlock {
    /// Create a new [`UserBlock`].
    pub fn new(initiator: Id, receiver: Id) -> Self {
        Self {
            id: Id::new(),
            created: unix_epoch_timestamp(),
            initiator,
            receiver,
        }
    }
}

#[derive(Serialize, Deserialize)]
pub struct IpBlock {
    pub id: Id,
    pub created: u128,
    pub initiator: Id,
    pub receiver: String,
}

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

#[derive(Serialize, Deserialize)]
pub struct IpBan {
    pub ip: String,
    pub created: u128,
    pub reason: String,
    pub moderator: Id,
}

impl IpBan {
    /// Create a new [`IpBan`].
    pub fn new(ip: String, moderator: Id, reason: String) -> Self {
        Self {
            ip,
            created: unix_epoch_timestamp(),
            reason,
            moderator,
        }
    }
}

#[derive(Serialize, Deserialize)]
pub struct UserWarning {
    pub id: Id,
    pub created: u128,
    pub receiver: Id,
    pub moderator: Id,
    pub content: String,
}

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

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct InviteCode {
    pub id: Id,
    pub created: u128,
    pub owner: crate::model::id::Id,
    pub code: String,
    pub is_used: bool,
}

impl InviteCode {
    /// Create a new [`InviteCode`].
    pub fn new(owner: crate::model::id::Id) -> Self {
        Self {
            id: Id::new(),
            created: unix_epoch_timestamp(),
            owner,
            code: salt(),
            is_used: false,
        }
    }
}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ProfileView {
    pub id: Id,
    pub created: u128,
    pub owner: crate::model::id::Id,
    pub profile: usize,
}

impl ProfileView {
    /// Create a new [`ProfileView`]
    pub fn new(owner: crate::model::id::Id, profile: usize) -> Self {
        Self {
            id: Id::new(),
            created: unix_epoch_timestamp(),
            owner,
            profile,
        }
    }
}
