use std::collections::HashSet;

use indexmap::{IndexMap, IndexSet};
use jsona::{
    dom::{visit_annotations, DomNode, Key, KeyOrIndex, Keys, Node, VisitControl, Visitor},
    util::quote,
};
use jsona_schema::{Schema, SchemaType};
use jsona_util::{
    environment::Environment,
    schema::associations::{SCHEMA_REF_KEY, SCHEMA_REF_SCHEMA},
};
use lsp_async_stub::{
    rpc::Error,
    util::{LspExt, Position},
    Context, Params,
};
use lsp_types::{
    Command, CompletionItem, CompletionItemKind, CompletionParams, CompletionResponse,
    CompletionTextEdit, Documentation, InsertTextFormat, MarkupContent, TextEdit,
};
use serde_json::Value;

use crate::World;
use crate::{
    query::{Query, ScopeKind},
    world::DocumentState,
};

#[tracing::instrument(skip_all)]
pub async fn completion<E: Environment>(
    context: Context<World<E>>,
    params: Params<CompletionParams>,
) -> Result<Option<CompletionResponse>, Error> {
    let p = params.required()?;

    let workspaces = context.workspaces.read().await;
    let document_uri = p.text_document_position.text_document.uri;
    let (ws, doc) = workspaces.try_get_document(&document_uri)?;
    if !ws.lsp_config.schema.enabled {
        return Ok(None);
    }
    let position = p.text_document_position.position;
    let offset = match doc.mapper.offset(Position::from_lsp(position)) {
        Some(ofs) => ofs,
        None => {
            tracing::error!(?position, "document position not found");
            return Ok(None);
        }
    };

    let query = Query::at(&doc.dom, offset, true);
    if query.scope == ScopeKind::Unknown {
        return Ok(None);
    }

    let (keys, node) = match Query::node_at(&doc.dom, query.node_at_offset) {
        Some(v) => v,
        None => return Ok(None),
    };
    let query_keys = match &query.scope {
        ScopeKind::AnnotationKey => {
            Keys::single(Key::annotation(query.key.as_ref().unwrap().text()))
        }
        ScopeKind::Array => {
            let idx = query.index_at().unwrap_or_default();
            keys.join(KeyOrIndex::Index(idx))
        }
        _ => keys.clone(),
    };

    let schemas = if query.scope == ScopeKind::Value
        && keys
            .last_annotation_key()
            .map(|v| v.value() == SCHEMA_REF_KEY)
            .unwrap_or_default()
    {
        Some(vec![ws.schemas.associations().schema_key_complete_schema()])
    } else {
        ws.query_schemas(&document_uri, &query_keys).await
    };
    tracing::debug!(
        ?query,
        "completion keys={} schemas={:?}",
        keys,
        schemas.as_ref().map(|v| v.len())
    );

    let result = match &query.scope {
        ScopeKind::AnnotationKey => {
            let mut props = node.annotations().map(|v| v.map_keys()).unwrap_or_default();
            if let Some(key) = query.key.as_ref() {
                props.push(key.to_string());
            }
            match schemas.as_ref() {
                Some(schemas) => complete_key(doc, &query, &props, schemas),
                None => complete_annotations_schemaless(doc, &query, &props),
            }
        }
        ScopeKind::PropertyKey | ScopeKind::Object => {
            let props: Vec<String> = node
                .as_object()
                .map(|v| {
                    v.value()
                        .read()
                        .iter()
                        .map(|(k, _)| k.value().to_string())
                        .collect()
                })
                .unwrap_or_default();
            match schemas.as_ref() {
                Some(schemas) => complete_key(doc, &query, &props, schemas),
                None => complete_properties_schemaless(doc, &query, &props, &keys),
            }
        }
        ScopeKind::Array => match schemas.as_ref() {
            Some(schemas) => complete_value(doc, &query, schemas),
            None => complete_value_schemaless(doc, &query, &keys, true),
        },
        ScopeKind::Value => match schemas.as_ref() {
            Some(schemas) => complete_value(doc, &query, schemas),
            None => complete_value_schemaless(doc, &query, &keys, false),
        },
        _ => return Ok(None),
    };

    Ok(result)
}

fn complete_key(
    doc: &DocumentState,
    query: &Query,
    exist_props: &[String],
    schemas: &[Schema],
) -> Option<CompletionResponse> {
    let mut comps = CompletionMap::default();
    for schema in schemas.iter() {
        if !schema.maybe_type(&SchemaType::Object) {
            continue;
        }
        match schema.properties.as_ref() {
            None => continue,
            Some(properties) => {
                for (prop_key, prop_value) in properties {
                    if exist_props.contains(prop_key) {
                        continue;
                    }
                    comps.add_key(prop_key, prop_value);
                }
            }
        }
    }
    comps.into_key_completions(doc, query)
}

