use std::borrow::{Borrow, Cow};
use std::collections::HashSet;
use std::str::FromStr;

use kdl_schema::{
    Children as ChildrenSchema, Format, Node as NodeSchema, Prop as PropSchema, Schema, Validation,
    Value as ValueSchema,
};
use knuffel::ast::Literal;
use knuffel::span::Span;
use miette::Diagnostic;
use regex::Regex;
use thiserror::Error;
use url::Url;

use super::DocumentAst;

// TODO preserve spans where possible
#[derive(Debug, Error, Diagnostic)]
#[allow(clippy::module_name_repetitions)]
pub enum CheckFailure {
    #[error("schema error: node ref `{0}` could not be resolved")]
    MissingNodeRef(String),
    #[error("schema error: prop ref `{0}` could not be resolved")]
    MissingPropRef(String),
    #[error("schema error: value ref `{0}` could not be resolved")]
    MissingValueRef(String),
    #[error("schema error: children ref `{0}` could not be resolved")]
    MissingChildrenRef(String),
    #[error(
        "wrong number of {expected_type}: expected {expected_number_range} but got {actual_number}"
    )]
    MinMaxViolation {
        expected_type: String,
        expected_number_range: String,
        actual_number: usize,
    },
    #[error("unexpected node {name}")]
    #[diagnostic()]
    UnexpectedNode {
        #[label]
        span: Span,
        name: String,
    },
    #[error("unexpected prop {key}")]
    #[diagnostic()]
    UnexpectedProp {
        #[label]
        span: Span,
        key: String,
    },
    #[error("prop {key} missing")]
    MissingProp { key: String },
    #[error("value {actual} not in enum list {expected:?}")]
    EnumViolation {
        actual: String,
        expected: Vec<String>,
    },
    #[error("value {value:?} does not have type {expected_type}")]
    #[diagnostic()]
    IncorrectType {
        #[label]
        span: Span,
        value: Value, // TODO display correctly
        expected_type: String,
    },
    #[error("value {value} not of any formats {formats:?}")]
    #[diagnostic()]
    IncorrectStringFormat {
        #[label]
        span: Span,
        value: String,
        formats: Vec<Format>,
        // TODO pass details upwards somehow
    },
}

type Result<T = ()> = std::result::Result<T, CheckFailure>;

type Name = knuffel::ast::SpannedName<Span>;
type Node = knuffel::ast::SpannedNode<Span>;
type PropsMap = std::collections::BTreeMap<Name, Value>;
type Value = knuffel::ast::Value<Span>;

fn check_min_max(
    actual: usize,
    min: Option<usize>,
    max: Option<usize>,
    get_expected_type: impl Fn() -> String,
) -> Result {
    let below_min = min.map_or(false, |min| actual < min);
    let above_max = max.map_or(false, |max| actual > max);
    if below_min || above_max {
        let what_was_expected = get_expected_type();
        let how_many_were_expected = match (min, max) {
            (Some(min), Some(max)) => {
                if min == max {
                    format!("exactly {}", min)
                } else {
                    format!("between {} and {}", min, max)
                }
            }
            (Some(min), None) => format!("at least {}", min),
            (None, Some(max)) => format!("no more than {}", max),
            (None, None) => unreachable!(),
        };
        Err(CheckFailure::MinMaxViolation {
            expected_type: what_was_expected,
            expected_number_range: how_many_were_expected,
            actual_number: actual,
        })
    } else {
        Ok(())
    }
}

