use std::{borrow::Cow, sync::Arc};

use itertools::Itertools;
use tombi_accessor::Accessors;
use tombi_future::{BoxFuture, Boxable};
use tombi_schema_type::BoolDefaultTrue;
use tombi_x_keyword::{
    ArrayValuesOrderBy, StringFormat, TableKeysOrder, TableKeysOrderGroupKind,
    X_TOMBI_ADDITIONAL_KEY_LABEL, X_TOMBI_ARRAY_VALUES_ORDER_BY, X_TOMBI_TABLE_KEYS_ORDER,
};

use super::{
    AnchorCollector, CurrentSchema, DynamicAnchorCollector, FindSchemaCandidates, NotSchema,
    PropertySchema, SchemaAccessor, SchemaDefinitions, SchemaItem, SchemaPatternProperties,
    SchemaUri, SchemaView, referable_from_schema_value, schema_item_from_schema_value,
    schema_item_from_schema_value_for_type,
};
use crate::{
    Accessor, AllOfSchema, AnyOfSchema, IfThenElseSchema, OneOfSchema, SchemaProperties,
    SchemaStore,
};

#[derive(Debug, Clone)]
pub enum Dependency {
    Property(Vec<String>),
    Schema(SchemaItem),
}

use tombi_json::StringNode;

#[derive(Debug, Default, Clone)]
pub struct TableSchema {
    pub title: Option<String>,
    pub description: Option<String>,
    pub range: tombi_text::Range,
    pub properties: SchemaProperties,
    pub pattern_properties: Option<SchemaPatternProperties>,
    additional_properties: Option<bool>,
    pub additional_property_schema: Option<(
        tombi_text::Range, // JSON Schema property name range (for GoToTypeDefinition)
        SchemaItem,
    )>,
    pub unevaluated_properties: Option<bool>,
    pub unevaluated_property_schema: Option<SchemaItem>,
    pub property_names: Option<SchemaItem>,
    pub required: Option<Vec<String>>,
    pub dependencies: Option<tombi_hashmap::IndexMap<String, Dependency>>,
    pub dependent_required: Option<tombi_hashmap::IndexMap<String, Vec<String>>>,
    pub dependent_schemas: Option<tombi_hashmap::IndexMap<String, SchemaItem>>,
    pub min_properties: Option<usize>,
    pub max_properties: Option<usize>,
    pub keys_order: Option<XTombiTableKeysOrder>,
    pub array_values_order_by: Option<ArrayValuesOrderBy>,
    pub default: Option<tombi_json::Object>,
    pub const_value: Option<tombi_json::Object>,
    pub r#enum: Option<Vec<tombi_json::Object>>,
    pub examples: Option<Vec<tombi_json::Object>>,
    pub deprecation: Option<crate::Deprecation>,
    pub additional_key_label: Option<String>,
    pub one_of: Option<Box<OneOfSchema>>,
    pub any_of: Option<Box<AnyOfSchema>>,
    pub all_of: Option<Box<AllOfSchema>>,
    pub not: Option<Box<NotSchema>>,
    pub if_then_else: Option<Box<IfThenElseSchema>>,
}

