//! GraphQL SDL extractor.
//!
//! Emits Symbols for `type`, `interface`, `union`, `enum`, `input`, `scalar`
//! plus Methods for fields. `implements` clauses become `Implements` edges.

use std::path::Path;
use std::sync::LazyLock;

use ckg_core::{Edge, EdgeKind, Kind, Result, Symbol, node_id};
use regex::Regex;

use crate::ParseOutput;

// Compile each pattern once. Recompiling per `extract` call costs ~200µs
// across 6 patterns; on a 100k-file index that's 20s of pure regex
// compilation. `LazyLock` (stable in Rust 1.80+) initialises on first
// use and re-uses across threads.
static TYPE_RE: LazyLock<Regex> = LazyLock::new(|| {
    Regex::new(
        r"(?ms)^\s*(?:extend\s+)?(type|interface|input)\s+(\w+)(?:\s+implements\s+([^{]+))?\s*\{([^}]*)\}",
    )
    .expect("graphql type_re is a static literal; compile must succeed")
});
static SCALAR_RE: LazyLock<Regex> =
    LazyLock::new(|| Regex::new(r"(?m)^\s*scalar\s+(\w+)").expect("static literal"));
static ENUM_RE: LazyLock<Regex> = LazyLock::new(|| {
    Regex::new(r"(?ms)^\s*(?:extend\s+)?enum\s+(\w+)\s*\{([^}]*)\}").expect("static literal")
});
static UNION_RE: LazyLock<Regex> = LazyLock::new(|| {
    Regex::new(r"(?m)^\s*(?:extend\s+)?union\s+(\w+)\s*=\s*([^\n]+)").expect("static literal")
});
static FIELD_RE: LazyLock<Regex> =
    LazyLock::new(|| Regex::new(r"(?m)^\s*(\w+)\s*(?:\([^)]*\))?\s*:").expect("static literal"));
static VALUE_RE: LazyLock<Regex> =
    LazyLock::new(|| Regex::new(r"(?m)^\s*(\w+)\s*$").expect("static literal"));

pub fn extract(repo_id: &str, path: &Path, source: &str) -> Result<ParseOutput> {
    let mut out = ParseOutput::default();
    let relpath = crate::util::path_to_relpath(path)?;

    let type_re = &*TYPE_RE;
    let scalar_re = &*SCALAR_RE;
    let enum_re = &*ENUM_RE;
    let union_re = &*UNION_RE;
    let field_re = &*FIELD_RE;
    let value_re = &*VALUE_RE;

    for cap in type_re.captures_iter(source) {
        let kw = &cap[1];
        let name = cap[2].to_string();
        let line = line_of(source, cap.get(0).unwrap().start());
        let kind = match kw {
            "type" | "input" => Kind::Class,
            "interface" => Kind::Interface,
            _ => Kind::Class,
        };
        let qname = name.clone();
        let id = push_symbol(&mut out, repo_id, &relpath, kind, qname, name.clone(), line);
        if let Some(impls) = cap.get(3) {
            for tr in impls.as_str().split('&') {
                let tr_clean = tr.trim();
                if !tr_clean.is_empty() {
                    out.edges.push(Edge {
                        kind: EdgeKind::Implements,
                        src: id.clone(),
                        dst: tr_clean.to_string(),
                        confidence: 1.0,
                    });
                }
            }
        }
        // Fields → Methods (for query/mutation/subscription resolvers, and
        // value-fields on regular types).
        let body = &cap[4];
        let body_start = cap.get(4).unwrap().start();
        for fcap in field_re.captures_iter(body) {
            let fname = fcap[1].to_string();
            // PARSE-H3: skip GraphQL keywords / reserved / metadata-only fields.
            // `description` lines are schema metadata, not real fields.
            if matches!(fname.as_str(), "type" | "interface" | "input" | "extend" | "description") {
                continue;
            }
            let fline = line_of(source, body_start + fcap.get(0).unwrap().start());
            let fqname = format!("{name}.{fname}");
            let fid = push_symbol(
                &mut out,
                repo_id,
                &relpath,
                Kind::Method,
                fqname,
                fname,
                fline,
            );
            out.edges.push(Edge {
                kind: EdgeKind::Defines,
                src: id.clone(),
                dst: fid,
                confidence: 1.0,
            });
        }
    }

    for cap in scalar_re.captures_iter(source) {
        let n = cap[1].to_string();
        let line = line_of(source, cap.get(0).unwrap().start());
        push_symbol(
            &mut out,
            repo_id,
            &relpath,
            Kind::TypeAlias,
            n.clone(),
            n,
            line,
        );
    }

    for cap in enum_re.captures_iter(source) {
        let n = cap[1].to_string();
        let line = line_of(source, cap.get(0).unwrap().start());
        let id = push_symbol(
            &mut out,
            repo_id,
            &relpath,
            Kind::Enum,
            n.clone(),
            n.clone(),
            line,
        );
        let body = &cap[2];
        let body_start = cap.get(2).unwrap().start();
        for v in value_re.captures_iter(body) {
            let val = v[1].to_string();
            let vline = line_of(source, body_start + v.get(0).unwrap().start());
            let vqname = format!("{n}.{val}");
            let vid = push_symbol(
                &mut out,
                repo_id,
                &relpath,
                Kind::Constant,
                vqname,
                val,
                vline,
            );
            out.edges.push(Edge {
                kind: EdgeKind::Defines,
                src: id.clone(),
                dst: vid,
                confidence: 1.0,
            });
        }
    }

    for cap in union_re.captures_iter(source) {
        let n = cap[1].to_string();
        let members = &cap[2];
        let line = line_of(source, cap.get(0).unwrap().start());
        let id = push_symbol(
            &mut out,
            repo_id,
            &relpath,
            Kind::TypeAlias,
            n.clone(),
            n,
            line,
        );
        for m in members.split('|') {
            let mm = m.trim();
            if !mm.is_empty() {
                out.edges.push(Edge {
                    kind: EdgeKind::Extends,
                    src: id.clone(),
                    dst: mm.to_string(),
                    confidence: 1.0,
                });
            }
        }
    }
    Ok(out)
}

