use crate::config::Config;
use crate::{HTTP_CLIENT};
use lemmy_api_common::community::{ListCommunities, ListCommunitiesResponse};
use lemmy_api_common::lemmy_db_views::structs::PostView;
use lemmy_api_common::person::{Login, LoginResponse};
use lemmy_api_common::post::{CreatePost, FeaturePost, GetPosts, GetPostsResponse};
use lemmy_api_common::sensitive::Sensitive;
use lemmy_db_schema::newtypes::{CommunityId, PostId};
use lemmy_db_schema::{ListingType, PostFeatureType};
use reqwest::StatusCode;
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use url::Url;
use systemd_journal_logger::connected_to_journal;

macro_rules! error {
    ($msg:tt) => {
        match connected_to_journal() {
            true => log::error!("[ERROR] {}", $msg),
            false => eprintln!("[ERROR] {}", $msg),
        }
    };
}

pub(crate) struct Lemmy {
    jwt_token: Sensitive<String>,
    instance: String,
}


#[derive(Debug, Clone)]
pub(crate) struct PostInfoInner {
    pub(crate) title: String,
    pub(crate) url: Url,
}

pub(crate) trait PostInfo {
    fn get_info(&self) -> PostInfoInner;

    fn get_description(&self) -> Option<String>;
}

pub(crate) async fn login(config: &Config) -> Result<Lemmy, ()> {
    let login_params = Login {
        username_or_email: config.get_username(),
        password: config.get_password(),
        totp_2fa_token: None,
    };

    let response = match HTTP_CLIENT
        .post(config.instance.to_owned() + "/api/v3/user/login")
        .json(&login_params)
        .send()
        .await
    {
        Ok(data) => data,
        Err(e) => {
            let err_msg = format!("{e}");
            error!(err_msg);
            return Err(());
        }
    };

    match response.status() {
        StatusCode::OK => {
            let data: LoginResponse = response
                .json()
                .await
                .expect("Successful Login Request should return JSON");
            match data.jwt {
                Some(token) => Ok(Lemmy {
                    jwt_token: token.clone(),
                    instance: config.instance.to_owned(),
                }),
                None => {
                    let err_msg = "Login did not return JWT token. Are the credentials valid?".to_owned();
                    error!(err_msg);
                    Err(())
                }
            }
        }
        status => {
            let err_msg = format!("Unexpected HTTP Status '{}' during Login", status);
            error!(err_msg);
            Err(())
        }
    }
}

impl Lemmy {
    pub(crate) async fn post(&self, post: CreatePost) -> Result<PostId, ()> {
        let response: String = self.post_data_json("/api/v3/post", &post).await?;
        let json_data: PostView = self.parse_json(&response).await?;

        Ok(json_data.post.id)
    }

    async fn feature(&self, params: FeaturePost) -> Result<PostView, ()> {
        let response: String = self.post_data_json("/api/v3/post/feature", &params).await?;
        let json_data: PostView = self.parse_json(&response).await?;

        Ok(json_data)
    }

    pub(crate) async fn unpin(&self, post_id: PostId, location: PostFeatureType) -> Result<PostView, ()> {
        let pin_params = FeaturePost {
            post_id,
            featured: false,
            feature_type: location,
        };
        self.feature(pin_params).await
    }

    pub(crate) async fn pin(&self, post_id: PostId, location: PostFeatureType) -> Result<PostView, ()> {
        let pin_params = FeaturePost {
            post_id,
            featured: true,
            feature_type: location,
        };
        self.feature(pin_params).await
    }

    pub(crate) async fn get_community_pinned(&self, community: CommunityId) -> Result<Vec<PostView>, ()> {
        let list_params = GetPosts {
            community_id: Some(community),
            type_: Some(ListingType::Local),
            ..Default::default()
        };

        let response: String = self.get_data_query("/api/v3/post/list", &list_params).await?;
        let json_data: GetPostsResponse = self.parse_json(&response).await?;

        Ok(json_data
            .posts
            .iter()
            .filter(|post| post.post.featured_community)
            .cloned()
            .collect())
    }

    pub(crate) async fn get_local_pinned(&self) -> Result<Vec<PostView>, ()> {
        let list_params = GetPosts {
            type_: Some(ListingType::Local),
            ..Default::default()
        };

        let response: String = self.get_data_query("/api/v3/post/list", &list_params).await?;
        let json_data: GetPostsResponse = self.parse_json(&response).await?;

        Ok(json_data
            .posts
            .iter()
            .filter(|post| post.post.featured_local)
            .cloned()
            .collect())
    }

    pub(crate) async fn get_communities(&self) -> Result<HashMap<String, CommunityId>, ()> {
        let list_params = ListCommunities {
            type_: Some(ListingType::Local),
            ..Default::default()
        };

        let response: String = self.get_data_query("/api/v3/community/list", &list_params).await?;
        let json_data: ListCommunitiesResponse = self.parse_json(&response).await?;

        let mut communities: HashMap<String, CommunityId> = HashMap::new();
        for community_view in json_data.communities {
            let community = community_view.community;
            communities.insert(community.name, community.id);
        }

        Ok(communities)
    }

    async fn post_data_json<T: Serialize>(&self, route: &str, json: &T ) -> Result<String,()> {
        let res = HTTP_CLIENT
            .post(format!("{}{route}", &self.instance))
            .bearer_auth(&self.jwt_token.to_string())
            .json(&json)
            .send()
            .await;
        self.extract_data(res).await
    }

    async fn get_data_query<T: Serialize>(&self, route: &str, param: &T ) -> Result<String,()> {
        let res = HTTP_CLIENT
            .get(format!("{}{route}", &self.instance))
            .bearer_auth(&self.jwt_token.to_string())
            .query(&param)
            .send()
            .await;
        self.extract_data(res).await
    }
    
    async fn extract_data(&self, response: Result<reqwest::Response, reqwest::Error>) -> Result<String,()> {
        match response {
            Ok(data) => match data.text().await {
                Ok(data) => Ok(data),
                Err(e) => {
                    let err_msg = format!("{e}");
                    error!(err_msg);
                    Err(())
                }
            },
            Err(e) => {
                let err_msg = format!("{e}");
                error!(err_msg);
                Err(())
            }
        }
    }
    
    async fn parse_json<'a, T: Deserialize<'a>>(&self, response: &'a str) -> Result<T,()> {
        match serde_json::from_str::<HashMap<&str, T>>(response) {
            Ok(mut data) => Ok(data.remove("post_view").expect("Element should be present")),
            Err(e) => {
                let err_msg = format!("{e}");
                error!(err_msg);
                Err(())
            }
        }
    }
}