impl TableSchema {
    pub fn new(
        object_node: &tombi_json::ObjectNode,
        string_formats: Option<&[StringFormat]>,
        dialect: Option<crate::JsonSchemaDialect>,
        anchor_collector: Option<&mut AnchorCollector>,
        dynamic_anchor_collector: Option<&mut DynamicAnchorCollector>,
    ) -> Self {
        let mut anchor_collector = anchor_collector;
        let mut dynamic_anchor_collector = dynamic_anchor_collector;
        let mut properties = tombi_hashmap::IndexMap::new();
        if let Some(tombi_json::ValueNode::Object(object_node)) = object_node.get("properties") {
            for (key_node, value_node) in object_node.properties.iter() {
                if let Some(property_schema) = referable_from_schema_value(
                    value_node,
                    string_formats,
                    dialect,
                    anchor_collector.as_deref_mut(),
                    dynamic_anchor_collector.as_deref_mut(),
                ) {
                    properties.insert(
                        SchemaAccessor::Key(key_node.value.to_string()),
                        PropertySchema {
                            property_schema,
                            key_range: key_node.range,
                        },
                    );
                }
            }
        }
        let pattern_properties = match object_node.get("patternProperties") {
            Some(tombi_json::ValueNode::Object(object_node)) => {
                let mut pattern_properties = tombi_hashmap::HashMap::new();
                for (pattern, value) in object_node.properties.iter() {
                    if let Some(schema_view) = referable_from_schema_value(
                        value,
                        string_formats,
                        dialect,
                        anchor_collector.as_deref_mut(),
                        dynamic_anchor_collector.as_deref_mut(),
                    ) {
                        pattern_properties.insert(pattern.clone(), schema_view);
                    }
                }
                Some(pattern_properties)
            }
            _ => None,
        };

        let (additional_properties, additional_property_schema) = match object_node
            .get("additionalProperties")
        {
            Some(tombi_json::ValueNode::Bool(allow)) => (Some(allow.value), None),
            Some(value @ tombi_json::ValueNode::Object(_)) => {
                let schema_view = referable_from_schema_value(
                    value,
                    string_formats,
                    dialect,
                    anchor_collector.as_deref_mut(),
                    dynamic_anchor_collector.as_deref_mut(),
                );
                (
                    Some(true),
                    schema_view
                        .map(|schema| (value.range(), Arc::new(tokio::sync::RwLock::new(schema)))),
                )
            }
            _ => (None, None),
        };
        let supports_unevaluated = crate::supports_keyword(dialect, "unevaluatedProperties");
        let (unevaluated_properties, unevaluated_property_schema) = if supports_unevaluated {
            match object_node.get("unevaluatedProperties") {
                Some(tombi_json::ValueNode::Bool(allow)) => (Some(allow.value), None),
                Some(value @ tombi_json::ValueNode::Object(_)) => (
                    Some(true),
                    schema_item_from_schema_value(
                        value,
                        string_formats,
                        dialect,
                        anchor_collector.as_deref_mut(),
                        dynamic_anchor_collector.as_deref_mut(),
                    ),
                ),
                _ => (None, None),
            }
        } else {
            (None, None)
        };

        let keys_order = object_node
            .get(X_TOMBI_TABLE_KEYS_ORDER)
            .and_then(XTombiTableKeysOrder::new);

        let array_values_order_by = object_node
            .get(X_TOMBI_ARRAY_VALUES_ORDER_BY)
            .and_then(|v| {
                if let Some(v) = v.as_str() {
                    if let Ok(v) = ArrayValuesOrderBy::try_from(v) {
                        Some(v)
                    } else {
                        log::warn!("invalid {X_TOMBI_ARRAY_VALUES_ORDER_BY}: {v}");
                        None
                    }
                } else {
                    log::warn!("invalid {X_TOMBI_ARRAY_VALUES_ORDER_BY}: {}", v);
                    None
                }
            });
        let (one_of, any_of, all_of, not) = crate::adjacent_applicators(
            object_node,
            string_formats,
            dialect,
            anchor_collector.as_deref_mut(),
            dynamic_anchor_collector.as_deref_mut(),
        );

        Self {
            title: object_node
                .get("title")
                .and_then(|v| v.as_str().map(|s| s.to_string())),
            description: object_node
                .get("description")
                .and_then(|v| v.as_str().map(|s| s.to_string())),
            range: object_node.range,
            properties: Arc::new(properties.into()),
            pattern_properties: pattern_properties.map(|props| {
                Arc::new(
                    props
                        .into_iter()
                        .map(|(key, property_schema)| {
                            (
                                key.value,
                                PropertySchema {
                                    property_schema,
                                    key_range: key.range,
                                },
                            )
                        })
                        .collect::<tombi_hashmap::HashMap<_, _>>()
                        .into(),
                )
            }),
            additional_properties,
            additional_property_schema,
            unevaluated_properties,
            unevaluated_property_schema,
            required: object_node.get("required").and_then(|v| {
                v.as_array().map(|arr| {
                    arr.items
                        .iter()
                        .filter_map(|v| v.as_str())
                        .map(ToString::to_string)
                        .collect()
                })
            }),
            dependencies: object_node
                .get("dependencies")
                .and_then(|v| v.as_object())
                .map(|deps_obj| {
                    let mut deps = tombi_hashmap::IndexMap::new();
                    for (key, value) in &deps_obj.properties {
                        match value {
                            tombi_json::ValueNode::Array(arr) => {
                                let required_keys: Vec<String> = arr
                                    .items
                                    .iter()
                                    .filter_map(|v| v.as_str().map(ToString::to_string))
                                    .collect();
                                deps.insert(
                                    key.value.to_string(),
                                    Dependency::Property(required_keys),
                                );
                            }
                            value => {
                                if let Some(schema) = schema_item_from_schema_value(
                                    value,
                                    string_formats,
                                    dialect,
                                    anchor_collector.as_deref_mut(),
                                    dynamic_anchor_collector.as_deref_mut(),
                                ) {
                                    deps.insert(key.value.to_string(), Dependency::Schema(schema));
                                }
                            }
                        }
                    }
                    deps
                }),
            dependent_required: object_node
                .get("dependentRequired")
                .and_then(|v| v.as_object())
                .map(|obj| {
                    let mut map = tombi_hashmap::IndexMap::new();
                    for (key, value) in &obj.properties {
                        if let Some(arr) = value.as_array() {
                            let required_keys: Vec<String> = arr
                                .items
                                .iter()
                                .filter_map(|v| v.as_str().map(ToString::to_string))
                                .collect();
                            map.insert(key.value.to_string(), required_keys);
                        }
                    }
                    map
                }),
            dependent_schemas: object_node
                .get("dependentSchemas")
                .and_then(|v| v.as_object())
                .map(|obj| {
                    let mut map = tombi_hashmap::IndexMap::new();
                    for (key, value) in &obj.properties {
                        if let Some(schema) = schema_item_from_schema_value(
                            value,
                            string_formats,
                            dialect,
                            anchor_collector.as_deref_mut(),
                            dynamic_anchor_collector.as_deref_mut(),
                        ) {
                            map.insert(key.value.to_string(), schema);
                        }
                    }
                    map
                }),
            min_properties: object_node
                .get("minProperties")
                .and_then(|v| v.as_u64().map(|u| u as usize)),
            max_properties: object_node
                .get("maxProperties")
                .and_then(|v| v.as_u64().map(|u| u as usize)),
            keys_order,
            array_values_order_by,
            r#enum: object_node.get("enum").and_then(|v| v.as_array()).map(|v| {
                v.items
                    .iter()
                    .filter_map(|v| v.as_object().map(|v| v.into()))
                    .collect()
            }),
            default: object_node
                .get("default")
                .and_then(|v| v.as_object())
                .map(|v| v.into()),
            const_value: object_node
                .get("const")
                .and_then(|v| v.as_object())
                .map(|v| v.into()),
            examples: object_node
                .get("examples")
                .and_then(|v| v.as_array())
                .map(|v| {
                    v.items
                        .iter()
                        .filter_map(|v| v.as_object().map(|v| v.into()))
                        .collect()
                }),
            deprecation: crate::Deprecation::new(object_node),
            additional_key_label: object_node
                .get(X_TOMBI_ADDITIONAL_KEY_LABEL)
                .and_then(|v| v.as_str().map(|s| s.to_string())),
            one_of,
            any_of,
            all_of,
            not,
            if_then_else: IfThenElseSchema::new(
                object_node,
                string_formats,
                dialect,
                anchor_collector.as_deref_mut(),
                dynamic_anchor_collector.as_deref_mut(),
            )
            .map(Box::new),
            property_names: object_node.get("propertyNames").and_then(|value| {
                schema_item_from_schema_value_for_type(
                    value,
                    super::SchemaType::String,
                    string_formats,
                    dialect,
                    anchor_collector,
                    dynamic_anchor_collector,
                )
            }),
        }
    }

    pub fn value_type(&self) -> crate::ValueType {
        crate::ValueType::Table
    }

    #[inline]
    pub fn additional_properties(&self) -> Option<bool> {
        self.additional_properties
    }

    #[inline]
    pub fn allows_any_additional_properties(&self, strict: bool) -> bool {
        self.allows_additional_properties(strict) || self.pattern_properties.is_some()
    }

    #[inline]
    pub fn allows_additional_properties(&self, strict: bool) -> bool {
        self.additional_properties.unwrap_or(!strict)
    }

    #[inline]
    pub fn check_strict_additional_properties_violation(&self, strict: bool) -> bool {
        strict && self.additional_properties.is_none() && self.pattern_properties.is_none()
    }

    pub async fn accessors(&self) -> Vec<Accessor> {
        self.properties
            .read()
            .await
            .keys()
            .map(|accessor| match accessor {
                SchemaAccessor::Key(key) => Accessor::Key(key.clone()),
                SchemaAccessor::AnyIndex | SchemaAccessor::Index(_) => {
                    unreachable!("Table keys should not be index")
                }
            })
            .collect_vec()
    }

    pub async fn resolve_property_schema(
        &self,
        accessor: &SchemaAccessor,
        schema_uri: Cow<'_, SchemaUri>,
        definitions: Cow<'_, SchemaDefinitions>,
        strict: Option<BoolDefaultTrue>,
        schema_store: &SchemaStore,
    ) -> Result<Option<CurrentSchema<'static>>, crate::Error> {
        let mut property_schema = {
            let properties = self.properties.read().await;
            let Some(PropertySchema {
                property_schema, ..
            }) = properties.get(accessor)
            else {
                return Ok(None);
            };

            if property_schema.is_resolved() {
                return property_schema
                    .to_current_schema(schema_uri, definitions, strict, schema_store)
                    .await;
            }

            property_schema.clone()
        };

        let resolved = property_schema
            .resolve(
                schema_uri.clone(),
                definitions.clone(),
                strict,
                schema_store,
            )
            .await?
            .map(CurrentSchema::into_owned);

        // Preserve policy: only perform Ref -> Resolved cache transition under write lock.
        if property_schema.is_resolved() {
            let mut properties = self.properties.write().await;
            if let Some(PropertySchema {
                property_schema: new_property_schema,
                ..
            }) = properties.get_mut(accessor)
                && new_property_schema.is_ref()
            {
                *new_property_schema = property_schema;
            }
        }

        Ok(resolved)
    }

    pub async fn resolve_pattern_property_schema(
        &self,
        pattern_key: &str,
        schema_uri: Cow<'_, SchemaUri>,
        definitions: Cow<'_, SchemaDefinitions>,
        strict: Option<BoolDefaultTrue>,
        schema_store: &SchemaStore,
    ) -> Result<Option<CurrentSchema<'static>>, crate::Error> {
        let Some(pattern_properties) = &self.pattern_properties else {
            return Ok(None);
        };

        let mut pattern_property_schema = {
            let pattern_properties = pattern_properties.read().await;
            let Some(PropertySchema {
                property_schema, ..
            }) = pattern_properties.get(pattern_key)
            else {
                return Ok(None);
            };

            if property_schema.is_resolved() {
                return property_schema
                    .to_current_schema(schema_uri, definitions, strict, schema_store)
                    .await;
            }

            property_schema.clone()
        };

        let resolved = pattern_property_schema
            .resolve(
                schema_uri.clone(),
                definitions.clone(),
                strict,
                schema_store,
            )
            .await?
            .map(CurrentSchema::into_owned);

        // Preserve policy: only perform Ref -> Resolved cache transition under write lock.
        if pattern_property_schema.is_resolved() {
            let mut pattern_properties = pattern_properties.write().await;
            if let Some(PropertySchema {
                property_schema: new_pattern_property_schema,
                ..
            }) = pattern_properties.get_mut(pattern_key)
                && new_pattern_property_schema.is_ref()
            {
                *new_pattern_property_schema = pattern_property_schema;
            }
        }

        Ok(resolved)
    }
}

