use crate::operation::LspCompletionItem;
use crate::schema::context::{CompletionMode, CursorContext};
use crate::schema::format;
use regex::Regex;
use serde_json::Value;
use std::path::Path;

// ---------------------------------------------------------------------------
// Public entry points
// ---------------------------------------------------------------------------

/// Produce schema-backed completion items when the parsed schema `root` is
/// already available.
pub fn completions_from_parsed_schema(
    root: &Value,
    lines: &[String],
    trigger: crate::editor::position::Position,
    path: Option<&Path>,
) -> Vec<LspCompletionItem> {
    match format::detect_cursor_context(lines, &trigger, path) {
        Some(ctx) => completions_for_context(root, &ctx),
        None => legacy_heuristic(root, lines, &trigger),
    }
}

/// Convenience helper that parses `schema_json` and falls back to
/// `completions_from_parsed_schema`.
pub fn completions_from_schema(
    schema_json: &str,
    lines: &[String],
    trigger: crate::editor::position::Position,
    path: Option<&Path>,
) -> Vec<LspCompletionItem> {
    match serde_json::from_str::<Value>(schema_json) {
        Ok(root) => completions_from_parsed_schema(&root, lines, trigger, path),
        Err(_) => Vec::new(),
    }
}

// ---------------------------------------------------------------------------
// Format-agnostic completion engine
// ---------------------------------------------------------------------------

/// Produce completions given a resolved [`CursorContext`].
pub fn completions_for_context(root: &Value, ctx: &CursorContext) -> Vec<LspCompletionItem> {
    match &ctx.mode {
        CompletionMode::Key => {
            // Walk section_path, then suggest properties.
            if let Some(section_schema) = walk_section(root, &ctx.section_path) {
                suggest_properties(root, section_schema)
            } else {
                Vec::new()
            }
        }

        CompletionMode::Value { key } => {
            // Walk section_path → get key schema → suggest values.
            if let Some(section_schema) = walk_section(root, &ctx.section_path)
                && let Some(props) = section_schema
                    .get("properties")
                    .and_then(|p| p.as_object())
                    && let Some(key_schema) = props.get(key.as_str()) {
                        return suggest_values(root, key_schema);
                    }
            Vec::new()
        }

        CompletionMode::InlineKey => {
            // Walk section_path → get additionalProperties → resolve → suggest.
            if let Some(section_schema) = walk_section(root, &ctx.section_path) {
                if let Some(add_props) = section_schema.get("additionalProperties") {
                    let resolved = resolve_concrete(root, add_props, 8);
                    return suggest_properties(root, resolved);
                }
                // Fallback: if no additionalProperties, suggest section properties.
                return suggest_properties(root, section_schema);
            }
            Vec::new()
        }
    }
}

/// Walk the schema along `section_path`, resolving each step with `$ref`/`anyOf`.
///
/// Returns the schema node for the innermost table, or `None` if the path
/// cannot be followed.
fn walk_section<'a>(root: &'a Value, section_path: &[String]) -> Option<&'a Value> {
    let mut cur = root;
    for key in section_path {
        let props = cur.get("properties")?.as_object()?;
        let next = props.get(key.as_str())?;
        cur = resolve_concrete(root, next, 8);
    }
    Some(cur)
}

// ---------------------------------------------------------------------------
// Schema helpers
// ---------------------------------------------------------------------------

/// Follow a `$ref` chain in the schema, resolving `#/$defs/TypeName` references
/// from `root`.  Handles chained refs with a depth limit to prevent loops.
pub fn resolve_ref<'a>(root: &'a Value, schema: &'a Value) -> &'a Value {
    let mut cur = schema;
    for _ in 0..16 {
        if let Some(ref_str) = cur.get("$ref").and_then(|r| r.as_str())
            && let Some(def_path) = ref_str.strip_prefix("#/") {
                let mut next = root;
                let mut ok = true;
                for part in def_path.split('/') {
                    match next.get(part) {
                        Some(n) => next = n,
                        None => { ok = false; break; }
                    }
                }
                if ok && !std::ptr::eq(cur, next) {
                    cur = next;
                    continue;
                }
            }
        break;
    }
    cur
}

