//! Schema/component extraction for OpenAPI 3.x and Swagger 2.x specs.
//!
//! Reads `components.schemas` (OpenAPI 3.x) or `definitions` (Swagger 2.x)
//! and emits:
//! - One `Class` Symbol per named schema.
//! - `Implements` edges for `allOf` parent schemas.
//! - `Constant`-kind Symbols for properties, with `Defines` edges.
//!   Nested `type: object` properties are walked recursively up to
//!   `MAX_REF_DEPTH` to emit the full `Parent.child.grandchild` chain.

use ckg_core::{Edge, EdgeKind, Kind, Symbol, node_id};
use serde_yml::Value;

use crate::ParseOutput;

use super::MAX_REF_DEPTH;

pub(super) fn extract_schemas(doc: &Value, repo_id: &str, relpath: &str, out: &mut ParseOutput) {
    // OpenAPI 3.x: components.schemas; Swagger 2.0: definitions
    let schemas = doc
        .get("components")
        .and_then(|c| c.get("schemas"))
        .and_then(Value::as_mapping)
        .or_else(|| doc.get("definitions").and_then(Value::as_mapping));
    let Some(schemas) = schemas else { return };
    for (name_key, schema) in schemas {
        let Some(name) = name_key.as_str() else {
            continue;
        };
        let qname = name.to_string();
        let id = node_id(repo_id, "openapi", relpath, &qname, Kind::Class);
        out.symbols.push(Symbol {
            id: id.clone(),
            qname: qname.clone(),
            name: name.to_string(),
            kind: Kind::Class,
            file: relpath.to_string(),
            line: 0,
            col: 0,
            is_public: true,
            doc: schema
                .get("description")
                .and_then(Value::as_str)
                .map(|s| s.to_string())
                .unwrap_or_default(),
            hash: blake3::hash(format!("{qname}:Class:{relpath}").as_bytes()).to_hex()[..16]
                .to_string(),
        });
        // allOf → Implements edges to each $ref'd schema.
        if let Some(all_of) = schema.get("allOf").and_then(Value::as_sequence) {
            for item in all_of {
                if let Some(reference) = item.get("$ref").and_then(Value::as_str)
                    && let Some(parent) = reference.split('/').next_back()
                {
                    out.edges.push(Edge {
                        kind: EdgeKind::Implements,
                        src: id.clone(),
                        dst: parent.to_string(),
                        confidence: 1.0,
                    });
                }
            }
        }
        // properties → Constant Symbols. Issue #9: recurse into nested
        // `type: object` properties so `User.address.city` emits as
        // `Defines` chain `User → address → city`. Cycle / depth
        // protection via `MAX_REF_DEPTH`.
        emit_properties(schema, &id, name, repo_id, relpath, 0, out);
    }
}

pub(super) fn emit_properties(
    schema: &Value,
    parent_id: &str,
    parent_qname: &str,
    repo_id: &str,
    relpath: &str,
    depth: usize,
    out: &mut ParseOutput,
) {
    // L8: use `>=` so the cap is enforced at exactly MAX_REF_DEPTH levels.
    if depth >= MAX_REF_DEPTH {
        // Cycle / runaway nesting; stop emitting deeper.
        return;
    }
    let Some(props) = schema.get("properties").and_then(Value::as_mapping) else {
        return;
    };
    let required: Vec<&str> = schema
        .get("required")
        .and_then(Value::as_sequence)
        .map(|s| s.iter().filter_map(Value::as_str).collect())
        .unwrap_or_default();
    for (prop_key, prop_schema) in props {
        let Some(prop_name) = prop_key.as_str() else {
            continue;
        };
        let prop_qname = format!("{parent_qname}.{prop_name}");
        let prop_id = node_id(repo_id, "openapi", relpath, &prop_qname, Kind::Constant);
        let ty = prop_schema
            .get("type")
            .and_then(Value::as_str)
            .or_else(|| {
                prop_schema
                    .get("$ref")
                    .and_then(Value::as_str)
                    .and_then(|r| r.split('/').next_back())
            })
            .unwrap_or("");
        let req_marker = if required.contains(&prop_name) {
            " required"
        } else {
            ""
        };
        out.symbols.push(Symbol {
            id: prop_id.clone(),
            qname: prop_qname.clone(),
            name: prop_name.to_string(),
            kind: Kind::Constant,
            file: relpath.to_string(),
            line: 0,
            col: 0,
            is_public: true,
            doc: format!("type={ty}{req_marker}").trim().to_string(),
            hash: blake3::hash(format!("{prop_qname}:Constant:{relpath}").as_bytes()).to_hex()[..16]
                .to_string(),
        });
        out.edges.push(Edge {
            kind: EdgeKind::Defines,
            src: parent_id.to_string(),
            dst: prop_id.clone(),
            confidence: 1.0,
        });
        // Recurse: if the property is itself `type: object` with its
        // own `properties`, walk into it. (Inline schemas, not $ref —
        // $ref resolution belongs in a separate post-pass since the
        // target schema may be in another file.)
        if prop_schema.get("type").and_then(Value::as_str) == Some("object") {
            emit_properties(
                prop_schema,
                &prop_id,
                &prop_qname,
                repo_id,
                relpath,
                depth + 1,
                out,
            );
        }
    }
}
