use shuck_ast::{
    ArithmeticAssignOp, ArithmeticBinaryOp, ArithmeticExpr, ArithmeticExprNode, ArithmeticLvalue,
    ArithmeticPostfixOp, ArithmeticUnaryOp, Name, Position, SourceText, Span, Word,
};

use crate::error::{Error, Result};

use super::{Parser, ShellDialect};

#[derive(Debug, Clone)]
enum TokenKind {
    End,
    LeftParen,
    RightParen,
    LeftBracket,
    RightBracket,
    Question,
    Colon,
    Comma,
    Plus,
    Minus,
    Star,
    Slash,
    Percent,
    DoubleStar,
    ShiftLeft,
    ShiftRight,
    LessThan,
    LessThanOrEqual,
    GreaterThan,
    GreaterThanOrEqual,
    Equal,
    NotEqual,
    BitwiseAnd,
    BitwiseXor,
    BitwiseOr,
    LogicalAnd,
    LogicalOr,
    Assign,
    PlusAssign,
    MinusAssign,
    StarAssign,
    SlashAssign,
    PercentAssign,
    ShiftLeftAssign,
    ShiftRightAssign,
    AndAssign,
    XorAssign,
    OrAssign,
    Increment,
    Decrement,
    Bang,
    Tilde,
    Ident(Name),
    Number(SourceText),
    ShellWord(Word),
}

#[derive(Debug, Clone)]
struct Token {
    kind: TokenKind,
    span: Span,
}

impl Token {
    fn end(span: Span) -> Self {
        Self {
            kind: TokenKind::End,
            span,
        }
    }
}

pub(super) fn parse_expression(
    input: &str,
    base: Span,
    dialect: ShellDialect,
    max_depth: usize,
    max_fuel: usize,
) -> Result<ArithmeticExprNode> {
    let mut parser = ArithmeticParser::new(input, base, dialect, max_depth, max_fuel);
    let expr = parser.parse_expression()?;
    if !matches!(parser.peek_token()?.kind, TokenKind::End) {
        let trailing_start = parser.peek_token()?.span.start;
        return Err(parser.error_at(trailing_start, "unexpected token in arithmetic expression"));
    }
    Ok(expr)
}

struct ArithmeticParser<'a> {
    input: &'a str,
    base: Span,
    dialect: ShellDialect,
    index: usize,
    peeked: Option<Token>,
    max_depth: usize,
    max_fuel: usize,
    fuel: usize,
}

impl<'a> ArithmeticParser<'a> {
    fn new(
        input: &'a str,
        base: Span,
        dialect: ShellDialect,
        max_depth: usize,
        max_fuel: usize,
    ) -> Self {
        Self {
            input,
            base,
            dialect,
            index: 0,
            peeked: None,
            max_depth,
            max_fuel,
            fuel: max_fuel.max(1),
        }
    }

    fn parse_expression(&mut self) -> Result<ArithmeticExprNode> {
        self.parse_comma()
    }

    fn parse_comma(&mut self) -> Result<ArithmeticExprNode> {
        let mut expr = self.parse_assignment()?;
        loop {
            if !matches!(self.peek_token()?.kind, TokenKind::Comma) {
                break;
            }
            self.next_token()?;
            let right = self.parse_assignment()?;
            let span = expr.span.merge(right.span);
            expr = ArithmeticExprNode::new(
                ArithmeticExpr::Binary {
                    left: Box::new(expr),
                    op: ArithmeticBinaryOp::Comma,
                    right: Box::new(right),
                },
                span,
            );
        }
        Ok(expr)
    }

    fn parse_assignment(&mut self) -> Result<ArithmeticExprNode> {
        let left = self.parse_conditional()?;
        let op = match &self.peek_token()?.kind {
            TokenKind::Assign => ArithmeticAssignOp::Assign,
            TokenKind::PlusAssign => ArithmeticAssignOp::AddAssign,
            TokenKind::MinusAssign => ArithmeticAssignOp::SubAssign,
            TokenKind::StarAssign => ArithmeticAssignOp::MulAssign,
            TokenKind::SlashAssign => ArithmeticAssignOp::DivAssign,
            TokenKind::PercentAssign => ArithmeticAssignOp::ModAssign,
            TokenKind::ShiftLeftAssign => ArithmeticAssignOp::ShiftLeftAssign,
            TokenKind::ShiftRightAssign => ArithmeticAssignOp::ShiftRightAssign,
            TokenKind::AndAssign => ArithmeticAssignOp::AndAssign,
            TokenKind::XorAssign => ArithmeticAssignOp::XorAssign,
            TokenKind::OrAssign => ArithmeticAssignOp::OrAssign,
            _ => return Ok(left),
        };
        self.next_token()?;
        let right = self.parse_assignment()?;
        let span = left.span.merge(right.span);
        let target = self.node_to_lvalue(left)?;
        Ok(ArithmeticExprNode::new(
            ArithmeticExpr::Assignment {
                target,
                op,
                value: Box::new(right),
            },
            span,
        ))
    }

