//! Shared sub-grammars for the DDL/DML migrations
//! (ADR-0024 §architecture, §sub-grammars).
//!
//! Phase B uses these for relationship endpoints and referential
//! actions; Phase D extends with `where_clause`,
//! `column_value_list`, and the typed value slots.

use crate::completion::TableColumn;
use crate::dsl::grammar::{
    HighlightClass, HintMode, IdentSource, IdentValidator, Node, NumberValidator, ValidationError,
    Word,
};
use crate::dsl::types::Type;
use crate::dsl::walker::context::WalkContext;
use std::str::FromStr;

// --- Type-name validator ------------------------------------------

/// Reject any identifier that isn't a known user-facing type name.
///
/// Mirrors the chumsky-side `Type::from_str` + `UnknownType`
/// flow — surfaces the same `parse.custom.unknown_type` catalog
/// wording with `{found}` and `{expected}` args.
pub fn validate_type_name(value: &str) -> Result<(), ValidationError> {
    if Type::from_str(value).is_ok() {
        Ok(())
    } else {
        let expected = Type::all()
            .iter()
            .map(|t| t.keyword())
            .collect::<Vec<_>>()
            .join(", ");
        Err(ValidationError {
            message_key: "parse.custom.unknown_type",
            args: vec![("found", value.to_string()), ("expected", expected)],
        })
    }
}

pub const TYPE_VALIDATOR: IdentValidator = validate_type_name;

// --- Type-slot leaf -----------------------------------------------

/// `Ident` slot for a column type. Validation runs after a
/// successful identifier-shape match.
pub const TYPE_SLOT: Node = Node::Ident {
    source: IdentSource::Types,
    role: "type",
    validator: Some(TYPE_VALIDATOR),
    highlight_override: Some(HighlightClass::Type),
    writes_table: false,
    writes_column: false,
    writes_user_listed_column: false,
    writes_table_alias: false,
    writes_cte_name: false,
    writes_projection_alias: false,
};

// --- Qualified column reference (`<Table>.<Column>`) --------------

const QUALIFIED_COLUMN_NODES: &[Node] = &[
    Node::Ident {
        source: IdentSource::Tables,
        role: "table_name",
        validator: None,
        highlight_override: None,
        writes_table: false,
        writes_column: false,
        writes_user_listed_column: false,
        writes_table_alias: false,
        writes_cte_name: false,
        writes_projection_alias: false,
    },
    Node::Punct('.'),
    Node::Ident {
        source: IdentSource::Columns,
        role: "column_name",
        validator: None,
        highlight_override: None,
        writes_table: false,
        writes_column: false,
        writes_user_listed_column: false,
        writes_table_alias: false,
        writes_cte_name: false,
        writes_projection_alias: false,
    },
];
pub const QUALIFIED_COLUMN: Node = Node::Seq(QUALIFIED_COLUMN_NODES);

// --- Relationship-endpoint clauses (`from <T>.<c> to <T>.<c>`) ----

const RELATIONSHIP_ENDPOINTS_NODES: &[Node] = &[
    Node::Word(Word::keyword("from")),
    QUALIFIED_COLUMN,
    Node::Word(Word::keyword("to")),
    QUALIFIED_COLUMN,
];
pub const RELATIONSHIP_ENDPOINTS: Node = Node::Seq(RELATIONSHIP_ENDPOINTS_NODES);

// --- Referential action (`cascade`, `restrict`, `set null`,
//     `no action`) -----------------------------------------------

const ACTION_SET_NULL: &[Node] = &[
    Node::Word(Word::keyword("set")),
    Node::Word(Word::keyword("null")),
];
const ACTION_NO_ACTION: &[Node] = &[
    Node::Word(Word::keyword("no")),
    Node::Word(Word::keyword("action")),
];
const ACTION_CHOICES: &[Node] = &[
    Node::Word(Word::keyword("cascade")),
    Node::Word(Word::keyword("restrict")),
    Node::Seq(ACTION_SET_NULL),
    Node::Seq(ACTION_NO_ACTION),
];
pub const ACTION_KEYWORD: Node = Node::Choice(ACTION_CHOICES);

// --- A single `on <delete|update> <action>` clause ----------------

