use crate::error::{Error, Result};
use crate::protocol::{
    ApiResponse, BooleanQuery, DocumentSet, Field, IndexDescription, Query, RangeQuery,
    SearchRequest, SearchResponse, UpdateDocumentsRequest, UpdateIndexRequest,
};
use log::debug;
use reqwest::{Response, StatusCode};
use serde::de::DeserializeOwned;
use std::{
    collections::{HashMap, HashSet},
    vec,
};
use url::Url;

pub enum Client {
    Concrete {
        base_url: String,
    },
    Dummy {
        indexes: HashSet<String>,
        version: u64,
    },
}

impl Client {
    pub async fn get_indexes(&self) -> Result<Vec<IndexDescription>> {
        match self {
            Client::Concrete { ref base_url } => Self::get_indexes_concrete(base_url).await,
            Client::Dummy {
                ref indexes,
                version,
            } => Self::get_indexes_dummy(indexes, *version).await,
        }
    }
    pub async fn search(
        &self,
        index_name: impl AsRef<str>,
        collection_id: u64,
        tenant_token: impl Into<String>,
        query: Query,
        limit: Option<u32>,
        max_version: Option<u64>,
    ) -> Result<SearchResponse> {
        match self {
            Client::Concrete { ref base_url } => {
                Self::search_concrete(
                    base_url,
                    index_name,
                    max_version,
                    collection_id,
                    tenant_token,
                    query,
                    limit,
                )
                .await
            }
            Client::Dummy {
                ref indexes,
                version,
            } => Self::search_dummy(indexes, *version).await,
        }
    }

    pub async fn get_versions(&self, index_name: &str) -> Result<(u64, u64)> {
        match self {
            Client::Concrete { ref base_url } => Ok(Self::client()?
                .get(format!("{}/indexes/{}/version", base_url, index_name))
                .timeout(std::time::Duration::from_millis(300))
                .send()
                .await?
                .json()
                .await
                .map(Self::debug_log)?),
            Client::Dummy { version, .. } => Ok((*version, *version)),
        }
    }
    pub async fn reload_index(&self) -> Result<u64> {
        match self {
            Client::Concrete { ref base_url } => Self::reload_index_concrete(base_url).await,
            Client::Dummy {
                ref indexes,
                version,
            } => Self::reload_index_dummy(indexes, *version).await,
        }
    }
    pub async fn update_index(&self, version: u64) -> Result<u64> {
        match self {
            Client::Concrete { ref base_url } => {
                Self::update_index_concrete(base_url, version).await
            }
            Client::Dummy {
                ref indexes,
                version,
            } => Self::update_index_dummy(indexes, *version).await,
        }
    }
    pub async fn update_documents(
        &self,
        index_name: impl AsRef<str>,
        version: u64,
        sets: Vec<DocumentSet>,
    ) -> Result<u64> {
        match self {
            Client::Concrete { ref base_url } => {
                Self::update_documents_concrete(base_url, index_name, version, sets).await
            }
            Client::Dummy {
                ref indexes,
                version,
            } => Self::update_documents_dummy(indexes, *version, sets).await,
        }
    }

    pub fn new(base_url: String) -> Result<Self> {
        let url = Url::parse(&base_url)?;
        let mut indexes = HashSet::new();
        let mut version = 0;
        for param in url.query_pairs() {
            match param.0.as_ref() {
                "index" => {
                    indexes.insert(param.1.to_string());
                }
                "version" => version = param.1.as_ref().parse::<u64>()?,
                _ => (),
            }
        }
        Ok(match url.scheme() {
            "dummy" => Self::Dummy { indexes, version },
            "https" => Self::Concrete { base_url },
            _ => {
                return Err(Error::new(format!(
                    "Invalid base url for FTS client: '{}'",
                    &base_url
                )))
            }
        })
    }

    fn client() -> Result<reqwest::Client> {
        Ok(reqwest::Client::builder()
            .danger_accept_invalid_certs(true)
            .timeout(std::time::Duration::from_secs(10))
            .build()?)
    }

    async fn extract<T>(response: Response) -> Result<T>
    where
        T: DeserializeOwned,
    {
        match response.status() {
            StatusCode::OK => Ok(response
                .json::<ApiResponse<T>>()
                .await
                .map(Self::debug_log)?),
            code => {
                let body = response.text().await?;
                Err(if body.is_empty() {
                    Error::new(code.to_string())
                } else {
                    Error::new(body)
                })
            }
        }
    }