fn complete_value(
    doc: &DocumentState,
    query: &Query,
    schemas: &[Schema],
) -> Option<CompletionResponse> {
    let mut comps = CompletionMap::default();
    for schema in schemas.iter() {
        comps.add_schema(schema);
    }
    comps.into_schema_completions(doc, query)
}

fn complete_annotations_schemaless(
    doc: &DocumentState,
    query: &Query,
    exist_props: &[String],
) -> Option<CompletionResponse> {
    let mut comps = CompletionMap::default();
    for (anno_keys, anno_value) in visit_annotations(&doc.dom) {
        if let Some(anno_key) = anno_keys.last_annotation_key() {
            let anno_key = anno_key.value().to_string();
            if &anno_key == "@" || exist_props.contains(&anno_key) {
                continue;
            }
            let anno_schema = node_to_schema(&anno_value);
            comps.add_key(&anno_key, &anno_schema)
        }
    }
    comps.add_key(SCHEMA_REF_KEY, &SCHEMA_REF_SCHEMA);
    comps.into_key_completions(doc, query)
}

fn complete_properties_schemaless(
    doc: &DocumentState,
    query: &Query,
    exist_props: &[String],
    keys: &Keys,
) -> Option<CompletionResponse> {
    let mut comps = CompletionMap::default();
    let parent_key = match keys.last_property_key() {
        None => return None,
        Some(v) => v,
    };
    let visitor = Visitor::new(&doc.dom, parent_key, |keys, node, parent_key| {
        match keys.last() {
            Some(key) => match key.as_property_key() {
                Some(key) => {
                    if key.value() == parent_key.value() && node.is_object() {
                        VisitControl::AddIter
                    } else {
                        VisitControl::NotAddIter
                    }
                }
                _ => VisitControl::NotAddNotIter,
            },
            _ => VisitControl::NotAddIter,
        }
    });
    for (_, value) in visitor.into_iter() {
        let obj = match value.as_object() {
            Some(obj) => obj,
            None => continue,
        };
        for (key, value) in obj.value().read().iter() {
            let key = key.value().to_string();
            if exist_props.contains(&key) {
                continue;
            }
            let schema = node_to_schema(value);
            comps.add_key(&key, &schema);
        }
    }
    comps.into_key_completions(doc, query)
}

fn complete_value_schemaless(
    doc: &DocumentState,
    query: &Query,
    keys: &Keys,
    is_array: bool,
) -> Option<CompletionResponse> {
    let mut comps = CompletionMap::default();
    let parent_key = match keys.last_property_key() {
        None => return None,
        Some(v) => v,
    };
    let visitor = Visitor::new(&doc.dom, parent_key, |keys, _, parent_key| {
        match keys.last() {
            Some(key) => match key.as_property_key() {
                Some(key) => {
                    if key.value() == parent_key.value() {
                        VisitControl::AddIter
                    } else {
                        VisitControl::NotAddIter
                    }
                }
                _ => VisitControl::NotAddNotIter,
            },
            _ => VisitControl::NotAddIter,
        }
    });
    if is_array {
        for (visitor_keys, value) in visitor.into_iter() {
            if &visitor_keys == keys {
                continue;
            }
            let arr = match value.as_array() {
                Some(v) => v,
                None => continue,
            };
            for value in arr.value().read().iter() {
                comps.add_node(value);
            }
        }
    } else {
        for (visitor_keys, value) in visitor.into_iter() {
            if &visitor_keys == keys {
                continue;
            }
            comps.add_node(&value);
        }
    }
    comps.into_node_completions(doc, query)
}

#[derive(Debug, Default)]
struct CompletionMap {
    keys: IndexMap<String, CompletionKeyData>,
    values: IndexMap<String, CompletionValueData>,
    types_add: IndexSet<SchemaType>,
    types_all: HashSet<SchemaType>,
}

#[derive(Debug)]
struct CompletionKeyData {
    types: HashSet<SchemaType>,
    document: Option<String>,
}

#[derive(Debug)]
struct CompletionValueData {
    plain: bool,
    value: String,
    kind: CompletionItemKind,
}

