//! ODIN parser — single-pass byte walker producing an `OdinDocument`.
//!
//! Walks the source bytes once and writes assignments directly into the
//! document. No token vector is materialized.
//!
//! Covers every ODIN shape: scalars, all value types (string/number/integer/
//! currency/percent/boolean/null/reference/binary/date/timestamp/time/
//! duration), `{section}` / `{section.sub}` / `{.relative}` /
//! `{records[N]}` / `{@TypeRef}` headers, tabular records, primitive arrays
//! (`{name[] : ~}`), relative dotted sub-blocks (`{.tags[] : ~}`), lookup
//! tables (`{$table.NAME[col1, col2]}`), modifiers (`!` / `*` / `-`),
//! trailing directives (`:type`, `:default`, …), verbs (`%verb …`), binary
//! (`^base64`), document separators (`---`), `@import` / `@schema` / `@if`
//! directives, escape sequences in strings, comment preservation, and
//! extension paths (`&com.acme.field = …`).

use rustc_hash::FxHashMap;

use crate::types::document::{OdinComment, OdinDocument, OdinImport, OdinSchema};
use crate::types::errors::{ParseError, ParseErrorCode};
use crate::types::ordered_map::OrderedMap;
use crate::types::options::ParseOptions;
use crate::types::values::{DirectiveValue, OdinDirective, OdinModifiers, OdinValue, OdinValues};

const MAX_ARRAY_INDEX: i64 = 1_000_000;
/// Per-tabular-section column-count cap. Bounds attacker-influenced
/// allocations from headers like `{x[] : a, b, c, ...}` with very many
/// comma-separated columns.
const MAX_TABULAR_COLUMNS: usize = 1024;
/// Per-tabular-section row-count cap.
const MAX_TABULAR_ROWS: usize = 1_000_000;

pub(super) fn parse(source: &str, options: &ParseOptions) -> Result<OdinDocument, ParseError> {
    if source.len() > options.max_size {
        return Err(ParseError::new(ParseErrorCode::MaximumDocumentSizeExceeded, 1, 1));
    }
    if source.len() > u32::MAX as usize {
        return Err(ParseError::new(ParseErrorCode::MaximumDocumentSizeExceeded, 1, 1));
    }
    Parser::new(source, options).run()
}

pub(super) fn parse_documents(source: &str, options: &ParseOptions) -> Result<Vec<OdinDocument>, ParseError> {
    if source.len() > options.max_size {
        return Err(ParseError::new(ParseErrorCode::MaximumDocumentSizeExceeded, 1, 1));
    }
    if source.len() > u32::MAX as usize {
        return Err(ParseError::new(ParseErrorCode::MaximumDocumentSizeExceeded, 1, 1));
    }
    let p = Parser::new(source, options);
    p.run_documents()
}

fn empty_doc() -> OdinDocument {
    OdinDocument {
        metadata: OrderedMap::new(),
        assignments: OrderedMap::new(),
        modifiers: OrderedMap::new(),
        imports: Vec::new(),
        schemas: Vec::new(),
        conditionals: Vec::new(),
        comments: Vec::new(),
    }
}

/// Catch-all when an internal `Bail` reaches the top-level run loop.
fn bail_to_err(line: usize, col: usize) -> ParseError {
    ParseError::with_message(
        ParseErrorCode::UnexpectedCharacter,
        line, col,
        "Unexpected input",
    )
}

fn find_comment_start_quote_aware(s: &str) -> Option<usize> {
    let mut in_quotes = false;
    for (i, ch) in s.char_indices() {
        match ch {
            '"' => in_quotes = !in_quotes,
            ';' if !in_quotes => return Some(i),
            _ => {}
        }
    }
    None
}

/// Match an inline header directive in a section header.
///
/// Supported forms:
/// - `name :type "value"` — quoted discriminator value.
/// - `name :if <expr>` / `name :elif <expr>` — unquoted expression captured to `}`.
/// - `name :else` — bare flag (value `true`).
///
/// Returns `(name, "_<directive>", value)` on success.
fn parse_inline_header_directive(header: &str) -> Option<(&str, String, String)> {
    let colon = header.find(" :")?;
    let name = header[..colon].trim();
    if name.is_empty() { return None; }
    let after_colon = &header[colon + 2..];
    let kw_end = after_colon
        .find(|c: char| !c.is_ascii_alphabetic())
        .unwrap_or(after_colon.len());
    let keyword = &after_colon[..kw_end];
    let rest = after_colon[kw_end..].trim();
    match keyword {
        "type" => {
            let value = rest.strip_prefix('"')?.strip_suffix('"')?;
            Some((name, "_type".to_string(), value.to_string()))
        }
        "if" | "elif" | "loop" | "counter" | "from" => {
            // Unquoted expression/value — capture everything up to the closing brace.
            if rest.is_empty() { return None; }
            Some((name, format!("_{keyword}"), rest.to_string()))
        }
        "else" => {
            if !rest.is_empty() { return None; }
            Some((name, "_else".to_string(), "true".to_string()))
        }
        _ => None,
    }
}

struct Parser<'a> {
    source: &'a str,
    bytes: &'a [u8],
    pos: usize,
    line: u32,
    line_start: usize,
    options: &'a ParseOptions,
    metadata: OrderedMap<String, OdinValue>,
    assignments: OrderedMap<String, OdinValue>,
    modifiers_map: OrderedMap<String, OdinModifiers>,
    imports: Vec<OdinImport>,
    schemas: Vec<OdinSchema>,
    comments: Vec<OdinComment>,
    array_indices: FxHashMap<String, usize>,
    current_header: Option<String>,
    previous_header: Option<String>,
    in_metadata: bool,
    path_buf: String,
    norm_buf: String,
    tabular: Option<TabularContext>,
    blank_line_break: bool,
}

/// Active tabular section state — set on `{name[] : ...}` headers.
struct TabularContext {
    base_name: String,
    columns: Vec<String>,
    row_index: usize,
    key_buf: String,
    is_primitive: bool,
    /// True for `{$table.NAME[...]}` lookup tables — rows go to metadata.
    is_metadata_table: bool,
}

impl<'a> Parser<'a> {
    fn new(source: &'a str, options: &'a ParseOptions) -> Self {
        let est_paths = source.len() / 28 + 16;
        Self {
            source,
            bytes: source.as_bytes(),
            pos: 0,
            line: 1,
            line_start: 0,
            options,
            metadata: OrderedMap::with_capacity(est_paths.min(32)),
            assignments: OrderedMap::with_capacity(est_paths),
            modifiers_map: OrderedMap::new(),
            imports: Vec::new(),
            schemas: Vec::new(),
            comments: Vec::new(),
            array_indices: FxHashMap::default(),
            current_header: None,
            previous_header: None,
            in_metadata: false,
            path_buf: String::with_capacity(64),
            norm_buf: String::with_capacity(64),
            tabular: None,
            blank_line_break: false,
        }
    }

    #[inline]
    fn col(&self) -> u32 {
        (self.pos - self.line_start) as u32 + 1
    }

    fn run(self) -> Result<OdinDocument, ParseError> {
        let mut docs = self.run_loop(false)?;
        Ok(docs.pop().unwrap_or_else(empty_doc))
    }

    fn run_documents(self) -> Result<Vec<OdinDocument>, ParseError> {
        let docs = self.run_loop(true)?;
        Ok(if docs.is_empty() { vec![empty_doc()] } else { docs })
    }

    fn run_loop(mut self, keep_all: bool) -> Result<Vec<OdinDocument>, ParseError> {
        let mut docs: Vec<OdinDocument> = Vec::new();
        loop {
            if !self.skip_blanks_and_comments() { break; }
            let b = self.bytes[self.pos];
            let line = self.line as usize;
            let col = self.col() as usize;
            // A new header, a blank-line break, or EOF closes any active tabular
            // section; a following root assignment then scopes to root.
            if self.tabular.is_some() && (b == b'{' || self.blank_line_break) {
                self.tabular = None;
            }
            self.blank_line_break = false;
            if self.tabular.is_some() {
                match self.parse_tabular_row() {
                    StepResult::Ok => continue,
                    StepResult::Bail => return Err(bail_to_err(line, col)),
                    StepResult::Err(e) => return Err(e),
                }
            }
            match b {
                b'{' => match self.parse_header() {
                    StepResult::Ok => {}
                    StepResult::Bail => return Err(bail_to_err(line, col)),
                    StepResult::Err(e) => return Err(e),
                },
                b'-' if self.peek_eq(b"---") => {
                    self.pos += 3;
                    while self.pos < self.bytes.len() && self.bytes[self.pos] != b'\n' {
                        self.pos += 1;
                    }
                    if keep_all {
                        docs.push(self.snapshot_doc());
                    }
                    self.reset_doc_state();
                    continue;
                }
                b'@' => match self.parse_top_directive() {
                    StepResult::Ok => {}
                    StepResult::Bail => return Err(bail_to_err(line, col)),
                    StepResult::Err(e) => return Err(e),
                },
                b':' if self.is_bare_directive_line() => match self.parse_bare_directive() {
                    StepResult::Ok => {}
                    StepResult::Bail => return Err(bail_to_err(line, col)),
                    StepResult::Err(e) => return Err(e),
                },
                b'"' if self.peek_eq(b"\"\"\"") => match self.parse_bare_literal_block() {
                    StepResult::Ok => {}
                    StepResult::Bail => return Err(bail_to_err(line, col)),
                    StepResult::Err(e) => return Err(e),
                },
                _ => match self.parse_assignment() {
                    StepResult::Ok => {}
                    StepResult::Bail => return Err(bail_to_err(line, col)),
                    StepResult::Err(e) => return Err(e),
                },
            }
        }
        docs.push(self.into_doc());
        Ok(docs)
    }

