//! CST → typed AST lowering.
//!
//! Walks the rowan-based CST wrappers and builds the existing
//! `ast::query::Query` tree. This is the path used by `parse()`.

mod shared;

use self::shared::{decode_string_content, parse_double, parse_integer};
use crate::error::{CypherError, ErrorKind, Result, Span};
use crate::syntax::SyntaxKind;
use crate::syntax::ast::AstNode;

// ── CST module aliases (source) ─────────────────────────────────────
use cst_c::*;

mod cst_c {
    pub use crate::syntax::ast::clauses::*;
    pub use crate::syntax::ast::expressions;
    pub use crate::syntax::ast::expressions::*;

    pub use crate::syntax::ast::patterns::*;
    pub use crate::syntax::ast::projection::*;
    pub use crate::syntax::ast::schema as schema_cst;
    pub use crate::syntax::ast::top_level;
    pub use crate::syntax::ast::top_level::*;
}

// ── AST module aliases (target) ─────────────────────────────────────
mod ast_c {
    pub use crate::ast::clause::*;
    pub use crate::ast::expr::*;
    pub use crate::ast::names::*;
    pub use crate::ast::pattern::*;
    pub use crate::ast::procedure::*;
    pub use crate::ast::query::*;
    pub use crate::ast::schema::*;
}

// ── Shared helpers ───────────────────────────────────────────────────

fn span_of(node: &rowan::SyntaxNode<crate::syntax::CypherLang>) -> Span {
    let r = node.text_range();
    Span::new(r.start().into(), r.end().into())
}

fn symbolic_name_text(sym: &SymbolicName) -> String {
    if let Some(ident) = sym.ident_token() {
        ident.unescape()
    } else {
        // Fallback: get text from any child token (handles keyword-based names like COUNT)
        sym.syntax()
            .children_with_tokens()
            .find_map(|el| el.as_token().map(|t| t.text().to_string()))
            .unwrap_or_default()
    }
}

fn internal(msg: &str, sp: Span) -> CypherError {
    CypherError {
        kind: ErrorKind::Internal {
            message: msg.into(),
        },
        span: sp,
        source_label: None,
        notes: Vec::new(),
        source: None,
    }
}

// ── Entry point ──────────────────────────────────────────────────────

pub fn build_source_file(src: SourceFile) -> Result<ast_c::Query> {
    let sp = span_of(src.syntax());
    let mut statements = Vec::new();

    for cmd in src.schema_commands() {
        statements.push(ast_c::QueryBody::SchemaCommand(build_schema_command(cmd)?));
    }

    for stmt in src.statements() {
        statements.extend(build_statement(stmt)?);
    }

    if statements.is_empty() {
        return Err(internal("empty source file", sp));
    }

    Ok(ast_c::Query {
        statements,
        span: sp,
    })
}

fn build_statement(stmt: Statement) -> Result<Vec<ast_c::QueryBody>> {
    let clauses: Vec<_> = stmt.clauses().collect();
    if clauses.is_empty() {
        return Err(internal("empty statement", span_of(stmt.syntax())));
    }

    if clauses.len() == 1 {
        match &clauses[0] {
            Clause::Show(c) => return Ok(vec![ast_c::QueryBody::Show(build_show(c.clone())?)]),
            Clause::Use(c) => return Ok(vec![ast_c::QueryBody::Use(build_use(c.clone())?)]),
            Clause::StandaloneCall(c) => {
                return Ok(vec![ast_c::QueryBody::Standalone(build_standalone_call(
                    c.clone(),
                )?)]);
            }
            _ => {}
        }
    }

    let unions: Vec<_> = stmt.syntax().children().filter_map(Union::cast).collect();

    if !unions.is_empty() {
        let regular = build_regular_query(&clauses, &unions)?;
        if regular.unions.is_empty() {
            Ok(vec![ast_c::QueryBody::SingleQuery(regular.single_query)])
        } else {
            Ok(vec![ast_c::QueryBody::Regular(regular)])
        }
    } else {
        let single = build_single_query_from_clauses(clauses)?;
        Ok(vec![ast_c::QueryBody::SingleQuery(single)])
    }
}

fn build_regular_query(clauses: &[Clause], unions: &[Union]) -> Result<ast_c::RegularQuery> {
    let single_query = build_single_query_from_clauses(clauses.to_vec())?;
    let mut result_unions = Vec::new();

    for union_node in unions {
        let all = union_node.all_token().is_some();
        let union_clauses: Vec<_> = union_node.clauses().collect();
        let inner_unions: Vec<_> = union_node.inner_unions().collect();
        let mut sq = build_single_query_from_clauses(union_clauses)?;

        // Handle nested unions
        for nested in inner_unions {
            let n_all = nested.all_token().is_some();
            let n_clauses: Vec<_> = nested.clauses().collect();
            let n_sq = build_single_query_from_clauses(n_clauses)?;
            let _ = n_all;
            sq = n_sq;
        }

        result_unions.push(ast_c::Union {
            all,
            single_query: sq,
            span: span_of(union_node.syntax()),
        });
    }

    Ok(ast_c::RegularQuery {
        single_query,
        unions: result_unions,
    })
}

fn build_single_query_from_clauses(clauses: Vec<Clause>) -> Result<ast_c::SingleQuery> {
    let with_indices: Vec<_> = clauses
        .iter()
        .enumerate()
        .filter(|(_, c)| matches!(c, Clause::With(_)))
        .map(|(i, _)| i)
        .collect();

    let total = clauses.len();
    let is_multipart_with = |idx: usize| -> bool { idx < total - 1 };

    if with_indices.is_empty() {
        let mut reading = Vec::new();
        let mut updating = Vec::new();
        let mut ret = None;

        for c in clauses {
            match c {
                Clause::Match(m) => reading.push(ast_c::ReadingClause::Match(build_match(m)?)),
                Clause::Unwind(u) => reading.push(ast_c::ReadingClause::Unwind(build_unwind(u)?)),
                Clause::InQueryCall(ic) => {
                    reading.push(ast_c::ReadingClause::InQueryCall(build_in_query_call(ic)?))
                }
                Clause::CallSubquery(cs) => reading.push(ast_c::ReadingClause::CallSubquery(
                    Box::new(build_call_subquery(cs)?),
                )),
                Clause::LoadCsv(lc) => {
                    reading.push(ast_c::ReadingClause::LoadCsv(build_load_csv(lc)?))
                }
                Clause::Create(c) => updating.push(ast_c::UpdatingClause::Create(build_create(c)?)),
                Clause::Merge(m) => updating.push(ast_c::UpdatingClause::Merge(build_merge(m)?)),
                Clause::Set(s) => updating.push(ast_c::UpdatingClause::Set(build_set(s)?)),
                Clause::Remove(r) => updating.push(ast_c::UpdatingClause::Remove(build_remove(r)?)),
                Clause::Delete(d) => updating.push(ast_c::UpdatingClause::Delete(build_delete(d)?)),
                Clause::Foreach(f) => {
                    updating.push(ast_c::UpdatingClause::Foreach(build_foreach(f)?))
                }
                Clause::Return(r) => ret = Some(build_return(r)?),
                Clause::Finish(f) => {
                    return Ok(ast_c::SingleQuery {
                        kind: ast_c::SingleQueryKind::SinglePart(ast_c::SinglePartQuery {
                            reading_clauses: reading,
                            body: ast_c::SinglePartBody::Finish(build_finish(f)?),
                        }),
                    });
                }
                Clause::With(_) => {}
                Clause::Where(_) => {}
                Clause::Show(_) | Clause::Use(_) | Clause::StandaloneCall(_) => {}
            }
        }

        let body = if !updating.is_empty() {
            ast_c::SinglePartBody::Updating {
                updating,
                return_clause: ret,
            }
        } else {
            ast_c::SinglePartBody::Return(ret.ok_or_else(|| {
                internal("single-part query must end with RETURN", Span::new(0, 0))
            })?)
        };

        Ok(ast_c::SingleQuery {
            kind: ast_c::SingleQueryKind::SinglePart(ast_c::SinglePartQuery {
                reading_clauses: reading,
                body,
            }),
        })
    } else {
        let mut parts = Vec::new();
        let mut final_part = None;
        let mut reading = Vec::new();
        let mut updating = Vec::new();

        for (i, c) in clauses.into_iter().enumerate() {
            match c {
                Clause::With(w) if is_multipart_with(i) => {
                    let wc = build_with(w)?;
                    parts.push(ast_c::MultiPartQueryPart {
                        reading_clauses: std::mem::take(&mut reading),
                        updating_clauses: std::mem::take(&mut updating),
                        with: wc,
                    });
                }
                Clause::Match(m) => reading.push(ast_c::ReadingClause::Match(build_match(m)?)),
                Clause::Unwind(u) => reading.push(ast_c::ReadingClause::Unwind(build_unwind(u)?)),
                Clause::InQueryCall(ic) => {
                    reading.push(ast_c::ReadingClause::InQueryCall(build_in_query_call(ic)?))
                }
                Clause::CallSubquery(cs) => reading.push(ast_c::ReadingClause::CallSubquery(
                    Box::new(build_call_subquery(cs)?),
                )),
                Clause::LoadCsv(lc) => {
                    reading.push(ast_c::ReadingClause::LoadCsv(build_load_csv(lc)?))
                }
                Clause::Create(c) => updating.push(ast_c::UpdatingClause::Create(build_create(c)?)),
                Clause::Merge(m) => updating.push(ast_c::UpdatingClause::Merge(build_merge(m)?)),
                Clause::Set(s) => updating.push(ast_c::UpdatingClause::Set(build_set(s)?)),
                Clause::Remove(r) => updating.push(ast_c::UpdatingClause::Remove(build_remove(r)?)),
                Clause::Delete(d) => updating.push(ast_c::UpdatingClause::Delete(build_delete(d)?)),
                Clause::Foreach(f) => {
                    updating.push(ast_c::UpdatingClause::Foreach(build_foreach(f)?))
                }
                Clause::Return(r) => {
                    let ret = build_return(r)?;
                    final_part = Some(ast_c::SinglePartQuery {
                        reading_clauses: std::mem::take(&mut reading),
                        body: ast_c::SinglePartBody::Return(ret),
                    });
                }
                Clause::Finish(f) => {
                    final_part = Some(ast_c::SinglePartQuery {
                        reading_clauses: std::mem::take(&mut reading),
                        body: ast_c::SinglePartBody::Finish(build_finish(f)?),
                    });
                }
                Clause::With(w) => {
                    let wc = build_with(w)?;
                    final_part = Some(ast_c::SinglePartQuery {
                        reading_clauses: std::mem::take(&mut reading),
                        body: ast_c::SinglePartBody::Return(ast_c::Return {
                            distinct: wc.distinct,
                            star: wc.star,
                            items: wc.items,
                            order: wc.order,
                            skip: wc.skip,
                            limit: wc.limit,
                            span: wc.span,
                        }),
                    });
                }
                Clause::Where(_) => {}
                Clause::Show(_) | Clause::Use(_) | Clause::StandaloneCall(_) => {}
            }
        }

        let final_part = final_part
            .ok_or_else(|| internal("multi-part query missing final part", Span::new(0, 0)))?;

        Ok(ast_c::SingleQuery {
            kind: ast_c::SingleQueryKind::MultiPart(ast_c::MultiPartQuery { parts, final_part }),
        })
    }
}

