use std::collections::HashSet;

use elasticsearch::Error;
use elasticsearch::http::response::Response;
use futures::executor::block_on;
use serde_json::{json, Value};

use crate::models::flickr_photo::FlickrPhoto;
use crate::models::picture::Picture;
use crate::processors::report::Report;
use crate::services::es_client::ElasticClient;

#[derive(Debug, Clone)]
pub struct ElasticHelper {
    es_client: ElasticClient,
}

impl ElasticHelper {
    pub fn new(index: &str) -> Self {
        ElasticHelper {
            es_client: ElasticClient::new(index),
        }
    }

    #[allow(unused_variables)]
    pub(crate) async fn find_pictures(&self, path: &String) -> Result<HashSet<Picture>, Vec<String>> {
        let query = json!({
                "query": {
                    "match_phrase_prefix": {
                        "path": {
                            "query" : path.as_str()
                        }
                    }
                }
            });
        let results: Result<HashSet<String>, Vec<String>> = self.get_hits_with_scroll(query).await;
        self.format_results_to_picture_list(results)
    }

    pub(crate) async fn is_picture_to_update(&self, picture: &Picture) -> bool {
        match self.get_picture(&picture.path).await {
            None => true,
            Some(picture_from_es) => {
                match picture_from_es.eq(picture) {
                    true => false,
                    false => true,
                }
            }
        }

    }

    #[allow(unused_variables)]
    async fn get_picture(&self, id: &str) -> Option<Picture> {
        let query = json!({
                "query": {
                    "term": {
                        "_id": id
                    }
                }
            });

        let results: Result<HashSet<String>, Vec<String>> = self.get_hits(query).await;
        let mut pictures = self.format_results_to_picture_list(results).expect("get hit error");
        if pictures.len() == 1 {
            let picture_list = pictures.drain().collect::<Vec<Picture>>();
            let picture = picture_list.first();
            if picture.is_some() {
                Some(picture.unwrap().clone())
            } else {
                None
            }
        } else {
            None
        }
    }

    pub(crate) async fn get_all_flickr_photos(&self) -> Result<HashSet<FlickrPhoto>, Vec<String>> {
        let query = json!({
                "query": {
                    "match_all": {}
                }
            });

        let results: Result<HashSet<String>, Vec<String>> = self.get_hits_with_scroll(query).await;
        self.format_results_to_flickr_photo_list(results)
    }

    #[allow(dead_code)]
    pub(crate) async fn get_flickr_photos_to_download(&self) -> Result<HashSet<FlickrPhoto>, Vec<String>> {
        let query = json!({
                "query": {
                    "term": {
                        "downloaded": false
                    }
                }
            });

        let results: Result<HashSet<String>, Vec<String>> = self.get_hits_with_scroll(query).await;
        self.format_results_to_flickr_photo_list(results)
    }

    #[allow(unused_variables)]
    pub(crate) async fn get_flickr_photos_to_download_by_server(&self, server: &str) -> Result<HashSet<FlickrPhoto>, Vec<String>> {
        let query = json!({
                "query": {
                    "bool": {
                        "must": [{
                            "term": {
                                "downloaded": false
                            }}, {
                            "term": {
                                "server": server
                            }}
                        ]
                    }
                }
            });

        let results: Result<HashSet<String>, Vec<String>> = self.get_hits_with_scroll(query).await;
        self.format_results_to_flickr_photo_list(results)
    }

    pub(crate) async fn is_flickr_photo_to_update(&self, photo: &FlickrPhoto) -> bool {
        match self.get_flickr_photo(ElasticClient::get_flickr_record_id(photo).as_str()).await {
            None => true,
            Some(photo_from_es) => {
                match photo_from_es.eq(photo) {
                    true => false,
                    false => true,
                }
            }
        }

    }

    #[allow(unused_variables)]
    async fn get_flickr_photo(&self, id: &str) -> Option<FlickrPhoto> {
        let query = json!({
                "query": {
                    "term": {
                        "_id": id
                    }
                }
            });

        let results: Result<HashSet<String>, Vec<String>> = self.get_hits(query).await;
        let mut photos = self.format_results_to_flickr_photo_list(results).expect("get hit error");
        if photos.len() == 1 {
            let photo_list = photos.drain().collect::<Vec<FlickrPhoto>>();
            let photo = photo_list.first();
            if photo.is_some() {
                Some(photo.unwrap().clone())
            } else {
                None
            }
        } else {
            None
        }
    }