const ON_TARGET_CHOICES: &[Node] = &[
    Node::Word(Word::keyword("delete")),
    Node::Word(Word::keyword("update")),
];

const ON_CLAUSE_NODES: &[Node] = &[
    Node::Word(Word::keyword("on")),
    Node::Choice(ON_TARGET_CHOICES),
    ACTION_KEYWORD,
];
pub const ON_CLAUSE: Node = Node::Seq(ON_CLAUSE_NODES);

/// Repeated `on <target> <action>` clauses (0..2 occurrences).
/// Validation of "specified twice" + max=2 lives in the
/// command's AST builder.
const REFERENTIAL_CLAUSES_INNER: Node = Node::Repeated {
    inner: &ON_CLAUSE,
    separator: None,
    min: 0,
};

/// As [`REFERENTIAL_CLAUSES_INNER`], tagged as the
/// `referential_actions` clause-concept region (issue #37).
///
/// Shared by the simple-mode `add 1:n relationship … on delete/update`
/// and the advanced-mode `references … on delete/update` clause, so the
/// one wrapper surfaces the concept in both modes (the mode-keyed
/// example is chosen by the live mode at F1 time, not by which grammar
/// matched).
pub const REFERENTIAL_CLAUSES: Node = Node::Concept {
    topic: "referential_actions",
    inner: &REFERENTIAL_CLAUSES_INNER,
};

// =================================================================
// Typed value slots (ADR-0024 §Phase D, §typed-value-slots)
// =================================================================
//
// Each `<ty>_slot()` factory returns a `Node` that accepts either
// `null` or a literal of the corresponding shape, with an
// optional content validator that rejects mis-typed values at
// parse time with localised catalog wording. Per-type prose
// hints attach via `Choice` HintMode — but Phase D's first
// landing keeps `Default` everywhere; the dispatch-by-column-type
// covers the central design claim, and per-type prose can layer
// on later without grammar surface changes.

fn validate_integer_only(value: &str) -> Result<(), ValidationError> {
    // The lexer-side number consumer accepts integers and
    // fractional forms (e.g. `3.14`). For int / serial / shortid
    // columns reject any literal that carries a decimal point.
    if value.contains('.') {
        Err(ValidationError {
            message_key: "parse.custom.bind_type_mismatch",
            args: vec![
                ("found", value.to_string()),
                ("expected", "integer".to_string()),
            ],
        })
    } else {
        Ok(())
    }
}

const INTEGER_ONLY_VALIDATOR: NumberValidator = validate_integer_only;

fn validate_decimal_string(value: &str) -> Result<(), ValidationError> {
    if value.parse::<f64>().is_ok() {
        Ok(())
    } else {
        Err(ValidationError {
            message_key: "parse.custom.bind_type_mismatch",
            args: vec![
                ("found", value.to_string()),
                ("expected", "number".to_string()),
            ],
        })
    }
}

const DECIMAL_VALIDATOR: NumberValidator = validate_decimal_string;

// Bare `null` keyword — used as the trailing branch of every
// typed value slot so a column always accepts the absence sentinel.
const NULL_WORD: Node = Node::Word(Word::keyword("null"));

const INT_SLOT_CHOICES: &[Node] = &[
    Node::NumberLit {
        validator: Some(INTEGER_ONLY_VALIDATOR),
    },
    NULL_WORD,
];
const INT_SLOT_INNER: Node = Node::Choice(INT_SLOT_CHOICES);
const INT_SLOT: Node = Node::TypedValueSlot {
    ty: Type::Int,
    column_name: None,
    inner: &INT_SLOT_INNER,
};
const SERIAL_SLOT: Node = Node::TypedValueSlot {
    ty: Type::Serial,
    column_name: None,
    inner: &INT_SLOT_INNER,
};

const REAL_SLOT_CHOICES: &[Node] = &[Node::NumberLit { validator: None }, NULL_WORD];
const REAL_SLOT_INNER: Node = Node::Choice(REAL_SLOT_CHOICES);
const REAL_SLOT: Node = Node::TypedValueSlot {
    ty: Type::Real,
    column_name: None,
    inner: &REAL_SLOT_INNER,
};