    fn snapshot_doc(&mut self) -> OdinDocument {
        OdinDocument {
            metadata: std::mem::take(&mut self.metadata),
            assignments: std::mem::take(&mut self.assignments),
            modifiers: std::mem::take(&mut self.modifiers_map),
            imports: std::mem::take(&mut self.imports),
            schemas: std::mem::take(&mut self.schemas),
            conditionals: Vec::new(),
            comments: std::mem::take(&mut self.comments),
        }
    }

    fn into_doc(self) -> OdinDocument {
        OdinDocument {
            metadata: self.metadata,
            assignments: self.assignments,
            modifiers: self.modifiers_map,
            imports: self.imports,
            schemas: self.schemas,
            conditionals: Vec::new(),
            comments: self.comments,
        }
    }

    fn reset_doc_state(&mut self) {
        self.metadata.clear();
        self.assignments.clear();
        self.modifiers_map.clear();
        self.array_indices.clear();
        self.imports.clear();
        self.schemas.clear();
        self.comments.clear();
        self.current_header = None;
        self.previous_header = None;
        self.in_metadata = false;
        self.tabular = None;
    }

    /// Skip whitespace, blank lines, and `;` comments. Two newlines inside
    /// the root `{$}` section exit metadata mode. Returns `false` on EOF.
    fn skip_blanks_and_comments(&mut self) -> bool {
        let mut newlines_in_a_row: u32 = 0;
        while self.pos < self.bytes.len() {
            match self.bytes[self.pos] {
                b' ' | b'\t' | b'\r' => { self.pos += 1; }
                b'\n' => {
                    self.pos += 1;
                    self.line += 1;
                    self.line_start = self.pos;
                    newlines_in_a_row += 1;
                    if newlines_in_a_row >= 2 {
                        self.blank_line_break = true;
                        if self.in_metadata && self.current_header.is_none() {
                            self.in_metadata = false;
                        }
                    }
                }
                b';' => {
                    // A comment line is not a blank-line break between rows.
                    newlines_in_a_row = 0;
                    self.skip_comment();
                }
                _ => return true,
            }
        }
        false
    }

    #[inline]
    fn peek_eq(&self, needle: &[u8]) -> bool {
        self.bytes[self.pos..].starts_with(needle)
    }

    /// Skip past a `;` comment, capturing it when `preserve_comments` is on.
    fn skip_comment(&mut self) {
        debug_assert_eq!(self.bytes[self.pos], b';');
        let end = match memchr::memchr(b'\n', &self.bytes[self.pos..]) {
            Some(off) => self.pos + off,
            None => self.bytes.len(),
        };
        if self.options.preserve_comments {
            let raw = &self.source[self.pos..end];
            let stripped = raw.strip_prefix(';').unwrap_or(raw);
            let text = stripped.strip_prefix(' ').unwrap_or(stripped).to_string();
            self.comments.push(OdinComment {
                text,
                associated_path: None,
                line: self.line as usize,
            });
        }
        self.pos = end;
    }