impl CompletionMap {
    fn add_key(&mut self, key: &str, schema: &Schema) {
        if let Some(key_data) = self.keys.get_mut(key) {
            key_data.types.extend(schema.types());
            if let (None, Some(description)) =
                (key_data.document.as_ref(), schema.description.as_ref())
            {
                key_data.document = Some(description.clone());
            }
        } else {
            self.keys.insert(
                key.to_string(),
                CompletionKeyData {
                    types: schema.types(),
                    document: schema.description.clone(),
                },
            );
        }
    }

    fn add_node(&mut self, node: &Node) {
        match node {
            Node::Null(_) => {
                self.add_null_value();
            }
            Node::Bool(_) => {
                self.add_bool_value();
            }
            Node::Number(v) => {
                let value = v.value().to_string();
                self.add_number_value(value);
            }
            Node::String(v) => {
                let value = match v.syntax() {
                    Some(s) => s.to_string(),
                    None => quote(v.value(), true),
                };
                self.add_string_value(value);
            }
            Node::Array(_) => {
                self.add_array_value();
            }
            Node::Object(_) => {
                self.add_object_value();
            }
        }
    }

    fn add_schema(&mut self, schema: &Schema) {
        let values = collect_schema_values(schema);
        self.types_all.extend(schema.types().into_iter());
        for value in values {
            match value {
                Value::Null => {
                    self.add_null_value();
                }
                Value::Bool(_) => {
                    self.add_bool_value();
                }
                Value::Number(v) => self.add_number_value(v.to_string()),
                Value::String(v) => self.add_string_value(quote(v.as_str(), true)),
                Value::Array(_) => {
                    self.add_array_value();
                }
                Value::Object(_) => {
                    self.add_object_value();
                }
            }
        }
    }

    fn into_key_completions(
        mut self,
        doc: &DocumentState,
        query: &Query,
    ) -> Option<CompletionResponse> {
        if self.keys.is_empty() {
            return None;
        }
        let mut output = vec![];
        let is_annotation = query.scope == ScopeKind::AnnotationKey;
        let (space, comma) = query.space_and_comma();
        for (key, data) in self.keys.drain(..) {
            let CompletionKeyData {
                document: description,
                types,
            } = data;
            let key = if is_annotation {
                key.to_string()
            } else {
                quote(&key, false)
            };
            let mut value = "$1".into();
            let mut command = None;
            let mut is_null = false;
            if query.add_value && types.len() == 1 {
                let schema_type = types.iter().next().unwrap();
                is_null = schema_type == &SchemaType::Null;
                value = insert_text_from_type(schema_type).to_string();
            }
            if value == "$1" {
                command = Some(Command::new(
                    "Suggest".into(),
                    "editor.action.triggerSuggest".into(),
                    None,
                ));
            }
            let label = sanitize_label(key.clone());
            let insert_text = if !query.add_value || (is_annotation && is_null) {
                key
            } else if is_annotation {
                format!("{}({})", key, value)
            } else {
                format!("{}:{}{}{}", key, space, value, comma)
            };
            let text_edit = query.key.as_ref().map(|r| {
                CompletionTextEdit::Edit(TextEdit {
                    range: doc.mapper.range(r.text_range()).unwrap().into_lsp(),
                    new_text: insert_text.to_string(),
                })
            });
            output.push(CompletionItem {
                label,
                kind: Some(CompletionItemKind::PROPERTY),
                insert_text: Some(insert_text),
                text_edit,
                insert_text_format: Some(InsertTextFormat::SNIPPET),
                command,
                documentation: to_document(&description),
                ..Default::default()
            });
        }
        if output.is_empty() {
            return None;
        }
        Some(CompletionResponse::Array(output))
    }

    fn into_node_completions(
        mut self,
        doc: &DocumentState,
        query: &Query,
    ) -> Option<CompletionResponse> {
        if self.values.is_empty() {
            return None;
        }
        let mut output = vec![];
        let (space, comma) = query.space_and_comma();
        for (label, data) in self.values.drain(..) {
            let CompletionValueData { plain, kind, value } = data;
            let format = if plain {
                InsertTextFormat::PLAIN_TEXT
            } else {
                InsertTextFormat::SNIPPET
            };
            let insert_text = format!("{}{}{}", space, value, comma);
            let text_edit = query.value.as_ref().map(|r| {
                CompletionTextEdit::Edit(TextEdit {
                    range: doc.mapper.range(r.text_range()).unwrap().into_lsp(),
                    new_text: insert_text.to_string(),
                })
            });
            output.push(CompletionItem {
                label,
                kind: Some(kind),
                insert_text: Some(insert_text),
                insert_text_format: Some(format),
                text_edit,
                ..Default::default()
            })
        }
        if output.is_empty() {
            return None;
        }
        Some(CompletionResponse::Array(output))
    }

