use std::collections::HashMap;

use log::warn;
use model::meta::tenant::{IndexType, PayloadIndex};
use serde::{Deserialize, Serialize};
use serde_json::Value;

use crate::error::QdrantError;

#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(untagged, rename_all = "snake_case")]
pub(crate) enum PayloadFieldSchema {
    FieldType(PayloadSchemaType),
    FieldParams(PayloadSchemaParams),
}

impl PayloadFieldSchema {
    pub fn is_text(&self) -> bool {
        matches!(
            self,
            PayloadFieldSchema::FieldType(PayloadSchemaType::Text)
                | PayloadFieldSchema::FieldParams(PayloadSchemaParams::Text(_))
        )
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct CreateFieldIndex {
    pub(crate) field_name: String,
    pub(crate) field_schema: Option<PayloadFieldSchema>,
}

impl From<(&String, &PayloadIndexInfo)> for CreateFieldIndex {
    fn from(value: (&String, &PayloadIndexInfo)) -> Self {
        CreateFieldIndex {
            field_name: value.0.clone(),
            field_schema: Some(match value.1.params.clone() {
                Some(p) => PayloadFieldSchema::FieldParams(p),
                None => PayloadFieldSchema::FieldType(value.1.data_type),
            }),
        }
    }
}

impl TryInto<PayloadIndex> for CreateFieldIndex {
    type Error = QdrantError;

    fn try_into(self) -> Result<PayloadIndex, Self::Error> {
        let schema = match self.field_schema {
            Some(s) => s,
            None => {
                warn!("Auto-detecting field type for field {}", self.field_name);
                return Err(QdrantError::BadRequestWithMsg(
                    "Can't auto-detect field type, please specify `field_schema` in the request"
                        .to_string(),
                ));
            }
        };

        let (length, index_type) = match schema {
            PayloadFieldSchema::FieldType(t) => match t {
                PayloadSchemaType::Keyword => (Some(255), IndexType::Varchar),
                PayloadSchemaType::Integer => (None, IndexType::BigInt),
                PayloadSchemaType::Float => (None, IndexType::Double),
                PayloadSchemaType::Geo => {
                    warn!("Geo field type is not supported");
                    return Err(QdrantError::BadRequestWithMsg(
                        "Geo field type is not supported".to_string(),
                    ));
                }
                PayloadSchemaType::Text => {
                    warn!("Text field type is not supported");
                    return Err(QdrantError::BadRequestWithMsg(
                        "Text field type is not supported".to_string(),
                    ));
                }
                PayloadSchemaType::Bool => (None, IndexType::TinyInt),
                PayloadSchemaType::Datetime => (None, IndexType::Datetime),
                PayloadSchemaType::Uuid => (Some(64), IndexType::Varchar),
            },
            PayloadFieldSchema::FieldParams(p) => match p {
                PayloadSchemaParams::Keyword(_) => (Some(255), IndexType::Varchar),
                PayloadSchemaParams::Integer(_) => (None, IndexType::BigInt),
                PayloadSchemaParams::Float(_) => (None, IndexType::Double),
                PayloadSchemaParams::Geo(_) => {
                    warn!("Geo field type is not supported");
                    return Err(QdrantError::BadRequestWithMsg(
                        "Geo field type is not supported".to_string(),
                    ));
                }
                PayloadSchemaParams::Text(_) => {
                    warn!("Text field type is not supported");
                    return Err(QdrantError::BadRequestWithMsg(
                        "Text field type is not supported".to_string(),
                    ));
                }
                PayloadSchemaParams::Bool(_) => (None, IndexType::TinyInt),
                PayloadSchemaParams::Datetime(_) => (None, IndexType::Datetime),
                PayloadSchemaParams::Uuid(_) => (Some(64), IndexType::Varchar),
            },
        };

        Ok(PayloadIndex {
            path: self.field_name.clone(),
            length,
            index_type,
        })
    }
}

#[derive(Debug, Deserialize, Serialize, Clone, Copy, PartialEq, Hash, Eq)]
#[serde(rename_all = "snake_case")]
pub enum PayloadSchemaType {
    Keyword,
    Integer,
    Float,
    Geo,
    Text,
    Bool,
    Datetime,
    Uuid,
}

#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Hash, Eq)]
#[serde(untagged, rename_all = "snake_case")]
pub enum PayloadSchemaParams {
    Keyword(KeywordIndexParams),
    Integer(IntegerIndexParams),
    Float(FloatIndexParams),
    Geo(GeoIndexParams),
    Text(TextIndexParams),
    Bool(BoolIndexParams),
    Datetime(DatetimeIndexParams),
    Uuid(UuidIndexParams),
}
#[derive(Default, Debug, Deserialize, Serialize, Clone, Copy, PartialEq, Hash, Eq)]
#[serde(rename_all = "snake_case")]
pub enum UuidIndexType {
    #[default]
    Uuid,
}

#[derive(Default, Debug, Deserialize, Serialize, Clone, PartialEq, Hash, Eq)]
#[serde(rename_all = "snake_case")]
pub struct UuidIndexParams {
    // Required for OpenAPI schema without anonymous types, versus #[serde(tag = "type")]
    pub r#type: UuidIndexType,

    /// If true - used for tenant optimization.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub is_tenant: Option<bool>,

    /// If true, store the index on disk. Default: false.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub on_disk: Option<bool>,
}

#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq, Hash, Eq)]
#[serde(rename_all = "snake_case")]
pub struct TextIndexParams {
    // Required for OpenAPI schema without anonymous types, versus #[serde(tag = "type")]
    pub r#type: TextIndexType,

    #[serde(default)]
    pub tokenizer: TokenizerType,

    /// Minimum characters to be tokenized.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub min_token_len: Option<usize>,

    /// Maximum characters to be tokenized.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub max_token_len: Option<usize>,

    /// If true, lowercase all tokens. Default: true.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub lowercase: Option<bool>,

    /// If true, store the index on disk. Default: false.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub on_disk: Option<bool>,
}

// Bool

#[derive(Default, Debug, Deserialize, Serialize, Clone, Copy, PartialEq, Hash, Eq)]
#[serde(rename_all = "snake_case")]
pub enum BoolIndexType {
    #[default]
    Bool,
}

#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq, Hash, Eq)]
#[serde(rename_all = "snake_case")]
pub struct BoolIndexParams {
    // Required for OpenAPI schema without anonymous types, versus #[serde(tag = "type")]
    pub r#type: BoolIndexType,

    /// If true, store the index on disk. Default: false.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    #[cfg(any())]
    pub on_disk: Option<bool>,
}

// Datetime

#[derive(Default, Debug, Deserialize, Serialize, Clone, Copy, PartialEq, Hash, Eq)]
#[serde(rename_all = "snake_case")]
pub enum DatetimeIndexType {
    #[default]
    Datetime,
}

#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq, Hash, Eq)]
#[serde(rename_all = "snake_case")]
pub struct DatetimeIndexParams {
    // Required for OpenAPI schema without anonymous types, versus #[serde(tag = "type")]
    pub r#type: DatetimeIndexType,

    /// If true - use this key to organize storage of the collection data.
    /// This option assumes that this key will be used in majority of filtered requests.
    pub is_principal: Option<bool>,

    /// If true, store the index on disk. Default: false.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub on_disk: Option<bool>,
}

#[derive(Default, Debug, Deserialize, Serialize, Clone, Copy, PartialEq, Hash, Eq)]
#[serde(rename_all = "snake_case")]
pub enum TextIndexType {
    #[default]
    Text,
}

#[derive(Default, Debug, Deserialize, Serialize, Clone, Copy, PartialEq, Hash, Eq)]
#[serde(rename_all = "snake_case")]
pub enum TokenizerType {
    #[default]
    Multilingual,
    Prefix,
    Whitespace,
    Word,
}

#[derive(Default, Debug, Deserialize, Serialize, Clone, Copy, PartialEq, Hash, Eq)]
#[serde(rename_all = "snake_case")]
pub enum FloatIndexType {
    #[default]
    Float,
}

#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq, Hash, Eq)]
#[serde(rename_all = "snake_case")]
pub struct FloatIndexParams {
    // Required for OpenAPI schema without anonymous types, versus #[serde(tag = "type")]
    pub r#type: FloatIndexType,

    /// If true - use this key to organize storage of the collection data.
    /// This option assumes that this key will be used in majority of filtered requests.
    pub is_principal: Option<bool>,

    /// If true, store the index on disk. Default: false.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub on_disk: Option<bool>,
}

#[derive(Default, Debug, Deserialize, Serialize, Clone, Copy, PartialEq, Hash, Eq)]
#[serde(rename_all = "snake_case")]
pub enum IntegerIndexType {
    #[default]
    Integer,
}

#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq, Hash, Eq)]
#[serde(rename_all = "snake_case")]
pub struct IntegerIndexParams {
    // Required for OpenAPI schema without anonymous types, versus #[serde(tag = "type")]
    pub r#type: IntegerIndexType,

    /// If true - support direct lookups.
    pub lookup: Option<bool>,

    /// If true - support ranges filters.
    pub range: Option<bool>,

    /// If true - use this key to organize storage of the collection data.
    /// This option assumes that this key will be used in majority of filtered requests.
    pub is_principal: Option<bool>,

    /// If true, store the index on disk. Default: false.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub on_disk: Option<bool>,
}

#[derive(Default, Debug, Deserialize, Serialize, Clone, Copy, PartialEq, Hash, Eq)]
#[serde(rename_all = "snake_case")]
pub enum KeywordIndexType {
    #[default]
    Keyword,
}

#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq, Hash, Eq)]
#[serde(rename_all = "snake_case")]
pub struct KeywordIndexParams {
    // Required for OpenAPI schema without anonymous types, versus #[serde(tag = "type")]
    pub r#type: KeywordIndexType,

    /// If true - used for tenant optimization. Default: false.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub is_tenant: Option<bool>,

    /// If true, store the index on disk. Default: false.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub on_disk: Option<bool>,
}

#[derive(Debug, Default, Deserialize, Serialize, Clone)]
pub struct PayloadIndexes(HashMap<String, PayloadIndexInfo>);

impl PayloadIndexes {
    pub fn exists(&self, key: &str) -> bool {
        self.0.contains_key(key)
    }

    pub fn insert(&mut self, key: String, value: PayloadIndexInfo) {
        self.0.insert(key, value);
    }

    pub fn iter(&self) -> impl Iterator<Item = (&String, &PayloadIndexInfo)> {
        self.0.iter()
    }

    pub fn remove(&mut self, key: &str) -> Option<PayloadIndexInfo> {
        self.0.remove(key)
    }
}

impl TryInto<String> for PayloadIndexes {
    type Error = QdrantError;

    fn try_into(self) -> Result<String, Self::Error> {
        serde_json::to_string(&self.0).map_err(|e| {
            QdrantError::InternalServerErrorWithMsg(format!("Can't serialize indexes: {}", e))
        })
    }
}

impl TryFrom<String> for PayloadIndexes {
    type Error = QdrantError;

    fn try_from(value: String) -> Result<Self, Self::Error> {
        let map: HashMap<String, PayloadIndexInfo> = serde_json::from_str(&value).map_err(|e| {
            warn!("Parsing indexes from string: {:?}", e);
            QdrantError::BadRequestWithMsg(format!("Can't parse indexes: {}", e))
        })?;
        Ok(PayloadIndexes(map))
    }
}

impl TryFrom<Value> for PayloadIndexes {
    type Error = QdrantError;

    fn try_from(value: Value) -> Result<Self, Self::Error> {
        let map: HashMap<String, PayloadIndexInfo> =
            serde_json::from_value(value).map_err(|e| {
                warn!("Parsing indexes from value: {:?}", e);
                QdrantError::BadRequestWithMsg(format!("Can't parse indexes: {}", e))
            })?;
        Ok(PayloadIndexes(map))
    }
}

#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub struct PayloadIndexInfo {
    pub data_type: PayloadSchemaType,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub params: Option<PayloadSchemaParams>,
    /// Number of points indexed with this index
    pub points: usize,
}

impl TryFrom<Value> for PayloadIndexInfo {
    type Error = QdrantError;

