//! Walker output types (ADR-0024 §architecture).
//!
//! `WalkResult` carries everything a consumer (parse / completion /
//! highlight / hint) needs from a single walk: the outcome
//! (matched, incomplete, mismatched, validation-failed), the
//! matched-node path the AST builder reads, and the per-byte
//! highlight class assignments collected as terminals matched.
//!
//! Phase A note: only the parse consumer is wired today. The
//! `per_byte_class` field is populated but unused outside
//! tests; completion + highlighting still flow through the
//! chumsky path until Phase D / F.

use crate::dsl::grammar::{HighlightClass, IdentSource, ValidationError};

/// How far into the input the walker should consume.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WalkBound {
    /// Consume all input. Trailing whitespace OK; trailing tokens
    /// fail the walk. Used by the parse consumer.
    EndOfInput,
    /// Consume up to (but not including) the given byte position.
    /// Used by completion / hint to ask "what was expected at the
    /// cursor?".
    #[allow(dead_code)]
    Position(usize),
}

/// Closed shape describing what could legally have continued the
/// walk at its stopping position. Phase A keeps this minimal —
/// only what the router needs to render a parse error.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Expectation {
    /// The walker expected this literal keyword.
    Word(&'static str),
    /// The walker expected this verbatim literal byte sequence
    /// (used today for the `1` in `add 1:n …`).
    Literal(&'static str),
    /// The walker expected an identifier slot. `source` drives
    /// the user-facing expected-label rendering ("table name",
    /// "column name", …) so the existing completion engine's
    /// `IdentSlot::from_expected_label` round-trip still works.
    /// `role` is the walker-internal slot tag.
    Ident {
        role: &'static str,
        source: IdentSource,
    },
    /// The walker expected this exact punctuation character.
    Punct(char),
    /// The walker expected a number literal.
    NumberLit,
    /// The walker expected a string literal.
    StringLit,
    /// The walker expected a blob literal.
    #[allow(dead_code)]
    BlobLit,
    /// The walker expected a flag with this name (without `--`).
    #[allow(dead_code)]
    Flag(&'static str),
    /// The walker expected a bare-path argument (non-whitespace
    /// run).
    BarePath,
    /// The walker expected end of input.
    EndOfInput,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum WalkOutcome {
    /// Input fully matched a command. `command_idx` is the
    /// position of the matched command in the active registry.
    Match { command_idx: usize },
    /// Input matched a prefix; more input would have continued
    /// the parse. `position` is the byte offset where input ran
    /// out (post-whitespace).
    Incomplete {
        position: usize,
        expected: Vec<Expectation>,
    },
    /// Input had a token at `position` that no expected node
    /// accepts.
    Mismatch {
        position: usize,
        expected: Vec<Expectation>,
    },
    /// The walker matched a terminal but a content validator
    /// rejected the value (e.g. `mode foo` matched the value
    /// slot's identifier shape, then the validator fired
    /// `mode.unknown`).
    ValidationFailed {
        position: usize,
        error: ValidationError,
    },
}

/// One terminal-node match. Combinators (Seq / Choice / Optional /
/// Repeated) shape the order; the AST builder reads the items in
/// declaration order.
#[derive(Debug, Clone)]
pub struct MatchedItem {
    pub kind: MatchedKind,
    pub text: String,
    pub span: (usize, usize),
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MatchedKind {
    /// A `Word` node matched. Carries the primary literal (not
    /// the alias actually typed) so the AST builder can match
    /// on it canonically.
    Word(&'static str),
    Punct(char),
    /// An `Ident` matched. `role` identifies which slot;
    /// `source` is the slot's `IdentSource` — the post-walk
    /// schema-existence check (ADR-0027 §2) reads it to decide
    /// whether a matched name must exist in the schema.
    Ident {
        role: &'static str,
        source: IdentSource,
    },
    NumberLit,
    StringLit,
    BlobLit,
    Flag(&'static str),
    BarePath,
}

/// The path of matched terminals, in order. Optional / Repeated
/// nodes that produced no match contribute nothing.
#[derive(Debug, Clone, Default)]
pub struct MatchedPath {
    pub items: Vec<MatchedItem>,
}

impl MatchedPath {
    pub fn new() -> Self {
        Self::default()
    }

    pub fn push(&mut self, item: MatchedItem) {
        self.items.push(item);
    }

    /// Convenience: find the first item matching the predicate.
    pub fn find<F: Fn(&MatchedItem) -> bool>(&self, pred: F) -> Option<&MatchedItem> {
        self.items.iter().find(|i| pred(i))
    }

    /// Convenience: did any item match this exact word literal
    /// (by primary)? Used by Optional-keyword discrimination
    /// (e.g., `save` vs `save as`).
    pub fn contains_word(&self, primary: &'static str) -> bool {
        self.items
            .iter()
            .any(|i| matches!(&i.kind, MatchedKind::Word(p) if *p == primary))
    }
}

/// Per-byte highlight class assignment, collected as terminals
/// match. Phase A keeps this for future consumers; not yet used
/// outside walker-internal tests.
#[derive(Debug, Clone)]
pub struct ByteClass {
    pub start: usize,
    pub end: usize,
    pub class: HighlightClass,
}

/// Severity of a pre-submit [`Diagnostic`] (ADR-0027 §1).
///
/// Ordered so `Error > Warning` — taking the `max` across a
/// diagnostic set gives the validity indicator's verdict.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum Severity {
    /// Valid and runnable, but very likely not what the user
    /// intends — a type-mismatched comparison, `= NULL`
    /// (ADR-0026 §7).
    Warning,
    /// Known to fail if submitted now — a parse error, or a
    /// reference to a table / column that does not exist.
    Error,
}

/// A problem the walker found in the input *before* submission
/// (ADR-0027 §2). The severity drives the validity indicator;
/// the span drives highlighting; the message is the hint text.
///
/// Note: the parse outcome (`Incomplete` / `Mismatch` /
/// `ValidationFailed`) is *not* recorded as a `Diagnostic` —
/// the indicator reads it from `WalkOutcome` directly
/// (ADR-0027 §2, "the highest severity across the outcome and
/// the diagnostics"). `diagnostics` carries the schema-aware
/// findings layered on top of a structurally-valid parse.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Diagnostic {
    pub severity: Severity,
    /// Byte range in the source the diagnostic refers to.
    pub span: (usize, usize),
    /// Ready-to-show message — already catalog-translated by
    /// whichever pass produced the diagnostic.
    pub message: String,
}

#[derive(Debug, Clone)]
pub struct WalkResult {
    pub outcome: WalkOutcome,
    pub matched_path: MatchedPath,
    pub per_byte_class: Vec<ByteClass>,
    /// Schema-aware pre-submit findings layered on a
    /// structurally-valid parse (ADR-0027): unknown table /
    /// column references (ERROR), type-mismatched WHERE
    /// comparisons and `= NULL` (WARNING). Empty when the
    /// input does not parse — those are the outcome's job.
    pub diagnostics: Vec<Diagnostic>,
    /// Optional-Optional expectations the walker could have
    /// accepted but didn't because the outer shape ran out at a
    /// node boundary (ADR-0024 §architecture, round-5 follow-up).
    ///
    /// Populated on `WalkOutcome::Match` so completion can offer
    /// optional-suffix candidates at the end of a valid command
    /// — e.g., after typing `save` the walker matches the
    /// Optional `as` as skipped, the suffix carries it here, and
    /// the completion engine surfaces `as` as a Tab candidate.
    /// Empty on the non-Match outcomes — those carry expected
    /// information inside the outcome variant itself.
    pub tail_expected: Vec<Expectation>,
}