impl FindSchemaCandidates for TableSchema {
    fn find_schema_candidates<'a: 'b, 'b>(
        &'a self,
        accessors: &'a [Accessor],
        schema_uri: &'a SchemaUri,
        definitions: &'a SchemaDefinitions,
        strict: Option<BoolDefaultTrue>,
        schema_store: &'a SchemaStore,
    ) -> BoxFuture<'b, (Vec<SchemaView>, Vec<crate::Error>)> {
        async move {
            let mut candidates = Vec::new();
            let mut errors = Vec::new();

            if accessors.is_empty() {
                let property_keys = self.properties.read().await.keys().cloned().collect_vec();
                for property_key in property_keys {
                    let current_schema = self
                        .resolve_property_schema(
                            &property_key,
                            Cow::Borrowed(schema_uri),
                            Cow::Borrowed(definitions),
                            strict,
                            schema_store,
                        )
                        .await
                        .inspect_err(|err| {
                            log::warn!(
                                "cannot resolve property schema: schema_uri={schema_uri} accessors={accessors} error={err}",
                                schema_uri = schema_uri,
                                accessors = Accessors::from(accessors.to_vec()),
                            )
                        })
                        .ok()
                        .flatten();

                    if let Some(CurrentSchema {
                        schema_view,
                        schema_uri,
                        definitions,
                        strict,
                        ..
                    }) = current_schema
                    {
                        let (schema_candidates, schema_errors) = schema_view
                            .find_schema_candidates(
                                accessors,
                                &schema_uri,
                                &definitions,
                                strict,
                                schema_store,
                            )
                            .await;
                        candidates.extend(schema_candidates);
                        errors.extend(schema_errors);
                    }
                }

                return (candidates, errors);
            }

            let current_schema = self
                .resolve_property_schema(
                    &SchemaAccessor::from(&accessors[0]),
                    Cow::Borrowed(schema_uri),
                    Cow::Borrowed(definitions),
                    strict,
                    schema_store,
                )
                .await
                .inspect_err(|err| {
                    log::warn!(
                        "cannot resolve property schema: schema_uri={schema_uri} accessors={accessors} error={err}",
                        schema_uri = schema_uri,
                        accessors = Accessors::from(accessors.to_vec()),
                    )
                })
                .ok()
                .flatten();

            if let Some(CurrentSchema {
                schema_view,
                schema_uri,
                definitions,
                strict,
                ..
            }) = current_schema
            {
                return schema_view
                    .find_schema_candidates(
                        &accessors[1..],
                        &schema_uri,
                        &definitions,
                        strict,
                        schema_store,
                    )
                    .await;
            }

            (candidates, errors)
        }
        .boxed()
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum XTombiTableKeysOrder {
    All(TableKeysOrder),
    Groups(Vec<TableKeysOrderGroup>),
}

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct TableKeysOrderGroup {
    pub target: TableKeysOrderGroupKind,
    pub order: TableKeysOrder,
}