    fn parse_header(&mut self) -> StepResult {
        let start_line = self.line;
        let start_col = self.col();
        debug_assert_eq!(self.bytes[self.pos], b'{');
        self.pos += 1;
        let content_start = self.pos;

        // Headers must close on the same line.
        let close = match memchr::memchr2(b'}', b'\n', &self.bytes[self.pos..]) {
            Some(off) => off,
            None => {
                return StepResult::Err(ParseError::with_message(
                    ParseErrorCode::InvalidHeaderSyntax,
                    start_line as usize, start_col as usize,
                    "Unclosed section header",
                ));
            }
        };
        let close_pos = self.pos + close;
        if self.bytes[close_pos] != b'}' {
            return StepResult::Err(ParseError::with_message(
                ParseErrorCode::InvalidHeaderSyntax,
                start_line as usize, start_col as usize,
                "Unclosed section header",
            ));
        }
        let header = &self.source[content_start..close_pos];

        // Inline directive header `{name :type "value"}` / `{name :if "expr"}`.
        // Emits a synthetic assignment `<name>._<directive> = "value"`.
        if let Some((name_part, key, dir_value)) = parse_inline_header_directive(header) {
            self.pos = close_pos + 1;
            let resolved = if let Some(rel) = name_part.strip_prefix('.') {
                match (&self.previous_header, rel.is_empty()) {
                    (Some(base), false) => format!("{base}.{rel}"),
                    (Some(base), true) => base.clone(),
                    (None, false) => rel.to_string(),
                    (None, true) => String::new(),
                }
            } else if let Some(rest) = name_part.strip_prefix('@') {
                rest.to_string()
            } else {
                name_part.to_string()
            };
            self.in_metadata = false;
            self.current_header = Some(resolved.clone());
            if !name_part.starts_with('.') {
                self.previous_header = Some(resolved.clone());
            }
            let full_path = if resolved.is_empty() { key } else { format!("{resolved}.{key}") };
            let synthetic = OdinValues::string(dir_value);
            if self.options.allow_duplicates {
                self.assignments.insert(full_path, synthetic);
            } else {
                match self.assignments.entry(full_path) {
                    indexmap::map::Entry::Occupied(o) => {
                        return StepResult::Err(ParseError::with_message(
                            ParseErrorCode::DuplicatePathAssignment,
                            start_line as usize, start_col as usize,
                            o.key(),
                        ));
                    }
                    indexmap::map::Entry::Vacant(e) => { e.insert(synthetic); }
                }
            }
            return self.skip_to_line_end(start_line, start_col);
        }

        // Tabular header `{name[] : col1, col2, ...}` (absolute or relative).
        if let Some(colon_pos) = header.find(" : ") {
            let name_part = &header[..colon_pos];
            let cols_str = &header[colon_pos + 3..];
            // Must end with `[]` after the name.
            let Some(name) = name_part.strip_suffix("[]") else { return StepResult::Bail; };
            if name.is_empty() { return StepResult::Bail; }

            // Resolve relative base `.name` against previous absolute header.
            let resolved_base = if let Some(rest) = name.strip_prefix('.') {
                if rest.is_empty() { return StepResult::Bail; }
                match &self.previous_header {
                    Some(base) => format!("{base}.{rest}"),
                    None => rest.to_string(),
                }
            } else {
                name.to_string()
            };

            // Primitive array mode: `{name[] : ~}` — single sentinel column `~`.
            let is_primitive = cols_str.trim() == "~";
            let columns = if is_primitive {
                Vec::new()
            } else {
                // Columns may include `name[N]` (uniform sub-array) and
                // `.name` (relative — inherits parent from prior dotted col).
                let mut cols = Vec::with_capacity(8);
                let mut last_parent = String::new();
                for raw in cols_str.split(',') {
                    if cols.len() >= MAX_TABULAR_COLUMNS {
                        return StepResult::Err(ParseError::with_message(
                            ParseErrorCode::InvalidHeaderSyntax,
                            start_line as usize, start_col as usize,
                            &format!("Tabular column count exceeds maximum {MAX_TABULAR_COLUMNS}"),
                        ));
                    }
                    let trimmed = raw.trim();
                    if trimmed.is_empty() {
                        return StepResult::Err(ParseError::with_message(
                            ParseErrorCode::InvalidHeaderSyntax,
                            start_line as usize, start_col as usize,
                            "Empty column name in tabular header",
                        ));
                    }
                    if let Some(rest) = trimmed.strip_prefix('.') {
                        if last_parent.is_empty() {
                            cols.push(rest.to_string());
                        } else {
                            cols.push(format!("{last_parent}.{rest}"));
                        }
                    } else {
                        if let Some(dot) = trimmed.find('.') {
                            last_parent = trimmed[..dot].to_string();
                        } else {
                            last_parent.clear();
                        }
                        cols.push(trimmed.to_string());
                    }
                }
                if cols.is_empty() {
                    return StepResult::Err(ParseError::with_message(
                        ParseErrorCode::InvalidHeaderSyntax,
                        start_line as usize, start_col as usize,
                        "Tabular header must have at least one column",
                    ));
                }
                cols
            };

            self.pos = close_pos + 1;
            // For relative bases, don't overwrite previous_header — sub-blocks
            // like `{.tags[] : ~}` should scope to their parent record.
            if !name.starts_with('.') {
                self.previous_header = Some(resolved_base.clone());
            }
            self.current_header = None;
            self.in_metadata = false;
            self.tabular = Some(TabularContext {
                base_name: resolved_base,
                columns,
                row_index: 0,
                key_buf: String::with_capacity(64),
                is_primitive,
                is_metadata_table: false,
            });
            return self.skip_to_line_end(start_line, start_col);
        }

        // Lookup-table header `{$table.NAME[col1, col2, ...]}` — cells are
        // fully typed ODIN values, written to metadata.
        if header.starts_with("$table.") || header.starts_with("$.table.") {
            let table_part = header
                .strip_prefix("$.")
                .or_else(|| header.strip_prefix('$'))
                .unwrap_or(header);
            let Some(rest) = table_part.strip_prefix("table.") else { return StepResult::Bail; };
            let Some(bracket_pos) = rest.find('[') else { return StepResult::Bail; };
            let Some(close_idx) = rest.find(']') else { return StepResult::Bail; };
            if close_idx <= bracket_pos { return StepResult::Bail; }
            let table_name = &rest[..bracket_pos];
            if table_name.is_empty() { return StepResult::Bail; }
            let cols_str = &rest[bracket_pos + 1..close_idx];
            let mut columns = Vec::with_capacity(8);
            for raw in cols_str.split(',') {
                if columns.len() >= MAX_TABULAR_COLUMNS {
                    return StepResult::Err(ParseError::with_message(
                        ParseErrorCode::InvalidHeaderSyntax,
                        start_line as usize, start_col as usize,
                        &format!("Tabular column count exceeds maximum {MAX_TABULAR_COLUMNS}"),
                    ));
                }
                let trimmed = raw.trim();
                if trimmed.is_empty() { return StepResult::Bail; }
                columns.push(trimmed.to_string());
            }
            if columns.is_empty() { return StepResult::Bail; }

            self.pos = close_pos + 1;
            self.in_metadata = true;
            self.current_header = None;
            self.tabular = Some(TabularContext {
                base_name: format!("table.{table_name}"),
                columns,
                row_index: 0,
                key_buf: String::with_capacity(64),
                is_primitive: false,
                is_metadata_table: true,
            });
            return self.skip_to_line_end(start_line, start_col);
        }
        // Brackets must balance and indices must be parseable. `{records[0]}`
        // is fine; `{invalid[}` is not.
        let mut bracket_depth: i32 = 0;
        let mut in_index = false;
        let mut idx_start = 0usize;
        for (i, b) in header.as_bytes().iter().enumerate() {
            match b {
                b'[' => {
                    if in_index { return StepResult::Bail; }
                    bracket_depth += 1;
                    in_index = true;
                    idx_start = i + 1;
                }
                b']' => {
                    if !in_index { return StepResult::Bail; }
                    bracket_depth -= 1;
                    if bracket_depth < 0 { return StepResult::Bail; }
                    let idx = &header[idx_start..i];
                    if !idx.is_empty() && idx.parse::<i64>().is_err() {
                        return StepResult::Err(ParseError::with_message(
                            ParseErrorCode::InvalidArrayIndex,
                            start_line as usize, start_col as usize,
                            &format!("Invalid array index: {idx}"),
                        ));
                    }
                    in_index = false;
                }
                _ => {}
            }
        }
        if bracket_depth != 0 {
            return StepResult::Err(ParseError::with_message(
                ParseErrorCode::InvalidArrayIndex,
                start_line as usize, start_col as usize,
                "Invalid array index",
            ));
        }

        self.pos = close_pos + 1;

        if header == "$" {
            self.in_metadata = true;
            self.current_header = None;
        } else if let Some(rest) = header.strip_prefix('$') {
            // Named metadata `{$const}`, `{$accumulator}`, ...
            self.in_metadata = true;
            self.current_header = Some(rest.to_string());
        } else if let Some(rest) = header.strip_prefix('@') {
            // `{@TypeRef}` — body fields land at `TypeRef.field`.
            self.in_metadata = false;
            self.current_header = Some(rest.to_string());
        } else if let Some(rel) = header.strip_prefix('.') {
            // `{.relative}` — resolve against previous absolute header.
            self.in_metadata = false;
            let resolved = match (&self.previous_header, rel.is_empty()) {
                (Some(base), false) => format!("{base}.{rel}"),
                (Some(base), true) => base.clone(),
                (None, false) => rel.to_string(),
                (None, true) => String::new(),
            };
            self.current_header = Some(resolved);
        } else if header.is_empty() {
            // `{}` resets section.
            self.in_metadata = false;
            self.current_header = None;
            self.previous_header = None;
        } else {
            self.in_metadata = false;
            self.current_header = Some(header.to_string());
            self.previous_header = Some(header.to_string());
        }

        // Trailing whitespace/newline after `}`.
        self.skip_to_line_end(start_line, start_col)
    }

    fn skip_to_line_end(&mut self, _line: u32, _col: u32) -> StepResult {
        while self.pos < self.bytes.len() {
            match self.bytes[self.pos] {
                b' ' | b'\t' | b'\r' => self.pos += 1,
                b';' => self.skip_comment(),
                b'\n' => return StepResult::Ok,
                _ => return StepResult::Bail,
            }
        }
        StepResult::Ok
    }

    /// True when the current `:` begins a bare segment-directive line
    /// (`:loop`, `:counter`, `:from`, `:if`, `:elif`, `:else`, `:literal`).
    fn is_bare_directive_line(&self) -> bool {
        let mut i = self.pos + 1;
        while i < self.bytes.len() && (self.bytes[i] == b' ' || self.bytes[i] == b'\t') {
            i += 1;
        }
        let name_start = i;
        while i < self.bytes.len() && self.bytes[i].is_ascii_alphabetic() {
            i += 1;
        }
        matches!(
            &self.source[name_start..i],
            "loop" | "counter" | "from" | "if" | "elif" | "else" | "literal"
        )
    }

    /// Parse a bare `:name rest-of-line` directive into a synthetic
    /// `<header>._name = "rest"` assignment (mirrors the `_name = "..."` form).
    fn parse_bare_directive(&mut self) -> StepResult {
        self.pos += 1; // consume ':'
        while self.pos < self.bytes.len() && (self.bytes[self.pos] == b' ' || self.bytes[self.pos] == b'\t') {
            self.pos += 1;
        }
        let name_start = self.pos;
        while self.pos < self.bytes.len() && self.bytes[self.pos].is_ascii_alphabetic() {
            self.pos += 1;
        }
        let name = self.source[name_start..self.pos].to_string();
        while self.pos < self.bytes.len() && (self.bytes[self.pos] == b' ' || self.bytes[self.pos] == b'\t') {
            self.pos += 1;
        }
        // Capture the value up to end-of-line / comment.
        let val_start = self.pos;
        while self.pos < self.bytes.len() && !matches!(self.bytes[self.pos], b'\n' | b'\r' | b';') {
            self.pos += 1;
        }
        let value = self.source[val_start..self.pos].trim();
        let value = if value.is_empty() { "true".to_string() } else { value.to_string() };

        let prefix = match &self.current_header {
            Some(h) if !h.is_empty() => format!("{h}."),
            _ => String::new(),
        };
        // Repeated `:loop` lines each take a distinct key (`_loop`, `_loop2`, …)
        // so all survive into the transform layer as a nested cross-product.
        let full_path = if name == "loop" {
            let mut n = 1;
            loop {
                let key = if n == 1 { format!("{prefix}_loop") } else { format!("{prefix}_loop{n}") };
                if !self.assignments.contains_key(&key) {
                    break key;
                }
                n += 1;
            }
        } else {
            format!("{prefix}_{name}")
        };

        // Trailing whitespace / comment / newline.
        while self.pos < self.bytes.len() {
            match self.bytes[self.pos] {
                b' ' | b'\t' | b'\r' => self.pos += 1,
                b';' => self.skip_comment(),
                b'\n' => break,
                _ => return StepResult::Bail,
            }
        }

        let synthetic = OdinValues::string(value);
        if self.options.allow_duplicates {
            self.assignments.insert(full_path, synthetic);
        } else {
            match self.assignments.entry(full_path) {
                indexmap::map::Entry::Occupied(_) => {} // first occurrence wins
                indexmap::map::Entry::Vacant(e) => { e.insert(synthetic); }
            }
        }
        StepResult::Ok
    }

