use super::{render_error, PaginatedQuery, ProfileQuery};
use crate::{
    assets::initial_context, check_user_blocked_or_private, get_lang, get_user_from_token,
    sanitize::clean_settings, State,
};
use axum::{
    Extension,
    extract::{Path, Query},
    response::{Html, IntoResponse},
};
use axum_extra::extract::CookieJar;
use serde::Deserialize;
use tera::Context;
use tetratto_core::model::{auth::User, communities::Community, permissions::FinePermission, Error};
use tetratto_shared::hash::hash;
use contrasted::{Color, MINIMUM_CONTRAST_THRESHOLD};

#[derive(Deserialize)]
pub struct SettingsProps {
    #[serde(default)]
    pub username: String,
}

/// `/settings`
pub async fn settings_request(
    jar: CookieJar,
    Extension(data): Extension<State>,
    Query(req): Query<SettingsProps>,
) -> impl IntoResponse {
    let data = data.read().await;
    let user = match get_user_from_token!(jar, data.0) {
        Some(ua) => ua,
        None => {
            return Err(Html(
                render_error(Error::NotAllowed, &jar, &data, &None).await,
            ));
        }
    };

    let profile = if req.username.is_empty() | !user.permissions.check(FinePermission::MANAGE_USERS)
    {
        user.clone()
    } else {
        match data.0.get_user_by_username(&req.username).await {
            Ok(ua) => ua,
            Err(e) => {
                return Err(Html(render_error(e, &jar, &data, &None).await));
            }
        }
    };

    let stacks = match data.0.get_stacks_by_owner(profile.id).await {
        Ok(ua) => ua,
        Err(e) => {
            return Err(Html(render_error(e, &jar, &data, &None).await));
        }
    };

    let blocks = match data
        .0
        .fill_userblocks_receivers(data.0.get_userblocks_by_initiator(profile.id).await)
        .await
    {
        Ok(r) => r,
        Err(e) => return Err(Html(render_error(e, &jar, &data, &None).await)),
    };

    let tokens = profile.tokens.clone();

    let lang = get_lang!(jar, data.0);
    let mut context = initial_context(&data.0.0, lang, &Some(user)).await;

    context.insert("profile", &profile);
    context.insert("stacks", &stacks);
    context.insert("blocks", &blocks);
    context.insert("user_settings_serde", &clean_settings(&profile.settings));
    context.insert(
        "user_tokens_serde",
        &serde_json::to_string(&tokens)
            .unwrap()
            .replace("\"", "\\\""),
    );

    // check color contrasts
    let mut failing_color_keys: Vec<(&str, f64)> = Vec::new();
    let settings_map = serde_json::from_str::<serde_json::Map<String, serde_json::Value>>(
        &serde_json::to_string(&profile.settings).unwrap(),
    )
    .unwrap();

    let light = serde_json::Value::from("Light");
    let mut profile_theme = settings_map
        .get("profile_theme")
        .unwrap_or(&light)
        .as_str()
        .unwrap();

    if profile_theme.is_empty() | (profile_theme == "Auto") {
        profile_theme = "Light";
    }

    let default_surface = serde_json::Value::from(if profile_theme == "Light" {
        "#f3f2f1"
    } else {
        "#19171c"
    });

    let mut color_surface = settings_map
        .get("theme_color_surface")
        .unwrap_or(&default_surface)
        .as_str()
        .unwrap();

    if color_surface.is_empty() {
        color_surface = default_surface.as_str().unwrap();
    }

    for setting in &settings_map {
        if !setting.0.starts_with("theme_color_text")
            | (setting.0 == "theme_color_text_primary")
            | (setting.0 == "theme_color_text_secondary")
        {
            continue;
        }

        let value = setting.1.as_str().unwrap();

        if !value.starts_with("#") {
            // we can only parse hex right now
            continue;
        }

        let c1 = Color::from(color_surface);
        let c2 = Color::from(value);
        let contrast = c1.contrast(&c2);

        if contrast < MINIMUM_CONTRAST_THRESHOLD {
            failing_color_keys.push((setting.0, contrast));
        }
    }

    context.insert("failing_color_keys", &failing_color_keys);

    // return
    Ok(Html(
        data.1.render("profile/settings.html", &context).unwrap(),
    ))
}

pub fn profile_context(
    context: &mut Context,
    user: &Option<User>,
    profile: &User,
    communities: &Vec<Community>,
    is_self: bool,
    is_following: bool,
    is_following_you: bool,
    is_blocking: bool,
) {
    context.insert("profile", &profile);
    context.insert("communities", &communities);
    context.insert("is_self", &is_self);
    context.insert("is_following", &is_following);
    context.insert("is_following_you", &is_following_you);
    context.insert("is_blocking", &is_blocking);
    context.insert("warning_hash", &hash(profile.settings.warning.clone()));

    context.insert(
        "is_supporter",
        &profile.permissions.check(FinePermission::SUPPORTER),
    );

    if let Some(ua) = user {
        if !ua.settings.disable_other_themes | is_self {
            context.insert("use_user_theme", &false);
        }
    } else {
        context.insert("use_user_theme", &false);
    }
}