    fn parse_conditional(&mut self) -> Result<ArithmeticExprNode> {
        let condition = self.parse_logical_or()?;
        if !matches!(self.peek_token()?.kind, TokenKind::Question) {
            return Ok(condition);
        }
        self.next_token()?;
        let then_expr = self.parse_comma()?;
        let colon = self.next_token()?;
        if !matches!(colon.kind, TokenKind::Colon) {
            return Err(self.error_at(colon.span.start, "expected ':' in arithmetic conditional"));
        }
        let else_expr = self.parse_conditional()?;
        let span = condition.span.merge(else_expr.span);
        Ok(ArithmeticExprNode::new(
            ArithmeticExpr::Conditional {
                condition: Box::new(condition),
                then_expr: Box::new(then_expr),
                else_expr: Box::new(else_expr),
            },
            span,
        ))
    }

    fn parse_logical_or(&mut self) -> Result<ArithmeticExprNode> {
        self.parse_left_associative(Self::parse_logical_and, |kind| match kind {
            TokenKind::LogicalOr => Some(ArithmeticBinaryOp::LogicalOr),
            _ => None,
        })
    }

    fn parse_logical_and(&mut self) -> Result<ArithmeticExprNode> {
        self.parse_left_associative(Self::parse_bitwise_or, |kind| match kind {
            TokenKind::LogicalAnd => Some(ArithmeticBinaryOp::LogicalAnd),
            _ => None,
        })
    }

    fn parse_bitwise_or(&mut self) -> Result<ArithmeticExprNode> {
        self.parse_left_associative(Self::parse_bitwise_xor, |kind| match kind {
            TokenKind::BitwiseOr => Some(ArithmeticBinaryOp::BitwiseOr),
            _ => None,
        })
    }

    fn parse_bitwise_xor(&mut self) -> Result<ArithmeticExprNode> {
        self.parse_left_associative(Self::parse_bitwise_and, |kind| match kind {
            TokenKind::BitwiseXor => Some(ArithmeticBinaryOp::BitwiseXor),
            _ => None,
        })
    }

    fn parse_bitwise_and(&mut self) -> Result<ArithmeticExprNode> {
        self.parse_left_associative(Self::parse_equality, |kind| match kind {
            TokenKind::BitwiseAnd => Some(ArithmeticBinaryOp::BitwiseAnd),
            _ => None,
        })
    }

    fn parse_equality(&mut self) -> Result<ArithmeticExprNode> {
        self.parse_left_associative(Self::parse_relational, |kind| match kind {
            TokenKind::Equal => Some(ArithmeticBinaryOp::Equal),
            TokenKind::NotEqual => Some(ArithmeticBinaryOp::NotEqual),
            _ => None,
        })
    }

    fn parse_relational(&mut self) -> Result<ArithmeticExprNode> {
        self.parse_left_associative(Self::parse_shift, |kind| match kind {
            TokenKind::LessThan => Some(ArithmeticBinaryOp::LessThan),
            TokenKind::LessThanOrEqual => Some(ArithmeticBinaryOp::LessThanOrEqual),
            TokenKind::GreaterThan => Some(ArithmeticBinaryOp::GreaterThan),
            TokenKind::GreaterThanOrEqual => Some(ArithmeticBinaryOp::GreaterThanOrEqual),
            _ => None,
        })
    }

    fn parse_shift(&mut self) -> Result<ArithmeticExprNode> {
        self.parse_left_associative(Self::parse_additive, |kind| match kind {
            TokenKind::ShiftLeft => Some(ArithmeticBinaryOp::ShiftLeft),
            TokenKind::ShiftRight => Some(ArithmeticBinaryOp::ShiftRight),
            _ => None,
        })
    }

    fn parse_additive(&mut self) -> Result<ArithmeticExprNode> {
        self.parse_left_associative(Self::parse_multiplicative, |kind| match kind {
            TokenKind::Plus => Some(ArithmeticBinaryOp::Add),
            TokenKind::Minus => Some(ArithmeticBinaryOp::Subtract),
            _ => None,
        })
    }

    fn parse_multiplicative(&mut self) -> Result<ArithmeticExprNode> {
        self.parse_left_associative(Self::parse_power, |kind| match kind {
            TokenKind::Star => Some(ArithmeticBinaryOp::Multiply),
            TokenKind::Slash => Some(ArithmeticBinaryOp::Divide),
            TokenKind::Percent => Some(ArithmeticBinaryOp::Modulo),
            _ => None,
        })
    }

    fn parse_power(&mut self) -> Result<ArithmeticExprNode> {
        let left = self.parse_unary()?;
        if !matches!(self.peek_token()?.kind, TokenKind::DoubleStar) {
            return Ok(left);
        }
        self.next_token()?;
        let right = self.parse_power()?;
        let span = left.span.merge(right.span);
        Ok(ArithmeticExprNode::new(
            ArithmeticExpr::Binary {
                left: Box::new(left),
                op: ArithmeticBinaryOp::Power,
                right: Box::new(right),
            },
            span,
        ))
    }