    /// Capture a bare `"""..."""` line as a synthetic `<header>._literalBody`
    /// assignment so the transform layer can pair it with a `:literal` directive.
    fn parse_bare_literal_block(&mut self) -> StepResult {
        let line = self.line as usize;
        let col = self.col() as usize;
        let value = match self.parse_multiline_string() {
            ValueStep::Ok(v) => v,
            ValueStep::Bail => return StepResult::Bail,
            ValueStep::Err(e) => return StepResult::Err(e),
        };
        // Trailing whitespace / comment / newline.
        while self.pos < self.bytes.len() {
            match self.bytes[self.pos] {
                b' ' | b'\t' | b'\r' => self.pos += 1,
                b';' => self.skip_comment(),
                b'\n' => break,
                _ => return StepResult::Bail,
            }
        }
        let full_path = match &self.current_header {
            Some(h) if !h.is_empty() => format!("{h}._literalBody"),
            _ => "_literalBody".to_string(),
        };
        let _ = (line, col);
        if self.options.allow_duplicates {
            self.assignments.insert(full_path, value);
        } else {
            match self.assignments.entry(full_path) {
                indexmap::map::Entry::Occupied(_) => {} // first occurrence wins
                indexmap::map::Entry::Vacant(e) => { e.insert(value); }
            }
        }
        StepResult::Ok
    }

    fn parse_assignment(&mut self) -> StepResult {
        let path_line = self.line as usize;
        let path_col = self.col() as usize;

        // Read the path: alphanumeric, `_`, `.`, `[`, `]`, leading `$` or `&`.
        let path_start = self.pos;
        let mut saw_bracket = false;
        let first = self.bytes[self.pos];
        if !is_path_start(first) {
            return StepResult::Bail;
        }
        while self.pos < self.bytes.len() {
            let b = self.bytes[self.pos];
            if is_path_byte(b) {
                if b == b'[' { saw_bracket = true; }
                self.pos += 1;
            } else {
                break;
            }
        }
        let path_end = self.pos;
        let raw_path = &self.source[path_start..path_end];

        // Build full_path into the reusable buffer.
        self.path_buf.clear();
        if let Some(ref h) = self.current_header {
            self.path_buf.push_str(h);
            self.path_buf.push('.');
        }
        self.path_buf.push_str(raw_path);

        // Normalize leading-zero indices `[007]` → `[7]` only when needed.
        if saw_bracket && needs_index_norm(self.path_buf.as_bytes()) {
            self.norm_buf.clear();
            normalize_indices(&self.path_buf, &mut self.norm_buf);
            std::mem::swap(&mut self.path_buf, &mut self.norm_buf);
        }

        // Whitespace then `=`.
        while self.pos < self.bytes.len() && (self.bytes[self.pos] == b' ' || self.bytes[self.pos] == b'\t') {
            self.pos += 1;
        }
        if self.pos >= self.bytes.len() || self.bytes[self.pos] != b'=' {
            return StepResult::Err(ParseError::with_message(
                ParseErrorCode::UnexpectedCharacter,
                path_line, path_col,
                &format!("Expected '=' after '{}'", self.path_buf),
            ));
        }
        self.pos += 1;
        while self.pos < self.bytes.len() && (self.bytes[self.pos] == b' ' || self.bytes[self.pos] == b'\t') {
            self.pos += 1;
        }

        // Validate path (depth + bracket ranges + first-bracket capture).
        match self.validate_path(path_line, path_col) {
            StepResult::Ok => {}
            other => return other,
        }

        // Collect modifiers (`!`, `*`, `-`) before the value.
        let mut mods = OdinModifiers::default();
        loop {
            match self.bytes.get(self.pos).copied() {
                Some(b'!') => { mods.required = true; self.pos += 1; }
                Some(b'*') => { mods.confidential = true; self.pos += 1; }
                Some(b'-') => {
                    if self.bytes.get(self.pos + 1).copied() == Some(b'-')
                        && self.bytes.get(self.pos + 2).copied() == Some(b'-')
                    {
                        return StepResult::Bail;
                    }
                    mods.deprecated = true;
                    self.pos += 1;
                }
                _ => break,
            }
        }

        // Empty value = empty string; modifiers/directives are dropped.
        let value = if self.pos >= self.bytes.len()
            || self.bytes[self.pos] == b'\n'
            || self.bytes[self.pos] == b'\r'
            || self.bytes[self.pos] == b';'
        {
            OdinValues::string("")
        } else {
            let mut v = match self.parse_value(path_line, path_col) {
                ValueStep::Ok(v) => v,
                ValueStep::Bail => return StepResult::Bail,
                ValueStep::Err(e) => return StepResult::Err(e),
            };
            if mods.has_any() {
                v = v.with_modifiers(mods.clone());
                self.modifiers_map.insert(self.path_buf.clone(), mods);
            }
            // Trailing directives: `:type integer`, `:format ssn`, ...
            let directives = match self.parse_trailing_directives() {
                Ok(d) => d,
                Err(e) => return StepResult::Err(e),
            };
            if !directives.is_empty() {
                v = v.with_directives(directives);
            }
            v
        };

        // Trailing whitespace, comment, or newline. Anything else bails.
        while self.pos < self.bytes.len() {
            match self.bytes[self.pos] {
                b' ' | b'\t' | b'\r' => self.pos += 1,
                b';' => self.skip_comment(),
                b'\n' => break,
                _ => return StepResult::Bail,
            }
        }
        // Trailing `\n` is left for skip_blanks_and_comments to count.

        // Bare-key when path starts with `$.` outside metadata mode.
        // Clone instead of mem::take to preserve path_buf capacity.
        let (key, target_is_metadata) = if self.in_metadata {
            (self.path_buf.clone(), true)
        } else if self.path_buf.starts_with("$.") {
            (self.path_buf[2..].to_string(), true)
        } else {
            (self.path_buf.clone(), false)
        };
        let target = if target_is_metadata { &mut self.metadata } else { &mut self.assignments };
        if self.options.allow_duplicates {
            target.insert(key, value);
        } else {
            match target.entry(key) {
                indexmap::map::Entry::Occupied(o) => {
                    return StepResult::Err(ParseError::with_message(
                        ParseErrorCode::DuplicatePathAssignment,
                        path_line, path_col,
                        o.key(),
                    ));
                }
                indexmap::map::Entry::Vacant(e) => { e.insert(value); }
            }
        }
        StepResult::Ok
    }

    /// Parse a top-level `@import` / `@schema` / `@if` directive.
    fn parse_top_directive(&mut self) -> StepResult {
        let line = self.line as usize;
        let col = self.col() as usize;
        debug_assert_eq!(self.bytes[self.pos], b'@');
        self.pos += 1;
        let kw_start = self.pos;
        while self.pos < self.bytes.len() {
            let b = self.bytes[self.pos];
            if matches!(b, b'a'..=b'z' | b'A'..=b'Z') {
                self.pos += 1;
            } else { break; }
        }
        let kw_len = self.pos - kw_start;
        if kw_len == 0 { return StepResult::Bail; }
        let kw = &self.source[kw_start..self.pos];
        let kw_owned = kw.to_string();

        // Whitespace then rest of line.
        while self.pos < self.bytes.len() && (self.bytes[self.pos] == b' ' || self.bytes[self.pos] == b'\t') {
            self.pos += 1;
        }
        let rest_start = self.pos;
        while self.pos < self.bytes.len() && !matches!(self.bytes[self.pos], b'\n' | b'\r') {
            self.pos += 1;
        }
        let raw_rest = self.source[rest_start..self.pos].trim();
        let rest = match find_comment_start_quote_aware(raw_rest) {
            Some(idx) => raw_rest[..idx].trim(),
            None => raw_rest,
        };

        match kw_owned.as_str() {
            "import" => {
                if rest.is_empty() {
                    return StepResult::Err(ParseError::with_message(
                        ParseErrorCode::InvalidDirective, line, col,
                        "Invalid import directive syntax",
                    ));
                }
                if rest.ends_with(" as") {
                    return StepResult::Err(ParseError::with_message(
                        ParseErrorCode::InvalidDirective, line, col,
                        "Import alias requires identifier",
                    ));
                }
                if let Some(as_pos) = rest.find(" as ") {
                    let path = rest[..as_pos].trim().to_string();
                    let alias = rest[as_pos + 4..].trim();
                    if alias.is_empty() {
                        return StepResult::Err(ParseError::with_message(
                            ParseErrorCode::InvalidDirective, line, col,
                            "Import alias requires identifier",
                        ));
                    }
                    self.imports.push(OdinImport { path, alias: Some(alias.to_string()), line });
                } else {
                    self.imports.push(OdinImport { path: rest.to_string(), alias: None, line });
                }
            }
            "schema" => {
                if rest.is_empty() {
                    return StepResult::Err(ParseError::with_message(
                        ParseErrorCode::InvalidDirective, line, col,
                        "Schema directive requires URL",
                    ));
                }
                self.schemas.push(OdinSchema { url: rest.to_string(), line });
            }
            "if" => {
                if rest.is_empty() {
                    return StepResult::Err(ParseError::with_message(
                        ParseErrorCode::InvalidDirective, line, col,
                        "Conditional directive requires expression",
                    ));
                }
                // Validate-only: the parsed expression is dropped.
            }
            _ => return StepResult::Bail,
        }
        StepResult::Ok
    }

