use std::collections::HashMap;

use crate::{schema::*, tokenizer::tokenize};
use serde::{Deserialize, Serialize};
use tokio::time::Instant;

#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum Field {
    DocId,
    TextFieldName,
    TenantToken,
    Content,
    CollectionId,
    Version,
}

#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub struct TermQuery {
    pub field: Field,
    pub value: TermValue,
}

#[derive(Debug, Deserialize, Serialize)]
pub enum TermValue {
    String(String),
    U64(u64),
}

impl TermQuery {
    pub fn string(field: Field, value: String) -> Self {
        Self {
            field,
            value: TermValue::String(value),
        }
    }

    pub fn u64(field: Field, value: u64) -> Self {
        Self {
            field,
            value: TermValue::U64(value),
        }
    }
}

#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum BooleanQueryOccur {
    Must,
    Should,
    MustNot,
}

#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub struct BooleanQuery {
    pub occur: BooleanQueryOccur,
    pub queries: Vec<Query>,
}

impl BooleanQuery {
    pub fn must(queries: Vec<Query>) -> Self {
        Self {
            occur: BooleanQueryOccur::Must,
            queries,
        }
    }

    pub fn should(queries: Vec<Query>) -> Self {
        Self {
            occur: BooleanQueryOccur::Should,
            queries,
        }
    }

    pub fn must_not(queries: Vec<Query>) -> Self {
        Self {
            occur: BooleanQueryOccur::MustNot,
            queries,
        }
    }
}

#[derive(Debug, Deserialize, Serialize)]
pub struct RangeQuery {
    pub field: Field,
    pub start: Option<i64>,
    pub end: Option<i64>,
}

#[derive(Debug, Deserialize, Serialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum Query {
    Boolean(BooleanQuery),
    Term(TermQuery),
    // potentially expensive operation:
    //   https://docs.rs/tantivy/latest/tantivy/query/struct.RangeQuery.html#default
    Range(RangeQuery),
}

impl From<String> for Query {
    fn from(value: String) -> Query {
        Query::Boolean(BooleanQuery {
            occur: BooleanQueryOccur::Must,
            queries: tokenize(&value, 2, 20)
                .into_iter()
                .map(|term| Query::Term(TermQuery::string(Field::Content, term)))
                .collect(),
        })
    }
}

impl From<&Query> for Box<dyn tantivy::query::Query> {
    fn from(query: &Query) -> Box<dyn tantivy::query::Query> {
        match query {
            Query::Boolean(query) => {
                let occur = match query.occur {
                    BooleanQueryOccur::Must => tantivy::query::Occur::Must,
                    BooleanQueryOccur::Should => tantivy::query::Occur::Should,
                    BooleanQueryOccur::MustNot => tantivy::query::Occur::MustNot,
                };
                Box::new(tantivy::query::BooleanQuery::new(
                    query
                        .queries
                        .iter()
                        .map(|query| (occur, query.into()))
                        .collect(),
                ))
            }
            Query::Term(term_query) => {
                let field = match term_query.field {
                    Field::DocId => *DOC_ID_FIELD,
                    Field::TextFieldName => *FIELD_NAME_FIELD,
                    Field::TenantToken => *TENANT_TOKEN_FIELD,
                    Field::Content => *CONTENT_FIELD,
                    Field::CollectionId => *COLLECTION_ID_FIELD,
                    Field::Version => *VERSION_FIELD,
                };
                let term = match &term_query.value {
                    TermValue::String(value) => tantivy::Term::from_field_text(field, value),
                    TermValue::U64(value) => tantivy::Term::from_field_u64(field, *value),
                };
                Box::new(tantivy::query::TermQuery::new(
                    term,
                    tantivy::schema::IndexRecordOption::Basic,
                ))
            }
            Query::Range(range_query) => {
                let field = match range_query.field {
                    Field::DocId => DOC_ID.to_string(),
                    Field::TextFieldName => FIELD_NAME.to_string(),
                    Field::TenantToken => TENANT_TOKEN.to_string(),
                    Field::Content => CONTENT.to_string(),
                    Field::CollectionId => COLLECTION_ID.to_string(),
                    Field::Version => VERSION.to_string(),
                };
                let start = range_query.start.unwrap_or(i64::MIN);
                let end = range_query.end.unwrap_or(i64::MAX);
                Box::new(tantivy::query::RangeQuery::new_i64(field, start..end))
            }
        }
    }
}

#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub struct SearchRequest {
    pub tenant_token: String,
    pub collection_id: u64,
    pub query: Query,
    pub limit: Option<u32>,
}

#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub struct SearchHit {
    pub doc_id: String,
    pub version: u64,
    pub score: f32,
}

impl SearchHit {
    pub fn doc_id(self) -> String {
        self.doc_id
    }
    pub fn version(self) -> u64 {
        self.version
    }
}

#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub struct SearchResponse {
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub hits: Option<Vec<SearchHit>>,
    pub index_version: u64,
}

#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub struct IndexDescription {
    pub version: u64,
    pub indexes: HashMap<String, (u64, u64)>,
    pub current: bool,
}

#[derive(Debug, Deserialize, Serialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum UpdateIndexRequest {
    Reload,
    Update { version: u64 },
}

#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub struct ApiResponse<T> {
    pub time: f32,
    pub response: T,
}

#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub struct Document {
    pub doc_id: String,
    pub content: Option<String>, // document is deleted from revert index if content is empty
    pub version: u64,
}

#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct DocumentFull {
    pub doc_id: String,
    pub content: Option<String>, // document is deleted from revert index if content is empty
    pub version: u64,
    pub tenant_token: String,
    pub collection_id: u64,
    pub field_name: String,
}

pub struct DocumentFullVec(pub Vec<DocumentFull>);

impl From<&Vec<String>> for DocumentFullVec {
    fn from(docs: &Vec<String>) -> DocumentFullVec {
        let mut documents = Vec::new();
        for doc in docs {
            documents.push(DocumentFull {
                doc_id: doc.clone(),
                content: None,
                version: 0,
                tenant_token: "".to_string(),
                collection_id: 0,
                field_name: "".to_string(),
            });
        }
        DocumentFullVec(documents)
    }
}

#[derive(Default, Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub struct DocumentSet {
    pub documents: Vec<Document>,
    pub tenant_token: String,
    pub collection_id: u64,
    pub field_name: String,
}

impl DocumentSet {
    pub fn to_document_full(&self) -> Vec<DocumentFull> {
        let mut docs = Vec::new();
        for d in &self.documents {
            docs.push(DocumentFull {
                doc_id: d.doc_id.clone(),
                content: d.content.clone(),
                version: d.version,
                tenant_token: self.tenant_token.clone(),
                collection_id: self.collection_id,
                field_name: self.field_name.clone(),
            });
        }
        docs
    }
}

#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub struct UpdateDocumentsRequest {
    pub documents: Vec<DocumentSet>,
    pub version: u64,
}

impl<T> ApiResponse<T> {
    pub fn new(start: Instant, response: T) -> Self {
        Self {
            time: start.elapsed().as_secs_f32(),
            response,
        }
    }
}
