use std::str::FromStr;

use tombi_future::Boxable;
use tombi_schema_store::{
    AllOfSchema, AnyOfSchema, OneOfSchema, SchemaContext, SchemaDefinitions, SchemaUri, SchemaView,
};

#[derive(Debug, Clone)]
pub enum DisplayValue {
    Boolean(bool),
    Integer(i64),
    Float(f64),
    String(String),
    OffsetDateTime(String),
    LocalDateTime(String),
    LocalDate(String),
    LocalTime(String),
    Array(Vec<DisplayValue>),
    Table(Vec<(String, DisplayValue)>),
}

impl PartialEq for DisplayValue {
    fn eq(&self, other: &Self) -> bool {
        match (self, other) {
            (Self::Boolean(a), Self::Boolean(b)) => a == b,
            (Self::Integer(a), Self::Integer(b)) => a == b,
            (Self::Float(a), Self::Float(b)) => a.to_bits() == b.to_bits(),
            (Self::String(a), Self::String(b)) => a == b,
            (Self::OffsetDateTime(a), Self::OffsetDateTime(b)) => a == b,
            (Self::LocalDateTime(a), Self::LocalDateTime(b)) => a == b,
            (Self::LocalDate(a), Self::LocalDate(b)) => a == b,
            (Self::LocalTime(a), Self::LocalTime(b)) => a == b,
            (Self::Array(a), Self::Array(b)) => a == b,
            (Self::Table(a), Self::Table(b)) => a == b,
            _ => false,
        }
    }
}

impl Eq for DisplayValue {}

impl std::hash::Hash for DisplayValue {
    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
        std::mem::discriminant(self).hash(state);
        match self {
            Self::Boolean(value) => value.hash(state),
            Self::Integer(value) => value.hash(state),
            Self::Float(value) => value.to_bits().hash(state),
            Self::String(value)
            | Self::OffsetDateTime(value)
            | Self::LocalDateTime(value)
            | Self::LocalDate(value)
            | Self::LocalTime(value) => value.hash(state),
            Self::Array(values) => values.hash(state),
            Self::Table(values) => values.hash(state),
        }
    }
}

impl DisplayValue {
    pub fn try_new_offset_date_time(
        local_date_time: &str,
    ) -> Result<Self, tombi_date_time::parse::Error> {
        tombi_date_time::LocalDateTime::from_str(local_date_time)?;
        Ok(DisplayValue::OffsetDateTime(local_date_time.to_string()))
    }

    pub fn try_new_local_date_time(
        local_date_time: &str,
    ) -> Result<Self, tombi_date_time::parse::Error> {
        tombi_date_time::LocalDateTime::from_str(local_date_time)?;
        Ok(DisplayValue::LocalDateTime(local_date_time.to_string()))
    }

    pub fn try_new_local_date(local_date: &str) -> Result<Self, tombi_date_time::parse::Error> {
        tombi_date_time::LocalDate::from_str(local_date)?;
        Ok(DisplayValue::LocalDate(local_date.to_string()))
    }

    pub fn try_new_local_time(local_time: &str) -> Result<Self, tombi_date_time::parse::Error> {
        tombi_date_time::LocalTime::from_str(local_time)?;
        Ok(DisplayValue::LocalTime(local_time.to_string()))
    }
}

impl TryFrom<&tombi_json::Value> for DisplayValue {
    type Error = ();

    fn try_from(value: &tombi_json::Value) -> Result<Self, Self::Error> {
        match value {
            tombi_json::Value::Bool(boolean) => Ok(DisplayValue::Boolean(*boolean)),
            tombi_json::Value::Number(number) => match number {
                tombi_json::Number::Integer(integer) => Ok(DisplayValue::Integer(*integer)),
                tombi_json::Number::Float(float) => Ok(DisplayValue::Float(*float)),
            },
            tombi_json::Value::String(string) => Ok(DisplayValue::String(string.clone())),
            tombi_json::Value::Array(array) => Ok(DisplayValue::Array(
                array
                    .iter()
                    .map(DisplayValue::try_from)
                    .collect::<Result<Vec<_>, _>>()?,
            )),
            tombi_json::Value::Object(object) => DisplayValue::try_from(object),
            tombi_json::Value::Null => Err(()),
        }
    }
}