/// Check (recursively) whether `schema` eventually resolves to a schema that
/// has `"properties"` (i.e. it describes an object with known keys).
pub fn has_concrete_properties(root: &Value, schema: &Value, depth: u8) -> bool {
    if depth == 0 {
        return false;
    }
    let resolved = resolve_ref(root, schema);
    if resolved.get("properties").is_some() {
        return true;
    }
    if let Some(any_of) = resolved.get("anyOf").and_then(|a| a.as_array()) {
        return any_of.iter().any(|opt| {
            let r = resolve_ref(root, opt);
            !is_null_schema(r) && has_concrete_properties(root, r, depth - 1)
        });
    }
    false
}

/// Resolve `schema` to the most concrete sub-schema that has `"properties"`.
///
/// - Follows `$ref` chains.
/// - When `anyOf` is encountered, prefers options that eventually have concrete
///   properties; falls back to the first non-null option.
pub fn resolve_concrete<'a>(root: &'a Value, schema: &'a Value, depth: u8) -> &'a Value {
    if depth == 0 {
        return schema;
    }
    let resolved = resolve_ref(root, schema);
    if resolved.get("properties").is_some() {
        return resolved;
    }
    if let Some(any_of) = resolved.get("anyOf").and_then(|a| a.as_array()) {
        // Prefer the first option that has (or will lead to) concrete properties.
        for option in any_of {
            let r = resolve_ref(root, option);
            if !is_null_schema(r) && has_concrete_properties(root, r, depth - 1) {
                return resolve_concrete(root, r, depth - 1);
            }
        }
        // Fallback: first non-null option.
        for option in any_of {
            let r = resolve_ref(root, option);
            if !is_null_schema(r) {
                return resolve_concrete(root, r, depth - 1);
            }
        }
    }
    resolved
}

/// Returns `true` if `schema` represents `null` or `{ "type": "null" }`.
pub fn is_null_schema(schema: &Value) -> bool {
    match schema.get("type") {
        Some(Value::String(s)) => s == "null",
        Some(Value::Array(arr)) => {
            arr.len() == 1 && arr.first().and_then(|v| v.as_str()) == Some("null")
        }
        _ => false,
    }
}

/// Returns `true` if `schema` (or any `anyOf` branch) allows a boolean value.
pub fn is_boolean_schema(root: &Value, schema: &Value, depth: u8) -> bool {
    if depth == 0 {
        return false;
    }
    let resolved = resolve_ref(root, schema);
    let ty_is_bool = |v: &Value| match v {
        Value::String(s) => s == "boolean" || s == "bool",
        Value::Array(arr) => arr
            .iter()
            .any(|x| x.as_str() == Some("boolean") || x.as_str() == Some("bool")),
        _ => false,
    };
    if let Some(t) = resolved.get("type")
        && ty_is_bool(t) {
            return true;
        }
    if let Some(any_of) = resolved.get("anyOf").and_then(|a| a.as_array()) {
        return any_of.iter().any(|opt| {
            let r = resolve_ref(root, opt);
            !is_null_schema(r) && is_boolean_schema(root, r, depth - 1)
        });
    }
    false
}

/// Suggest property name completions from a schema.
pub fn suggest_properties(root: &Value, schema: &Value) -> Vec<LspCompletionItem> {
    let mut out = Vec::new();
    // If the schema itself has a `properties` map, use it.
    if let Some(props) = schema.get("properties").and_then(|p| p.as_object()) {
        for (k, v) in props {
            let resolved = resolve_concrete(root, v, 3);
            let detail = resolved
                .get("type")
                .and_then(|t| t.as_str())
                .map(String::from);
            out.push(LspCompletionItem {
                label: k.clone(),
                kind: Some(10),
                detail,
                insert_text: None,
            });
        }
        return out;
    }
    // If the schema has anyOf, try to find a concrete schema with properties.
    let resolved = resolve_concrete(root, schema, 6);
    if let Some(props) = resolved.get("properties").and_then(|p| p.as_object()) {
        for (k, v) in props {
            let res = resolve_concrete(root, v, 3);
            let detail = res
                .get("type")
                .and_then(|t| t.as_str())
                .map(String::from);
            out.push(LspCompletionItem {
                label: k.clone(),
                kind: Some(10),
                detail,
                insert_text: None,
            });
        }
    }
    out
}