fn line_of(src: &str, byte_offset: usize) -> u32 {
    src[..byte_offset].matches('\n').count() as u32 + 1
}

fn push_symbol(
    out: &mut ParseOutput,
    repo_id: &str,
    relpath: &str,
    kind: Kind,
    qname: String,
    name: String,
    line: u32,
) -> String {
    let id = node_id(repo_id, "graphql", relpath, &qname, kind);
    out.symbols.push(Symbol {
        id: id.clone(),
        qname: qname.clone(),
        name,
        kind,
        file: relpath.to_string(),
        line,
        col: 0,
        is_public: true,
        doc: String::new(),
        hash: blake3::hash(format!("{qname}:{kind:?}:{relpath}").as_bytes()).to_hex()[..16]
            .to_string(),
    });
    id
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn extracts_types_interfaces_unions_enums() {
        let src = r#"
scalar DateTime

interface Node {
    id: ID!
}

type User implements Node {
    id: ID!
    name: String!
    posts: [Post!]!
}

input CreateUserInput {
    name: String!
}

union SearchResult = User | Post

enum Status {
    ACTIVE
    INACTIVE
}

type Query {
    user(id: ID!): User
    search(q: String!): SearchResult
}

type Mutation {
    createUser(input: CreateUserInput!): User
}
"#;
        let out = extract("repo", Path::new("schema.graphql"), src).unwrap();
        let names: Vec<&str> = out.symbols.iter().map(|s| s.name.as_str()).collect();
        for expected in [
            "DateTime",
            "Node",
            "User",
            "CreateUserInput",
            "SearchResult",
            "Status",
            "Query",
            "Mutation",
            "ACTIVE",
            "INACTIVE",
            "user",
            "search",
            "createUser",
        ] {
            assert!(names.contains(&expected), "missing {expected}: {names:?}");
        }
        // User implements Node
        assert!(
            out.edges
                .iter()
                .any(|e| matches!(e.kind, EdgeKind::Implements) && e.dst == "Node")
        );
        // SearchResult = User | Post → Extends edges
        assert!(
            out.edges
                .iter()
                .any(|e| matches!(e.kind, EdgeKind::Extends) && e.dst == "User")
        );
        assert!(
            out.edges
                .iter()
                .any(|e| matches!(e.kind, EdgeKind::Extends) && e.dst == "Post")
        );
    }
}