    fn parse_unary(&mut self) -> Result<ArithmeticExprNode> {
        let (op, start_span) = match self.peek_token()? {
            Token {
                kind: TokenKind::Increment,
                span,
            } => (ArithmeticUnaryOp::PreIncrement, *span),
            Token {
                kind: TokenKind::Decrement,
                span,
            } => (ArithmeticUnaryOp::PreDecrement, *span),
            Token {
                kind: TokenKind::Plus,
                span,
            } => (ArithmeticUnaryOp::Plus, *span),
            Token {
                kind: TokenKind::Minus,
                span,
            } => (ArithmeticUnaryOp::Minus, *span),
            Token {
                kind: TokenKind::Bang,
                span,
            } => (ArithmeticUnaryOp::LogicalNot, *span),
            Token {
                kind: TokenKind::Tilde,
                span,
            } => (ArithmeticUnaryOp::BitwiseNot, *span),
            _ => return self.parse_postfix(),
        };
        self.next_token()?;
        let expr = self.parse_unary()?;
        let span = start_span.merge(expr.span);
        self.ensure_lvalue_if_update(op, &expr)?;
        Ok(ArithmeticExprNode::new(
            ArithmeticExpr::Unary {
                op,
                expr: Box::new(expr),
            },
            span,
        ))
    }

    fn parse_postfix(&mut self) -> Result<ArithmeticExprNode> {
        let mut expr = self.parse_primary()?;
        loop {
            if matches!(self.peek_token()?.kind, TokenKind::LeftBracket) {
                self.next_token()?;
                let index = self.parse_comma()?;
                let closing = self.next_token()?;
                if !matches!(closing.kind, TokenKind::RightBracket) {
                    return Err(
                        self.error_at(closing.span.start, "expected ']' in arithmetic index")
                    );
                }
                let span = expr.span.merge(closing.span);
                expr = self.index_expr(expr, index, span)?;
                continue;
            }

            let (op, at, span) = match self.peek_token()? {
                Token {
                    kind: TokenKind::Increment,
                    span,
                } => (ArithmeticPostfixOp::Increment, span.start, *span),
                Token {
                    kind: TokenKind::Decrement,
                    span,
                } => (ArithmeticPostfixOp::Decrement, span.start, *span),
                _ => break,
            };

            self.next_token()?;
            self.ensure_postfix_target(&expr, at)?;
            let expr_span = expr.span;
            expr = ArithmeticExprNode::new(
                ArithmeticExpr::Postfix {
                    expr: Box::new(expr),
                    op,
                },
                expr_span.merge(span),
            );
        }
        Ok(expr)
    }

    fn parse_primary(&mut self) -> Result<ArithmeticExprNode> {
        let token = self.next_token()?;
        match token.kind {
            TokenKind::LeftParen => {
                let expr = self.parse_comma()?;
                let closing = self.next_token()?;
                if !matches!(closing.kind, TokenKind::RightParen) {
                    return Err(
                        self.error_at(closing.span.start, "expected ')' in arithmetic expression")
                    );
                }
                Ok(ArithmeticExprNode::new(
                    ArithmeticExpr::Parenthesized {
                        expression: Box::new(expr),
                    },
                    token.span.merge(closing.span),
                ))
            }
            TokenKind::Number(number) => Ok(ArithmeticExprNode::new(
                ArithmeticExpr::Number(number),
                token.span,
            )),
            TokenKind::Ident(name) => Ok(ArithmeticExprNode::new(
                ArithmeticExpr::Variable(name),
                token.span,
            )),
            TokenKind::ShellWord(word) => Ok(ArithmeticExprNode::new(
                ArithmeticExpr::ShellWord(word),
                token.span,
            )),
            TokenKind::End => {
                Err(self.error_at(token.span.start, "unexpected end of arithmetic expression"))
            }
            _ => Err(self.error_at(token.span.start, "expected arithmetic operand")),
        }
    }

    fn parse_left_associative(
        &mut self,
        subparser: fn(&mut Self) -> Result<ArithmeticExprNode>,
        op_of: fn(&TokenKind) -> Option<ArithmeticBinaryOp>,
    ) -> Result<ArithmeticExprNode> {
        let mut expr = subparser(self)?;
        while let Some(op) = op_of(&self.peek_token()?.kind) {
            self.next_token()?;
            let right = subparser(self)?;
            let span = expr.span.merge(right.span);
            expr = ArithmeticExprNode::new(
                ArithmeticExpr::Binary {
                    left: Box::new(expr),
                    op,
                    right: Box::new(right),
                },
                span,
            );
        }
        Ok(expr)
    }

    fn index_expr(
        &self,
        expr: ArithmeticExprNode,
        index: ArithmeticExprNode,
        span: Span,
    ) -> Result<ArithmeticExprNode> {
        match expr.kind {
            ArithmeticExpr::Variable(name) => Ok(ArithmeticExprNode::new(
                ArithmeticExpr::Indexed {
                    name,
                    index: Box::new(index),
                },
                span,
            )),
            ArithmeticExpr::Indexed { .. } => Err(self.error_at(
                expr.span.start,
                "nested arithmetic indices are not supported",
            )),
            _ => Err(self.error_at(expr.span.start, "expected variable before arithmetic index")),
        }
    }