// ── Clauses ──────────────────────────────────────────────────────────

fn build_match(c: MatchClause) -> Result<ast_c::Match> {
    let sp = span_of(c.syntax());
    let optional = c.optional_token().is_some();
    let pattern = c
        .pattern()
        .map(build_pattern)
        .transpose()?
        .ok_or_else(|| internal("missing pattern in MATCH", sp))?;
    let where_clause = c
        .where_clause()
        .and_then(|w| w.expr())
        .map(build_expression)
        .transpose()?;
    Ok(ast_c::Match {
        optional,
        pattern,
        where_clause,
        span: sp,
    })
}

fn build_unwind(c: UnwindClause) -> Result<ast_c::Unwind> {
    let sp = span_of(c.syntax());
    let expr = c
        .expr()
        .map(build_expression)
        .transpose()?
        .ok_or_else(|| internal("missing expression in UNWIND", sp))?;
    let variable = c
        .as_name()
        .map(build_top_variable)
        .ok_or_else(|| internal("missing variable in UNWIND", sp))?;
    Ok(ast_c::Unwind {
        expression: expr,
        variable,
        span: sp,
    })
}

fn build_create(c: CreateClause) -> Result<ast_c::Create> {
    let sp = span_of(c.syntax());
    let pattern = c
        .pattern()
        .map(build_pattern)
        .transpose()?
        .ok_or_else(|| internal("missing pattern in CREATE", sp))?;
    Ok(ast_c::Create { pattern, span: sp })
}

fn build_merge(c: MergeClause) -> Result<ast_c::Merge> {
    let sp = span_of(c.syntax());
    let pattern = c
        .pattern()
        .map(build_pattern_part)
        .transpose()?
        .ok_or_else(|| internal("missing pattern in MERGE", sp))?;
    let actions: Result<Vec<_>> = c.actions().map(build_merge_action).collect();
    Ok(ast_c::Merge {
        pattern,
        actions: actions?,
        span: sp,
    })
}

fn build_merge_action(a: MergeAction) -> Result<ast_c::MergeAction> {
    let sp = span_of(a.syntax());
    let on_match = a
        .match_or_create_token()
        .map(|t| t.kind() == SyntaxKind::KW_MATCH)
        .unwrap_or(false);
    let items: Result<Vec<_>> = a.set_items().map(build_set_item).collect();
    Ok(ast_c::MergeAction {
        on_match,
        set_items: items?,
        span: sp,
    })
}

fn build_set(c: SetClause) -> Result<ast_c::Set> {
    let sp = span_of(c.syntax());
    let items: Result<Vec<_>> = c.items().map(build_set_item).collect();
    Ok(ast_c::Set {
        items: items?,
        span: sp,
    })
}

fn build_set_item(item: SetItem) -> Result<ast_c::SetItem> {
    let sp = span_of(item.syntax());

    if let Some(labels) = item.node_labels()
        && let Some(_prop_expr) = item.property_expr()
    {
        let ast_labels: Result<Vec<_>> = labels
            .labels()
            .map(|l| {
                l.name()
                    .and_then(|ln| ln.symbolic_name())
                    .map(|s| ast_c::SymbolicName {
                        name: symbolic_name_text(&s),
                        span: span_of(s.syntax()),
                    })
                    .ok_or_else(|| internal("missing label name", sp))
            })
            .collect();
        let v = ast_c::Variable {
            name: ast_c::SymbolicName {
                name: String::new(),
                span: sp,
            },
        };
        return Ok(ast_c::SetItem::Labels {
            variable: v,
            labels: ast_labels?,
        });
    }

    let property = item
        .property_expr()
        .ok_or_else(|| internal("missing property expression in SET item", sp))?;
    let value = item
        .value_expr()
        .ok_or_else(|| internal("missing value expression in SET item", sp))?;
    let value_ast = build_expression(value)?;
    let operator = if item.plus_eq_token().is_some() {
        ast_c::SetOperator::Add
    } else {
        ast_c::SetOperator::Assign
    };

    let prop_ast = build_expression(property)?;
    match &prop_ast {
        ast_c::Expression::Variable(v) => Ok(ast_c::SetItem::Variable {
            variable: v.clone(),
            value: value_ast,
            operator,
        }),
        _ => Ok(ast_c::SetItem::Property {
            property: prop_ast,
            value: value_ast,
            operator,
        }),
    }
}

fn build_delete(c: DeleteClause) -> Result<ast_c::Delete> {
    let sp = span_of(c.syntax());
    let detach = c.detach_token().is_some();
    let targets: Result<Vec<_>> = c.exprs().map(build_expression).collect();
    Ok(ast_c::Delete {
        detach,
        targets: targets?,
        span: sp,
    })
}

fn build_remove(c: RemoveClause) -> Result<ast_c::Remove> {
    let sp = span_of(c.syntax());
    let items: Result<Vec<_>> = c.items().map(build_remove_item).collect();
    Ok(ast_c::Remove {
        items: items?,
        span: sp,
    })
}

fn build_remove_item(item: RemoveItem) -> Result<ast_c::RemoveItem> {
    let sp = span_of(item.syntax());
    if let Some(labels) = item.node_labels()
        && let Some(_prop_expr) = item.property_expr()
    {
        let ast_labels: Result<Vec<_>> = labels
            .labels()
            .map(|l| {
                l.name()
                    .and_then(|ln| ln.symbolic_name())
                    .map(|s| ast_c::SymbolicName {
                        name: symbolic_name_text(&s),
                        span: span_of(s.syntax()),
                    })
                    .ok_or_else(|| internal("missing label name", sp))
            })
            .collect();
        let v = ast_c::Variable {
            name: ast_c::SymbolicName {
                name: String::new(),
                span: sp,
            },
        };
        return Ok(ast_c::RemoveItem::Labels {
            variable: v,
            labels: ast_labels?,
        });
    }
    let prop = item
        .property_expr()
        .ok_or_else(|| internal("missing property in REMOVE item", sp))?;
    let prop_ast = build_expression(prop)?;
    Ok(ast_c::RemoveItem::Property(prop_ast))
}

fn build_with(c: WithClause) -> Result<ast_c::With> {
    let sp = span_of(c.syntax());
    let proj = c
        .projection_body()
        .map(build_projection_body)
        .transpose()?
        .ok_or_else(|| internal("missing projection in WITH", sp))?;
    let where_clause = c
        .where_clause()
        .and_then(|w| w.expr())
        .map(build_expression)
        .transpose()?;
    Ok(ast_c::With {
        distinct: proj.distinct,
        star: proj.star,
        items: proj.items,
        order: proj.order,
        skip: proj.skip,
        limit: proj.limit,
        where_clause,
        span: sp,
    })
}

fn build_return(c: ReturnClause) -> Result<ast_c::Return> {
    let sp = span_of(c.syntax());
    let proj = c.projection_body().map(build_projection_body).transpose()?;
    let proj = match proj {
        Some(p) if !p.items.is_empty() || p.star => p,
        _ => {
            return Err(CypherError {
                kind: ErrorKind::MissingClause {
                    clause: "projection",
                    after: "RETURN",
                },
                span: sp,
                source_label: None,
                notes: Vec::new(),
                source: None,
            });
        }
    };
    Ok(ast_c::Return {
        distinct: proj.distinct,
        star: proj.star,
        items: proj.items,
        order: proj.order,
        skip: proj.skip,
        limit: proj.limit,
        span: sp,
    })
}

fn build_foreach(c: ForeachClause) -> Result<ast_c::Foreach> {
    let sp = span_of(c.syntax());
    let variable = c
        .variable()
        .map(build_top_variable)
        .ok_or_else(|| internal("missing variable in FOREACH", sp))?;
    let list = c
        .list()
        .map(build_expression)
        .transpose()?
        .ok_or_else(|| internal("missing list in FOREACH", sp))?;
    let updates: Result<Vec<_>> = c
        .clauses()
        .filter_map(|cl| match cl {
            Clause::Create(cc) => Some(build_create(cc).map(ast_c::ForeachUpdate::Create)),
            Clause::Merge(mc) => Some(build_merge(mc).map(ast_c::ForeachUpdate::Merge)),
            Clause::Set(sc) => Some(build_set(sc).map(ast_c::ForeachUpdate::Set)),
            Clause::Remove(rc) => Some(build_remove(rc).map(ast_c::ForeachUpdate::Remove)),
            Clause::Delete(dc) => Some(build_delete(dc).map(ast_c::ForeachUpdate::Delete)),
            Clause::Foreach(fc) => Some(build_foreach(fc).map(ast_c::ForeachUpdate::Foreach)),
            _ => None,
        })
        .collect();
    Ok(ast_c::Foreach {
        variable,
        list,
        updates: updates?,
        span: sp,
    })
}

// ── Projection ───────────────────────────────────────────────────────

struct ProjResult {
    distinct: bool,
    star: bool,
    items: Vec<ast_c::ProjectionItem>,
    order: Option<ast_c::Order>,
    skip: Option<ast_c::Expression>,
    limit: Option<ast_c::Expression>,
}

fn build_projection_body(body: ProjectionBody) -> Result<ProjResult> {
    let distinct = body.distinct_token().is_some();
    let star = body.star_token().is_some();
    let items: Result<Vec<_>> = body.items().map(build_projection_item).collect();
    let order = body.order_by().map(build_order).transpose()?;
    let skip = body
        .skip()
        .and_then(|s| s.expr().map(build_expression))
        .transpose()?;
    let limit = body
        .limit()
        .and_then(|l| l.expr().map(build_expression))
        .transpose()?;
    Ok(ProjResult {
        distinct,
        star,
        items: items?,
        order,
        skip,
        limit,
    })
}

fn build_projection_item(item: ProjectionItem) -> Result<ast_c::ProjectionItem> {
    let sp = span_of(item.syntax());
    let expr = if let Some(e) = item.expr() {
        build_expression(e)?
    } else if item.syntax().children_with_tokens().any(|t| {
        t.as_token()
            .is_some_and(|t| t.kind() == SyntaxKind::NULL_KW)
    }) {
        ast_c::Expression::Literal(ast_c::Literal::Null)
    } else {
        let int_text = item.syntax().children_with_tokens().find_map(|t| {
            t.as_token()
                .filter(|t| t.kind() == SyntaxKind::INTEGER)
                .map(|t| t.text().to_string())
        });
        if let Some(text) = int_text {
            let val = parse_integer(&text).ok_or_else(|| internal("invalid integer", sp))?;
            ast_c::Expression::Literal(ast_c::Literal::Number(ast_c::NumberLiteral::Integer(val)))
        } else {
            let float_text = item.syntax().children_with_tokens().find_map(|t| {
                t.as_token()
                    .filter(|t| t.kind() == SyntaxKind::FLOAT)
                    .map(|t| t.text().to_string())
            });
            if let Some(text) = float_text {
                let val = parse_double(&text).ok_or_else(|| internal("invalid float", sp))?;
                ast_c::Expression::Literal(ast_c::Literal::Number(ast_c::NumberLiteral::Float(val)))
            } else {
                return Err(internal("missing expr in projection item", sp));
            }
        }
    };
    let alias = item.as_name().map(build_top_variable);
    Ok(ast_c::ProjectionItem {
        expression: expr,
        alias,
    })
}

