use std::cmp::Ordering;
use crate::config::{PostConfig, SeriesConfig};
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_db_schema::newtypes::{CommunityId, LanguageId, PostId};
use lemmy_db_schema::{ListingType, PostFeatureType};
use reqwest::StatusCode;
use std::collections::HashMap;
use lemmy_db_schema::sensitive::SensitiveString;
use serde::{Deserialize, Serialize};
use crate::logging::Logging;
use crate::settings::{BotSettings, PostBody};

pub(crate) struct Lemmy {
    jwt_token: SensitiveString,
    instance: String,
    communities: HashMap<String, CommunityId>,
}


#[derive(Debug, Clone)]
pub(crate) struct PostInfoInner {
    pub(crate) title: String,
    pub(crate) url: String,
    pub(crate) thumbnail: Option<String>
}

#[derive(Debug, Copy, Clone)]
pub(crate) enum PartInfo {
    NoParts,
    Part(u8),
}

impl PartInfo {
    pub(crate) fn as_u8(&self) -> u8 {
        match self {
            PartInfo::Part(number) => *number,
            PartInfo::NoParts => 0,
        }
    }

    pub(crate) fn as_string(&self) -> String {
        self.as_u8().to_string()
    }
}

impl PartialEq for PartInfo {
    fn eq(&self, other: &Self) -> bool {
        let self_numeric = self.as_u8();
        let other_numeric = other.as_u8();
        self_numeric == other_numeric
    }
}

impl PartialOrd for PartInfo {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        if self.gt(other) {
            Some(Ordering::Greater)
        } else if self.eq(other) {
            Some(Ordering::Equal)
        } else {
            Some(Ordering::Less)
        }
    }

    fn lt(&self, other: &Self) -> bool {
        let self_numeric = self.as_u8();
        let other_numeric = other.as_u8();

        self_numeric < other_numeric
    }

    fn le(&self, other: &Self) -> bool {
        !self.gt(other)
    }

    fn gt(&self, other: &Self) -> bool {
        let self_numeric = self.as_u8();
        let other_numeric = other.as_u8();

        self_numeric > other_numeric
    }

    fn ge(&self, other: &Self) -> bool {
        !self.lt(other)
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub(crate) enum PostType {
    Chapter,
    Volume
}

#[derive(Debug, Clone)]
pub(crate) struct PostInfo {
    pub(crate) part: Option<PartInfo>,
    pub(crate) lemmy_info: PostInfoInner,
    pub(crate) description: Option<String>,
    pub(crate) post_type: Option<PostType>
}

impl PostInfo {
    pub(crate)fn get_info(&self) -> PostInfoInner {
        self.lemmy_info.clone()
    }

    pub(crate)fn get_description(&self) -> Option<String> {
        self.description.clone()
    }

    pub(crate) fn get_part_info(&self) -> Option<PartInfo> {
        self.part
    }

    pub(crate) fn get_post_config(&self, series: &SeriesConfig) -> PostConfig {
        match self.post_type {
            Some(post_type) => {
                match post_type {
                    PostType::Chapter => series.prepub_community.clone(),
                    PostType::Volume => series.volume_community.clone(),
                }
            }
                None => series.prepub_community.clone(),
        }
    }

    pub(crate) fn get_post_data(&self, series: &SeriesConfig, lemmy: &Lemmy) -> CreatePost {
        let post_config = self.get_post_config(series);

        let post_body = match &post_config.post_body {
            PostBody::None => None,
            PostBody::Description => self.get_description(),
            PostBody::Custom(text) => Some(text.clone()),
        };

        let community_id: CommunityId = lemmy.get_community_id(&post_config.name);

        CreatePost {
            name: self.get_info().title.clone(),
            community_id,
            url: Some(self.get_info().url),
            custom_thumbnail: self.get_info().thumbnail,
            body: post_body,
            alt_text: None,
            honeypot: None,
            nsfw: None,
            language_id: Some(LanguageId(37)), // TODO get this id once every few hours per API request, the ordering of IDs suggests that the EN Id might change in the future
        }
    }
}

impl PartialEq for PostInfo {
    fn eq(&self, other: &Self) -> bool {
        self.part.eq(&other.part)
    }
}

impl PartialOrd for PostInfo {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        if self.gt(other) {
            Some(Ordering::Greater)
        } else if self.eq(other) {
            Some(Ordering::Equal)
        } else {
            Some(Ordering::Less)
        }
    }

    fn lt(&self, other: &Self) -> bool {
        self.part < other.part
    }

    fn le(&self, other: &Self) -> bool {
        !self.gt(other)
    }

    fn gt(&self, other: &Self) -> bool {
        self.part > other.part
    }

    fn ge(&self, other: &Self) -> bool {
        !self.lt(other)
    }
}