    fn node_to_lvalue(&self, expr: ArithmeticExprNode) -> Result<ArithmeticLvalue> {
        match expr.kind {
            ArithmeticExpr::Variable(name) => Ok(ArithmeticLvalue::Variable(name)),
            ArithmeticExpr::Indexed { name, index } => {
                Ok(ArithmeticLvalue::Indexed { name, index })
            }
            _ => Err(self.error_at(expr.span.start, "expected assignable arithmetic target")),
        }
    }

    fn ensure_postfix_target(&self, expr: &ArithmeticExprNode, at: Position) -> Result<()> {
        match expr.kind {
            ArithmeticExpr::Variable(_) | ArithmeticExpr::Indexed { .. } => Ok(()),
            _ => Err(self.error_at(
                at,
                "expected variable or indexed reference before update operator",
            )),
        }
    }

    fn ensure_lvalue_if_update(
        &self,
        op: ArithmeticUnaryOp,
        expr: &ArithmeticExprNode,
    ) -> Result<()> {
        if !matches!(
            op,
            ArithmeticUnaryOp::PreIncrement | ArithmeticUnaryOp::PreDecrement
        ) {
            return Ok(());
        }
        self.ensure_postfix_target(expr, expr.span.start)
    }

    fn next_token(&mut self) -> Result<Token> {
        if let Some(token) = self.peeked.take() {
            return Ok(token);
        }
        self.lex_token()
    }

    fn peek_token(&mut self) -> Result<&Token> {
        if self.peeked.is_none() {
            self.peeked = Some(self.lex_token()?);
        }
        self.peeked.as_ref().ok_or_else(|| {
            self.error_at(
                self.safe_position_at(self.index),
                "internal arithmetic parser failed to cache lookahead token",
            )
        })
    }

    fn lex_token(&mut self) -> Result<Token> {
        self.tick()?;
        self.skip_whitespace();
        if self.index >= self.input.len() {
            return Ok(Token::end(self.empty_span()));
        }

        let start = self.index;
        let ch = self.require_current_char()?;

        let token = if is_ident_start(ch) {
            self.lex_identifier_or_word(start)?
        } else if ch.is_ascii_digit() {
            self.lex_number(start)?
        } else if ch == '#'
            && self.dialect == ShellDialect::Zsh
            && self
                .char_at(start + 1)
                .is_some_and(|next| !next.is_whitespace())
        {
            self.lex_zsh_char_literal(start)
        } else {
            match ch {
                '(' => self.simple_token(start, ch.len_utf8(), TokenKind::LeftParen),
                ')' => self.simple_token(start, ch.len_utf8(), TokenKind::RightParen),
                '[' => self.simple_token(start, ch.len_utf8(), TokenKind::LeftBracket),
                ']' => self.simple_token(start, ch.len_utf8(), TokenKind::RightBracket),
                '?' => self.simple_token(start, ch.len_utf8(), TokenKind::Question),
                ':' => self.simple_token(start, ch.len_utf8(), TokenKind::Colon),
                ',' => self.simple_token(start, ch.len_utf8(), TokenKind::Comma),
                '+' => {
                    if self.consume_str("++") {
                        self.token_for_range(start, self.index, TokenKind::Increment)
                    } else if self.consume_str("+=") {
                        self.token_for_range(start, self.index, TokenKind::PlusAssign)
                    } else {
                        self.index += 1;
                        self.token_for_range(start, self.index, TokenKind::Plus)
                    }
                }
                '-' => {
                    if self.consume_str("--") {
                        self.token_for_range(start, self.index, TokenKind::Decrement)
                    } else if self.consume_str("-=") {
                        self.token_for_range(start, self.index, TokenKind::MinusAssign)
                    } else {
                        self.index += 1;
                        self.token_for_range(start, self.index, TokenKind::Minus)
                    }
                }
                '*' => {
                    if self.consume_str("**") {
                        self.token_for_range(start, self.index, TokenKind::DoubleStar)
                    } else if self.consume_str("*=") {
                        self.token_for_range(start, self.index, TokenKind::StarAssign)
                    } else {
                        self.index += 1;
                        self.token_for_range(start, self.index, TokenKind::Star)
                    }
                }
                '/' => {
                    if self.consume_str("/=") {
                        self.token_for_range(start, self.index, TokenKind::SlashAssign)
                    } else {
                        self.index += 1;
                        self.token_for_range(start, self.index, TokenKind::Slash)
                    }
                }
                '%' => {
                    if self.consume_str("%=") {
                        self.token_for_range(start, self.index, TokenKind::PercentAssign)
                    } else {
                        self.index += 1;
                        self.token_for_range(start, self.index, TokenKind::Percent)
                    }
                }
                '<' => {
                    if self.consume_str("<<=") {
                        self.token_for_range(start, self.index, TokenKind::ShiftLeftAssign)
                    } else if self.consume_str("<<") {
                        self.token_for_range(start, self.index, TokenKind::ShiftLeft)
                    } else if self.consume_str("<=") {
                        self.token_for_range(start, self.index, TokenKind::LessThanOrEqual)
                    } else {
                        self.index += 1;
                        self.token_for_range(start, self.index, TokenKind::LessThan)
                    }
                }
                '>' => {
                    if self.consume_str(">>=") {
                        self.token_for_range(start, self.index, TokenKind::ShiftRightAssign)
                    } else if self.consume_str(">>") {
                        self.token_for_range(start, self.index, TokenKind::ShiftRight)
                    } else if self.consume_str(">=") {
                        self.token_for_range(start, self.index, TokenKind::GreaterThanOrEqual)
                    } else {
                        self.index += 1;
                        self.token_for_range(start, self.index, TokenKind::GreaterThan)
                    }
                }
                '=' => {
                    if self.consume_str("==") {
                        self.token_for_range(start, self.index, TokenKind::Equal)
                    } else {
                        self.index += 1;
                        self.token_for_range(start, self.index, TokenKind::Assign)
                    }
                }
                '!' => {
                    if self.consume_str("!=") {
                        self.token_for_range(start, self.index, TokenKind::NotEqual)
                    } else {
                        self.index += 1;
                        self.token_for_range(start, self.index, TokenKind::Bang)
                    }
                }
                '&' => {
                    if self.consume_str("&&") {
                        self.token_for_range(start, self.index, TokenKind::LogicalAnd)
                    } else if self.consume_str("&=") {
                        self.token_for_range(start, self.index, TokenKind::AndAssign)
                    } else {
                        self.index += 1;
                        self.token_for_range(start, self.index, TokenKind::BitwiseAnd)
                    }
                }
                '^' => {
                    if self.consume_str("^=") {
                        self.token_for_range(start, self.index, TokenKind::XorAssign)
                    } else {
                        self.index += 1;
                        self.token_for_range(start, self.index, TokenKind::BitwiseXor)
                    }
                }
                '|' => {
                    if self.consume_str("||") {
                        self.token_for_range(start, self.index, TokenKind::LogicalOr)
                    } else if self.consume_str("|=") {
                        self.token_for_range(start, self.index, TokenKind::OrAssign)
                    } else {
                        self.index += 1;
                        self.token_for_range(start, self.index, TokenKind::BitwiseOr)
                    }
                }
                '~' => self.simple_token(start, ch.len_utf8(), TokenKind::Tilde),
                '$' | '"' | '\'' | '`' | '\\' => self.lex_shell_word(start)?,
                _ if !ch.is_ascii() => self.lex_shell_word(start)?,
                _ => {
                    return Err(self.error_at(
                        self.position_at(start),
                        "unexpected character in arithmetic expression",
                    ));
                }
            }
        };

        Ok(token)
    }