fn build_order(o: OrderBy) -> Result<ast_c::Order> {
    let items: Result<Vec<_>> = o.items().map(build_sort_item).collect();
    Ok(ast_c::Order { items: items? })
}

fn build_sort_item(s: SortItem) -> Result<ast_c::SortItem> {
    let expr = s
        .expr()
        .map(build_expression)
        .transpose()?
        .ok_or_else(|| internal("missing expr in sort item", span_of(s.syntax())))?;
    let direction = s.direction().map(|d| match d {
        SortDirection::Ascending => ast_c::SortDirection::Ascending,
        SortDirection::Descending => ast_c::SortDirection::Descending,
    });
    Ok(ast_c::SortItem {
        expression: expr,
        direction,
    })
}

// ── Patterns ─────────────────────────────────────────────────────────

fn build_pattern(p: Pattern) -> Result<ast_c::Pattern> {
    let sp = span_of(p.syntax());
    let parts: Result<Vec<_>> = p.parts().map(build_pattern_part).collect();
    Ok(ast_c::Pattern {
        parts: parts?,
        span: sp,
    })
}

fn build_pattern_part(pp: PatternPart) -> Result<ast_c::PatternPart> {
    let sp = span_of(pp.syntax());
    let variable = pp.variable().map(build_top_variable);
    let anonymous = pp
        .anonymous_part()
        .map(build_anonymous_pattern_part)
        .transpose()?
        .ok_or_else(|| internal("missing anonymous part", sp))?;
    Ok(ast_c::PatternPart {
        variable,
        anonymous,
        span: sp,
    })
}

fn build_anonymous_pattern_part(app: AnonymousPatternPart) -> Result<ast_c::AnonymousPatternPart> {
    let element = app
        .element()
        .map(build_pattern_element)
        .transpose()?
        .ok_or_else(|| internal("missing element", span_of(app.syntax())))?;
    Ok(ast_c::AnonymousPatternPart { element })
}

fn build_pattern_element(pe: PatternElement) -> Result<ast_c::PatternElement> {
    if pe.syntax().kind() == SyntaxKind::QUANTIFIED_PATH_PATTERN {
        let inner = pe
            .inner()
            .map(build_pattern_element)
            .transpose()?
            .ok_or_else(|| {
                internal(
                    "missing inner quantified path element",
                    span_of(pe.syntax()),
                )
            })?;
        let quantifier = pe
            .quantifier()
            .map(build_quantifier)
            .transpose()?
            .ok_or_else(|| internal("missing quantified path quantifier", span_of(pe.syntax())))?;
        return Ok(ast_c::PatternElement::Quantified {
            element: Box::new(inner),
            quantifier,
            span: span_of(pe.syntax()),
        });
    }
    if let Some(inner) = pe.syntax().children().find_map(PatternElement::cast) {
        return build_pattern_element(inner);
    }
    let node = pe
        .syntax()
        .children()
        .find_map(NodePattern::cast)
        .or_else(|| pe.node());
    let chains: Vec<_> = pe.chains().collect();
    if chains.is_empty()
        && let Some(n) = node
    {
        return Ok(ast_c::PatternElement::Path {
            start: build_node_pattern(n)?,
            chains: Vec::new(),
        });
    }
    let start = node
        .map(build_node_pattern)
        .transpose()?
        .ok_or_else(|| internal("missing node", span_of(pe.syntax())))?;
    let built: Result<Vec<_>> = chains
        .into_iter()
        .map(build_pattern_element_chain)
        .collect();
    Ok(ast_c::PatternElement::Path {
        start,
        chains: built?,
    })
}

fn build_node_pattern(np: NodePattern) -> Result<ast_c::NodePattern> {
    let sp = span_of(np.syntax());
    let variable = np.variable().map(build_top_variable);
    let labels: Result<Vec<_>> = np
        .labels()
        .map(|container| {
            container
                .expression()
                .map(build_label_expression)
                .transpose()?
                .ok_or_else(|| internal("missing label expression", sp))
        })
        .collect();
    let properties = np.properties().map(build_properties).transpose()?;
    Ok(ast_c::NodePattern {
        variable,
        labels: labels?,
        properties,
        span: sp,
    })
}

fn build_pattern_element_chain(pec: PatternElementChain) -> Result<ast_c::PatternElementChain> {
    let sp = span_of(pec.syntax());
    let rel = pec
        .relationship()
        .ok_or_else(|| internal("missing relationship pattern in chain", sp))?;
    let has_left = rel.syntax().children_with_tokens().any(|t| {
        t.as_token()
            .is_some_and(|t| t.kind() == SyntaxKind::ARROW_LEFT || t.kind() == SyntaxKind::LT)
    });
    let has_right = rel.syntax().children_with_tokens().any(|t| {
        t.as_token()
            .is_some_and(|t| t.kind() == SyntaxKind::ARROW_RIGHT || t.kind() == SyntaxKind::GT)
    });
    let direction = match (has_left, has_right) {
        (true, true) => ast_c::RelationshipDirection::Both,
        (true, false) => ast_c::RelationshipDirection::Left,
        (false, true) => ast_c::RelationshipDirection::Right,
        (false, false) => ast_c::RelationshipDirection::Undirected,
    };
    let detail = rel.detail().map(build_relationship_detail).transpose()?;
    let relationship = ast_c::RelationshipPattern {
        direction,
        detail,
        quantifier: rel.quantifier().map(build_quantifier).transpose()?,
        span: span_of(rel.syntax()),
    };
    let node = pec
        .node()
        .map(build_node_pattern)
        .transpose()?
        .ok_or_else(|| internal("missing node in chain", span_of(pec.syntax())))?;
    Ok(ast_c::PatternElementChain { relationship, node })
}

fn build_relationship_detail(rd: RelationshipDetail) -> Result<ast_c::RelationshipDetail> {
    let sp = span_of(rd.syntax());
    let variable = rd.variable().map(build_top_variable);
    let types = rd
        .types()
        .map(|container| {
            container
                .expression()
                .map(build_label_expression)
                .transpose()?
                .ok_or_else(|| internal("missing relationship type expression", sp))
        })
        .transpose()?;
    let range = rd.range().map(build_range_literal).transpose()?;
    let properties = rd.properties().map(build_properties).transpose()?;
    Ok(ast_c::RelationshipDetail {
        variable,
        types,
        range,
        properties,
        span: sp,
    })
}

fn build_quantifier(rq: RelationshipQuantifier) -> Result<ast_c::Quantifier> {
    let sp = span_of(rq.syntax());
    let nums: Result<Vec<i64>> = rq
        .numbers()
        .map(|lit| match build_literal(lit)? {
            ast_c::Expression::Literal(ast_c::Literal::Number(ast_c::NumberLiteral::Integer(
                i,
            ))) => Ok(i),
            _ => Err(internal("expected integer quantifier bound", sp)),
        })
        .collect();
    let nums = nums?;
    let has_comma = rq.syntax().children_with_tokens().any(|t| {
        t.as_token()
            .is_some_and(|tok| tok.kind() == SyntaxKind::COMMA)
    });
    let (start, end) = match nums.as_slice() {
        [a, b] => (Some(*a), Some(*b)),
        // `{n}` (no comma) means exactly n; `{n,}` (with comma) means n or more.
        [a] => {
            if has_comma {
                (Some(*a), None)
            } else {
                (Some(*a), Some(*a))
            }
        }
        _ => (None, None),
    };
    Ok(ast_c::Quantifier {
        start,
        end,
        span: sp,
    })
}

fn build_label_expression(expr: LabelExpression) -> Result<ast_c::LabelExpression> {
    let root = expr
        .root()
        .ok_or_else(|| internal("missing label expression root", span_of(expr.syntax())))?;
    build_label_expr_node(root)
}

fn build_label_expr_node(node: LabelExprNode) -> Result<ast_c::LabelExpression> {
    let sp = span_of(node.syntax());
    match node {
        LabelExprNode::Or(or) => {
            let mut items = or.items();
            let lhs = items
                .next()
                .map(build_label_expr_node)
                .transpose()?
                .ok_or_else(|| internal("missing lhs in label OR", sp))?;
            let Some(rhs) = items.next().map(build_label_expr_node).transpose()? else {
                return Ok(lhs);
            };
            let mut acc = ast_c::LabelExpression::Or {
                lhs: Box::new(lhs),
                rhs: Box::new(rhs),
                span: sp,
            };
            for item in items {
                acc = ast_c::LabelExpression::Or {
                    lhs: Box::new(acc),
                    rhs: Box::new(build_label_expr_node(item)?),
                    span: sp,
                };
            }
            Ok(acc)
        }
        LabelExprNode::And(and) => {
            let mut items = and.items();
            let lhs = items
                .next()
                .map(build_label_expr_node)
                .transpose()?
                .ok_or_else(|| internal("missing lhs in label AND", sp))?;
            let Some(rhs) = items.next().map(build_label_expr_node).transpose()? else {
                return Ok(lhs);
            };
            let mut acc = ast_c::LabelExpression::And {
                lhs: Box::new(lhs),
                rhs: Box::new(rhs),
                span: sp,
            };
            for item in items {
                acc = ast_c::LabelExpression::And {
                    lhs: Box::new(acc),
                    rhs: Box::new(build_label_expr_node(item)?),
                    span: sp,
                };
            }
            Ok(acc)
        }
        LabelExprNode::Not(not) => Ok(ast_c::LabelExpression::Not {
            inner: Box::new(build_label_expr_node(
                not.inner()
                    .ok_or_else(|| internal("missing inner label NOT", sp))?,
            )?),
            span: sp,
        }),
        LabelExprNode::Paren(paren) => Ok(ast_c::LabelExpression::Group {
            inner: Box::new(build_label_expr_node(
                paren
                    .inner()
                    .ok_or_else(|| internal("missing inner label paren", sp))?,
            )?),
            span: sp,
        }),
        LabelExprNode::Atom(atom) => {
            if let Some(label) = atom.node_label() {
                let sym = label
                    .name()
                    .and_then(|ln| ln.symbolic_name())
                    .ok_or_else(|| internal("missing node label name", sp))?;
                return Ok(ast_c::LabelExpression::Static(ast_c::SymbolicName {
                    name: symbolic_name_text(&sym),
                    span: span_of(sym.syntax()),
                }));
            }
            if let Some(rel_type) = atom.rel_type_name() {
                let sym = rel_type
                    .symbolic_name()
                    .ok_or_else(|| internal("missing relationship type name", sp))?;
                return Ok(ast_c::LabelExpression::Static(ast_c::SymbolicName {
                    name: symbolic_name_text(&sym),
                    span: span_of(sym.syntax()),
                }));
            }
            if let Some(dynamic) = atom.dynamic_label() {
                return Ok(ast_c::LabelExpression::Dynamic {
                    expression: Box::new(build_expression(
                        dynamic
                            .expression()
                            .ok_or_else(|| internal("missing dynamic label expression", sp))?,
                    )?),
                    span: sp,
                });
            }
            if let Some(dynamic) = atom.dynamic_rel_type() {
                return Ok(ast_c::LabelExpression::Dynamic {
                    expression: Box::new(build_expression(
                        dynamic
                            .expression()
                            .ok_or_else(|| internal("missing dynamic rel type expression", sp))?,
                    )?),
                    span: sp,
                });
            }
            Err(internal("unsupported label atom", sp))
        }
    }
}