    /// Parse trailing `:name [value]` directives until newline/comment.
    fn parse_trailing_directives(&mut self) -> Result<Vec<OdinDirective>, ParseError> {
        let mut directives = Vec::new();
        loop {
            while self.pos < self.bytes.len() && (self.bytes[self.pos] == b' ' || self.bytes[self.pos] == b'\t') {
                self.pos += 1;
            }
            if self.pos >= self.bytes.len() { break; }
            if !matches!(self.bytes[self.pos], b':') { break; }
            self.pos += 1;
            let name_start = self.pos;
            while self.pos < self.bytes.len() {
                let b = self.bytes[self.pos];
                if matches!(b, b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'_') {
                    self.pos += 1;
                } else { break; }
            }
            if self.pos == name_start {
                return Err(ParseError::with_message(
                    ParseErrorCode::UnexpectedCharacter,
                    self.line as usize, self.col() as usize,
                    "directive name missing",
                ));
            }
            let name = self.source[name_start..self.pos].to_string();
            while self.pos < self.bytes.len() && (self.bytes[self.pos] == b' ' || self.bytes[self.pos] == b'\t') {
                self.pos += 1;
            }
            let dir_value = if self.pos < self.bytes.len() {
                let next = self.bytes[self.pos];
                if matches!(next, b'\n' | b'\r' | b';' | b':') {
                    None
                } else if next == b'"' {
                    // Quoted directive value — parse the same way as a value
                    // string so escapes and quote-stripping are handled.
                    let line = self.line as usize;
                    let col = self.col() as usize;
                    match self.parse_quoted_string(line, col) {
                        ValueStep::Ok(OdinValue::String { value, .. }) => {
                            if let Ok(n) = value.parse::<f64>() {
                                Some(DirectiveValue::Number(n))
                            } else {
                                Some(DirectiveValue::String(value))
                            }
                        }
                        ValueStep::Err(e) => return Err(e),
                        _ => return Err(ParseError::with_message(
                            ParseErrorCode::UnexpectedCharacter,
                            line, col,
                            "expected quoted directive value",
                        )),
                    }
                } else {
                    let val_start = self.pos;
                    while self.pos < self.bytes.len() {
                        let b = self.bytes[self.pos];
                        if matches!(b, b' ' | b'\t' | b'\n' | b'\r' | b';' | b':') { break; }
                        self.pos += 1;
                    }
                    let v = self.source[val_start..self.pos].to_string();
                    if let Ok(n) = v.parse::<f64>() {
                        Some(DirectiveValue::Number(n))
                    } else {
                        Some(DirectiveValue::String(v))
                    }
                }
            } else {
                None
            };
            directives.push(OdinDirective { name, value: dir_value });
        }
        Ok(directives)
    }

    /// Parse one tabular row → `name[row].col = val` assignments.
    fn parse_tabular_row(&mut self) -> StepResult {
        let Some(mut ctx) = self.tabular.take() else {
            return StepResult::Ok;
        };
        let result = self.parse_tabular_row_inner(&mut ctx);
        if !matches!(result, StepResult::Bail | StepResult::Err(_)) {
            self.tabular = Some(ctx);
        }
        result
    }

    /// Handle `field = value` lines inside a tabular section, stored at
    /// `{base_name}[].{field}`. Position must be just past the `=`.
    fn parse_tabular_assignment(
        &mut self,
        ctx: &TabularContext,
        field: &str,
        line: usize,
        col: usize,
    ) -> StepResult {
        while self.pos < self.bytes.len() && (self.bytes[self.pos] == b' ' || self.bytes[self.pos] == b'\t') {
            self.pos += 1;
        }
        // Empty value at EOF or end-of-line — match parse_assignment semantics.
        let value = if self.pos >= self.bytes.len()
            || self.bytes[self.pos] == b'\n'
            || self.bytes[self.pos] == b'\r'
            || self.bytes[self.pos] == b';'
        {
            OdinValues::string("")
        } else {
            match self.parse_value(line, col) {
                ValueStep::Ok(v) => v,
                ValueStep::Bail => return StepResult::Err(bail_to_err(line, col)),
                ValueStep::Err(e) => return StepResult::Err(e),
            }
        };
        // Skip any trailing directives or whitespace until end of line.
        while self.pos < self.bytes.len() {
            match self.bytes[self.pos] {
                b'\n' => break,
                b';' => { self.skip_comment(); break; }
                _ => self.pos += 1,
            }
        }
        let key = format!("{}[].{}", ctx.base_name, field);
        if ctx.is_metadata_table {
            self.metadata.insert(key, value);
        } else {
            self.assignments.insert(key, value);
        }
        StepResult::Ok
    }

    fn parse_tabular_row_inner(&mut self, ctx: &mut TabularContext) -> StepResult {
        let line = self.line as usize;
        let col = self.col() as usize;

        // Assignment lines mixed into tabular data (`_loop = "@x"`) store at
        // `{base_name}[].{field}` and don't advance the row index.
        if let Some(eq_off) = memchr::memchr2(b'=', b'\n', &self.bytes[self.pos..]) {
            if self.bytes[self.pos + eq_off] == b'=' {
                let prefix = &self.bytes[self.pos..self.pos + eq_off];
                let mut p = 0;
                while p < prefix.len() && (prefix[p] == b' ' || prefix[p] == b'\t') { p += 1; }
                let id_start = p;
                while p < prefix.len() && is_path_byte(prefix[p]) { p += 1; }
                if p > id_start {
                    let mut q = p;
                    while q < prefix.len() && (prefix[q] == b' ' || prefix[q] == b'\t') { q += 1; }
                    if q == prefix.len() {
                        let field_start = self.pos + id_start;
                        let field_end = self.pos + p;
                        let field = self.source[field_start..field_end].to_string();
                        self.pos += eq_off + 1;
                        return self.parse_tabular_assignment(ctx, &field, line, col);
                    }
                }
            }
        }

        // Build prefix `base[row]` (primitive) or `base[row].` (record).
        ctx.key_buf.clear();
        ctx.key_buf.push_str(&ctx.base_name);
        ctx.key_buf.push('[');
        use std::fmt::Write as _;
        let _ = write!(ctx.key_buf, "{}", ctx.row_index);
        ctx.key_buf.push(']');
        if !ctx.is_primitive {
            ctx.key_buf.push('.');
        }
        let prefix_len = ctx.key_buf.len();

        let mut col_idx: usize = 0;
        let mut had_value = false;

        loop {
            // Whitespace before value
            while self.pos < self.bytes.len() && (self.bytes[self.pos] == b' ' || self.bytes[self.pos] == b'\t') {
                self.pos += 1;
            }
            if self.pos >= self.bytes.len() { break; }
            match self.bytes[self.pos] {
                b'\n' | b'\r' | b';' => break,
                // Empty cell (`,,` or `, ,`) — column stays unassigned.
                b',' if !ctx.is_primitive => {
                    self.pos += 1;
                    col_idx += 1;
                    had_value = true;
                    continue;
                }
                _ => {}
            }

            let value = match self.parse_value(line, col) {
                ValueStep::Ok(v) => v,
                ValueStep::Bail => return StepResult::Bail,
                ValueStep::Err(e) => return StepResult::Err(e),
            };
            had_value = true;

            if ctx.is_primitive {
                if col_idx == 0 {
                    self.assignments.insert(ctx.key_buf.clone(), value);
                }
            } else if col_idx < ctx.columns.len() {
                ctx.key_buf.truncate(prefix_len);
                ctx.key_buf.push_str(&ctx.columns[col_idx]);
                if ctx.is_metadata_table {
                    self.metadata.insert(ctx.key_buf.clone(), value);
                } else {
                    self.assignments.insert(ctx.key_buf.clone(), value);
                }
            }
            col_idx += 1;

            // Optional whitespace, then `,` or end of row
            while self.pos < self.bytes.len() && (self.bytes[self.pos] == b' ' || self.bytes[self.pos] == b'\t') {
                self.pos += 1;
            }
            if self.pos >= self.bytes.len() { break; }
            match self.bytes[self.pos] {
                b',' => {
                    if ctx.is_primitive { return StepResult::Bail; }
                    self.pos += 1;
                }
                b'\n' | b'\r' | b';' => break,
                _ => return StepResult::Bail,
            }
        }

        // Trailing comment / newline.
        while self.pos < self.bytes.len() {
            match self.bytes[self.pos] {
                b' ' | b'\t' | b'\r' => self.pos += 1,
                b';' => self.skip_comment(),
                b'\n' => break,
                _ => return StepResult::Bail,
            }
        }

        if had_value {
            ctx.row_index += 1;
            if ctx.row_index > MAX_TABULAR_ROWS {
                return StepResult::Err(ParseError::with_message(
                    ParseErrorCode::ArrayIndexOutOfRange,
                    line, col,
                    &format!("Tabular row count exceeds maximum {MAX_TABULAR_ROWS}"),
                ));
            }
        }
        StepResult::Ok
    }