fn resolve_node_refs<'a>(
    schema: &'a Schema,
    nodes: &'a [NodeSchema],
) -> Result<Vec<&'a NodeSchema>> {
    nodes
        .iter()
        .map(|node| match &node.ref_ {
            None => Ok(node),
            Some(r#ref) => schema
                .resolve_node_ref(r#ref)
                .ok_or_else(|| CheckFailure::MissingNodeRef(r#ref.clone())),
        })
        .collect()
}

fn resolve_prop_refs<'a>(
    schema: &'a Schema,
    props: &'a [PropSchema],
) -> Result<Vec<&'a PropSchema>> {
    props
        .iter()
        .map(|prop| match &prop.ref_ {
            None => Ok(prop),
            Some(r#ref) => schema
                .resolve_prop_ref(r#ref)
                .ok_or_else(|| CheckFailure::MissingPropRef(r#ref.clone())),
        })
        .collect()
}

fn resolve_value_refs<'a>(
    schema: &'a Schema,
    values: &'a [ValueSchema],
) -> Result<Vec<&'a ValueSchema>> {
    values
        .iter()
        .map(|value| match &value.ref_ {
            None => Ok(value),
            Some(r#ref) => schema
                .resolve_value_ref(r#ref)
                .ok_or_else(|| CheckFailure::MissingValueRef(r#ref.clone())),
        })
        .collect()
}

fn resolve_children_refs<'a>(
    schema: &'a Schema,
    children: &'a [ChildrenSchema],
) -> Result<Vec<&'a ChildrenSchema>> {
    children
        .iter()
        .map(|children| match &children.ref_ {
            None => Ok(children),
            Some(r#ref) => schema
                .resolve_children_ref(r#ref)
                .ok_or_else(|| CheckFailure::MissingChildrenRef(r#ref.clone())),
        })
        .collect()
}

pub(crate) fn check(document: &DocumentAst, schema: &Schema) -> Result {
    check_nodes(
        &document.nodes,
        schema,
        &resolve_node_refs(schema, &schema.document.nodes)?,
    )
}

fn check_nodes(nodes: &[Node], schema: &Schema, nodes_schema: &[&NodeSchema]) -> Result {
    let nodes: Vec<(usize, &Node)> = nodes.iter().enumerate().collect();
    // TODO check node-names
    // TODO check other-nodes-allowed
    // TODO check tags, tag-names, other-tags-allowed
    let mut nodes_pending_validation: HashSet<usize> = nodes.iter().map(|(i, _)| *i).collect();
    for node_schema in nodes_schema {
        let applicable_nodes: Vec<(usize, &Node)> = nodes
            .iter()
            .filter(|(_, node)| match &node_schema.name {
                Some(schema_name) => schema_name.as_str() == node.node_name.as_ref(),
                None => true,
            })
            .copied()
            .collect();
        check_min_max(
            applicable_nodes.len(),
            node_schema.min,
            node_schema.max,
            || match &node_schema.name {
                Some(schema_name) => format!("`{}` nodes", schema_name),
                None => "nodes".to_string(),
            },
        )?;
        for (index, node) in applicable_nodes {
            check_node(node, schema, node_schema)?;
            nodes_pending_validation.remove(&index);
        }
    }
    let invalid_node = nodes
        .into_iter()
        .find(|(i, _)| nodes_pending_validation.contains(i));
    match invalid_node {
        Some((_, node)) => Err(CheckFailure::UnexpectedNode {
            span: node.span().clone(),
            name: node.node_name.to_string(),
        }),
        None => Ok(()),
    }
}

fn check_node(node: &Node, schema: &Schema, node_schema: &NodeSchema) -> Result {
    let node_name = &node.node_name;
    // schema is relevant if either its name matches this node's name or it has no name
    let schema_applies = match &node_schema.name {
        Some(schema_name) => schema_name == node_name.as_ref(),
        None => true,
    };
    if !schema_applies {
        return Ok(());
    }
    // TODO check prop-names
    // TODO check other-props-allowed
    // TODO check tag
    check_props(
        &node.properties,
        // TODO only do this once per node_schema
        &resolve_prop_refs(schema, &node_schema.props)?,
    )?;
    check_values(
        &node.arguments,
        // TODO only do this once per node_schema
        &resolve_value_refs(schema, &node_schema.values)?,
    )?;
    if let Some(children) = &node.children {
        check_children(
            children,
            schema,
            // TODO only do this once per node_schema
            &resolve_children_refs(schema, &node_schema.children)?,
        )?;
    }
    Ok(())
}

fn check_props(props: &PropsMap, props_schema: &[&PropSchema]) -> Result {
    let props: Vec<(usize, (&Name, &Value))> = props.iter().enumerate().collect();
    let mut props_pending_validation: HashSet<usize> = props.iter().map(|(i, _)| *i).collect();
    for prop_schema in props_schema {
        let applicable_props: Vec<(usize, (&Name, &Value))> = props
            .iter()
            .filter(|(_, (key, _))| match &prop_schema.key {
                Some(schema_key) => schema_key.as_str() == key.as_ref(),
                None => true,
            })
            .copied()
            .collect();
        if prop_schema.required && applicable_props.is_empty() {
            // TODO preserve node span
            // TODO reasonably handle non-keyed required
            return Err(CheckFailure::MissingProp {
                key: prop_schema.key.clone().unwrap(),
            });
        }
        for (index, (_, value)) in applicable_props {
            check_prop(value, prop_schema)?;
            props_pending_validation.remove(&index);
        }
    }
    let invalid_prop = props
        .into_iter()
        .find(|(i, _)| props_pending_validation.contains(i));
    match invalid_prop {
        Some((_, (name, _))) => Err(CheckFailure::UnexpectedProp {
            span: name.span().clone(),
            key: name.to_string(),
        }),
        None => Ok(()),
    }
}

fn check_prop(prop_value: &Value, prop_schema: &PropSchema) -> Result {
    for validation in &prop_schema.validations {
        check_validation(prop_value, validation)?;
    }
    Ok(())
}

fn check_values(values: &[Value], values_schemas: &[&ValueSchema]) -> Result {
    // Cases that are currently handled are
    // - one schema that applies to all values
    // - no schemas, no values
    // TODO allow several required (min 1; max 1;) values
    // TODO allow several optional (max 1;) values
    // TODO error properly when not that
    if values.is_empty() && values_schemas.is_empty() {
        return Ok(());
    }
    let schemas_coherent = values_schemas.len() == 1;
    assert!(schemas_coherent, "values schemas confusing");
    let values_schema = values_schemas[0];
    check_min_max(values.len(), values_schema.min, values_schema.max, || {
        "values".to_string()
    })?;
    for value in values {
        check_value(value, values_schema)?;
    }
    Ok(())
}

fn check_value(value: &Value, values_schema: &ValueSchema) -> Result {
    for validation in &values_schema.validations {
        check_validation(value, validation)?;
    }
    Ok(())
}

fn check_children(
    children: &[Node],
    schema: &Schema,
    children_schema: &[&ChildrenSchema],
) -> Result {
    let node_schemas: Vec<&NodeSchema> = children_schema
        .iter()
        .map(|children| resolve_node_refs(schema, &children.nodes))
        // TODO don't allocate twice here
        .collect::<Result<Vec<Vec<&NodeSchema>>>>()?
        .into_iter()
        .flatten()
        .collect();
    check_nodes(children, schema, &node_schemas)
}

fn check_validation(value: &Value, validation: &Validation) -> Result {
    match validation {
        // TODO tag
        Validation::Type(r#type) if r#type == "string" => {
            let _ = get_string_value(value)?;
        }
        Validation::Type(r#type) if r#type == "number" => match value.literal.borrow() {
            Literal::Int(_) | Literal::Decimal(_) => {}
            _ => {
                return Err(CheckFailure::IncorrectType {
                    span: value.literal.span().clone(),
                    value: value.clone(),
                    expected_type: r#type.clone(),
                });
            }
        },
        Validation::Type(r#type) => todo!("validate type {}", r#type),
        Validation::Enum(enum_values) => {
            let search_target = match value.literal.borrow() {
                Literal::Null => Cow::Borrowed("enum"),
                Literal::Bool(x) => {
                    if *x {
                        Cow::Borrowed("true")
                    } else {
                        Cow::Borrowed("false")
                    }
                }
                Literal::Int(x) => match i64::try_from(x) {
                    Ok(x) => Cow::Owned(x.to_string()),
                    _ => todo!("get value from Integer"),
                },
                Literal::String(x) => Cow::Borrowed(x.as_ref()),
                Literal::Decimal(_) => todo!("get value from Decimal"),
            };
            if !enum_values
                .iter()
                .any(|enum_value| enum_value == search_target.as_ref())
            {
                return Err(CheckFailure::EnumViolation {
                    actual: search_target.into_owned(),
                    expected: enum_values.clone(),
                });
            }
        }
        Validation::Format(valid_formats) => {
            for format in valid_formats {
                match format {
                    Format::Date => {
                        if let Ok(value) = get_string_value(value) {
                            if chrono::NaiveDate::from_str(value).is_ok() {
                                return Ok(());
                            }
                        }
                    }
                    Format::Url => {
                        if let Ok(value) = get_string_value(value) {
                            if Url::parse(value).is_ok() {
                                return Ok(());
                            }
                        }
                    }
                    Format::Regex => {
                        if let Ok(value) = get_string_value(value) {
                            if Regex::new(value).is_ok() {
                                return Ok(());
                            }
                        }
                    }
                    Format::KdlQuery => {
                        if let Ok(_value) = get_string_value(value) {
                            // TODO validate KDL queries
                            return Ok(());
                        }
                    }
                    format => todo!("validate format {:?}", format),
                }
            }
            return match value.literal.borrow() {
                Literal::String(string_value) => Err(CheckFailure::IncorrectStringFormat {
                    span: value.literal.span().clone(),
                    value: string_value.to_string(),
                    formats: valid_formats.clone(),
                }),
                value => todo!("no valid format for {:?}", value),
            };
        }
        Validation::Pattern(regex) => {
            // TODO should that be a full-string match or just a partial-string match
            let regex = Regex::new(&format!("^{}$", regex)).expect("invalid regex in schema");
            let value = if let Literal::String(value) = value.literal.borrow() {
                value
            } else {
                todo!("error for can't regex a non-string")
            };
            if !regex.is_match(value) {
                todo!("error for regex failure")
            }
        }
    }
    Ok(())
}

fn get_string_value(value: &Value) -> Result<&str> {
    match value.literal.borrow() {
        Literal::String(x) => Ok(x),
        _ => Err(CheckFailure::IncorrectType {
            span: value.literal.span().clone(),
            value: value.clone(),
            expected_type: "string".to_string(),
        }),
    }
}