const DECIMAL_SLOT_CHOICES: &[Node] = &[
    Node::NumberLit {
        validator: Some(DECIMAL_VALIDATOR),
    },
    NULL_WORD,
];
const DECIMAL_SLOT_INNER: Node = Node::Choice(DECIMAL_SLOT_CHOICES);
const DECIMAL_SLOT: Node = Node::TypedValueSlot {
    ty: Type::Decimal,
    column_name: None,
    inner: &DECIMAL_SLOT_INNER,
};

const BOOL_SLOT_CHOICES: &[Node] = &[
    Node::Word(Word::keyword("true")),
    Node::Word(Word::keyword("false")),
    NULL_WORD,
];
const BOOL_SLOT_INNER: Node = Node::Choice(BOOL_SLOT_CHOICES);
const BOOL_SLOT: Node = Node::TypedValueSlot {
    ty: Type::Bool,
    column_name: None,
    inner: &BOOL_SLOT_INNER,
};

const TEXT_SLOT_CHOICES: &[Node] = &[Node::StringLit, NULL_WORD];
const TEXT_SLOT_INNER: Node = Node::Choice(TEXT_SLOT_CHOICES);
const TEXT_SLOT: Node = Node::TypedValueSlot {
    ty: Type::Text,
    column_name: None,
    inner: &TEXT_SLOT_INNER,
};

// Date / datetime share the StringLit-or-null shape with text
// but get distinct catalog-prose entries so the hint surfaces
// the YYYY-MM-DD / YYYY-MM-DD HH:MM:SS format examples.
const DATE_SLOT: Node = Node::TypedValueSlot {
    ty: Type::Date,
    column_name: None,
    inner: &TEXT_SLOT_INNER,
};
const DATETIME_SLOT: Node = Node::TypedValueSlot {
    ty: Type::DateTime,
    column_name: None,
    inner: &TEXT_SLOT_INNER,
};
// shortid columns store base58 text (ADR-0011 fk_target_type
// shortid → text); the slot accepts a quoted-text literal or
// null.
const SHORTID_SLOT: Node = Node::TypedValueSlot {
    ty: Type::ShortId,
    column_name: None,
    inner: &TEXT_SLOT_INNER,
};

/// Dispatch a value slot per user-facing type
/// (ADR-0024 §slot_for_type).
///
/// Returns the same node every time for a given Type — fine to
/// call from within a `DynamicSubgrammar` factory. The
/// returned slot does not carry a column name; callers that
/// have one (e.g. `column_value_list` building per-column
/// slots) should use [`slot_for_column`] instead.
#[must_use]
pub const fn slot_for_type(ty: Type) -> Node {
    match ty {
        Type::Int => INT_SLOT,
        Type::Serial => SERIAL_SLOT,
        Type::ShortId => SHORTID_SLOT,
        Type::Real => REAL_SLOT,
        Type::Decimal => DECIMAL_SLOT,
        Type::Bool => BOOL_SLOT,
        Type::Text => TEXT_SLOT,
        Type::Date => DATE_SLOT,
        Type::DateTime => DATETIME_SLOT,
    }
}

/// Look up just the inner Choice (no `TypedValueSlot` wrapper)
/// for a given user-facing type. Used by `slot_for_column` to
/// rebuild a TypedValueSlot with an embedded column name.
const fn slot_inner_for_type(ty: Type) -> &'static Node {
    match ty {
        Type::Int | Type::Serial => &INT_SLOT_INNER,
        Type::Real => &REAL_SLOT_INNER,
        Type::Decimal => &DECIMAL_SLOT_INNER,
        Type::Bool => &BOOL_SLOT_INNER,
        Type::Text | Type::Date | Type::DateTime | Type::ShortId => &TEXT_SLOT_INNER,
    }
}

/// Build a typed value slot with an embedded column name
/// (ADR-0024 §Phase D §typed-value-slots). Used by
/// `column_value_list` to attach each column's name so the
/// hint resolver can render "for `<column>`:" prefixes.
///
/// The walker writes the (leaked) name into
/// `WalkContext::pending_value_column` on entry to the slot
/// and clears it on successful inner match.
#[must_use]
fn slot_for_column(ty: Type, name: &str) -> Node {
    // `Box::leak`: column names from the schema cache need a
    // `&'static str`-compatible lifetime to plug into the
    // static Node enum. The leak is per dynamic walk (factory
    // invocation), bounded by the column count — consistent
    // with the `DynamicSubgrammar` Box::leak in the walker
    // driver.
    let leaked: &'static str = Box::leak(name.to_string().into_boxed_str());
    Node::TypedValueSlot {
        ty,
        column_name: Some(leaked),
        inner: slot_inner_for_type(ty),
    }
}