    fn validate_path(&mut self, line: usize, col: usize) -> StepResult {
        let bytes = self.path_buf.as_bytes();
        let mut depth: usize = 1;
        // Locate the first `[` or `]` in one scan; absent → no-bracket fast path.
        match memchr::memchr2(b'[', b']', bytes) {
            None => {
                depth += memchr::memchr_iter(b'.', bytes).count();
                if depth > self.options.max_depth {
                    return StepResult::Err(ParseError::with_message(
                        ParseErrorCode::MaximumDepthExceeded,
                        line, col,
                        &format!("Maximum nesting depth exceeded: {depth} > {}", self.options.max_depth),
                    ));
                }
                return StepResult::Ok;
            }
            Some(pos) if bytes[pos] == b']' => {
                return StepResult::Err(ParseError::with_message(
                    ParseErrorCode::InvalidArrayIndex,
                    line, col,
                    "Stray ']' in path",
                ));
            }
            Some(_) => {}
        }
        let mut cumulative: i64 = 0;
        let mut first_bracket: Option<(usize, usize, usize)> = None;
        let mut i = 0;
        while i < bytes.len() {
            let b = bytes[i];
            if b == b'.' { depth += 1; i += 1; }
            else if b == b']' {
                return StepResult::Err(ParseError::with_message(
                    ParseErrorCode::InvalidArrayIndex,
                    line, col,
                    "Stray ']' in path",
                ));
            }
            else if b == b'[' {
                depth += 1;
                let bracket_start = i;
                let idx_start = i + 1;
                let close = bytes[idx_start..].iter().position(|&c| c == b']');
                match close {
                    None => return StepResult::Err(ParseError::with_message(
                        ParseErrorCode::InvalidArrayIndex,
                        line, col,
                        "Unclosed '[' in path",
                    )),
                    Some(off) => {
                        let idx_end = idx_start + off;
                        let idx_slice = &self.path_buf[idx_start..idx_end];
                        // Negative indices like `[-1]` — error early to match
                        // the regular parser's contract.
                        if idx_slice.starts_with('-') {
                            return StepResult::Err(ParseError::with_message(
                                ParseErrorCode::InvalidArrayIndex,
                                line, col,
                                &format!("Negative array index: {idx_slice}"),
                            ));
                        }
                        if !idx_slice.is_empty() && idx_slice.bytes().all(|c| c.is_ascii_digit()) {
                            match idx_slice.parse::<i64>() {
                                Ok(idx) => {
                                    if idx > MAX_ARRAY_INDEX {
                                        return StepResult::Err(ParseError::with_message(
                                            ParseErrorCode::ArrayIndexOutOfRange,
                                            line, col,
                                            &format!("Array index {idx} exceeds maximum allowed value of {MAX_ARRAY_INDEX}"),
                                        ));
                                    }
                                    cumulative += idx;
                                    if cumulative > MAX_ARRAY_INDEX {
                                        return StepResult::Err(ParseError::with_message(
                                            ParseErrorCode::ArrayIndexOutOfRange,
                                            line, col,
                                            &format!("Cumulative array indices exceed maximum allowed value of {MAX_ARRAY_INDEX}"),
                                        ));
                                    }
                                }
                                Err(_) => {
                                    return StepResult::Err(ParseError::with_message(
                                        ParseErrorCode::ArrayIndexOutOfRange,
                                        line, col,
                                        &format!("Array index {idx_slice} exceeds maximum allowed value of {MAX_ARRAY_INDEX}"),
                                    ));
                                }
                            }
                        }
                        if first_bracket.is_none() {
                            first_bracket = Some((bracket_start, idx_start, idx_end));
                        }
                        i = idx_end + 1;
                    }
                }
            } else { i += 1; }
        }
        if depth > self.options.max_depth {
            return StepResult::Err(ParseError::with_message(
                ParseErrorCode::MaximumDepthExceeded,
                line, col,
                &format!("Maximum nesting depth exceeded: {depth} > {}", self.options.max_depth),
            ));
        }
        if let Some((base_end, idx_start, idx_end)) = first_bracket {
            let idx_slice = &self.path_buf[idx_start..idx_end];
            if !idx_slice.is_empty() {
                if let Ok(idx) = idx_slice.parse::<usize>() {
                    let array_base = &self.path_buf[..base_end];
                    if let Some(expected) = self.array_indices.get_mut(array_base) {
                        if idx == *expected { *expected += 1; }
                        else if idx > *expected {
                            return StepResult::Err(ParseError::with_message(
                                ParseErrorCode::NonContiguousArrayIndices,
                                line, col,
                                &format!("Non-contiguous array indices: expected {}, got {idx}", *expected),
                            ));
                        }
                    } else if idx == 0 {
                        self.array_indices.insert(array_base.to_string(), 1);
                    } else {
                        return StepResult::Err(ParseError::with_message(
                            ParseErrorCode::NonContiguousArrayIndices,
                            line, col,
                            &format!("Non-contiguous array indices: expected 0, got {idx}"),
                        ));
                    }
                }
            }
        }
        StepResult::Ok
    }

    fn parse_value(&mut self, line: usize, col: usize) -> ValueStep {
        let b = self.bytes[self.pos];
        match b {
            b'"' => self.parse_quoted_string(line, col),
            b'#' => self.parse_hash_value(line, col),
            b'?' => self.parse_boolean_prefix(line, col),
            b'~' => { self.pos += 1; ValueStep::Ok(OdinValues::null()) }
            b'@' => self.parse_reference(line, col),
            b'%' => self.parse_verb(line, col),
            b'^' => self.parse_binary_value(line, col),
            b't' if self.peek_eq(b"true") && !self.is_value_continuation(4) => {
                self.pos += 4;
                ValueStep::Ok(OdinValues::boolean(true))
            }
            b'f' if self.peek_eq(b"false") && !self.is_value_continuation(5) => {
                self.pos += 5;
                ValueStep::Ok(OdinValues::boolean(false))
            }
            b'T' => self.parse_time(line, col),
            b'P' => self.parse_duration_or_bail(),
            b'0'..=b'9' => self.parse_date_like(line, col),
            // Bare unquoted string in value position (`name = John`).
            b'a'..=b'z' | b'A'..=b'Z' | b'_' => ValueStep::Err(ParseError::with_message(
                ParseErrorCode::BareStringNotAllowed,
                line, col,
                "Strings must be quoted",
            )),
            _ => ValueStep::Err(ParseError::with_message(
                ParseErrorCode::UnexpectedCharacter,
                line, col,
                &format!("Unexpected character '{}'", b as char),
            )),
        }
    }

    fn is_value_continuation(&self, offset: usize) -> bool {
        let p = self.pos + offset;
        p < self.bytes.len() && {
            let b = self.bytes[p];
            !(b == b'\n' || b == b'\r' || b == b' ' || b == b'\t' || b == b';' || b == b',')
        }
    }