    fn debug_log<T>(response: ApiResponse<T>) -> T {
        debug!("Executing request took {}s", response.time);
        response.response
    }

    async fn get_indexes_concrete(base_url: &str) -> Result<Vec<IndexDescription>> {
        Ok(Self::client()?
            .get(format!("{}/indexes", base_url))
            .send()
            .await?
            .json()
            .await
            .map(Self::debug_log)?)
    }

    async fn search_concrete(
        base_url: &str,
        index_name: impl AsRef<str>,
        max_version: Option<u64>,
        collection_id: u64,
        tenant_token: impl Into<String>,
        query: Query,
        limit: Option<u32>,
    ) -> Result<SearchResponse> {
        let mut query: Query = query;
        if let Some(max_version) = max_version {
            query = Query::Boolean(BooleanQuery::must(vec![
                query,
                Query::Range(RangeQuery {
                    field: Field::Version,
                    start: Some(max_version as i64),
                    end: None,
                }),
            ]));
        }
        Self::extract(
            Self::client()?
                .post(format!(
                    "{}/indexes/{}/search",
                    base_url,
                    index_name.as_ref()
                ))
                .timeout(std::time::Duration::from_secs(2))
                .json(&SearchRequest {
                    tenant_token: tenant_token.into(),
                    collection_id,
                    query,
                    limit,
                })
                .send()
                .await?,
        )
        .await
    }

    async fn reload_index_concrete(base_url: &str) -> Result<u64> {
        Ok(Self::client()?
            .post(format!("{}/indexes", base_url))
            .json(&UpdateIndexRequest::Reload)
            .send()
            .await?
            .json()
            .await
            .map(Self::debug_log)?)
    }

    async fn update_index_concrete(base_url: &str, version: u64) -> Result<u64> {
        let timeout = std::env::var("UPDATE_INDEX_TIMEOUT")
            .ok()
            .and_then(|val| val.parse::<u64>().ok())
            .unwrap_or(120);
        Ok(Self::client()?
            .post(format!("{}/indexes", base_url))
            .timeout(std::time::Duration::from_secs(timeout))
            .json(&UpdateIndexRequest::Update { version })
            .send()
            .await?
            .json()
            .await
            .map(Self::debug_log)?)
    }

    async fn update_documents_concrete(
        base_url: &str,
        index_name: impl AsRef<str>,
        version: u64,
        sets: Vec<DocumentSet>,
    ) -> Result<u64> {
        Ok(Self::client()?
            .post(format!(
                "{}/indexes/{}/documents",
                base_url,
                index_name.as_ref()
            ))
            .json(&UpdateDocumentsRequest {
                version,
                documents: sets,
            })
            .send()
            .await?
            .json()
            .await
            .map(Self::debug_log)?)
    }

    fn check_index_name(_indexes: &HashSet<String>) -> Result<()> {
        Ok(())
    }

    async fn get_indexes_dummy(
        indexes: &HashSet<String>,
        version: u64,
    ) -> Result<Vec<IndexDescription>> {
        Ok(indexes
            .iter()
            .map(|index| {
                let mut indexes = HashMap::new();
                indexes.insert(index.clone(), (0, version));
                IndexDescription {
                    version,
                    indexes,
                    current: true,
                }
            })
            .collect())
    }

    async fn search_dummy(indexes: &HashSet<String>, version: u64) -> Result<SearchResponse> {
        Self::check_index_name(indexes)?;
        Ok(SearchResponse {
            hits: None,
            index_version: version,
        })
    }

    async fn reload_index_dummy(indexes: &HashSet<String>, version: u64) -> Result<u64> {
        Self::check_index_name(indexes)?;
        Ok(version)
    }

    async fn update_index_dummy(indexes: &HashSet<String>, version: u64) -> Result<u64> {
        Self::check_index_name(indexes)?;
        Ok(version)
    }

    async fn update_documents_dummy(
        indexes: &HashSet<String>,
        version: u64,
        _: Vec<DocumentSet>,
    ) -> Result<u64> {
        Self::check_index_name(indexes)?;
        Ok(version)
    }
}