    fn try_from(value: Value) -> Result<Self, Self::Error> {
        serde_json::from_value(value).map_err(|e| {
            warn!("Parsing index info from value: {:?}", e);
            QdrantError::BadRequestWithMsg(format!("Can't parse index info: {}", e))
        })
    }
}

impl From<&PayloadFieldSchema> for PayloadIndexInfo {
    fn from(value: &PayloadFieldSchema) -> Self {
        match value {
            PayloadFieldSchema::FieldType(t) => PayloadIndexInfo {
                data_type: *t,
                params: None,
                points: 0,
            },
            PayloadFieldSchema::FieldParams(p) => PayloadIndexInfo {
                data_type: match p {
                    PayloadSchemaParams::Keyword(_) => PayloadSchemaType::Keyword,
                    PayloadSchemaParams::Integer(_) => PayloadSchemaType::Integer,
                    PayloadSchemaParams::Float(_) => PayloadSchemaType::Float,
                    PayloadSchemaParams::Geo(_) => PayloadSchemaType::Geo,
                    PayloadSchemaParams::Text(_) => PayloadSchemaType::Text,
                    PayloadSchemaParams::Bool(_) => PayloadSchemaType::Bool,
                    PayloadSchemaParams::Datetime(_) => PayloadSchemaType::Datetime,
                    PayloadSchemaParams::Uuid(_) => PayloadSchemaType::Uuid,
                },
                params: Some(p.clone()),
                points: 0,
            },
        }
    }
}

#[derive(Default, Debug, Deserialize, Serialize, Clone, Copy, PartialEq, Hash, Eq)]
#[serde(rename_all = "snake_case")]
pub enum GeoIndexType {
    #[default]
    Geo,
}

#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq, Hash, Eq)]
#[serde(rename_all = "snake_case")]
pub struct GeoIndexParams {
    // Required for OpenAPI schema without anonymous types, versus #[serde(tag = "type")]
    pub r#type: GeoIndexType,

    /// If true, store the index on disk. Default: false.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub on_disk: Option<bool>,
}