fn build_range_literal(rl: RangeLiteral) -> Result<ast_c::RangeLiteral> {
    let sp = span_of(rl.syntax());
    // The CST stores a RANGE_LITERAL as `*` plus up to two INTEGER tokens,
    // separated by a DOT_DOT token. Determine which INTEGER is start vs end
    // by its position relative to the DOT_DOT.
    let mut start: Option<i64> = None;
    let mut end: Option<i64> = None;
    let mut seen_dot_dot = false;
    let mut seen_star = false;
    for child in rl.syntax().children_with_tokens() {
        if let Some(tok) = child.as_token() {
            match tok.kind() {
                SyntaxKind::STAR => seen_star = true,
                SyntaxKind::DOT_DOT => seen_dot_dot = true,
                SyntaxKind::INTEGER => {
                    if let Some(val) = parse_integer(tok.text()) {
                        if !seen_dot_dot {
                            start = Some(val);
                        } else {
                            end = Some(val);
                        }
                    }
                }
                _ => {}
            }
        } else if let Some(node) = child.as_node() {
            // Some grammars wrap integers in NUMBER_LITERAL nodes; descend.
            for inner in node.children_with_tokens() {
                if let Some(tok) = inner.as_token()
                    && tok.kind() == SyntaxKind::INTEGER
                    && let Some(val) = parse_integer(tok.text())
                {
                    if !seen_dot_dot {
                        start = Some(val);
                    } else {
                        end = Some(val);
                    }
                }
            }
        }
    }
    // If no DOT_DOT was present, a single integer after `*` means both bounds
    // are the same (e.g. `*3` is shorthand for `*3..3`). Cypher also allows
    // `*` alone for unbounded; leave both as None in that case.
    if !seen_dot_dot && seen_star && start.is_some() {
        end = start;
    }
    Ok(ast_c::RangeLiteral {
        start,
        end,
        span: sp,
    })
}

fn build_properties(p: Properties) -> Result<ast_c::Properties> {
    if let Some(map) = p.map_literal() {
        let entries: Result<Vec<_>> = map
            .entries()
            .map(|e| {
                let key = e
                    .key()
                    .map(|k| {
                        k.symbolic_name()
                            .map(|s| ast_c::PropertyKeyName {
                                name: ast_c::SymbolicName {
                                    name: symbolic_name_text(&s),
                                    span: span_of(s.syntax()),
                                },
                            })
                            .ok_or_else(|| internal("missing prop key", span_of(k.syntax())))
                    })
                    .transpose()?
                    .ok_or_else(|| internal("missing key", span_of(e.syntax())))?;
                let value = e
                    .value()
                    .map(build_expression)
                    .transpose()?
                    .ok_or_else(|| internal("missing value", span_of(e.syntax())))?;
                Ok((key, value))
            })
            .collect();
        Ok(ast_c::Properties::Map(ast_c::MapLiteral {
            entries: entries?,
            span: span_of(map.syntax()),
        }))
    } else {
        Err(internal(
            "unsupported properties shape",
            span_of(p.syntax()),
        ))
    }
}

// ── Expressions ──────────────────────────────────────────────────────

fn build_expression(e: Expression) -> Result<ast_c::Expression> {
    match e {
        Expression::BinaryExpr(b) => build_binary_expr(b),
        Expression::UnaryExpr(u) => build_unary_expr(u),
        Expression::Atom(a) => build_atom(a),
    }
}

fn build_binary_expr(b: BinaryExpr) -> Result<ast_c::Expression> {
    let sp = span_of(b.syntax());
    let lhs = b
        .lhs()
        .map(build_expression)
        .transpose()?
        .ok_or_else(|| internal("missing lhs", sp))?;

    match b.op_kind() {
        Some(BinOp::IsNull) => Ok(ast_c::Expression::IsNull {
            operand: Box::new(lhs),
            negated: false,
            span: sp,
        }),
        Some(BinOp::IsNotNull) => Ok(ast_c::Expression::IsNull {
            operand: Box::new(lhs),
            negated: true,
            span: sp,
        }),
        Some(BinOp::HasLabel) => {
            let labels: Result<Vec<_>> = b
                .syntax()
                .children()
                .filter_map(NodeLabels::cast)
                .map(|labels| {
                    labels
                        .expression()
                        .map(build_label_expression)
                        .transpose()?
                        .ok_or_else(|| internal("missing postfix label expression", sp))
                })
                .collect();
            Ok(ast_c::Expression::NodeLabels {
                base: Box::new(lhs),
                labels: labels?,
                span: sp,
            })
        }
        _ => {
            let rhs = b
                .rhs()
                .map(build_expression)
                .transpose()?
                .ok_or_else(|| internal("missing rhs", sp))?;

            match b.op_kind() {
                Some(BinOp::Or) => Ok(ast_c::Expression::BinaryOp {
                    op: ast_c::BinaryOperator::Or,
                    lhs: Box::new(lhs),
                    rhs: Box::new(rhs),
                    span: sp,
                }),
                Some(BinOp::Xor) => Ok(ast_c::Expression::BinaryOp {
                    op: ast_c::BinaryOperator::Xor,
                    lhs: Box::new(lhs),
                    rhs: Box::new(rhs),
                    span: sp,
                }),
                Some(BinOp::And) => Ok(ast_c::Expression::BinaryOp {
                    op: ast_c::BinaryOperator::And,
                    lhs: Box::new(lhs),
                    rhs: Box::new(rhs),
                    span: sp,
                }),
                Some(BinOp::Eq) => Ok(ast_c::Expression::Comparison {
                    lhs: Box::new(lhs),
                    operators: vec![(ast_c::ComparisonOperator::Eq, Box::new(rhs))],
                    span: sp,
                }),
                Some(BinOp::Ne) => Ok(ast_c::Expression::Comparison {
                    lhs: Box::new(lhs),
                    operators: vec![(ast_c::ComparisonOperator::Ne, Box::new(rhs))],
                    span: sp,
                }),
                Some(BinOp::Lt) => Ok(ast_c::Expression::Comparison {
                    lhs: Box::new(lhs),
                    operators: vec![(ast_c::ComparisonOperator::Lt, Box::new(rhs))],
                    span: sp,
                }),
                Some(BinOp::Gt) => Ok(ast_c::Expression::Comparison {
                    lhs: Box::new(lhs),
                    operators: vec![(ast_c::ComparisonOperator::Gt, Box::new(rhs))],
                    span: sp,
                }),
                Some(BinOp::Le) => Ok(ast_c::Expression::Comparison {
                    lhs: Box::new(lhs),
                    operators: vec![(ast_c::ComparisonOperator::Le, Box::new(rhs))],
                    span: sp,
                }),
                Some(BinOp::Ge) => Ok(ast_c::Expression::Comparison {
                    lhs: Box::new(lhs),
                    operators: vec![(ast_c::ComparisonOperator::Ge, Box::new(rhs))],
                    span: sp,
                }),
                Some(BinOp::RegexMatch) => Ok(ast_c::Expression::Comparison {
                    lhs: Box::new(lhs),
                    operators: vec![(ast_c::ComparisonOperator::RegexMatch, Box::new(rhs))],
                    span: sp,
                }),
                Some(BinOp::Add) => Ok(ast_c::Expression::BinaryOp {
                    op: ast_c::BinaryOperator::Add,
                    lhs: Box::new(lhs),
                    rhs: Box::new(rhs),
                    span: sp,
                }),
                Some(BinOp::Sub) => Ok(ast_c::Expression::BinaryOp {
                    op: ast_c::BinaryOperator::Subtract,
                    lhs: Box::new(lhs),
                    rhs: Box::new(rhs),
                    span: sp,
                }),
                Some(BinOp::Mul) => Ok(ast_c::Expression::BinaryOp {
                    op: ast_c::BinaryOperator::Multiply,
                    lhs: Box::new(lhs),
                    rhs: Box::new(rhs),
                    span: sp,
                }),
                Some(BinOp::Div) => Ok(ast_c::Expression::BinaryOp {
                    op: ast_c::BinaryOperator::Divide,
                    lhs: Box::new(lhs),
                    rhs: Box::new(rhs),
                    span: sp,
                }),
                Some(BinOp::Mod) => Ok(ast_c::Expression::BinaryOp {
                    op: ast_c::BinaryOperator::Modulo,
                    lhs: Box::new(lhs),
                    rhs: Box::new(rhs),
                    span: sp,
                }),
                Some(BinOp::Power) => Ok(ast_c::Expression::BinaryOp {
                    op: ast_c::BinaryOperator::Power,
                    lhs: Box::new(lhs),
                    rhs: Box::new(rhs),
                    span: sp,
                }),
                Some(BinOp::StartsWith) => Ok(ast_c::Expression::Comparison {
                    lhs: Box::new(lhs),
                    operators: vec![(ast_c::ComparisonOperator::StartsWith, Box::new(rhs))],
                    span: sp,
                }),
                Some(BinOp::EndsWith) => Ok(ast_c::Expression::Comparison {
                    lhs: Box::new(lhs),
                    operators: vec![(ast_c::ComparisonOperator::EndsWith, Box::new(rhs))],
                    span: sp,
                }),
                Some(BinOp::Contains) => Ok(ast_c::Expression::Comparison {
                    lhs: Box::new(lhs),
                    operators: vec![(ast_c::ComparisonOperator::Contains, Box::new(rhs))],
                    span: sp,
                }),
                Some(BinOp::In) => Ok(ast_c::Expression::In {
                    lhs: Box::new(lhs),
                    rhs: Box::new(rhs),
                    span: sp,
                }),
                Some(BinOp::Index) => {
                    // Detect slice form `[start..end]` / `[start..]` / `[..end]`
                    // by presence of a DOT_DOT token inside the LIST_OP_EXPR.
                    let has_dot_dot = b.syntax().children_with_tokens().any(|c| {
                        c.as_token()
                            .is_some_and(|t| t.kind() == SyntaxKind::DOT_DOT)
                    });
                    if !has_dot_dot {
                        Ok(ast_c::Expression::ListIndex {
                            list: Box::new(lhs),
                            index: Box::new(rhs),
                            span: sp,
                        })
                    } else {
                        // Walk children_with_tokens, partitioning expressions
                        // before and after DOT_DOT into start/end.
                        let mut seen_dot_dot = false;
                        let mut start_expr: Option<ast_c::Expression> = None;
                        let mut end_expr: Option<ast_c::Expression> = None;
                        // Skip the opening L_BRACKET; LIST_OP_EXPR starts with `[`.
                        for child in b.syntax().children_with_tokens() {
                            if let Some(tok) = child.as_token() {
                                if tok.kind() == SyntaxKind::DOT_DOT {
                                    seen_dot_dot = true;
                                }
                                continue;
                            }
                            if let Some(node) = child.as_node()
                                && let Some(e) = Expression::cast(node.clone())
                            {
                                let built = build_expression(e)?;
                                if !seen_dot_dot {
                                    start_expr = Some(built);
                                } else {
                                    end_expr = Some(built);
                                }
                            }
                        }
                        Ok(ast_c::Expression::ListSlice {
                            list: Box::new(lhs),
                            start: start_expr.map(Box::new),
                            end: end_expr.map(Box::new),
                            span: sp,
                        })
                    }
                }
                Some(BinOp::PropertyLookup) => Ok(ast_c::Expression::PropertyLookup {
                    base: Box::new(lhs),
                    property: extract_property_key(&rhs)?,
                    span: sp,
                }),
                None => Err(internal("unknown binary op", sp)),
                _ => Err(internal("unexpected binary op", sp)),
            }
        }
    }
}