    fn lex_identifier_or_word(&mut self, start: usize) -> Result<Token> {
        let bytes = self.input.as_bytes();
        let mut end = start;
        while end < bytes.len() && is_ident_continue_byte(bytes[end]) {
            end += 1;
        }
        if self.starts_arithmetic_shell_word_tail_at(end) {
            self.index = start;
            return self.lex_shell_word(start);
        }
        self.index = end;
        Ok(Token {
            kind: TokenKind::Ident(Name::from(&self.input[start..end])),
            span: self.span_for(start, end),
        })
    }

    fn lex_number(&mut self, start: usize) -> Result<Token> {
        let bytes = self.input.as_bytes();
        let mut end = start;
        while end < bytes.len() && is_number_literal_byte(bytes[end]) {
            end += 1;
        }
        if self.starts_arithmetic_shell_word_tail_at(end) {
            self.index = start;
            return self.lex_shell_word(start);
        }
        self.index = end;
        Ok(Token {
            kind: TokenKind::Number(SourceText::source(self.span_for(start, end))),
            span: self.span_for(start, end),
        })
    }

    fn lex_zsh_char_literal(&mut self, start: usize) -> Token {
        let Some(next) = self.char_at(start + 1) else {
            unreachable!("zsh char literal requires a following character");
        };
        let end = start + 1 + next.len_utf8();
        self.index = end;
        Token {
            kind: TokenKind::Number(SourceText::source(self.span_for(start, end))),
            span: self.span_for(start, end),
        }
    }

    fn lex_shell_word(&mut self, start: usize) -> Result<Token> {
        let end = self.scan_shell_word_end(start)?;
        let raw = &self.input[start..end];
        let mut word = Parser::parse_word_string_with_limits_and_dialect(
            raw,
            self.max_depth,
            self.max_fuel,
            self.dialect,
        );
        Parser::rebase_word(&mut word, self.position_at(start));
        self.index = end;
        Ok(Token {
            kind: TokenKind::ShellWord(word),
            span: self.span_for(start, end),
        })
    }