/// Suggest value completions for a schema (enum values, booleans).
pub fn suggest_values(root: &Value, schema: &Value) -> Vec<LspCompletionItem> {
    let mut out = Vec::new();
    // Enum values.
    if let Some(enum_vals) = schema.get("enum").and_then(|e| e.as_array()) {
        for v in enum_vals {
            let label = v.as_str().map(String::from).unwrap_or_else(|| v.to_string());
            out.push(LspCompletionItem {
                label: label.clone(),
                kind: Some(12),
                detail: None,
                insert_text: Some(label),
            });
        }
        return out;
    }
    // Boolean values.
    if is_boolean_schema(root, schema, 8) {
        out.push(LspCompletionItem {
            label: "true".into(),
            kind: Some(12),
            detail: Some("boolean".into()),
            insert_text: Some("true".into()),
        });
        out.push(LspCompletionItem {
            label: "false".into(),
            kind: Some(12),
            detail: Some("boolean".into()),
            insert_text: Some("false".into()),
        });
    }
    out
}

// ---------------------------------------------------------------------------
// Legacy heuristic (JSON/YAML, kept for backward compatibility)
// ---------------------------------------------------------------------------

fn legacy_heuristic(
    schema_val: &Value,
    lines: &[String],
    trigger: &crate::editor::position::Position,
) -> Vec<LspCompletionItem> {
    let mut out: Vec<LspCompletionItem> = Vec::new();

    // Build text up to cursor.
    let mut text_up_to_cursor = String::new();
    for (i, line) in lines.iter().enumerate() {
        if i < trigger.line {
            text_up_to_cursor.push_str(line);
            text_up_to_cursor.push('\n');
        } else if i == trigger.line {
            let col = trigger.column.min(line.len());
            text_up_to_cursor.push_str(&line[..col]);
            break;
        } else {
            break;
        }
    }

    // Quoted keys ("foo":) then fallback to unquoted (yaml/toml: foo:)
    let re_q = Regex::new("\"([^\"\\\\]+)\"\\s*:").unwrap();
    let re_unq = Regex::new(r"(?m)^\s*([A-Za-z0-9_\-]+)\s*:").unwrap();

    let mut keys: Vec<String> = Vec::new();
    for caps in re_q.captures_iter(&text_up_to_cursor) {
        if let Some(m) = caps.get(1) {
            keys.push(m.as_str().to_string());
        }
    }
    if keys.is_empty() {
        for caps in re_unq.captures_iter(&text_up_to_cursor) {
            if let Some(m) = caps.get(1) {
                keys.push(m.as_str().to_string());
            }
        }
    }

    // No context: suggest top-level properties.
    if keys.is_empty() {
        if let Some(props) = schema_val.get("properties").and_then(|p| p.as_object()) {
            for (k, v) in props.iter() {
                let detail = v
                    .get("type")
                    .and_then(|t| t.as_str())
                    .map(|s| s.to_string());
                out.push(LspCompletionItem {
                    label: k.clone(),
                    kind: Some(10),
                    detail,
                    insert_text: None,
                });
            }
        }
        return out;
    }

    // Walk schema properties along detected path.
    let mut cur = schema_val;
    for key in &keys {
        if let Some(props) = cur.get("properties").and_then(|p| p.as_object()) {
            if let Some(next) = props.get(key) {
                cur = next;
                continue;
            } else {
                break;
            }
        } else {
            break;
        }
    }

    // Enum values.
    if let Some(enum_vals) = cur.get("enum").and_then(|e| e.as_array()) {
        for v in enum_vals {
            let label = if let Some(s) = v.as_str() {
                s.to_string()
            } else {
                v.to_string()
            };
            out.push(LspCompletionItem {
                label: label.clone(),
                kind: Some(12),
                detail: None,
                insert_text: Some(label),
            });
        }
        return out;
    }

    // Child properties.
    if let Some(props) = cur.get("properties").and_then(|p| p.as_object()) {
        for (k, v) in props.iter() {
            let detail = v
                .get("type")
                .and_then(|t| t.as_str())
                .map(|s| s.to_string());
            out.push(LspCompletionItem {
                label: k.clone(),
                kind: Some(10),
                detail,
                insert_text: None,
            });
        }
        return out;
    }

    // Boolean fallback.
    if let Some(t) = cur.get("type").and_then(|t| t.as_str())
        && (t == "boolean" || t == "bool") {
            out.push(LspCompletionItem {
                label: "true".into(),
                kind: Some(12),
                detail: Some("boolean".into()),
                insert_text: Some("true".into()),
            });
            out.push(LspCompletionItem {
                label: "false".into(),
                kind: Some(12),
                detail: Some("boolean".into()),
                insert_text: Some("false".into()),
            });
        }

    out
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

#[cfg(test)]
mod tests {
    use super::*;
    use crate::editor::position::Position;

    fn pos(line: usize, col: usize) -> Position {
        Position { line, column: col }
    }

    // --- Legacy heuristic (backward compat) ---

    #[test]
    fn top_level_keys_suggested() {
        let schema =
            r#"{"type":"object","properties":{"foo":{"type":"string"},"bar":{"type":"number"}}}"#;
        let lines = vec![String::new()];
        let items = completions_from_schema(schema, &lines, pos(0, 0), None);
        let labels: Vec<String> = items.into_iter().map(|i| i.label).collect();
        assert!(labels.contains(&"foo".to_string()));
        assert!(labels.contains(&"bar".to_string()));
    }

    #[test]
    fn nested_enum_values_suggested() {
        let schema = r#"{"type":"object","properties":{"parent":{"type":"object","properties":{"child":{"type":"string","enum":["a","b"]}}}}}"#;
        let lines = vec![r#"{"parent": {"child":"#.to_string()];
        let items = completions_from_schema(schema, &lines, pos(0, lines[0].len()), None);
        let labels: Vec<String> = items.into_iter().map(|i| i.label).collect();
        assert!(labels.contains(&"a".to_string()));
        assert!(labels.contains(&"b".to_string()));
    }

    // --- Schema helpers ---

    #[test]
    fn resolve_ref_follows_defs() {
        let schema: Value = serde_json::from_str(
            r##"{"$defs":{"Foo":{"type":"object","properties":{"x":{"type":"string"}}}},"properties":{"a":{"$ref":"#/$defs/Foo"}}}"##
        ).unwrap();
        let a = schema["properties"]["a"].clone();
        let resolved = resolve_ref(&schema, &a);
        assert!(resolved.get("properties").is_some());
    }

    #[test]
    fn is_boolean_schema_detects_boolean_in_any_of() {
        let root: Value = serde_json::from_str(
            r#"{"$defs":{"BoolOrNull":{"anyOf":[{"type":"boolean"},{"type":"null"}]}}}"#,
        )
        .unwrap();
        let schema: Value = serde_json::from_str(r##"{"$ref":"#/$defs/BoolOrNull"}"##).unwrap();
        assert!(is_boolean_schema(&root, &schema, 5));
    }

    #[test]
    fn suggest_values_returns_bool_literals() {
        let root: Value = serde_json::from_str("{}").unwrap();
        let schema: Value = serde_json::from_str(r#"{"type":"boolean"}"#).unwrap();
        let items = suggest_values(&root, &schema);
        let labels: Vec<_> = items.iter().map(|i| i.label.as_str()).collect();
        assert!(labels.contains(&"true"));
        assert!(labels.contains(&"false"));
    }
}