fn extract_property_key(e: &ast_c::Expression) -> Result<ast_c::PropertyKeyName> {
    if let ast_c::Expression::Literal(ast_c::Literal::String(s)) = e {
        Ok(ast_c::PropertyKeyName {
            name: ast_c::SymbolicName {
                name: s.value.clone(),
                span: s.span,
            },
        })
    } else if let ast_c::Expression::Variable(v) = e {
        Ok(ast_c::PropertyKeyName {
            name: v.name.clone(),
        })
    } else {
        Err(internal("expected property key", Span::new(0, 0)))
    }
}

fn build_unary_expr(u: UnaryExpr) -> Result<ast_c::Expression> {
    let sp = span_of(u.syntax());
    let operand = u
        .operand()
        .map(build_expression)
        .transpose()?
        .ok_or_else(|| internal("missing operand", sp))?;
    match u.op() {
        Some(UnOp::Not) => Ok(ast_c::Expression::UnaryOp {
            op: ast_c::UnaryOperator::Not,
            operand: Box::new(operand),
            span: sp,
        }),
        Some(UnOp::Neg) => Ok(ast_c::Expression::UnaryOp {
            op: ast_c::UnaryOperator::Negate,
            operand: Box::new(operand),
            span: sp,
        }),
        Some(UnOp::Pos) => Ok(ast_c::Expression::UnaryOp {
            op: ast_c::UnaryOperator::Plus,
            operand: Box::new(operand),
            span: sp,
        }),
        None => Err(internal("unknown unary op", sp)),
    }
}

fn build_atom(a: Atom) -> Result<ast_c::Expression> {
    match a {
        Atom::Literal(l) => build_literal(l),
        Atom::Variable(v) => Ok(ast_c::Expression::Variable(build_variable(v))),
        Atom::Parameter(p) => build_parameter(p),
        Atom::FunctionInvocation(f) => build_function_invocation(f),
        Atom::Parenthesized(pe) => build_parenthesized(pe),
        Atom::Case(c) => build_case(c),
        Atom::ListLiteral(ll) => build_list_literal(ll),
        Atom::MapLiteral(ml) => build_map_literal(ml),
        Atom::ListComprehension(lc) => build_list_comprehension(lc),
        Atom::PatternComprehension(pc) => build_pattern_comprehension(pc),
        Atom::FilterExpression(fe) => build_filter_expression(fe),
        Atom::ExistsSubquery(es) => build_exists_subquery(es),
        Atom::CountSubquery(cs) => build_count_subquery(cs),
        Atom::CollectSubquery(cs) => build_collect_subquery(cs),
        Atom::MapProjection(mp) => build_map_projection(mp),
        Atom::ImplicitProcedureInvocation(ipi) => {
            let proc = build_implicit_procedure_invocation(ipi)?;
            Ok(ast_c::Expression::FunctionCall(proc.name))
        }
        Atom::PropertyLookup(pl) => {
            let sp = span_of(pl.syntax());
            let key = pl
                .key()
                .ok_or_else(|| internal("missing property key", sp))?;
            let base = pl
                .base()
                .and_then(|e| build_expression(e).ok())
                .ok_or_else(|| internal("missing base in property lookup", sp))?;
            Ok(ast_c::Expression::PropertyLookup {
                base: Box::new(base),
                property: ast_c::PropertyKeyName {
                    name: ast_c::SymbolicName {
                        name: symbolic_name_text(&key.symbolic_name().ok_or_else(|| {
                            internal("missing symbolic name", span_of(key.syntax()))
                        })?),
                        span: span_of(key.syntax()),
                    },
                },
                span: sp,
            })
        }
        Atom::Null(_n) => Ok(ast_c::Expression::Literal(ast_c::Literal::Null)),
    }
}

fn build_literal(l: Literal) -> Result<ast_c::Expression> {
    match l {
        Literal::Number(n) => {
            let num = build_number_literal(n)?;
            Ok(ast_c::Expression::Literal(ast_c::Literal::Number(num)))
        }
        Literal::String(s) => {
            let sl = build_string_literal(s)?;
            Ok(ast_c::Expression::Literal(ast_c::Literal::String(sl)))
        }
        Literal::Boolean(b) => Ok(ast_c::Expression::Literal(ast_c::Literal::Boolean(
            b.value(),
        ))),
        Literal::Null(_n) => Ok(ast_c::Expression::Literal(ast_c::Literal::Null)),
    }
}

fn build_number_literal(n: NumberLiteral) -> Result<ast_c::NumberLiteral> {
    let sp = span_of(n.syntax());
    if let Some(tok) = n.token() {
        let text = tok.text();
        if tok.kind() == SyntaxKind::INTEGER {
            if let Some(val) = parse_integer(text) {
                return Ok(ast_c::NumberLiteral::Integer(val));
            }
        } else if tok.kind() == SyntaxKind::FLOAT
            && let Some(val) = parse_double(text)
        {
            return Ok(ast_c::NumberLiteral::Float(val));
        }
    }
    Err(internal("invalid number", sp))
}

fn build_string_literal(s: StringLiteral) -> Result<ast_c::StringLiteral> {
    let sp = span_of(s.syntax());
    if let Some(tok) = s.token() {
        let raw = tok.text();
        let content = if (raw.starts_with('"') && raw.ends_with('"'))
            || (raw.starts_with('\'') && raw.ends_with('\''))
        {
            &raw[1..raw.len() - 1]
        } else {
            raw
        };
        let (value, err) = decode_string_content(content, sp);
        if let Some(e) = err {
            return Err(e);
        }
        return Ok(ast_c::StringLiteral {
            value,
            span: sp,
            raw: Some(raw.to_string()),
        });
    }
    Err(internal("missing string token", sp))
}

fn build_parameter(p: expressions::Parameter) -> Result<ast_c::Expression> {
    let sp = span_of(p.syntax());
    let tok = p
        .name_token()
        .ok_or_else(|| internal("missing parameter name", sp))?;
    let name_span = Span::new(
        tok.text_range().start().into(),
        tok.text_range().end().into(),
    );
    let name_text = tok.text().to_string();
    Ok(ast_c::Expression::Parameter(ast_c::Parameter {
        name: ast_c::SymbolicName {
            name: name_text,
            span: name_span,
        },
        span: sp,
    }))
}

fn build_function_invocation(f: FunctionInvocation) -> Result<ast_c::Expression> {
    let sp = span_of(f.syntax());
    let name_parts: Vec<ast_c::SymbolicName> = f
        .name()
        .into_iter()
        .flat_map(|fn_name| {
            fn_name
                .symbolic_names()
                .map(|s| ast_c::SymbolicName {
                    name: symbolic_name_text(&s),
                    span: span_of(s.syntax()),
                })
                .collect::<Vec<_>>()
        })
        .collect();
    let distinct = f.distinct_token().is_some();
    let star = f.star_token().is_some();
    let args: Result<Vec<_>> = f.arguments().map(build_expression).collect();
    let args = args?;

    if star
        && args.is_empty()
        && name_parts.len() == 1
        && name_parts[0].name.to_lowercase() == "count"
    {
        return Ok(ast_c::Expression::CountStar { span: sp });
    }

    Ok(ast_c::Expression::FunctionCall(ast_c::FunctionInvocation {
        name: name_parts,
        distinct,
        arguments: args,
        span: sp,
    }))
}

fn build_parenthesized(pe: ParenthesizedExpr) -> Result<ast_c::Expression> {
    let inner = pe
        .expr()
        .map(build_expression)
        .transpose()?
        .ok_or_else(|| internal("missing expr in parens", span_of(pe.syntax())))?;
    Ok(ast_c::Expression::Parenthesized(Box::new(inner)))
}

fn build_case(c: CaseExpr) -> Result<ast_c::Expression> {
    let sp = span_of(c.syntax());
    let scrutinee = c.value().map(build_expression).transpose()?;
    let alts: Result<Vec<_>> = c
        .alternatives()
        .map(|a| {
            let when = a
                .when_expr()
                .map(build_expression)
                .transpose()?
                .ok_or_else(|| internal("missing when", span_of(a.syntax())))?;
            let then = a
                .then_expr()
                .map(build_expression)
                .transpose()?
                .ok_or_else(|| internal("missing then", span_of(a.syntax())))?;
            Ok(ast_c::CaseAlternative { when, then })
        })
        .collect();
    let default = c.else_expr().map(build_expression).transpose()?;
    Ok(ast_c::Expression::Case(ast_c::CaseExpression {
        scrutinee: scrutinee.map(Box::new),
        alternatives: alts?,
        default: default.map(Box::new),
        span: sp,
    }))
}

fn build_list_literal(ll: ListLiteral) -> Result<ast_c::Expression> {
    let sp = span_of(ll.syntax());
    let elems: Result<Vec<_>> = ll.elements().map(build_expression).collect();
    Ok(ast_c::Expression::Literal(ast_c::Literal::List(
        ast_c::ListLiteral {
            elements: elems?,
            span: sp,
        },
    )))
}

fn build_map_literal(ml: MapLiteral) -> Result<ast_c::Expression> {
    let sp = span_of(ml.syntax());
    let entries: Result<Vec<_>> = ml
        .entries()
        .map(|e| {
            let key = e
                .key()
                .map(|k| {
                    k.symbolic_name()
                        .map(|s| ast_c::PropertyKeyName {
                            name: ast_c::SymbolicName {
                                name: symbolic_name_text(&s),
                                span: span_of(s.syntax()),
                            },
                        })
                        .ok_or_else(|| internal("missing prop key", span_of(k.syntax())))
                })
                .transpose()?
                .ok_or_else(|| internal("missing key", span_of(e.syntax())))?;
            let value = e
                .value()
                .map(build_expression)
                .transpose()?
                .ok_or_else(|| internal("missing value", span_of(e.syntax())))?;
            Ok((key, value))
        })
        .collect();
    Ok(ast_c::Expression::Literal(ast_c::Literal::Map(
        ast_c::MapLiteral {
            entries: entries?,
            span: sp,
        },
    )))
}

fn build_list_comprehension(lc: ListComprehension) -> Result<ast_c::Expression> {
    let sp = span_of(lc.syntax());
    if let Some(filter) = lc.filter() {
        let var = filter
            .id_in_coll()
            .and_then(|id| id.variable())
            .map(build_variable)
            .ok_or_else(|| internal("missing variable in list comp", sp))?;
        let _coll = filter
            .id_in_coll()
            .and_then(|id| id.collection())
            .map(build_expression)
            .ok_or_else(|| internal("missing collection in list comp", sp))?;
        let pred = filter
            .where_clause()
            .and_then(|w| w.expr())
            .map(build_expression)
            .transpose()?;
        let map = lc.body().map(build_expression).transpose()?;
        Ok(ast_c::Expression::ListComprehension(Box::new(
            ast_c::ListComprehension {
                variable: var,
                filter: pred.map(Box::new),
                map,
                span: sp,
            },
        )))
    } else {
        Err(internal("missing filter in list comp", sp))
    }
}