    fn into_schema_completions(
        mut self,
        doc: &DocumentState,
        query: &Query,
    ) -> Option<CompletionResponse> {
        let mut types_miss = vec![];
        for schema_type in self.types_all.iter() {
            if self.types_add.contains(schema_type) {
                continue;
            }
            types_miss.push(schema_type.clone());
        }
        for schema_type in types_miss.into_iter() {
            match schema_type {
                SchemaType::String => {
                    self.add_string_value(r#""""#.into());
                }
                SchemaType::Number => {
                    self.add_number_value("0.0".into());
                }
                SchemaType::Integer => {
                    self.add_number_value("0".into());
                }
                SchemaType::Boolean => {
                    self.add_bool_value();
                }
                SchemaType::Null => {
                    self.add_null_value();
                }
                SchemaType::Object => {
                    self.add_object_value();
                }
                SchemaType::Array => {
                    self.add_array_value();
                }
            }
        }
        self.into_node_completions(doc, query)
    }

    fn add_null_value(&mut self) {
        self.types_add.insert(SchemaType::Null);
        self.values.insert(
            "null".into(),
            CompletionValueData {
                plain: true,
                value: "null".into(),
                kind: CompletionItemKind::VALUE,
            },
        );
    }
    fn add_bool_value(&mut self) {
        self.types_add.insert(SchemaType::Boolean);
        self.values.insert(
            "true".into(),
            CompletionValueData {
                plain: true,
                value: "true".into(),
                kind: CompletionItemKind::VALUE,
            },
        );
        self.values.insert(
            "false".into(),
            CompletionValueData {
                plain: true,
                value: "false".into(),
                kind: CompletionItemKind::VALUE,
            },
        );
    }
    fn add_number_value(&mut self, value: String) {
        self.types_add.insert(SchemaType::Number);
        let label = sanitize_label(value.clone());
        self.values.insert(
            label,
            CompletionValueData {
                plain: true,
                value,
                kind: CompletionItemKind::VALUE,
            },
        );
    }
    fn add_string_value(&mut self, value: String) {
        self.types_add.insert(SchemaType::String);
        let label = sanitize_label(value.clone());
        self.values.insert(
            label,
            CompletionValueData {
                plain: true,
                value,
                kind: CompletionItemKind::VALUE,
            },
        );
    }
    fn add_array_value(&mut self) {
        self.types_add.insert(SchemaType::Array);
        self.values.insert(
            "[]".into(),
            CompletionValueData {
                plain: false,
                value: insert_text_from_type(&SchemaType::Array).to_string(),
                kind: CompletionItemKind::VALUE,
            },
        );
    }
    fn add_object_value(&mut self) {
        self.types_add.insert(SchemaType::Array);
        self.values.insert(
            "{}".into(),
            CompletionValueData {
                plain: false,
                value: insert_text_from_type(&SchemaType::Object).to_string(),
                kind: CompletionItemKind::MODULE,
            },
        );
    }
}

fn insert_text_from_type(schema_type: &SchemaType) -> &'static str {
    match schema_type {
        SchemaType::String => r#""$1""#,
        SchemaType::Number => "$1",
        SchemaType::Integer => "$1",
        SchemaType::Boolean => "$1",
        SchemaType::Null => "${1:null}",
        SchemaType::Object => "{$1}",
        SchemaType::Array => "[$1]",
    }
}

fn node_to_schema(node: &Node) -> Schema {
    Schema {
        schema_type: SchemaType::from_node(node).map(|v| v.into()),
        ..Default::default()
    }
}

fn collect_schema_values(schema: &Schema) -> Vec<&Value> {
    let mut values = vec![];
    if let Some(value) = schema.const_value.as_ref() {
        values.push(value);
    }
    if let Some(value) = schema.default.as_ref() {
        values.push(value);
    }
    if let Some(value) = schema.enum_value.as_ref() {
        values.extend(value);
    }
    if let Some(value) = schema.examples.as_ref() {
        values.extend(value);
    }
    values
}

fn sanitize_label(mut value: String) -> String {
    if value.contains('\n') {
        value = value.replace('\n', "↵");
    }
    if value.len() > 60 {
        value = format!("{}...", &value[0..57])
    }
    value
}

fn to_document(data: &Option<String>) -> Option<Documentation> {
    if let Some(doc) = data.as_ref() {
        return Some(Documentation::MarkupContent(MarkupContent {
            kind: lsp_types::MarkupKind::Markdown,
            value: doc.into(),
        }));
    }
    None
}