    fn parse_quoted_string(&mut self, line: usize, col: usize) -> ValueStep {
        debug_assert_eq!(self.bytes[self.pos], b'"');
        // Triple-quoted multiline string: content captured verbatim across newlines.
        if self.bytes.get(self.pos + 1) == Some(&b'"') && self.bytes.get(self.pos + 2) == Some(&b'"') {
            return self.parse_multiline_string();
        }
        let start_line = self.line;
        let start_col = self.col();
        self.pos += 1;
        let content_start = self.pos;

        match memchr::memchr3(b'"', b'\\', b'\n', &self.bytes[self.pos..]) {
            Some(off) => {
                let p = content_start + off;
                // memchr3 returns a position whose byte is `"`, `\\`, or `\n`.
                match self.bytes[p] {
                    b'"' => {
                        let s = &self.source[content_start..p];
                        self.pos = p + 1;
                        ValueStep::Ok(OdinValues::string(s))
                    }
                    b'\n' => ValueStep::Err(ParseError::new(
                        ParseErrorCode::UnterminatedString,
                        start_line as usize, start_col as usize,
                    )),
                    // Otherwise the byte is `\\` — escape sequence path.
                    _ => {
                        self.pos = p;
                        self.parse_quoted_string_escaped(content_start, line, col, start_line, start_col)
                    }
                }
            }
            None => ValueStep::Err(ParseError::new(
                ParseErrorCode::UnterminatedString,
                start_line as usize, start_col as usize,
            )),
        }
    }

    /// Scan a triple-quoted multiline string. Content is captured verbatim
    /// (no escape decoding) and may span newlines; closes at the next `"""`.
    fn parse_multiline_string(&mut self, ) -> ValueStep {
        let start_line = self.line;
        let start_col = self.col();
        self.pos += 3; // consume opening """
        let content_start = self.pos;
        while self.pos < self.bytes.len() {
            if self.bytes[self.pos] == b'"'
                && self.bytes.get(self.pos + 1) == Some(&b'"')
                && self.bytes.get(self.pos + 2) == Some(&b'"')
            {
                let s = &self.source[content_start..self.pos];
                self.pos += 3; // consume closing """
                return ValueStep::Ok(OdinValues::string(s));
            }
            if self.bytes[self.pos] == b'\n' {
                self.line += 1;
                self.line_start = self.pos + 1;
            }
            self.pos += 1;
        }
        ValueStep::Err(ParseError::new(
            ParseErrorCode::UnterminatedString,
            start_line as usize, start_col as usize,
        ))
    }

    /// Walk to the closing quote, validating each escape, then unescape.
    fn parse_quoted_string_escaped(
        &mut self,
        content_start: usize,
        _line: usize,
        _col: usize,
        start_line: u32,
        start_col: u32,
    ) -> ValueStep {
        while self.pos < self.bytes.len() {
            let ch = self.bytes[self.pos];
            if ch == b'"' {
                let raw = &self.source[content_start..self.pos];
                self.pos += 1;
                return ValueStep::Ok(OdinValues::string(super::parse_values::unescape_string(raw)));
            }
            if ch == b'\n' {
                return ValueStep::Err(ParseError::new(
                    ParseErrorCode::UnterminatedString,
                    start_line as usize, start_col as usize,
                ));
            }
            if ch == b'\\' {
                self.pos += 1;
                if self.pos >= self.bytes.len() {
                    return ValueStep::Err(ParseError::new(
                        ParseErrorCode::UnterminatedString,
                        start_line as usize, start_col as usize,
                    ));
                }
                let esc = self.bytes[self.pos];
                self.pos += 1;
                match esc {
                    b'n' | b'r' | b't' | b'\\' | b'"' | b'/' | b'0' | b'$' => {}
                    b'u' => {
                        if let Err(e) = self.consume_unicode_hex(4, start_line, start_col) {
                            return ValueStep::Err(e);
                        }
                        // Optional surrogate continuation: \uXXXX (low surrogate)
                        if self.pos + 1 < self.bytes.len()
                            && self.bytes[self.pos] == b'\\'
                            && self.bytes[self.pos + 1] == b'u'
                        {
                            self.pos += 2;
                            if let Err(e) = self.consume_unicode_hex(4, start_line, start_col) {
                                return ValueStep::Err(e);
                            }
                        }
                    }
                    b'U' => {
                        if let Err(e) = self.consume_unicode_hex(8, start_line, start_col) {
                            return ValueStep::Err(e);
                        }
                    }
                    _ => {
                        return ValueStep::Err(ParseError::with_message(
                            ParseErrorCode::InvalidEscapeSequence,
                            self.line as usize, self.col() as usize,
                            &format!("unknown escape: \\{}", esc as char),
                        ));
                    }
                }
            } else {
                self.pos += 1;
            }
        }
        ValueStep::Err(ParseError::new(
            ParseErrorCode::UnterminatedString,
            start_line as usize, start_col as usize,
        ))
    }

    fn consume_unicode_hex(&mut self, digits: usize, start_line: u32, start_col: u32) -> Result<(), ParseError> {
        let hex_start = self.pos;
        for _ in 0..digits {
            if self.pos >= self.bytes.len() {
                return Err(ParseError::with_message(
                    ParseErrorCode::InvalidEscapeSequence,
                    start_line as usize, start_col as usize,
                    "incomplete unicode escape",
                ));
            }
            self.pos += 1;
        }
        let hex = &self.source[hex_start..self.pos];
        let code = u32::from_str_radix(hex, 16).map_err(|_| {
            ParseError::with_message(
                ParseErrorCode::InvalidEscapeSequence,
                start_line as usize, start_col as usize,
                &format!("invalid hex in unicode escape: \\u{hex}"),
            )
        })?;
        if char::from_u32(code).is_none() && !(0xD800..=0xDFFF).contains(&code) {
            return Err(ParseError::with_message(
                ParseErrorCode::InvalidEscapeSequence,
                start_line as usize, start_col as usize,
                &format!("invalid unicode code point: U+{code:04X}"),
            ));
        }
        Ok(())
    }

    fn parse_hash_value(&mut self, line: usize, col: usize) -> ValueStep {
        debug_assert_eq!(self.bytes[self.pos], b'#');
        let next = self.bytes.get(self.pos + 1).copied();
        match next {
            Some(b'#') => {
                self.pos += 2;
                self.parse_typed_numeric(line, col, NumKind::Integer)
            }
            Some(b'$') => {
                self.pos += 2;
                self.parse_typed_numeric(line, col, NumKind::Currency)
            }
            Some(b'%') => {
                self.pos += 2;
                self.parse_typed_numeric(line, col, NumKind::Percent)
            }
            Some(_) => {
                self.pos += 1;
                self.parse_typed_numeric(line, col, NumKind::Number)
            }
            None => ValueStep::Err(ParseError::with_message(
                ParseErrorCode::InvalidTypePrefix,
                line, col,
                "empty number after '#'",
            )),
        }
    }

    fn parse_typed_numeric(&mut self, line: usize, col: usize, kind: NumKind) -> ValueStep {
        // A numeric type prefix on a reference (`##@.year`, `#$@.premium`) carries a
        // synthetic `:type` directive so the value is coerced when resolved.
        if self.bytes.get(self.pos).copied() == Some(b'@') {
            return self.parse_prefixed_reference(line, col, kind);
        }
        let val_start = self.pos;
        // Optional leading `-`, then digits / `.` / exponent. Letters are
        // refused so `##abc` falls through to parse_integer's empty-value error.
        if self.bytes.get(self.pos).copied() == Some(b'-') {
            self.pos += 1;
        }
        while self.pos < self.bytes.len() {
            match self.bytes[self.pos] {
                b'0'..=b'9' | b'.' | b'e' | b'E' | b'+' | b'-' => self.pos += 1,
                _ => break,
            }
        }
        // Currency may carry a trailing `:CODE` (e.g. `#$100:USD`).
        if matches!(kind, NumKind::Currency) && self.bytes.get(self.pos).copied() == Some(b':') {
            self.pos += 1;
            while self.pos < self.bytes.len() {
                match self.bytes[self.pos] {
                    b'A'..=b'Z' | b'a'..=b'z' => self.pos += 1,
                    _ => break,
                }
            }
        }
        let raw = &self.source[val_start..self.pos];
        match kind {
            NumKind::Integer => match super::parse_values::parse_integer(raw, line, col) {
                Ok(v) => ValueStep::Ok(v),
                Err(e) => ValueStep::Err(e),
            },
            NumKind::Number => match super::parse_values::parse_number(raw, line, col) {
                Ok(v) => ValueStep::Ok(v),
                Err(e) => ValueStep::Err(e),
            },
            NumKind::Currency => match super::parse_values::parse_currency(raw, line, col) {
                Ok(v) => ValueStep::Ok(v),
                Err(e) => ValueStep::Err(e),
            },
            NumKind::Percent => match super::parse_values::parse_percent(raw, line, col) {
                Ok(v) => ValueStep::Ok(v),
                Err(e) => ValueStep::Err(e),
            },
        }
    }