fn build_pattern_comprehension(pc: PatternComprehension) -> Result<ast_c::Expression> {
    let sp = span_of(pc.syntax());
    let variable = pc.variable().map(build_variable);
    let _pat = pc.pattern();
    let where_clause = pc
        .where_clause()
        .and_then(|w| w.expr())
        .map(build_expression)
        .transpose()?;
    let map = pc
        .body()
        .map(build_expression)
        .transpose()?
        .ok_or_else(|| internal("missing body in pattern comp", sp))?;
    let placeholder = ast_c::RelationshipsPattern {
        start: ast_c::NodePattern {
            variable: variable.clone(),
            labels: Vec::new(),
            properties: None,
            span: sp,
        },
        chains: Vec::new(),
        span: sp,
    };
    Ok(ast_c::Expression::PatternComprehension(Box::new(
        ast_c::PatternComprehension {
            variable,
            pattern: placeholder,
            where_clause,
            map,
            span: sp,
        },
    )))
}

fn build_filter_expression(fe: FilterExpression) -> Result<ast_c::Expression> {
    let sp = span_of(fe.syntax());
    let id = fe
        .id_in_coll()
        .ok_or_else(|| internal("missing IdInColl", sp))?;
    let var = id
        .variable()
        .map(build_variable)
        .ok_or_else(|| internal("missing variable", sp))?;
    let coll = id
        .collection()
        .map(build_expression)
        .transpose()?
        .ok_or_else(|| internal("missing collection", sp))?;
    let pred = fe
        .where_clause()
        .and_then(|w| w.expr())
        .map(build_expression)
        .transpose()?;
    Ok(ast_c::Expression::Any(Box::new(ast_c::FilterExpression {
        variable: var,
        collection: Box::new(coll),
        predicate: pred.map(Box::new),
        span: sp,
    })))
}

fn build_exists_subquery(es: ExistsSubquery) -> Result<ast_c::Expression> {
    let sp = span_of(es.syntax());
    if es.clauses().next().is_some() {
        return Ok(ast_c::Expression::Exists(Box::new(
            ast_c::ExistsExpression {
                inner: Box::new(ast_c::ExistsInner::RegularQuery(Box::new(
                    build_regular_query_from_syntax(es.syntax())?,
                ))),
                span: sp,
            },
        )));
    }

    let mut parts = Vec::new();
    if let Some(node) = es.syntax().children().find_map(NodePattern::cast) {
        let mut text_children: Vec<rowan::SyntaxNode<crate::syntax::CypherLang>> =
            es.syntax().children().collect();
        if let Some(first) = text_children.first()
            && first.kind() == SyntaxKind::NODE_PATTERN
        {
            let _ = text_children.remove(0);
        }
        let mut chains = Vec::new();
        for child in text_children {
            if let Some(chain) = PatternElementChain::cast(child) {
                chains.push(build_pattern_element_chain(chain)?);
            }
        }
        parts.push(ast_c::PatternPart {
            variable: None,
            anonymous: ast_c::AnonymousPatternPart {
                element: ast_c::PatternElement::Path {
                    start: build_node_pattern(node)?,
                    chains,
                },
            },
            span: sp,
        });
    }
    let where_clause = es
        .where_clause()
        .and_then(|w| w.expr())
        .map(build_expression)
        .transpose()?;
    Ok(ast_c::Expression::Exists(Box::new(
        ast_c::ExistsExpression {
            inner: Box::new(ast_c::ExistsInner::Pattern(
                ast_c::Pattern { parts, span: sp },
                where_clause.map(Box::new),
            )),
            span: sp,
        },
    )))
}

fn build_count_subquery(cs: CountSubquery) -> Result<ast_c::Expression> {
    Ok(ast_c::Expression::CountSubquery(Box::new(
        ast_c::CountSubqueryExpression {
            query: Box::new(build_regular_query_from_syntax(cs.syntax())?),
            span: span_of(cs.syntax()),
        },
    )))
}

fn build_collect_subquery(cs: CollectSubquery) -> Result<ast_c::Expression> {
    Ok(ast_c::Expression::CollectSubquery(Box::new(
        ast_c::CollectSubqueryExpression {
            query: Box::new(build_regular_query_from_syntax(cs.syntax())?),
            span: span_of(cs.syntax()),
        },
    )))
}

fn build_regular_query_from_syntax(
    node: &rowan::SyntaxNode<crate::syntax::CypherLang>,
) -> Result<ast_c::RegularQuery> {
    let clauses: Vec<_> = node.children().filter_map(Clause::cast).collect();
    let unions: Vec<_> = node.children().filter_map(Union::cast).collect();
    if clauses.is_empty() {
        return Err(internal(
            "subquery expression is missing clauses",
            span_of(node),
        ));
    }
    if unions.is_empty() {
        Ok(ast_c::RegularQuery {
            single_query: build_subquery_single_query_from_clauses(clauses)?,
            unions: Vec::new(),
        })
    } else {
        let single_query = build_subquery_single_query_from_clauses(clauses)?;
        let mut result_unions = Vec::new();
        for union_node in unions {
            let all = union_node.all_token().is_some();
            let union_clauses: Vec<_> = union_node.clauses().collect();
            result_unions.push(ast_c::Union {
                all,
                single_query: build_subquery_single_query_from_clauses(union_clauses)?,
                span: span_of(union_node.syntax()),
            });
        }
        Ok(ast_c::RegularQuery {
            single_query,
            unions: result_unions,
        })
    }
}

fn build_subquery_single_query_from_clauses(clauses: Vec<Clause>) -> Result<ast_c::SingleQuery> {
    let with_indices: Vec<_> = clauses
        .iter()
        .enumerate()
        .filter(|(_, c)| matches!(c, Clause::With(_)))
        .map(|(i, _)| i)
        .collect();

    let total = clauses.len();
    let is_multipart_with = |idx: usize| -> bool { idx < total - 1 };

    if with_indices.is_empty() {
        let mut reading = Vec::new();
        let mut updating = Vec::new();
        let mut ret = None;

        for c in clauses {
            match c {
                Clause::Match(m) => reading.push(ast_c::ReadingClause::Match(build_match(m)?)),
                Clause::Unwind(u) => reading.push(ast_c::ReadingClause::Unwind(build_unwind(u)?)),
                Clause::InQueryCall(ic) => {
                    reading.push(ast_c::ReadingClause::InQueryCall(build_in_query_call(ic)?))
                }
                Clause::CallSubquery(cs) => reading.push(ast_c::ReadingClause::CallSubquery(
                    Box::new(build_call_subquery(cs)?),
                )),
                Clause::LoadCsv(lc) => {
                    reading.push(ast_c::ReadingClause::LoadCsv(build_load_csv(lc)?))
                }
                Clause::Create(c) => updating.push(ast_c::UpdatingClause::Create(build_create(c)?)),
                Clause::Merge(m) => updating.push(ast_c::UpdatingClause::Merge(build_merge(m)?)),
                Clause::Set(s) => updating.push(ast_c::UpdatingClause::Set(build_set(s)?)),
                Clause::Remove(r) => updating.push(ast_c::UpdatingClause::Remove(build_remove(r)?)),
                Clause::Delete(d) => updating.push(ast_c::UpdatingClause::Delete(build_delete(d)?)),
                Clause::Foreach(f) => {
                    updating.push(ast_c::UpdatingClause::Foreach(build_foreach(f)?))
                }
                Clause::Return(r) => ret = Some(build_return(r)?),
                Clause::Finish(f) => {
                    return Ok(ast_c::SingleQuery {
                        kind: ast_c::SingleQueryKind::SinglePart(ast_c::SinglePartQuery {
                            reading_clauses: reading,
                            body: ast_c::SinglePartBody::Finish(build_finish(f)?),
                        }),
                    });
                }
                Clause::With(_)
                | Clause::Where(_)
                | Clause::Show(_)
                | Clause::Use(_)
                | Clause::StandaloneCall(_) => {}
            }
        }

        let body = if let Some(ret) = ret {
            ast_c::SinglePartBody::Return(ret)
        } else {
            ast_c::SinglePartBody::Updating {
                updating,
                return_clause: None,
            }
        };

        Ok(ast_c::SingleQuery {
            kind: ast_c::SingleQueryKind::SinglePart(ast_c::SinglePartQuery {
                reading_clauses: reading,
                body,
            }),
        })
    } else {
        let mut parts = Vec::new();
        let mut final_part = None;
        let mut reading = Vec::new();
        let mut updating = Vec::new();

        for (i, c) in clauses.into_iter().enumerate() {
            match c {
                Clause::With(w) if is_multipart_with(i) => {
                    let wc = build_with(w)?;
                    parts.push(ast_c::MultiPartQueryPart {
                        reading_clauses: std::mem::take(&mut reading),
                        updating_clauses: std::mem::take(&mut updating),
                        with: wc,
                    });
                }
                Clause::Match(m) => reading.push(ast_c::ReadingClause::Match(build_match(m)?)),
                Clause::Unwind(u) => reading.push(ast_c::ReadingClause::Unwind(build_unwind(u)?)),
                Clause::InQueryCall(ic) => {
                    reading.push(ast_c::ReadingClause::InQueryCall(build_in_query_call(ic)?))
                }
                Clause::CallSubquery(cs) => reading.push(ast_c::ReadingClause::CallSubquery(
                    Box::new(build_call_subquery(cs)?),
                )),
                Clause::LoadCsv(lc) => {
                    reading.push(ast_c::ReadingClause::LoadCsv(build_load_csv(lc)?))
                }
                Clause::Create(c) => updating.push(ast_c::UpdatingClause::Create(build_create(c)?)),
                Clause::Merge(m) => updating.push(ast_c::UpdatingClause::Merge(build_merge(m)?)),
                Clause::Set(s) => updating.push(ast_c::UpdatingClause::Set(build_set(s)?)),
                Clause::Remove(r) => updating.push(ast_c::UpdatingClause::Remove(build_remove(r)?)),
                Clause::Delete(d) => updating.push(ast_c::UpdatingClause::Delete(build_delete(d)?)),
                Clause::Foreach(f) => {
                    updating.push(ast_c::UpdatingClause::Foreach(build_foreach(f)?))
                }
                Clause::Return(r) => {
                    final_part = Some(ast_c::SinglePartQuery {
                        reading_clauses: std::mem::take(&mut reading),
                        body: ast_c::SinglePartBody::Return(build_return(r)?),
                    });
                }
                Clause::Finish(f) => {
                    final_part = Some(ast_c::SinglePartQuery {
                        reading_clauses: std::mem::take(&mut reading),
                        body: ast_c::SinglePartBody::Finish(build_finish(f)?),
                    });
                }
                Clause::With(w) => {
                    let wc = build_with(w)?;
                    final_part = Some(ast_c::SinglePartQuery {
                        reading_clauses: std::mem::take(&mut reading),
                        body: ast_c::SinglePartBody::Return(ast_c::Return {
                            distinct: wc.distinct,
                            star: wc.star,
                            items: wc.items,
                            order: wc.order,
                            skip: wc.skip,
                            limit: wc.limit,
                            span: wc.span,
                        }),
                    });
                }
                Clause::Where(_) | Clause::Show(_) | Clause::Use(_) | Clause::StandaloneCall(_) => {
                }
            }
        }

        let final_part = final_part.unwrap_or(ast_c::SinglePartQuery {
            reading_clauses: std::mem::take(&mut reading),
            body: ast_c::SinglePartBody::Updating {
                updating,
                return_clause: None,
            },
        });

        Ok(ast_c::SingleQuery {
            kind: ast_c::SingleQueryKind::MultiPart(ast_c::MultiPartQuery { parts, final_part }),
        })
    }
}