/// `/@{username}`
pub async fn posts_request(
    jar: CookieJar,
    Path(username): Path<String>,
    Query(props): Query<ProfileQuery>,
    Extension(data): Extension<State>,
) -> impl IntoResponse {
    let data = data.read().await;
    let user = get_user_from_token!(jar, data.0);

    let other_user = match data.0.get_user_by_username(&username).await {
        Ok(ua) => ua,
        Err(e) => return Err(Html(render_error(e, &jar, &data, &user).await)),
    };

    check_user_blocked_or_private!(user, other_user, data, jar);

    // check for warning
    if props.warning {
        let lang = get_lang!(jar, data.0);
        let mut context = initial_context(&data.0.0, lang, &user).await;

        context.insert("profile", &other_user);
        context.insert("warning_hash", &hash(other_user.settings.warning.clone()));

        return Ok(Html(
            data.1.render("profile/warning.html", &context).unwrap(),
        ));
    }

    // fetch data
    let ignore_users = if let Some(ref ua) = user {
        data.0.get_userblocks_receivers(ua.id).await
    } else {
        Vec::new()
    };

    let posts = if props.tag.is_empty() {
        match data
            .0
            .get_posts_by_user(other_user.id, 12, props.page, &user)
            .await
        {
            Ok(p) => match data
                .0
                .fill_posts_with_community(
                    p,
                    if let Some(ref ua) = user { ua.id } else { 0 },
                    &ignore_users,
                )
                .await
            {
                Ok(p) => p,
                Err(e) => return Err(Html(render_error(e, &jar, &data, &user).await)),
            },
            Err(e) => return Err(Html(render_error(e, &jar, &data, &user).await)),
        }
    } else {
        match data
            .0
            .get_posts_by_user_tag(other_user.id, &props.tag, 12, props.page, &user)
            .await
        {
            Ok(p) => match data
                .0
                .fill_posts_with_community(
                    p,
                    if let Some(ref ua) = user { ua.id } else { 0 },
                    &ignore_users,
                )
                .await
            {
                Ok(p) => p,
                Err(e) => return Err(Html(render_error(e, &jar, &data, &user).await)),
            },
            Err(e) => return Err(Html(render_error(e, &jar, &data, &user).await)),
        }
    };

    let pinned = if props.tag.is_empty() {
        match data.0.get_pinned_posts_by_user(other_user.id).await {
            Ok(p) => match data
                .0
                .fill_posts_with_community(
                    p,
                    if let Some(ref ua) = user { ua.id } else { 0 },
                    &ignore_users,
                )
                .await
            {
                Ok(p) => Some(p),
                Err(e) => return Err(Html(render_error(e, &jar, &data, &user).await)),
            },
            Err(e) => return Err(Html(render_error(e, &jar, &data, &user).await)),
        }
    } else {
        None
    };

    let communities = match data.0.get_memberships_by_owner(other_user.id).await {
        Ok(m) => match data.0.fill_communities(m).await {
            Ok(m) => m,
            Err(e) => return Err(Html(render_error(e, &jar, &data, &user).await)),
        },
        Err(e) => return Err(Html(render_error(e, &jar, &data, &user).await)),
    };

    // init context
    let lang = get_lang!(jar, data.0);
    let mut context = initial_context(&data.0.0, lang, &user).await;

    let is_self = if let Some(ref ua) = user {
        ua.id == other_user.id
    } else {
        false
    };

    let is_following = if let Some(ref ua) = user {
        data.0
            .get_userfollow_by_initiator_receiver(ua.id, other_user.id)
            .await
            .is_ok()
    } else {
        false
    };

    let is_following_you = if let Some(ref ua) = user {
        data.0
            .get_userfollow_by_receiver_initiator(ua.id, other_user.id)
            .await
            .is_ok()
    } else {
        false
    };

    let is_blocking = if let Some(ref ua) = user {
        data.0
            .get_userblock_by_initiator_receiver(ua.id, other_user.id)
            .await
            .is_ok()
    } else {
        false
    };

    context.insert("posts", &posts);
    context.insert("pinned", &pinned);
    context.insert("page", &props.page);
    context.insert("tag", &props.tag);
    profile_context(
        &mut context,
        &user,
        &other_user,
        &communities,
        is_self,
        is_following,
        is_following_you,
        is_blocking,
    );

    // return
    Ok(Html(data.1.render("profile/posts.html", &context).unwrap()))
}