// =================================================================
// Dynamic sub-grammar: column_value_list
// =================================================================

/// Fallback when no schema-resolved column list is available
/// (schemaless parse, missing table, empty schema cache).
/// Mirrors the pre-Phase-D `value_literal` Choice.
const FALLBACK_VALUE_LITERAL_CHOICES: &[Node] = &[
    Node::Word(Word::keyword("null")),
    Node::Word(Word::keyword("true")),
    Node::Word(Word::keyword("false")),
    Node::NumberLit { validator: None },
    Node::StringLit,
];
const FALLBACK_VALUE_LITERAL_INNER: Node = Node::Choice(FALLBACK_VALUE_LITERAL_CHOICES);
/// The schemaless value-literal slot. The `ProseOnly` HintMode
/// (ADR-0024 §HintMode-per-node) tells the hint resolver to
/// surface the generic "Type a value: number, 'text', …" prose
/// here rather than the misleading `null`/`true`/`false`
/// candidate trio.
/// The schemaless value-literal slot. `pub(crate)` so the
/// `default <literal>` column constraint (ADR-0029) can reuse
/// it from `grammar::ddl`.
pub(crate) const FALLBACK_VALUE_LITERAL: Node = Node::Hinted {
    mode: HintMode::ProseOnly("hint.value_literal_slot"),
    inner: &FALLBACK_VALUE_LITERAL_INNER,
};

/// The type-blind value list. `pub(crate)` so the insert value-list
/// arity gate (`data.rs`, issue #17) can route a wrong-count tuple here
/// — exactly as the advanced grammar's `tuple_value_list` does — so the
/// tuple still structurally matches and the per-tuple arity diagnostic
/// (ADR-0033 §8.1) fires instead of a bare "expected `,`/`)`".
pub(crate) const FALLBACK_VALUE_LIST: Node = Node::Repeated {
    inner: &FALLBACK_VALUE_LITERAL,
    separator: Some(&Node::Punct(',')),
    min: 1,
};

/// The columns an insert value tuple maps onto (ADR-0024 §Phase D).
///
/// Mirrors `db::do_insert`'s `user_cols` logic. `None` when the walker
/// is schemaless, the table is unknown, or — for Form B/C — every column
/// is auto-generated (callers fall back to the type-blind value list).
///
/// - **Form A** (`user_listed_columns` set): the listed columns, in the
///   user's order; names the schema doesn't know are dropped.
/// - **Form B/C** (no column list): the table's non-auto-generated
///   columns, in declaration order. `serial` / `shortid` are skipped
///   because the simple-mode dispatch auto-fills them (ADR-0018 §3).
///
/// This is the single source of truth shared by [`column_value_list`]
/// (which builds the typed slots) and the `data.rs` arity gate (which
/// counts them) so the two never disagree (issue #17).
pub fn insert_target_columns<'c>(ctx: &'c WalkContext<'_>) -> Option<Vec<&'c TableColumn>> {
    let table_cols = ctx.current_table_columns.as_ref()?;
    if table_cols.is_empty() {
        return None;
    }
    let cols: Vec<&TableColumn> = ctx.user_listed_columns.as_ref().map_or_else(
        || {
            table_cols
                .iter()
                .filter(|c| !matches!(c.user_type, Type::Serial | Type::ShortId))
                .collect()
        },
        |user_listed| {
            user_listed
                .iter()
                .filter_map(|name| {
                    table_cols
                        .iter()
                        .find(|c| c.name.eq_ignore_ascii_case(name))
                })
                .collect()
        },
    );
    if cols.is_empty() { None } else { Some(cols) }
}