    fn scan_shell_word_end(&self, start: usize) -> Result<usize> {
        let mut index = start;
        while index < self.input.len() {
            let ch = self.require_char_at(index)?;
            if index > start && (ch.is_whitespace() || Self::is_arithmetic_boundary(ch)) {
                break;
            }
            index = match ch {
                '\'' => self.consume_single_quoted(index)?,
                '"' => self.consume_double_quoted(index)?,
                '`' => self.consume_backticks(index)?,
                '$' => self.consume_dollar(index)?,
                '\\' => self.consume_escape(index),
                _ => index + ch.len_utf8(),
            };
        }
        Ok(index)
    }

    fn consume_single_quoted(&self, start: usize) -> Result<usize> {
        let mut index = start + 1;
        while index < self.input.len() {
            let ch = self.require_char_at(index)?;
            index += ch.len_utf8();
            if ch == '\'' {
                return Ok(index);
            }
        }
        Err(self.error_at(
            self.position_at(start),
            "unterminated single-quoted string in arithmetic expression",
        ))
    }

    fn consume_double_quoted(&self, start: usize) -> Result<usize> {
        let mut index = start + 1;
        while index < self.input.len() {
            let ch = self.require_char_at(index)?;
            match ch {
                '"' => return Ok(index + 1),
                '\\' => index = self.consume_escape(index),
                '$' => index = self.consume_dollar(index)?,
                '`' => index = self.consume_backticks(index)?,
                _ => index += ch.len_utf8(),
            }
        }
        Err(self.error_at(
            self.position_at(start),
            "unterminated double-quoted string in arithmetic expression",
        ))
    }

    fn consume_backticks(&self, start: usize) -> Result<usize> {
        let mut index = start + 1;
        while index < self.input.len() {
            let ch = self.require_char_at(index)?;
            match ch {
                '`' => return Ok(index + 1),
                '\\' => index = self.consume_escape(index),
                _ => index += ch.len_utf8(),
            }
        }
        Err(self.error_at(
            self.position_at(start),
            "unterminated backtick substitution in arithmetic expression",
        ))
    }

    fn consume_dollar(&self, start: usize) -> Result<usize> {
        let Some(next) = self.char_at(start + 1) else {
            return Ok(start + 1);
        };
        let mut index = match next {
            '\'' | '"' => {
                if next == '\'' {
                    self.consume_single_quoted(start + 1)
                } else {
                    self.consume_double_quoted(start + 1)
                }
            }
            '{' => self.consume_braced(start),
            '(' => {
                if self.char_at(start + 2) == Some('(') {
                    self.consume_dollar_arithmetic(start)
                } else {
                    self.consume_command_substitution(start)
                }
            }
            '+' if self.dialect == ShellDialect::Zsh
                && self.char_at(start + 2).is_some_and(is_ident_start) =>
            {
                let mut index = start + 2;
                while let Some(ch) = self.char_at(index) {
                    if is_ident_continue(ch) {
                        index += ch.len_utf8();
                    } else {
                        break;
                    }
                }
                Ok(index)
            }
            '#' if self.dialect == ShellDialect::Zsh => {
                let Some(next) = self.char_at(start + 2) else {
                    return Ok(start + 2);
                };
                let mut index = start + 2;

                if is_ident_start(next) {
                    index += next.len_utf8();
                    while let Some(ch) = self.char_at(index) {
                        if is_ident_continue(ch) {
                            index += ch.len_utf8();
                        } else {
                            break;
                        }
                    }
                } else if next.is_ascii_digit() || is_special_parameter(next) {
                    index += next.len_utf8();
                }

                Ok(index)
            }
            ch if is_special_parameter(ch) => Ok(start + 1 + ch.len_utf8()),
            ch if is_ident_start(ch) => {
                let mut index = start + 1 + ch.len_utf8();
                while let Some(ch) = self.char_at(index) {
                    if is_ident_continue(ch) {
                        index += ch.len_utf8();
                    } else {
                        break;
                    }
                }
                Ok(index)
            }
            ch if ch.is_ascii_digit() => Ok(start + 1 + ch.len_utf8()),
            _ => Ok(start + 1),
        }?;

        if self.dialect == ShellDialect::Zsh {
            while self.char_at(index) == Some('[') {
                index = self.consume_zsh_subscript(index)?;
            }
        }

        Ok(index)
    }

    fn consume_zsh_subscript(&self, start: usize) -> Result<usize> {
        let mut index = start + 1;
        let mut depth = 1usize;
        let mut in_single = false;
        let mut in_double = false;
        let mut escaped = false;

        while index < self.input.len() {
            let ch = self.require_char_at(index)?;
            if escaped {
                escaped = false;
                index += ch.len_utf8();
                continue;
            }
            if in_single {
                index += ch.len_utf8();
                if ch == '\'' {
                    in_single = false;
                }
                continue;
            }
            if in_double {
                match ch {
                    '"' => {
                        in_double = false;
                        index += 1;
                    }
                    '\\' => index = self.consume_escape(index),
                    '$' => index = self.consume_dollar(index)?,
                    '`' => index = self.consume_backticks(index)?,
                    _ => index += ch.len_utf8(),
                }
                continue;
            }

            match ch {
                '\\' => {
                    escaped = true;
                    index += 1;
                }
                '\'' => {
                    in_single = true;
                    index += 1;
                }
                '"' => {
                    in_double = true;
                    index += 1;
                }
                '$' => index = self.consume_dollar(index)?,
                '`' => index = self.consume_backticks(index)?,
                '[' => {
                    depth += 1;
                    index += 1;
                }
                ']' => {
                    depth -= 1;
                    index += 1;
                    if depth == 0 {
                        return Ok(index);
                    }
                }
                _ => index += ch.len_utf8(),
            }
        }

        Err(self.error_at(
            self.position_at(start),
            "unterminated zsh subscript in arithmetic expression",
        ))
    }