/// `/@{username}/following`
pub async fn following_request(
    jar: CookieJar,
    Path(username): Path<String>,
    Query(props): Query<PaginatedQuery>,
    Extension(data): Extension<State>,
) -> impl IntoResponse {
    let data = data.read().await;
    let user = get_user_from_token!(jar, data.0);

    let other_user = match data.0.get_user_by_username(&username).await {
        Ok(ua) => ua,
        Err(e) => return Err(Html(render_error(e, &jar, &data, &user).await)),
    };

    check_user_blocked_or_private!(user, other_user, data, jar);

    // check for private profile
    if other_user.settings.private_profile {
        if let Some(ref ua) = user {
            if ua.id != other_user.id
                && data
                    .0
                    .get_userfollow_by_initiator_receiver(other_user.id, ua.id)
                    .await
                    .is_err()
            {
                return Err(Html(
                    render_error(Error::NotAllowed, &jar, &data, &user).await,
                ));
            }
        } else {
            return Err(Html(
                render_error(Error::NotAllowed, &jar, &data, &user).await,
            ));
        }
    }

    // fetch data
    let list = match data
        .0
        .get_userfollows_by_initiator(other_user.id, 12, props.page)
        .await
    {
        Ok(l) => match data.0.fill_userfollows_with_receiver(l).await {
            Ok(l) => l,
            Err(e) => return Err(Html(render_error(e, &jar, &data, &user).await)),
        },
        Err(e) => return Err(Html(render_error(e, &jar, &data, &user).await)),
    };

    let communities = match data.0.get_memberships_by_owner(other_user.id).await {
        Ok(m) => match data.0.fill_communities(m).await {
            Ok(m) => m,
            Err(e) => return Err(Html(render_error(e, &jar, &data, &user).await)),
        },
        Err(e) => return Err(Html(render_error(e, &jar, &data, &user).await)),
    };

    // init context
    let lang = get_lang!(jar, data.0);
    let mut context = initial_context(&data.0.0, lang, &user).await;

    let is_self = if let Some(ref ua) = user {
        ua.id == other_user.id
    } else {
        false
    };

    let is_following = if let Some(ref ua) = user {
        data.0
            .get_userfollow_by_initiator_receiver(ua.id, other_user.id)
            .await
            .is_ok()
    } else {
        false
    };

    let is_following_you = if let Some(ref ua) = user {
        data.0
            .get_userfollow_by_receiver_initiator(ua.id, other_user.id)
            .await
            .is_ok()
    } else {
        false
    };

    let is_blocking = if let Some(ref ua) = user {
        data.0
            .get_userblock_by_initiator_receiver(ua.id, other_user.id)
            .await
            .is_ok()
    } else {
        false
    };

    context.insert("list", &list);
    context.insert("page", &props.page);
    profile_context(
        &mut context,
        &user,
        &other_user,
        &communities,
        is_self,
        is_following,
        is_following_you,
        is_blocking,
    );

    // return
    Ok(Html(
        data.1.render("profile/following.html", &context).unwrap(),
    ))
}

/// `/@{username}/followers`
pub async fn followers_request(
    jar: CookieJar,
    Path(username): Path<String>,
    Query(props): Query<PaginatedQuery>,
    Extension(data): Extension<State>,
) -> impl IntoResponse {
    let data = data.read().await;
    let user = get_user_from_token!(jar, data.0);

    let other_user = match data.0.get_user_by_username(&username).await {
        Ok(ua) => ua,
        Err(e) => return Err(Html(render_error(e, &jar, &data, &user).await)),
    };

    check_user_blocked_or_private!(user, other_user, data, jar);

    // fetch data
    let list = match data
        .0
        .get_userfollows_by_receiver(other_user.id, 12, props.page)
        .await
    {
        Ok(l) => match data.0.fill_userfollows_with_initiator(l).await {
            Ok(l) => l,
            Err(e) => return Err(Html(render_error(e, &jar, &data, &user).await)),
        },
        Err(e) => return Err(Html(render_error(e, &jar, &data, &user).await)),
    };

    let communities = match data.0.get_memberships_by_owner(other_user.id).await {
        Ok(m) => match data.0.fill_communities(m).await {
            Ok(m) => m,
            Err(e) => return Err(Html(render_error(e, &jar, &data, &user).await)),
        },
        Err(e) => return Err(Html(render_error(e, &jar, &data, &user).await)),
    };

    // init context
    let lang = get_lang!(jar, data.0);
    let mut context = initial_context(&data.0.0, lang, &user).await;

    let is_self = if let Some(ref ua) = user {
        ua.id == other_user.id
    } else {
        false
    };

    let is_following = if let Some(ref ua) = user {
        data.0
            .get_userfollow_by_initiator_receiver(ua.id, other_user.id)
            .await
            .is_ok()
    } else {
        false
    };

    let is_following_you = if let Some(ref ua) = user {
        data.0
            .get_userfollow_by_receiver_initiator(ua.id, other_user.id)
            .await
            .is_ok()
    } else {
        false
    };

    let is_blocking = if let Some(ref ua) = user {
        data.0
            .get_userblock_by_initiator_receiver(ua.id, other_user.id)
            .await
            .is_ok()
    } else {
        false
    };

    context.insert("list", &list);
    context.insert("page", &props.page);
    profile_context(
        &mut context,
        &user,
        &other_user,
        &communities,
        is_self,
        is_following,
        is_following_you,
        is_blocking,
    );

    // return
    Ok(Html(
        data.1.render("profile/followers.html", &context).unwrap(),
    ))
}