impl TryFrom<&tombi_json::Object> for DisplayValue {
    type Error = ();
    fn try_from(value: &tombi_json::Object) -> Result<Self, Self::Error> {
        Ok(DisplayValue::Table(
            value
                .iter()
                .filter_map(|(key, value)| {
                    DisplayValue::try_from(value)
                        .ok()
                        .map(|display_value| (key.clone(), display_value))
                })
                .collect(),
        ))
    }
}

impl std::fmt::Display for DisplayValue {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            DisplayValue::Boolean(boolean) => write!(f, "{boolean}"),
            DisplayValue::Integer(integer) => write!(f, "{integer}"),
            DisplayValue::Float(float) => write!(f, "{float}"),
            DisplayValue::String(string) => {
                write!(f, "{}", tombi_toml_text::to_basic_string(string))
            }
            DisplayValue::OffsetDateTime(offset_date_time) => write!(f, "{offset_date_time}"),
            DisplayValue::LocalDateTime(local_date_time) => write!(f, "{local_date_time}"),
            DisplayValue::LocalDate(local_date) => write!(f, "{local_date}"),
            DisplayValue::LocalTime(local_time) => write!(f, "{local_time}"),
            DisplayValue::Array(array) => {
                write!(f, "[")?;
                for (i, value) in array.iter().enumerate() {
                    if i > 0 {
                        write!(f, ", ")?;
                    }
                    write!(f, "{value}")?;
                }
                write!(f, "]")
            }
            DisplayValue::Table(table) => {
                write!(f, "{{ ")?;
                for (i, (key, value)) in table.iter().enumerate() {
                    if i > 0 {
                        write!(f, ", ")?;
                    }
                    write!(f, "{} = {value}", tombi_toml_text::to_key_string(key))?;
                }
                write!(f, " }}")
            }
        }
    }
}

pub trait GetEnum {
    fn get_enum<'a: 'b, 'b>(
        &'a self,
        schema_uri: &'a SchemaUri,
        definitions: &'a SchemaDefinitions,
        strict: Option<tombi_schema_type::BoolDefaultTrue>,
        schema_context: &'a SchemaContext,
    ) -> tombi_future::BoxFuture<'b, Option<Vec<DisplayValue>>>;
}