    fn consume_braced(&self, start: usize) -> Result<usize> {
        let mut index = start + 2;
        let mut depth = 1usize;
        let mut in_single = false;
        let mut in_double = false;

        while index < self.input.len() {
            let ch = self.require_char_at(index)?;
            if in_single {
                index += ch.len_utf8();
                if ch == '\'' {
                    in_single = false;
                }
                continue;
            }
            if in_double {
                match ch {
                    '"' => {
                        in_double = false;
                        index += 1;
                    }
                    '\\' => index = self.consume_escape(index),
                    '$' => index = self.consume_dollar(index)?,
                    '`' => index = self.consume_backticks(index)?,
                    _ => index += ch.len_utf8(),
                }
                continue;
            }

            match ch {
                '\'' => {
                    in_single = true;
                    index += 1;
                }
                '"' => {
                    in_double = true;
                    index += 1;
                }
                '\\' => index = self.consume_escape(index),
                '$' => {
                    if self.char_at(index + 1) == Some('{') {
                        depth += 1;
                        index += 2;
                    } else {
                        index = self.consume_dollar(index)?;
                    }
                }
                '`' => index = self.consume_backticks(index)?,
                '}' => {
                    depth -= 1;
                    index += 1;
                    if depth == 0 {
                        return Ok(index);
                    }
                }
                _ => index += ch.len_utf8(),
            }
        }
        Err(self.error_at(
            self.position_at(start),
            "unterminated parameter expansion in arithmetic expression",
        ))
    }

    fn consume_command_substitution(&self, start: usize) -> Result<usize> {
        let mut index = start + 2;
        let mut depth = 1usize;
        let mut in_single = false;
        let mut in_double = false;

        while index < self.input.len() {
            let ch = self.require_char_at(index)?;
            if in_single {
                index += ch.len_utf8();
                if ch == '\'' {
                    in_single = false;
                }
                continue;
            }
            if in_double {
                match ch {
                    '"' => {
                        in_double = false;
                        index += 1;
                    }
                    '\\' => index = self.consume_escape(index),
                    '$' => index = self.consume_dollar(index)?,
                    '`' => index = self.consume_backticks(index)?,
                    _ => index += ch.len_utf8(),
                }
                continue;
            }

            match ch {
                '\'' => {
                    in_single = true;
                    index += 1;
                }
                '"' => {
                    in_double = true;
                    index += 1;
                }
                '\\' => index = self.consume_escape(index),
                '$' if self.char_at(index + 1) == Some('(') => {
                    if self.char_at(index + 2) == Some('(') {
                        index = self.consume_dollar_arithmetic(index)?;
                    } else {
                        depth += 1;
                        index += 2;
                    }
                }
                '`' => index = self.consume_backticks(index)?,
                '(' => index += 1,
                ')' => {
                    depth -= 1;
                    index += 1;
                    if depth == 0 {
                        return Ok(index);
                    }
                }
                _ => index += ch.len_utf8(),
            }
        }
        Err(self.error_at(
            self.position_at(start),
            "unterminated command substitution in arithmetic expression",
        ))
    }

    fn consume_dollar_arithmetic(&self, start: usize) -> Result<usize> {
        let mut index = start + 3;
        let mut depth = 2i32;
        let mut in_single = false;
        let mut in_double = false;

        while index < self.input.len() {
            let ch = self.require_char_at(index)?;
            if in_single {
                index += ch.len_utf8();
                if ch == '\'' {
                    in_single = false;
                }
                continue;
            }
            if in_double {
                match ch {
                    '"' => {
                        in_double = false;
                        index += 1;
                    }
                    '\\' => index = self.consume_escape(index),
                    '$' => index = self.consume_dollar(index)?,
                    '`' => index = self.consume_backticks(index)?,
                    _ => index += ch.len_utf8(),
                }
                continue;
            }

            match ch {
                '\'' => {
                    in_single = true;
                    index += 1;
                }
                '"' => {
                    in_double = true;
                    index += 1;
                }
                '\\' => index = self.consume_escape(index),
                '$' => index = self.consume_dollar(index)?,
                '`' => index = self.consume_backticks(index)?,
                '(' => {
                    depth += 1;
                    index += 1;
                }
                ')' => {
                    depth -= 1;
                    index += 1;
                    if depth == 0 {
                        return Ok(index);
                    }
                }
                _ => index += ch.len_utf8(),
            }
        }
        Err(self.error_at(self.position_at(start), "unterminated arithmetic expansion"))
    }