    async fn get_hits(&self, query: Value) -> Result<HashSet<String>, Vec<String>> {
        let mut hits: HashSet<String> = HashSet::new();
        let mut errors: Vec<String> = Vec::new();

        let response = self.es_client.simple_search(query).await;
        if response.is_ok() {
            let response_body = block_on(response.unwrap().json::<Value>()).unwrap_or_default();
            let response_hits = response_body["hits"]["hits"].as_array().unwrap();
            for hit in response_hits {
                hits.insert(hit["_source"].to_string());
            }
        } else {
            errors.push(response.err().unwrap().to_string())
        }

        if errors.is_empty() {
            Ok(hits)
        } else {
            Err(errors)
        }
    }

    async fn get_hits_with_scroll(&self, query: Value) -> Result<HashSet<String>, Vec<String>> {
        let mut hits: HashSet<String> = HashSet::new();
        let mut errors: Vec<String> = Vec::new();
        let mut scroll = true;

        let response = self.es_client.scrolled_search(query).await;
        if response.is_ok() {
            let response_body = block_on(response.unwrap().json::<Value>()).unwrap_or_default();
            let scroll_id = &response_body["_scroll_id"].as_str().unwrap();
            let response_hits = response_body["hits"]["hits"].as_array().unwrap();
            for hit in response_hits {
                hits.insert(hit["_source"].to_string());
            }
            if response_hits.is_empty() {
                scroll = false
            }
            while scroll {
                scroll = self.scroll_hits(&mut hits, &mut errors, scroll_id).await
            }
            let _clear_scroll = self.es_client.clear_scroll(scroll_id);
        } else {
            errors.push(response.err().unwrap().to_string())
        }

        if errors.is_empty() {
            Ok(hits)
        } else {
            Err(errors)
        }
    }

    async fn scroll_hits(&self, hits: &mut HashSet<String>, errors: &mut Vec<String>, scroll_id: &&str) -> bool {
        let mut scroll = true;
        let scroll_response = self.es_client.scroll(scroll_id).await;
        if scroll_response.is_ok() {
            let response_body = block_on(scroll_response.unwrap().json::<Value>()).unwrap_or_default();
            let response_hits = response_body["hits"]["hits"].as_array().unwrap();
            for hit in response_hits {
                hits.insert(hit["_source"].to_string());
            }
            if response_hits.is_empty() {
                scroll = false
            }
        } else {
            errors.push(scroll_response.err().unwrap().to_string())
        }
        scroll
    }

    fn format_results_to_picture_list(&self, results: Result<HashSet<String>, Vec<String>>) -> Result<HashSet<Picture>, Vec<String>> {
        if results.is_ok() {
            let mut files: HashSet<Picture> = HashSet::new();
            for result in results.unwrap() {
                files.insert(serde_json::from_str(result.as_str()).unwrap());
            }
            Ok(files)
        } else {
            Err(results.err().unwrap())
        }
    }

    fn format_results_to_flickr_photo_list(&self, results: Result<HashSet<String>, Vec<String>>) -> Result<HashSet<FlickrPhoto>, Vec<String>> {
        if results.is_ok() {
            let mut files: HashSet<FlickrPhoto> = HashSet::new();
            for result in results.unwrap() {
                files.insert(serde_json::from_str(result.as_str()).unwrap());
            }
            Ok(files)
        } else {
            Err(results.err().unwrap())
        }
    }

    pub(crate) fn generate_harvest_report(harvest_report: &mut Report, errors: &mut Vec<String>, response: Result<Response, Error>) {
        if response.is_ok() {
            harvest_report.files_processed += 1;
            let response_body = block_on(response.unwrap().json::<Value>()).unwrap_or_default();
            if response_body["errors"].as_bool().unwrap_or_default() {
                harvest_report.files_aborted += 1;
                println!("files harvesting aborted for : {:?}", response_body["errors"].as_str());
            }
        } else {
            harvest_report.errors += 1;
            let error = response.err().unwrap().to_string();
            errors.push(error.clone());
            println!("import aborted : {}", error);
        }
    }

    pub(crate) fn finalize_clean_report(clean_report: &mut Report, errors: &mut Vec<String>, deletion_response: Result<Response, Error>) {
        if deletion_response.is_ok() {
            clean_report.files_deleted += 1
        } else {
            clean_report.errors += 1;
            let error = deletion_response.err().unwrap().to_string();
            errors.push(error.clone());
            println!("deletion aborted : {}", error)
        }
    }

    #[allow(dead_code)]
    fn set_es_client(&mut self, index: &str) {
        self.es_client = ElasticClient::new(index)
    }
}