impl GetEnum for SchemaView {
    fn get_enum<'a: 'b, 'b>(
        &'a self,
        schema_uri: &'a SchemaUri,
        definitions: &'a SchemaDefinitions,
        strict: Option<tombi_schema_type::BoolDefaultTrue>,
        schema_context: &'a SchemaContext,
    ) -> tombi_future::BoxFuture<'b, Option<Vec<DisplayValue>>> {
        async move {
            match self {
                SchemaView::Boolean(schema) => {
                    let mut enum_values = Vec::new();

                    // Add const_value if present
                    if let Some(const_value) = &schema.const_value {
                        enum_values.push(DisplayValue::Boolean(*const_value));
                    }

                    // Add enum values if present
                    if let Some(r#enum) = &schema.r#enum {
                        enum_values.extend(r#enum.iter().map(|v| DisplayValue::Boolean(*v)));
                    }

                    if !enum_values.is_empty() {
                        Some(enum_values)
                    } else {
                        None
                    }
                }
                SchemaView::Integer(schema) => {
                    let mut enum_values = Vec::new();

                    if let Some(const_value) = &schema.const_value {
                        enum_values.push(DisplayValue::Integer(*const_value));
                    }

                    if let Some(r#enum) = &schema.r#enum {
                        enum_values.extend(r#enum.iter().map(|v| DisplayValue::Integer(*v)));
                    }

                    if !enum_values.is_empty() {
                        Some(enum_values)
                    } else {
                        None
                    }
                }
                SchemaView::Float(schema) => {
                    let mut enum_values = Vec::new();

                    if let Some(const_value) = &schema.const_value {
                        enum_values.push(DisplayValue::Float(*const_value));
                    }

                    if let Some(r#enum) = &schema.r#enum {
                        enum_values.extend(r#enum.iter().map(|v| DisplayValue::Float(*v)));
                    }

                    if !enum_values.is_empty() {
                        Some(enum_values)
                    } else {
                        None
                    }
                }
                SchemaView::String(schema) => {
                    let mut enum_values = Vec::new();

                    if let Some(const_value) = &schema.const_value {
                        enum_values.push(DisplayValue::String(const_value.clone()));
                    }

                    if let Some(r#enum) = &schema.r#enum {
                        enum_values.extend(r#enum.iter().map(|v| DisplayValue::String(v.clone())));
                    }

                    if !enum_values.is_empty() {
                        Some(enum_values)
                    } else {
                        None
                    }
                }
                SchemaView::OffsetDateTime(schema) => {
                    let mut enum_values = Vec::new();

                    if let Some(const_value) = &schema.const_value {
                        enum_values.push(DisplayValue::OffsetDateTime(const_value.clone()));
                    }

                    if let Some(r#enum) = &schema.r#enum {
                        enum_values.extend(
                            r#enum
                                .iter()
                                .map(|v| DisplayValue::OffsetDateTime(v.clone())),
                        );
                    }

                    if !enum_values.is_empty() {
                        Some(enum_values)
                    } else {
                        None
                    }
                }
                SchemaView::LocalDateTime(schema) => {
                    let mut enum_values = Vec::new();

                    if let Some(const_value) = &schema.const_value {
                        enum_values.push(DisplayValue::LocalDateTime(const_value.clone()));
                    }

                    if let Some(r#enum) = &schema.r#enum {
                        enum_values.extend(
                            r#enum
                                .iter()
                                .map(|v| DisplayValue::LocalDateTime(v.clone())),
                        );
                    }

                    if !enum_values.is_empty() {
                        Some(enum_values)
                    } else {
                        None
                    }
                }
                SchemaView::LocalDate(schema) => {
                    let mut enum_values = Vec::new();

                    if let Some(const_value) = &schema.const_value {
                        enum_values.push(DisplayValue::LocalDate(const_value.clone()));
                    }

                    if let Some(r#enum) = &schema.r#enum {
                        enum_values
                            .extend(r#enum.iter().map(|v| DisplayValue::LocalDate(v.clone())));
                    }

                    if !enum_values.is_empty() {
                        Some(enum_values)
                    } else {
                        None
                    }
                }
                SchemaView::LocalTime(schema) => {
                    let mut enum_values = Vec::new();

                    if let Some(const_value) = &schema.const_value {
                        enum_values.push(DisplayValue::LocalTime(const_value.clone()));
                    }

                    if let Some(r#enum) = &schema.r#enum {
                        enum_values
                            .extend(r#enum.iter().map(|v| DisplayValue::LocalTime(v.clone())));
                    }

                    if !enum_values.is_empty() {
                        Some(enum_values)
                    } else {
                        None
                    }
                }
                SchemaView::Anything(_)
                | SchemaView::Nothing(_)
                | SchemaView::Array(_)
                | SchemaView::Table(_)
                | SchemaView::Null => None,
                SchemaView::OneOf(OneOfSchema { schemas, .. })
                | SchemaView::AnyOf(AnyOfSchema { schemas, .. })
                | SchemaView::AllOf(AllOfSchema { schemas, .. }) => {
                    get_enum_from_schemas(schemas, schema_uri, definitions, strict, schema_context)
                        .await
                }
            }
        }
        .boxed()
    }
}

/// Helper function to get enum values from a collection of schemas
fn get_enum_from_schemas<'a: 'b, 'b>(
    schemas: &'a tombi_schema_store::ReferableSchemaViews,
    schema_uri: &'a SchemaUri,
    definitions: &'a SchemaDefinitions,
    strict: Option<tombi_schema_type::BoolDefaultTrue>,
    schema_context: &'a SchemaContext,
) -> tombi_future::BoxFuture<'b, Option<Vec<DisplayValue>>> {
    async move {
        let mut enum_values = Vec::new();
        let resolved_schemas = tombi_schema_store::resolve_and_collect_schemas(
            schemas,
            std::borrow::Cow::Borrowed(schema_uri),
            std::borrow::Cow::Borrowed(definitions),
            strict,
            schema_context.store,
            &schema_context.schema_visits,
            &[],
        )
        .await?;

        for resolved in &resolved_schemas {
            if let Some(values) = resolved
                .schema_view
                .get_enum(
                    &resolved.schema_uri,
                    &resolved.definitions,
                    resolved.strict,
                    schema_context,
                )
                .await
            {
                enum_values.extend(values);
            }
        }

        if enum_values.is_empty() {
            None
        } else {
            Some(enum_values)
        }
    }
    .boxed()
}