    fn consume_escape(&self, start: usize) -> usize {
        let mut index = start + 1;
        if let Some(ch) = self.char_at(index) {
            index += ch.len_utf8();
        }
        index
    }

    fn simple_token(&mut self, start: usize, width: usize, kind: TokenKind) -> Token {
        self.index = start + width;
        self.token_for_range(start, self.index, kind)
    }

    fn token_for_range(&self, start: usize, end: usize, kind: TokenKind) -> Token {
        Token {
            kind,
            span: self.span_for(start, end),
        }
    }

    fn skip_whitespace(&mut self) {
        loop {
            let Some(ch) = self.current_char() else {
                break;
            };

            if ch.is_whitespace() {
                self.index += ch.len_utf8();
                continue;
            }

            if self.input[self.index..].starts_with("\\\r\n") {
                self.index += "\\\r\n".len();
                continue;
            }

            if self.input[self.index..].starts_with("\\\n") {
                self.index += "\\\n".len();
                continue;
            }

            break;
        }
    }

    fn current_char(&self) -> Option<char> {
        self.char_at(self.index)
    }

    fn char_at(&self, index: usize) -> Option<char> {
        self.input.get(index..)?.chars().next()
    }

    fn consume_str(&mut self, value: &str) -> bool {
        if self.input[self.index..].starts_with(value) {
            self.index += value.len();
            true
        } else {
            false
        }
    }

    fn position_at(&self, index: usize) -> Position {
        self.base.start.advanced_by(&self.input[..index])
    }

    fn safe_position_at(&self, index: usize) -> Position {
        let safe_index = if index <= self.input.len() && self.input.is_char_boundary(index) {
            index
        } else {
            self.input.len()
        };
        self.position_at(safe_index)
    }

    fn require_current_char(&self) -> Result<char> {
        self.require_char_at(self.index)
    }

    fn require_char_at(&self, index: usize) -> Result<char> {
        self.char_at(index).ok_or_else(|| {
            self.error_at(
                self.safe_position_at(index),
                "internal arithmetic parser cursor became invalid",
            )
        })
    }

    fn span_for(&self, start: usize, end: usize) -> Span {
        Span::from_positions(self.position_at(start), self.position_at(end))
    }

    fn empty_span(&self) -> Span {
        Span::from_positions(
            self.position_at(self.input.len()),
            self.position_at(self.input.len()),
        )
    }

    fn starts_arithmetic_shell_word_tail_at(&self, index: usize) -> bool {
        let Some(&byte) = self.input.as_bytes().get(index) else {
            return false;
        };
        if byte.is_ascii() {
            return Self::starts_arithmetic_shell_word_tail_ascii(byte);
        }
        self.char_at(index)
            .is_some_and(Self::starts_arithmetic_shell_word_tail)
    }

    fn starts_arithmetic_shell_word_tail(ch: char) -> bool {
        if ch.is_ascii() {
            Self::starts_arithmetic_shell_word_tail_ascii(ch as u8)
        } else {
            !ch.is_whitespace()
        }
    }

    fn starts_arithmetic_shell_word_tail_ascii(byte: u8) -> bool {
        match byte {
            b'$' | b'"' | b'\'' | b'`' | b'\\' => true,
            byte if byte.is_ascii_whitespace() => false,
            _ => !Self::is_arithmetic_boundary_ascii(byte),
        }
    }

    fn is_arithmetic_boundary(ch: char) -> bool {
        ch.is_ascii() && Self::is_arithmetic_boundary_ascii(ch as u8)
    }

    fn is_arithmetic_boundary_ascii(byte: u8) -> bool {
        matches!(
            byte,
            b'(' | b')'
                | b'['
                | b']'
                | b'?'
                | b':'
                | b','
                | b'+'
                | b'-'
                | b'*'
                | b'/'
                | b'%'
                | b'<'
                | b'>'
                | b'='
                | b'!'
                | b'~'
                | b'&'
                | b'^'
                | b'|'
        )
    }

    fn error_at(&self, pos: Position, message: impl Into<String>) -> Error {
        Error::parse_at(message, pos.line, pos.column)
    }

    fn tick(&mut self) -> Result<()> {
        if self.fuel == 0 {
            return Err(Error::parse("arithmetic parser exceeded operation limit"));
        }
        self.fuel -= 1;
        Ok(())
    }
}

fn is_ident_start(ch: char) -> bool {
    ch == '_' || ch.is_ascii_alphabetic()
}

fn is_ident_continue(ch: char) -> bool {
    ch == '_' || ch.is_ascii_alphanumeric()
}

fn is_ident_continue_byte(byte: u8) -> bool {
    byte == b'_' || byte.is_ascii_alphanumeric()
}

fn is_number_literal_byte(byte: u8) -> bool {
    byte == b'#' || byte == b'_' || byte.is_ascii_alphanumeric()
}

fn is_special_parameter(ch: char) -> bool {
    matches!(ch, '?' | '#' | '@' | '*' | '!' | '$' | '-')
}