/// Count the value positions in a `VALUES`/insert tuple whose contents
/// begin at `pos` (just past the opening `(`), and whether the tuple is
/// *closed* (a depth-0 `)` was reached) vs still being typed (scan hit
/// end-of-input first). Depth-aware: commas nested in a function call /
/// subquery (paren depth ≥ 1) or inside a string literal are not
/// separators. Returns `(0, _)` for an empty tuple `()`.
///
/// Shared by the advanced grammar's `tuple_value_list` (`sql_insert.rs`)
/// and the simple-mode DSL insert arity gate (`data.rs`) so both modes
/// count tuple values identically (issue #17).
pub(crate) fn count_tuple_values(source: &str, pos: usize) -> (usize, bool) {
    let bytes = source.as_bytes();
    let mut i = pos;
    let mut depth: i32 = 0;
    let mut commas = 0usize;
    let mut seen_value = false;
    let mut closed = false;
    while i < bytes.len() {
        match bytes[i] {
            b'\'' => {
                // Skip a single-quoted string literal (`''` escape).
                i += 1;
                seen_value = true;
                while i < bytes.len() {
                    if bytes[i] == b'\'' {
                        if bytes.get(i + 1) == Some(&b'\'') {
                            i += 2;
                            continue;
                        }
                        i += 1;
                        break;
                    }
                    i += 1;
                }
                continue;
            }
            b'(' => {
                depth += 1;
                seen_value = true;
            }
            b')' => {
                if depth == 0 {
                    closed = true;
                    break; // tuple close
                }
                depth -= 1;
            }
            b',' if depth == 0 => commas += 1,
            b if !b.is_ascii_whitespace() => seen_value = true,
            _ => {}
        }
        i += 1;
    }
    (if seen_value { commas + 1 } else { 0 }, closed)
}

/// Value slot keyed on `WalkContext::current_column`.
///
/// Picks the typed slot for the column whose name was most
/// recently matched by an `Ident { source: Columns,
/// writes_column: true }` node (ADR-0024 §Phase D). Fallback
/// when no current_column is resolved: the schemaless
/// value-literal choice.
pub fn current_column_value(ctx: &WalkContext) -> Node {
    ctx.current_column
        .as_ref()
        .map_or(FALLBACK_VALUE_LITERAL, |col| slot_for_type(col.user_type))
}

/// Comma-separated list of typed value slots, one per column.
///
/// Reads `current_table_columns` from the WalkContext (ADR-0024
/// §Phase D §column_value_list). When the schema cache holds
/// no entry for the current table — or the walker is
/// schemaless — falls back to the schema-unaware
/// `Repeated(VALUE_LITERAL, ',', 1)` shape so existing
/// callers/tests continue to work.
pub fn column_value_list(ctx: &WalkContext) -> Node {
    // Target columns per the shared insert mapping (Form A = listed,
    // Form B/C = non-auto-generated). `None` → schemaless / unknown
    // table / all-auto-generated → the type-blind fallback list.
    let Some(target_cols) = insert_target_columns(ctx) else {
        return FALLBACK_VALUE_LIST;
    };
    // Build a Seq of typed slots interleaved with commas. Each
    // slot embeds its column name so the hint resolver can
    // mention the column by name ("for `Email`: Type a quoted
    // string …").
    let mut children: Vec<Node> = Vec::with_capacity(target_cols.len() * 2);
    for (i, col) in target_cols.iter().enumerate() {
        if i > 0 {
            children.push(Node::Punct(','));
        }
        children.push(slot_for_column(col.user_type, &col.name));
    }
    Node::Seq(Box::leak(children.into_boxed_slice()))
}

// =================================================================
// Advanced-mode SQL `SET col = <rhs>` value slot (ADR-0036 Phase 3a)
// =================================================================
//
// A SQL UPDATE / UPSERT `SET col = <rhs>` value position routes a
// *lone literal* (a string / number / `null` / `true` / `false` that
// fills the whole position up to the next `,` / `where` / `returning`
// / `;` / end-of-input) to the column-typed slot — so the learner gets
// the same per-column hint + numeric-shape highlight the DSL gives —
// and routes anything else (an expression: arithmetic, a literal-
// prefixed form like `1 + 2`, a function call, a scalar subquery, a
// column / `excluded` reference) to the full `sql_expr` grammar,
// unchanged (ADR-0030 §4).
//
// Discrimination is by *lookahead*, NOT `Choice([typed_slot,
// sql_expr])`. A naive Choice would let the typed slot greedily match
// the leading literal of `1 + 2` and commit, leaving `+ 2` dangling —
// the walker's `Choice` is first-match-wins with no cross-branch
// backtrack — turning a valid expression into a parse error. The
// lookahead peeks the whole value position first, so a literal routes
// to the typed slot only when it is the *entire* value (ADR-0036
// Amendment 1).