impl XTombiTableKeysOrder {
    pub fn new(value_node: &tombi_json::ValueNode) -> Option<Self> {
        match value_node {
            tombi_json::ValueNode::String(StringNode { value: order, .. }) => {
                match TableKeysOrder::try_from(order.as_str()) {
                    Ok(val) => Some(XTombiTableKeysOrder::All(val)),
                    Err(_) => {
                        log::warn!("invalid {X_TOMBI_TABLE_KEYS_ORDER}: {order}");
                        None
                    }
                }
            }
            tombi_json::ValueNode::Object(object_node) => {
                let mut sort_orders = vec![];
                for (group_name, order) in &object_node.properties {
                    let Ok(target) = TableKeysOrderGroupKind::try_from(group_name.value.as_str())
                    else {
                        log::warn!("invalid {X_TOMBI_TABLE_KEYS_ORDER} group: {group_name}");
                        return None;
                    };

                    let Some(Ok(order)) = order.as_str().map(TableKeysOrder::try_from) else {
                        log::warn!(
                            "invalid {X_TOMBI_TABLE_KEYS_ORDER} {group_name} group: {order}"
                        );
                        return None;
                    };

                    if order == TableKeysOrder::Schema && target != TableKeysOrderGroupKind::Keys {
                        log::warn!(
                            "invalid {X_TOMBI_TABLE_KEYS_ORDER} {group_name} group: {order}"
                        );
                        return None;
                    }

                    sort_orders.push(TableKeysOrderGroup { target, order });
                }
                Some(Self::Groups(sort_orders))
            }
            order => {
                log::warn!("invalid {X_TOMBI_TABLE_KEYS_ORDER}: {}", order);
                None
            }
        }
    }
}