fn build_map_projection(mp: MapProjection) -> Result<ast_c::Expression> {
    let sp = span_of(mp.syntax());
    let base = mp
        .variable()
        .map(build_variable)
        .ok_or_else(|| internal("missing base in map proj", sp))?;
    let items: Result<Vec<_>> = mp.items().map(build_map_projection_item).collect();
    Ok(ast_c::Expression::MapProjection(Box::new(
        ast_c::MapProjection {
            base,
            items: items?,
            span: sp,
        },
    )))
}

fn build_map_projection_item(mi: MapProjectionItem) -> Result<ast_c::MapProjectionItem> {
    let sp = span_of(mi.syntax());
    if mi.is_star() {
        return Ok(ast_c::MapProjectionItem::AllProperties { span: sp });
    }
    if let Some(key) = mi.property_name() {
        let pk = ast_c::PropertyKeyName {
            name: key
                .symbolic_name()
                .map(|s| ast_c::SymbolicName {
                    name: symbolic_name_text(&s),
                    span: span_of(s.syntax()),
                })
                .ok_or_else(|| internal("missing prop key", sp))?,
        };
        if let Some(expr) = mi.expression() {
            let expr_ast = build_expression(expr)?;
            if let ast_c::Expression::Variable(v) = &expr_ast
                && v.name.name == pk.name.name
            {
                return Ok(ast_c::MapProjectionItem::PropertyLookup { property: pk });
            }
            return Ok(ast_c::MapProjectionItem::Literal {
                key: pk,
                value: expr_ast,
            });
        }
        return Ok(ast_c::MapProjectionItem::PropertyLookup { property: pk });
    }
    Err(internal("unrecognized map proj item", sp))
}

// ── Procedure calls ──────────────────────────────────────────────────

fn build_standalone_call(c: StandaloneCall) -> Result<ast_c::StandaloneCall> {
    let sp = span_of(c.syntax());
    let call = if let Some(exp) = c.explicit_invocation() {
        build_explicit_procedure_invocation(exp)?
    } else if let Some(imp) = c.implicit_invocation() {
        build_implicit_procedure_invocation(imp)?
    } else {
        return Err(internal("missing proc invocation", sp));
    };
    let yield_items = c
        .yield_items()
        .map(|y| -> Result<ast_c::YieldSpec> {
            if y.star_token().is_some() {
                Ok(ast_c::YieldSpec::Star {
                    span: span_of(y.syntax()),
                })
            } else {
                let items: Result<Vec<_>> = y.items().map(build_yield_item).collect();
                let wc = y.where_expr().map(build_expression).transpose()?;
                Ok(ast_c::YieldSpec::Items(ast_c::YieldItems {
                    items: items?,
                    where_clause: wc,
                }))
            }
        })
        .transpose()?;
    Ok(ast_c::StandaloneCall {
        call,
        yield_items,
        span: sp,
    })
}

fn build_explicit_procedure_invocation(
    e: ExplicitProcedureInvocation,
) -> Result<ast_c::ProcedureInvocation> {
    let sp = span_of(e.syntax());
    let name = if let Some(pn) = e.procedure_name() {
        build_procedure_name(pn)?
    } else {
        return Err(internal("missing proc name", sp));
    };
    let _args: Result<Vec<_>> = e.arguments().map(build_expression).collect();
    Ok(ast_c::ProcedureInvocation { name, span: sp })
}

fn build_implicit_procedure_invocation(
    i: ImplicitProcedureInvocation,
) -> Result<ast_c::ProcedureInvocation> {
    let sp = span_of(i.syntax());
    let name = if let Some(pn) = i.procedure_name() {
        build_procedure_name(pn)?
    } else {
        return Err(internal("missing proc name", sp));
    };
    Ok(ast_c::ProcedureInvocation { name, span: sp })
}

fn build_procedure_name(pn: ProcedureName) -> Result<ast_c::FunctionInvocation> {
    let sp = span_of(pn.syntax());
    let names: Vec<_> = pn
        .symbolic_names()
        .map(|s| ast_c::SymbolicName {
            name: symbolic_name_text(&s),
            span: span_of(s.syntax()),
        })
        .collect();
    let (ns, pn_name) = if names.is_empty() {
        (
            Vec::new(),
            ast_c::SymbolicName {
                name: String::new(),
                span: sp,
            },
        )
    } else {
        let last = names.last().unwrap().clone();
        (names[..names.len() - 1].to_vec(), last)
    };
    let full: Vec<_> = ns.into_iter().chain(std::iter::once(pn_name)).collect();
    Ok(ast_c::FunctionInvocation {
        name: full,
        distinct: false,
        arguments: Vec::new(),
        span: sp,
    })
}

fn build_in_query_call(c: InQueryCall) -> Result<ast_c::InQueryCall> {
    let sp = span_of(c.syntax());
    let yield_items = c
        .yield_items()
        .map(|y| -> Result<ast_c::YieldItems> {
            let items: Result<Vec<_>> = y.items().map(build_yield_item).collect();
            let wc = y.where_expr().map(build_expression).transpose()?;
            Ok(ast_c::YieldItems {
                items: items?,
                where_clause: wc,
            })
        })
        .transpose()?;
    Ok(ast_c::InQueryCall {
        call: ast_c::ProcedureInvocation {
            name: ast_c::FunctionInvocation {
                name: Vec::new(),
                distinct: false,
                arguments: Vec::new(),
                span: sp,
            },
            span: sp,
        },
        yield_items,
        span: sp,
    })
}

fn build_yield_item(yi: YieldItem) -> Result<ast_c::YieldItem> {
    let pf = yi
        .field_name()
        .and_then(|f| f.symbolic_name())
        .map(|s| ast_c::SymbolicName {
            name: symbolic_name_text(&s),
            span: span_of(s.syntax()),
        })
        .ok_or_else(|| internal("missing proc field", span_of(yi.syntax())))?;
    let alias = yi.alias().map(build_top_variable);
    Ok(ast_c::YieldItem {
        procedure_field: pf,
        alias,
    })
}

fn build_call_subquery(c: CallSubqueryClause) -> Result<ast_c::CallSubquery> {
    let sp = span_of(c.syntax());
    let inner_clauses: Vec<_> = c.inner_clauses().collect();
    let inner_unions: Vec<_> = c.inner_unions().collect();
    let query = if inner_unions.is_empty() {
        let single = build_single_query_from_clauses(inner_clauses)?;
        ast_c::RegularQuery {
            single_query: single,
            unions: Vec::new(),
        }
    } else {
        build_regular_query(&inner_clauses, &inner_unions)?
    };
    let in_tx = c.in_transactions().map(build_in_transactions).transpose()?;
    Ok(ast_c::CallSubquery {
        query,
        in_transactions: in_tx,
        span: sp,
    })
}

fn build_in_transactions(it: InTransactions) -> Result<ast_c::InTransactions> {
    let sp = span_of(it.syntax());
    let of_rows = it.rows_expr().and_then(|n| {
        let text = n.token().map(|t| t.text().to_string());
        text.and_then(|t| parse_integer(&t)).map(|v| {
            ast_c::Expression::Literal(ast_c::Literal::Number(ast_c::NumberLiteral::Integer(v)))
        })
    });
    let on_error = it.on_error_action().and_then(|tok| match tok.kind() {
        SyntaxKind::KW_CONTINUE => Some(ast_c::OnErrorBehavior::Continue),
        SyntaxKind::KW_BREAK => Some(ast_c::OnErrorBehavior::Break),
        SyntaxKind::KW_FAIL => Some(ast_c::OnErrorBehavior::Fail),
        _ => None,
    });
    Ok(ast_c::InTransactions {
        of_rows,
        on_error,
        span: sp,
    })
}

// ── Schema commands ──────────────────────────────────────────────────

fn build_schema_command(cmd: cst_c::schema_cst::SchemaCommand) -> Result<ast_c::SchemaCommand> {
    match cmd {
        cst_c::schema_cst::SchemaCommand::CreateIndex(c) => {
            Ok(ast_c::SchemaCommand::CreateIndex(build_create_index(c)?))
        }
        cst_c::schema_cst::SchemaCommand::DropIndex(d) => {
            Ok(ast_c::SchemaCommand::DropIndex(build_drop_index(d)?))
        }
        cst_c::schema_cst::SchemaCommand::CreateConstraint(c) => Ok(
            ast_c::SchemaCommand::CreateConstraint(build_create_constraint(c)?),
        ),
        cst_c::schema_cst::SchemaCommand::DropConstraint(d) => Ok(
            ast_c::SchemaCommand::DropConstraint(build_drop_constraint(d)?),
        ),
    }
}

fn build_create_index(c: cst_c::schema_cst::CreateIndex) -> Result<ast_c::CreateIndex> {
    let sp = span_of(c.syntax());
    let if_not_exists = c.if_not_exists();
    let name = c
        .name()
        .and_then(|n| n.symbolic_name())
        .map(|s| ast_c::SymbolicName {
            name: symbolic_name_text(&s),
            span: span_of(s.syntax()),
        });
    let options = c
        .options()
        .and_then(|o| o.map())
        .map(build_map_literal_cst)
        .transpose()?;
    // Extract target from FOR pattern (NODE_PATTERN or RELATIONSHIP_DETAIL)
    let target = c
        .label()
        .and_then(|l| l.variable())
        .map(build_top_variable)
        .map(|v| v.name)
        .or_else(|| {
            c.syntax()
                .children()
                .find(|n| n.kind() == SyntaxKind::RELATIONSHIP_DETAIL)
                .and_then(cst_c::RelationshipDetail::cast)
                .and_then(|d| d.variable())
                .map(|v| ast_c::SymbolicName {
                    name: symbolic_name_text(
                        &v.name().unwrap_or_else(|| panic!("rel var missing name")),
                    ),
                    span: span_of(v.syntax()),
                })
        })
        .unwrap_or_else(|| ast_c::SymbolicName {
            name: String::new(),
            span: Span::new(0, 0),
        });
    Ok(ast_c::CreateIndex {
        kind: None,
        if_not_exists,
        name,
        target,
        options,
        span: sp,
    })
}

fn build_map_literal_cst(m: MapLiteral) -> Result<ast_c::MapLiteral> {
    let sp = span_of(m.syntax());
    let entries: Result<Vec<_>> = m
        .entries()
        .map(|e| {
            let key = e
                .key()
                .map(|k| {
                    k.symbolic_name()
                        .map(|s| ast_c::PropertyKeyName {
                            name: ast_c::SymbolicName {
                                name: symbolic_name_text(&s),
                                span: span_of(s.syntax()),
                            },
                        })
                        .ok_or_else(|| internal("missing prop key", span_of(k.syntax())))
                })
                .transpose()?
                .ok_or_else(|| internal("missing key", span_of(e.syntax())))?;
            let value = e
                .value()
                .map(build_expression)
                .transpose()?
                .ok_or_else(|| internal("missing value", span_of(e.syntax())))?;
            Ok((key, value))
        })
        .collect();
    Ok(ast_c::MapLiteral {
        entries: entries?,
        span: sp,
    })
}