/// Lookahead factory for a SQL `SET col = <rhs>` value position
/// (ADR-0036 Phase 3a). Routes a lone literal to the column-typed slot
/// (live hint + highlight) and everything else to `sql_expr`.
fn set_value_node(_ctx: &WalkContext, source: &str, pos: usize) -> Node {
    if set_rhs_is_lone_literal(source, pos) {
        // The column type was resolved into `current_column` by the
        // preceding `SET` column ident (`writes_column: true`), so the
        // typed slot drives the per-column hint + highlight.
        Node::DynamicSubgrammar(current_column_value)
    } else {
        // An expression — the engine evaluates it; keep the full
        // `sql_expr` surface. Returned from a `fn` (not a `const`) so
        // the reference doesn't enter the sql_expr ⇄ sql_select
        // const-evaluation cycle (see the matching note in `data.rs`).
        Node::Subgrammar(&crate::dsl::grammar::sql_expr::SQL_OR_EXPR)
    }
}

/// The SQL `SET col = <rhs>` value slot (ADR-0036 Phase 3a). Shared by
/// `sql_update`'s assignment list and the `sql_insert` UPSERT
/// `DO UPDATE SET`.
pub const SET_VALUE: Node = Node::Lookahead(set_value_node);

/// True when the `SET` RHS starting at `pos` is empty, a partial string
/// still being typed, or exactly one complete literal token that fills
/// the value position (the next token is a position boundary). Such
/// positions route to the column-typed slot so its hint/highlight fire;
/// everything else is an expression and routes to `sql_expr`.
///
/// An empty RHS counts as lone-literal so the typed-slot hint shows
/// while the cursor sits right after `=`. A signed number (`-5`) counts
/// too: `consume_number_literal` — the same consumer the slot's
/// `NumberLit` uses — folds the leading sign into the literal, so the
/// slot can match it.
fn set_rhs_is_lone_literal(source: &str, pos: usize) -> bool {
    use crate::dsl::walker::lex_helpers::{
        consume_ident, consume_number_literal, consume_string_literal, skip_whitespace,
    };
    let p = skip_whitespace(source, pos);
    if p >= source.len() {
        return true; // empty RHS — show the typed-slot hint
    }
    // A string literal — single-quoted (SQL strings). Complete (a
    // boundary must follow) or a partial one still being typed (route
    // to the slot so the hint persists while typing).
    if source.as_bytes()[p] == b'\'' {
        return match consume_string_literal(source, p) {
            Some(((_, end), _)) => next_is_set_boundary(source, end),
            None => true, // unterminated string, mid-typing
        };
    }
    // A number literal (incl. a leading sign) that fills the position.
    if let Some((_, end)) = consume_number_literal(source, p) {
        return next_is_set_boundary(source, end);
    }
    // `null` / `true` / `false` filling the position.
    if let Some((s, e)) = consume_ident(source, p) {
        let word = &source[s..e];
        if word.eq_ignore_ascii_case("null")
            || word.eq_ignore_ascii_case("true")
            || word.eq_ignore_ascii_case("false")
        {
            return next_is_set_boundary(source, e);
        }
        return false; // any other identifier → column ref / function → expression
    }
    false // punctuation / sign-then-space / `(` → expression
}

/// True when the next non-whitespace token after `pos` ends a `SET`
/// value position: `,` (next assignment), `)` / `;` / end-of-input, or
/// a `where` / `returning` clause keyword.
fn next_is_set_boundary(source: &str, pos: usize) -> bool {
    use crate::dsl::walker::lex_helpers::{consume_ident, skip_whitespace};
    let p = skip_whitespace(source, pos);
    let Some(b) = source.as_bytes().get(p) else {
        return true; // end of input
    };
    if *b == b',' || *b == b';' || *b == b')' {
        return true;
    }
    if let Some((s, e)) = consume_ident(source, p) {
        let word = &source[s..e];
        return word.eq_ignore_ascii_case("where") || word.eq_ignore_ascii_case("returning");
    }
    false
}