    fn parse_boolean_prefix(&mut self, _line: usize, _col: usize) -> ValueStep {
        debug_assert_eq!(self.bytes[self.pos], b'?');
        self.pos += 1;
        if self.peek_eq(b"true") && !self.is_value_continuation(4) {
            self.pos += 4;
            ValueStep::Ok(OdinValues::boolean(true))
        } else if self.peek_eq(b"false") && !self.is_value_continuation(5) {
            self.pos += 5;
            ValueStep::Ok(OdinValues::boolean(false))
        } else {
            ValueStep::Ok(OdinValues::boolean(true))
        }
    }

    fn parse_reference(&mut self, _line: usize, _col: usize) -> ValueStep {
        debug_assert_eq!(self.bytes[self.pos], b'@');
        self.pos += 1;
        let start = self.pos;
        while self.pos < self.bytes.len() {
            let b = self.bytes[self.pos];
            if matches!(b, b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'_' | b'.' | b'[' | b']' | b'$' | b':' | b'-' | b'@') {
                self.pos += 1;
            } else { break; }
        }
        let raw = &self.source[start..self.pos];
        let normalized = if needs_index_norm(raw.as_bytes()) {
            let mut buf = String::with_capacity(raw.len());
            normalize_indices(raw, &mut buf);
            buf
        } else {
            raw.to_string()
        };
        ValueStep::Ok(OdinValues::reference(&normalized))
    }

    /// Parse a reference following a numeric type prefix, attaching a `:type` directive.
    fn parse_prefixed_reference(&mut self, line: usize, col: usize, kind: NumKind) -> ValueStep {
        use crate::types::values::{OdinDirective, DirectiveValue};
        let step = self.parse_reference(line, col);
        let type_name = match kind {
            NumKind::Integer => "integer",
            NumKind::Number => "number",
            NumKind::Currency => "currency",
            NumKind::Percent => "percent",
        };
        if let ValueStep::Ok(value) = step {
            return ValueStep::Ok(value.with_directives(vec![OdinDirective {
                name: "type".to_string(),
                value: Some(DirectiveValue::String(type_name.to_string())),
            }]));
        }
        step
    }

    fn parse_date_like(&mut self, line: usize, col: usize) -> ValueStep {
        let start = self.pos;
        while self.pos < self.bytes.len() {
            match self.bytes[self.pos] {
                b'\n' | b'\r' | b' ' | b'\t' | b';' | b',' => break,
                _ => self.pos += 1,
            }
        }
        let raw = &self.source[start..self.pos];
        if raw.contains('T') {
            if let Err(e) = super::parse_values::validate_timestamp_value(raw, line, col) {
                return ValueStep::Err(e);
            }
            ValueStep::Ok(OdinValues::timestamp(0, raw))
        } else {
            match super::parse_values::parse_date_value(raw, line, col) {
                Ok(v) => ValueStep::Ok(v),
                Err(e) => ValueStep::Err(e),
            }
        }
    }

    fn parse_time(&mut self, line: usize, col: usize) -> ValueStep {
        let start = self.pos;
        while self.pos < self.bytes.len() {
            match self.bytes[self.pos] {
                b'0'..=b'9' | b'T' | b':' | b'.' => self.pos += 1,
                _ => break,
            }
        }
        let raw = &self.source[start..self.pos];
        if let Err(e) = super::parse_values::validate_time_value(raw, line, col) {
            return ValueStep::Err(e);
        }
        ValueStep::Ok(OdinValues::time(raw))
    }

    fn parse_verb(&mut self, _line: usize, _col: usize) -> ValueStep {
        debug_assert_eq!(self.bytes[self.pos], b'%');
        let start = self.pos;
        self.pos += 1; // skip `%`
        let is_custom = self.bytes.get(self.pos).copied() == Some(b'&');
        // Verb expression runs to newline or `;` comment terminator.
        let raw_end = match memchr::memchr2(b'\n', b';', &self.bytes[self.pos..]) {
            Some(off) => self.pos + off,
            None => self.bytes.len(),
        };
        // Trim trailing whitespace.
        let mut trimmed = raw_end;
        while trimmed > self.pos && matches!(self.bytes[trimmed - 1], b' ' | b'\t' | b'\r') {
            trimmed -= 1;
        }
        // Canonicalize: collapse runs of internal whitespace to single spaces.
        let slice = &self.source[start..trimmed];
        let mut raw_expr = String::with_capacity(slice.len());
        let mut last_was_space = false;
        for ch in slice.chars() {
            let is_space = ch == ' ' || ch == '\t';
            if is_space {
                if !last_was_space { raw_expr.push(' '); }
            } else {
                raw_expr.push(ch);
            }
            last_was_space = is_space;
        }
        self.pos = trimmed;
        ValueStep::Ok(OdinValue::Verb {
            verb: raw_expr,
            is_custom,
            args: vec![],
            modifiers: None,
            directives: vec![],
        })
    }

    fn parse_binary_value(&mut self, line: usize, col: usize) -> ValueStep {
        debug_assert_eq!(self.bytes[self.pos], b'^');
        self.pos += 1; // skip `^`
        let val_start = self.pos;
        while self.pos < self.bytes.len() {
            match self.bytes[self.pos] {
                b'\n' | b'\r' | b' ' | b'\t' | b';' | b',' => break,
                _ => self.pos += 1,
            }
        }
        let raw = &self.source[val_start..self.pos];
        match super::parse_values::parse_binary(raw, line, col) {
            Ok(v) => ValueStep::Ok(v),
            Err(e) => ValueStep::Err(e),
        }
    }

    fn parse_duration_or_bail(&mut self) -> ValueStep {
        // ISO-8601 durations start with `P` followed by a digit or `T`.
        // Anything else (`P` alone, `Pfoo`) is malformed.
        let line = self.line as usize;
        let col = self.col() as usize;
        if self.bytes.get(self.pos + 1).copied().is_none_or(|b| !(b.is_ascii_digit() || b == b'T')) {
            return ValueStep::Err(ParseError::with_message(
                ParseErrorCode::BareStringNotAllowed,
                line, col,
                "Invalid duration format",
            ));
        }
        let start = self.pos;
        while self.pos < self.bytes.len() {
            match self.bytes[self.pos] {
                b'\n' | b'\r' | b' ' | b'\t' | b';' | b',' => break,
                _ => self.pos += 1,
            }
        }
        ValueStep::Ok(OdinValues::duration(&self.source[start..self.pos]))
    }
}

#[derive(Debug)]
enum StepResult { Ok, Bail, Err(ParseError) }

#[derive(Debug)]
enum ValueStep { Ok(OdinValue), Bail, Err(ParseError) }

#[derive(Debug, Clone, Copy)]
enum NumKind { Integer, Number, Currency, Percent }

#[inline]
fn is_path_start(b: u8) -> bool {
    matches!(b, b'a'..=b'z' | b'A'..=b'Z' | b'_' | b'$' | b'&')
}

#[inline]
fn is_path_byte(b: u8) -> bool {
    matches!(b,
        b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'_' | b'.' | b'[' | b']' | b'$' | b'-' | b'&')
}

#[inline]
fn needs_index_norm(bytes: &[u8]) -> bool {
    let mut i = 0;
    while i + 2 < bytes.len() {
        if bytes[i] == b'[' && bytes[i + 1] == b'0' && bytes[i + 2].is_ascii_digit() {
            return true;
        }
        i += 1;
    }
    false
}

fn normalize_indices(path: &str, out: &mut String) {
    let bytes = path.as_bytes();
    let mut i = 0;
    while i < bytes.len() {
        if bytes[i] == b'[' {
            out.push('[');
            i += 1;
            let start = i;
            while i < bytes.len() && bytes[i].is_ascii_digit() {
                i += 1;
            }
            if i > start && i < bytes.len() && bytes[i] == b']' {
                match path[start..i].parse::<i64>() {
                    Ok(idx) => out.push_str(&idx.to_string()),
                    Err(_) => out.push_str(&path[start..i]),
                }
            } else {
                out.push_str(&path[start..i]);
            }
        } else {
            let run_end = bytes[i..].iter().position(|&b| b == b'[').map(|p| i + p).unwrap_or(bytes.len());
            out.push_str(&path[i..run_end]);
            i = run_end;
        }
    }
}