impl Lemmy {
    pub(crate) fn get_community_id(&self, name: &str) -> CommunityId {
        *self.communities.get(name).expect(format!("Community '{name}' is invalid").as_str())
    }
    pub(crate) fn new(config: &BotSettings) -> Result<Self, ()> {
        let login_params = Login {
            username_or_email: config.username(),
            password: config.password(),
            totp_2fa_token: None,
        };

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

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

    pub fn logout(&self) {
        let _ = self.post_data_json("/api/v3/user/logout", &"");
    }

    pub fn login(&self) {
        
    }

    pub fn post(&self, post: CreatePost) -> Option<PostId> {
        let response: String = match self.post_data_json("/api/v3/post", &post) {
            Some(data) => data,
            None => return None,
        };
        let json_data: PostView = match self.parse_json_map(&response) {
            Some(data) => data,
            None => return None,
        };

        Some(json_data.post.id)
    }

    fn feature(&self, params: FeaturePost) -> Option<PostView> {
        let response: String = match self.post_data_json("/api/v3/post/feature", &params) {
            Some(data) => data,
            None => return None,
        };
        let json_data: PostView = match self.parse_json_map(&response) {
            Some(data) => data,
            None => return None,
        };

        Some(json_data)
    }

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

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

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

        let response: String = match self.get_data_query("/api/v3/post/list", &list_params) {
            Some(data) => data,
            None => return None,
        };
        let json_data: GetPostsResponse = match self.parse_json(&response) {
            Some(data) => data,
            None => return None,
        };

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

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

        let response: String = match self.get_data_query("/api/v3/post/list", &list_params) {
            Some(data) => data,
            None => return None,
        };
        let json_data: GetPostsResponse = match self.parse_json(&response) {
            Some(data) => data,
            None => return None,
        };

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

    pub(crate) fn get_communities(&mut self) {
        let list_params = ListCommunities {
            type_: Some(ListingType::Local),
            ..Default::default()
        };

        let response: String = match self.get_data_query("/api/v3/community/list", &list_params) {
            Some(data) => data,
            None => return,
        };
        let json_data: ListCommunitiesResponse = match self.parse_json::<ListCommunitiesResponse>(&response) {
            Some(data) => data,
            None => return,
        };

        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);
        }

        self.communities = communities;
    }

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

    fn get_data_query<T: Serialize>(&self, route: &str, param: &T ) -> Option<String> {
        let res = HTTP_CLIENT
            .get(format!("{}{route}", &self.instance))
            .bearer_auth(&self.jwt_token.to_string())
            .query(&param)
            .send();
        self.extract_data(res)
    }
    
    fn extract_data(&self, response: Result<reqwest::blocking::Response, reqwest::Error>) -> Option<String> {
        match response {
            Ok(data) => {
                let msg = format!("Status Code: '{}'", data.status().clone());
                Logging::debug(msg.as_str());
                if data.status().is_success() {
                    match data.text() {
                        Ok(data) => Some(data),
                        Err(e) => {
                            let err_msg = format!("{e}");
                            Logging::error(err_msg.as_str());
                            None
                        }
                    }
                }
                else {
                    let err_msg = format!("HTTP Request failed: {}", data.text().unwrap());
                    Logging::error(err_msg.as_str());
                    None
                }
            },
            Err(e) => {
                let err_msg = format!("{e}");
                Logging::error(err_msg.as_str());
                None
            }
        }
    }
    
    fn parse_json<'a, T: Deserialize<'a>>(&self, response: &'a str) -> Option<T> {
        match serde_json::from_str::<T>(response) {
            Ok(data) => Some(data),
            Err(e) => {
                let err_msg = format!("while parsing JSON: {e} ");
                Logging::error(err_msg.as_str());
                None
            }
        }
    }

    fn parse_json_map<'a, T: Deserialize<'a>>(&self, response: &'a str) -> Option<T> {
        Logging::debug(response);
        match serde_json::from_str::<HashMap<&str, T>>(response) {
            Ok(mut data) => Some(data.remove("post_view").expect("Element should be present")),
            Err(e) => {
                let err_msg = format!("while parsing JSON HashMap: {e}");
                Logging::error(err_msg.as_str());
                None
            }
        }
    }
}