fn build_drop_index(d: cst_c::schema_cst::DropIndex) -> Result<ast_c::DropIndex> {
    let sp = span_of(d.syntax());
    let if_exists = d.if_exists();
    let name = d
        .name()
        .and_then(|n| n.symbolic_name())
        .map(|s| ast_c::SymbolicName {
            name: symbolic_name_text(&s),
            span: span_of(s.syntax()),
        })
        .ok_or_else(|| internal("missing index name", sp))?;
    Ok(ast_c::DropIndex {
        if_exists,
        name,
        span: sp,
    })
}

fn build_create_constraint(
    c: cst_c::schema_cst::CreateConstraint,
) -> Result<ast_c::CreateConstraint> {
    let sp = span_of(c.syntax());
    let name = c
        .name()
        .and_then(|n| n.symbolic_name())
        .map(|s| ast_c::SymbolicName {
            name: symbolic_name_text(&s),
            span: span_of(s.syntax()),
        });
    // Extract variable from FOR pattern (NODE_PATTERN or RELATIONSHIP_DETAIL)
    let variable = c
        .syntax()
        .children()
        .find(|n| n.kind() == SyntaxKind::NODE_PATTERN)
        .and_then(|np| cst_c::NodePattern::cast(np).and_then(|n| n.variable()))
        .map(|v| ast_c::Variable {
            name: ast_c::SymbolicName {
                name: symbolic_name_text(&v.name().unwrap()),
                span: span_of(v.syntax()),
            },
        })
        .or_else(|| {
            c.syntax()
                .children()
                .find(|n| n.kind() == SyntaxKind::RELATIONSHIP_DETAIL)
                .and_then(cst_c::RelationshipDetail::cast)
                .and_then(|d| d.variable())
                .map(|v| ast_c::Variable {
                    name: ast_c::SymbolicName {
                        name: symbolic_name_text(&v.name().unwrap()),
                        span: span_of(v.syntax()),
                    },
                })
        })
        .unwrap_or_else(|| ast_c::Variable {
            name: ast_c::SymbolicName {
                name: String::new(),
                span: Span::new(0, 0),
            },
        });
    // Determine constraint kind from REQUIRE expression's CONSTRAINT_KIND
    let kind = c
        .constraint_kind()
        .map(|ck| {
            let syntax = ck.syntax();
            let tokens: Vec<_> = syntax
                .children_with_tokens()
                .filter_map(|e| e.into_token())
                .collect();
            // Helper: extract property names from property chains under a node
            let extract_props = |node: &rowan::SyntaxNode<crate::syntax::CypherLang>| {
                node.children()
                    .filter(|n| n.kind() == SyntaxKind::PROPERTY_LOOKUP)
                    .filter_map(|pl| {
                        pl.children()
                            .find(|n| n.kind() == SyntaxKind::PROPERTY_KEY_NAME)
                            .and_then(|pk| {
                                pk.children()
                                    .find(|n| n.kind() == SyntaxKind::SYMBOLIC_NAME)
                            })
                            .and_then(|sn| cst_c::SymbolicName::cast(sn.clone()))
                            .map(|sn| ast_c::PropertyKeyName {
                                name: ast_c::SymbolicName {
                                    name: symbolic_name_text(&sn),
                                    span: span_of(sn.syntax()),
                                },
                            })
                    })
                    .collect::<Vec<_>>()
            };
            // For composite: look inside PROPERTIES → LIST_LITERAL
            let composite_props = c.properties().and_then(|p| p.list_literal()).map(|ll| {
                ll.elements()
                    .filter_map(|expr| {
                        expr.syntax()
                            .children()
                            .find(|n| n.kind() == SyntaxKind::PROPERTY_LOOKUP)
                            .and_then(|pl| {
                                pl.children()
                                    .find(|n| n.kind() == SyntaxKind::PROPERTY_KEY_NAME)
                                    .and_then(|pk| {
                                        pk.children()
                                            .find(|n| n.kind() == SyntaxKind::SYMBOLIC_NAME)
                                    })
                                    .and_then(|sn| cst_c::SymbolicName::cast(sn.clone()))
                                    .map(|sn| ast_c::PropertyKeyName {
                                        name: ast_c::SymbolicName {
                                            name: symbolic_name_text(&sn),
                                            span: span_of(sn.syntax()),
                                        },
                                    })
                            })
                    })
                    .collect::<Vec<_>>()
            });
            if tokens.iter().any(|t| t.kind() == SyntaxKind::KW_UNIQUE) {
                ast_c::ConstraintKind::Unique
            } else if tokens.iter().any(|t| t.kind() == SyntaxKind::NULL_KW) {
                ast_c::ConstraintKind::NotNull
            } else if tokens.iter().any(|t| t.kind() == SyntaxKind::KW_KEY) {
                let props = if let Some(p) = composite_props {
                    p
                } else {
                    extract_props(c.syntax())
                };
                ast_c::ConstraintKind::NodeKey { properties: props }
            } else {
                // Property type or default
                let types: Vec<ast_c::SymbolicName> = ck
                    .property_types()
                    .map(|sn| ast_c::SymbolicName {
                        name: symbolic_name_text(&sn),
                        span: span_of(sn.syntax()),
                    })
                    .collect();
                if types.is_empty() {
                    ast_c::ConstraintKind::Unique
                } else {
                    ast_c::ConstraintKind::PropertyType { types }
                }
            }
        })
        .unwrap_or(ast_c::ConstraintKind::Unique);
    Ok(ast_c::CreateConstraint {
        name,
        variable,
        kind,
        span: sp,
    })
}

fn build_drop_constraint(d: cst_c::schema_cst::DropConstraint) -> Result<ast_c::DropConstraint> {
    let sp = span_of(d.syntax());
    let if_exists = d.if_exists();
    let name = d
        .name()
        .and_then(|n| n.symbolic_name())
        .map(|s| ast_c::SymbolicName {
            name: symbolic_name_text(&s),
            span: span_of(s.syntax()),
        })
        .ok_or_else(|| internal("missing constraint name", sp))?;
    Ok(ast_c::DropConstraint {
        if_exists,
        name,
        span: sp,
    })
}

fn build_show(c: ShowClause) -> Result<ast_c::Show> {
    let sp = span_of(c.syntax());
    let kind = c
        .kind()
        .map(build_show_kind)
        .transpose()?
        .ok_or_else(|| internal("missing SHOW kind", sp))?;
    let yield_items = c
        .show_return()
        .map(|sr| -> Result<ast_c::ShowYieldSpec> {
            if sr.star_token().is_some() {
                Ok(ast_c::ShowYieldSpec::Star {
                    span: span_of(sr.syntax()),
                })
            } else {
                let items: Result<Vec<_>> = sr
                    .yield_items()
                    .map(|yi| {
                        let pf = yi
                            .field_name()
                            .and_then(|f| f.symbolic_name())
                            .map(|s| ast_c::SymbolicName {
                                name: symbolic_name_text(&s),
                                span: span_of(s.syntax()),
                            })
                            .ok_or_else(|| internal("missing proc field", span_of(yi.syntax())))?;
                        let alias = yi.alias().map(build_top_variable);
                        Ok(ast_c::ShowYieldItem {
                            procedure_field: pf,
                            alias,
                        })
                    })
                    .collect();
                Ok(ast_c::ShowYieldSpec::Items(items?))
            }
        })
        .transpose()?;
    let where_clause = c
        .show_return()
        .and_then(|sr| sr.where_expr())
        .map(build_expression)
        .transpose()?;
    let ret_clause = c.return_clause().map(build_return_body).transpose()?;
    Ok(ast_c::Show {
        kind,
        yield_items,
        where_clause,
        return_clause: ret_clause,
        span: sp,
    })
}

fn build_show_kind(k: ShowKind) -> Result<ast_c::ShowKind> {
    let text = k.syntax().text().to_string().to_uppercase();
    if text.contains("INDEX") {
        Ok(ast_c::ShowKind::Indexes)
    } else if text.contains("CONSTRAINT") {
        Ok(ast_c::ShowKind::Constraints)
    } else if text.contains("FUNCTION") {
        Ok(ast_c::ShowKind::Functions)
    } else if text.contains("PROCEDURE") {
        Ok(ast_c::ShowKind::Procedures)
    } else if text.contains("DATABASE") {
        if text == "DATABASES" || text.ends_with("ES") {
            Ok(ast_c::ShowKind::Databases)
        } else {
            let name = k
                .syntax()
                .children()
                .filter_map(top_level::SymbolicName::cast)
                .next()
                .map(|s| ast_c::SymbolicName {
                    name: symbolic_name_text(&s),
                    span: span_of(s.syntax()),
                })
                .ok_or_else(|| internal("missing db name", span_of(k.syntax())))?;
            Ok(ast_c::ShowKind::Database(name))
        }
    } else {
        Ok(ast_c::ShowKind::Indexes)
    }
}

fn build_return_body(rc: ReturnClause) -> Result<ast_c::ReturnBody> {
    let proj = rc
        .projection_body()
        .map(build_projection_body)
        .transpose()?
        .ok_or_else(|| internal("missing projection", span_of(rc.syntax())))?;
    Ok(ast_c::ReturnBody {
        distinct: proj.distinct,
        items: proj.items,
        order: proj.order,
        skip: proj.skip,
        limit: proj.limit,
    })
}

fn build_use(c: UseClause) -> Result<ast_c::Use> {
    let sp = span_of(c.syntax());
    let name = c
        .schema_name()
        .and_then(|sn| sn.symbolic_name())
        .map(|s| ast_c::SymbolicName {
            name: symbolic_name_text(&s),
            span: span_of(s.syntax()),
        })
        .ok_or_else(|| internal("missing graph name in USE", sp))?;
    Ok(ast_c::Use {
        graph: name,
        span: sp,
    })
}

fn build_load_csv(c: LoadCsvClause) -> Result<ast_c::LoadCsv> {
    let sp = span_of(c.syntax());
    let with_headers = c.with_headers();
    let source = c
        .source()
        .map(build_expression)
        .transpose()?
        .ok_or_else(|| internal("missing source in LOAD CSV", sp))?;
    let variable = c
        .variable()
        .map(build_top_variable)
        .ok_or_else(|| internal("missing variable in LOAD CSV", sp))?;
    Ok(ast_c::LoadCsv {
        with_headers,
        source,
        variable,
        span: sp,
    })
}

fn build_finish(c: FinishClause) -> Result<ast_c::Finish> {
    let sp = span_of(c.syntax());
    Ok(ast_c::Finish { span: sp })
}

// ── Names ────────────────────────────────────────────────────────────

fn build_variable(v: expressions::Variable) -> ast_c::Variable {
    let name = v
        .name()
        .map(|s| ast_c::SymbolicName {
            name: symbolic_name_text(&s),
            span: span_of(s.syntax()),
        })
        .unwrap_or_else(|| ast_c::SymbolicName {
            name: String::new(),
            span: span_of(v.syntax()),
        });
    ast_c::Variable { name }
}

fn build_top_variable(v: expressions::Variable) -> ast_c::Variable {
    let name = v
        .name()
        .map(|s| ast_c::SymbolicName {
            name: symbolic_name_text(&s),
            span: span_of(s.syntax()),
        })
        .unwrap_or_else(|| ast_c::SymbolicName {
            name: String::new(),
            span: span_of(v.syntax()),
        });
    ast_c::Variable { name }
}
