///|
/// CSS Stylesheet Parser
/// Parses full stylesheets with selectors and declaration blocks

///|
/// Maximum allowed CSS input size (10 MB)
let css_max_input_size : Int = 10 * 1024 * 1024

///|
/// Parse a full stylesheet from CSS text
pub fn parse_stylesheet(css : String) -> @cascade.Stylesheet {
  // Security: reject excessively large CSS inputs
  if css.length() > css_max_input_size {
    return @cascade.Stylesheet::new(@cascade.Author)
  }
  let stylesheet = @cascade.Stylesheet::new(@cascade.Author)
  let tokens = @token.tokenize(css)
  let mut pos = 0
  while pos < tokens.length() {
    // Skip whitespace
    pos = skip_ws(tokens, pos)
    if pos >= tokens.length() {
      break
    }
    match tokens[pos] {
      @token.Token::EOF => break
      @token.Token::AtKeyword(name) =>
        if name.to_lower() == "media" {
          // Parse @media rule
          pos = parse_media_rule(stylesheet, tokens, pos)
        } else if name.to_lower() == "supports" {
          // Parse @supports rule (treat condition as always true)
          pos = parse_supports_rule(stylesheet, tokens, pos)
        } else if is_keyframes_keyword(name) {
          // Parse @keyframes rule
          pos = parse_keyframes_rule(stylesheet, tokens, pos)
        } else {
          // Skip other @-rules (e.g., @import, @font-face)
          pos = skip_at_rule(tokens, pos)
        }
      _ =>
        // Try to parse a style rule
        match parse_style_rule(tokens, pos) {
          Some((selectors, declarations, new_pos)) => {
            // Add rules for each selector
            for item in selectors {
              let selector_text = item.0
              let selector = item.1
              stylesheet.add_rule(selector_text, selector, declarations.copy())
            }
            let nested_rules = extract_ampersand_nested_rules(
              tokens, pos, new_pos, selectors,
            )
            for nested_rule in nested_rules {
              stylesheet.add_rule(
                nested_rule.0,
                nested_rule.1,
                nested_rule.2.copy(),
              )
            }
            pos = new_pos
          }
          None => pos = pos + 1
        }
    }
  }
  stylesheet
}

///|
/// Parse an @media rule and add its rules to the stylesheet
fn parse_media_rule(
  stylesheet : @cascade.Stylesheet,
  tokens : Array[@token.Token],
  start : Int,
) -> Int {
  let mut pos = start + 1 // Skip @media token

  // Collect media query tokens until '{'
  let query_start = pos
  while pos < tokens.length() {
    match tokens[pos] {
      @token.Token::LeftBrace => break
      @token.Token::EOF => return pos
      _ => pos = pos + 1
    }
  }
  if pos >= tokens.length() {
    return pos
  }

  // Extract and parse media query
  let query_text = tokens_to_string(tokens, query_start, pos).trim().to_owned()
  let media_query = @media.parse_media_query_list(query_text)

  // Skip '{'
  pos = pos + 1

  // Parse rules inside @media block until '}'
  let mut brace_depth = 1
  while pos < tokens.length() && brace_depth > 0 {
    pos = skip_ws(tokens, pos)
    if pos >= tokens.length() {
      break
    }
    match tokens[pos] {
      @token.Token::RightBrace => {
        brace_depth = brace_depth - 1
        pos = pos + 1
        if brace_depth <= 0 {
          break
        }
      }
      @token.Token::LeftBrace => {
        // This shouldn't happen at top level, skip past it
        brace_depth = brace_depth + 1
        pos = pos + 1
      }
      @token.Token::EOF => break
      @token.Token::AtKeyword(at_name) =>
        if at_name.to_lower() == "supports" {
          // Parse @supports inside @media — rules get the media query
          pos = parse_supports_rule_with_media(
            stylesheet, tokens, pos, media_query,
          )
        } else {
          // Skip other nested @-rules inside @media
          pos = skip_at_rule(tokens, pos)
        }
      _ =>
        // Try to parse a style rule
        match parse_style_rule(tokens, pos) {
          Some((selectors, declarations, new_pos)) => {
            // Add rules with media query for each selector
            for item in selectors {
              let selector_text = item.0
              let selector = item.1
              stylesheet.add_rule_with_media(
                selector_text,
                selector,
                declarations.copy(),
                media_query,
              )
            }
            pos = new_pos
          }
          None => pos = pos + 1
        }
    }
  }
  pos
}

///|
/// Parse an @supports rule inside @media, attaching the media query to rules.
fn parse_supports_rule_with_media(
  stylesheet : @cascade.Stylesheet,
  tokens : Array[@token.Token],
  start : Int,
  media_query : @media.MediaQueryList,
) -> Int {
  let mut pos = start + 1 // Skip @supports token

  // Skip condition tokens until '{' (handling nested parens)
  let condition_start = pos
  pos = skip_supports_condition_to_block(tokens, pos)
  if pos >= tokens.length() {
    return pos
  }
  let condition_text = tokens_to_string(tokens, condition_start, pos)
  if !supports_condition_matches(condition_text) {
    return skip_at_rule(tokens, start)
  }

  // Skip '{'
  pos = pos + 1

  // Parse rules inside @supports block until '}'
  let mut brace_depth = 1
  while pos < tokens.length() && brace_depth > 0 {
    pos = skip_ws(tokens, pos)
    if pos >= tokens.length() {
      break
    }
    match tokens[pos] {
      @token.Token::RightBrace => {
        brace_depth -= 1
        pos = pos + 1
        if brace_depth <= 0 {
          break
        }
      }
      @token.Token::EOF => break
      @token.Token::AtKeyword(_) => pos = skip_at_rule(tokens, pos)
      _ =>
        match parse_style_rule(tokens, pos) {
          Some((selectors, declarations, new_pos)) => {
            for item in selectors {
              let selector_text = item.0
              let selector = item.1
              stylesheet.add_rule_with_media(
                selector_text,
                selector,
                declarations.copy(),
                media_query,
              )
            }
            pos = new_pos
          }
          None => pos = pos + 1
        }
    }
  }
  pos
}

///|
/// Parse an @supports rule and add its rules to the stylesheet.
/// The condition is treated as always true (minimal implementation).
fn parse_supports_rule(
  stylesheet : @cascade.Stylesheet,
  tokens : Array[@token.Token],
  start : Int,
) -> Int {
  let mut pos = start + 1 // Skip @supports token

  // Skip condition tokens until '{' (handling nested parens)
  let condition_start = pos
  pos = skip_supports_condition_to_block(tokens, pos)
  if pos >= tokens.length() {
    return pos
  }
  let condition_text = tokens_to_string(tokens, condition_start, pos)
  if !supports_condition_matches(condition_text) {
    return skip_at_rule(tokens, start)
  }

  // Skip '{'
  pos = pos + 1

  // Parse rules inside @supports block until '}'
  let mut brace_depth = 1
  while pos < tokens.length() && brace_depth > 0 {
    pos = skip_ws(tokens, pos)
    if pos >= tokens.length() {
      break
    }
    match tokens[pos] {
      @token.Token::RightBrace => {
        brace_depth -= 1
        pos = pos + 1
        if brace_depth <= 0 {
          break
        }
      }
      @token.Token::EOF => break
      @token.Token::AtKeyword(name) =>
        if name.to_lower() == "media" {
          pos = parse_media_rule(stylesheet, tokens, pos)
        } else if name.to_lower() == "supports" {
          pos = parse_supports_rule(stylesheet, tokens, pos)
        } else {
          pos = skip_at_rule(tokens, pos)
        }
      _ =>
        match parse_style_rule(tokens, pos) {
          Some((selectors, declarations, new_pos)) => {
            for item in selectors {
              let selector_text = item.0
              let selector = item.1
              stylesheet.add_rule(selector_text, selector, declarations.copy())
            }
            let nested_rules = extract_ampersand_nested_rules(
              tokens, pos, new_pos, selectors,
            )
            for nested_rule in nested_rules {
              stylesheet.add_rule(
                nested_rule.0,
                nested_rule.1,
                nested_rule.2.copy(),
              )
            }
            pos = new_pos
          }
          None => pos = pos + 1
        }
    }
  }
  pos
}

///|
/// Skip whitespace tokens
fn skip_ws(tokens : Array[@token.Token], pos : Int) -> Int {
  let mut p = pos
  while p < tokens.length() {
    match tokens[p] {
      @token.Token::Whitespace => p = p + 1
      _ => break
    }
  }
  p
}

///|
fn skip_supports_condition_to_block(
  tokens : Array[@token.Token],
  start : Int,
) -> Int {
  let mut pos = start
  let mut paren_depth = 0
  while pos < tokens.length() {
    match tokens[pos] {
      @token.Token::Function(_) | @token.Token::LeftParen => {
        paren_depth += 1
        pos = pos + 1
      }
      @token.Token::RightParen => {
        if paren_depth > 0 {
          paren_depth -= 1
        }
        pos = pos + 1
      }
      @token.Token::LeftBrace if paren_depth == 0 => break
      @token.Token::EOF => return pos
      _ => pos = pos + 1
    }
  }
  pos
}

///|
fn compact_supports_condition(condition : String) -> String {
  let lower = condition.to_lower()
  let out = StringBuilder::new()
  for i = 0; i < lower.length(); i = i + 1 {
    let c = lower[i].to_int().unsafe_to_char()
    if c == ' ' || c == '\t' || c == '\n' {
      continue
    }
    out.write_char(c)
  }
  out.to_string()
}

///|
fn supports_known_positive_condition(compact : String) -> Bool? {
  if compact.contains("color:light-dark(") {
    return Some(true)
  }
  None
}

///|
fn supports_condition_matches(condition : String) -> Bool {
  let compact = compact_supports_condition(condition)
  if compact.has_prefix("not(") && compact.has_suffix(")") {
    let inner = compact.unsafe_substring(start=4, end=compact.length() - 1)
    match supports_known_positive_condition(inner) {
      Some(value) => return !value
      None => return true
    }
  }
  if compact.has_prefix("not") {
    let inner = compact.unsafe_substring(start=3, end=compact.length())
    match supports_known_positive_condition(inner) {
      Some(value) => return !value
      None => return true
    }
  }
  match supports_known_positive_condition(compact) {
    Some(value) => value
    None => true
  }
}

///|
/// Parse an `@keyframes name { * }` rule and add it to the
/// stylesheet. `start` points at the `@keyframes` (or `@-webkit-keyframes`)
/// AtKeyword token. Returns the position just past the closing brace.
fn parse_keyframes_rule(
  stylesheet : @cascade.Stylesheet,
  tokens : Array[@token.Token],
  start : Int,
) -> Int {
  let mut pos = skip_ws(tokens, start + 1)
  // Animation name: a custom-ident or a string.
  let name = match tokens[pos] {
    @token.Token::Ident(n) => {
      pos = pos + 1
      n
    }
    @token.Token::String(n) => {
      pos = pos + 1
      n
    }
    _ => return skip_at_rule(tokens, start)
  }
  pos = skip_ws(tokens, pos)
  match tokens[pos] {
    @token.Token::LeftBrace => pos = pos + 1
    _ => return skip_at_rule(tokens, start)
  }
  let blocks : Array[@cascade.KeyframeBlock] = []
  while pos < tokens.length() {
    pos = skip_ws(tokens, pos)
    match tokens[pos] {
      @token.Token::RightBrace => {
        pos = pos + 1
        break
      }
      @token.Token::EOF => break
      _ => {
        // Keyframe selector list: from | to |  (comma-separated),
        // up to the block's '{'.
        let offsets : Array[Double] = []
        while pos < tokens.length() {
          pos = skip_ws(tokens, pos)
          match tokens[pos] {
            @token.Token::LeftBrace => break
            @token.Token::EOF => break
            @token.Token::RightBrace => break
            @token.Token::Percentage(v) => {
              offsets.push(v / 100.0)
              pos = pos + 1
            }
            @token.Token::Number(v, _) => {
              // Only `0` is a valid unitless keyframe selector (= 0%).
              if v == 0.0 {
                offsets.push(0.0)
              }
              pos = pos + 1
            }
            @token.Token::Ident(s) => {
              let ls = s.to_lower()
              if ls == "from" {
                offsets.push(0.0)
              } else if ls == "to" {
                offsets.push(1.0)
              }
              pos = pos + 1
            }
            @token.Token::Comma => pos = pos + 1
            _ => pos = pos + 1
          }
        }
        if pos >= tokens.length() {
          break
        }
        match tokens[pos] {
          @token.Token::LeftBrace => pos = pos + 1
          _ => break
        }
        // Declarations for this block, until '}'.
        let declarations : Array[@cascade.Declaration] = []
        let mut decl_order = 0
        while pos < tokens.length() {
          pos = skip_ws(tokens, pos)
          match tokens[pos] {
            @token.Token::RightBrace => {
              pos = pos + 1
              break
            }
            @token.Token::EOF => break
            _ =>
              match parse_rule_declaration(tokens, pos) {
                Some((property, value, importance, new_pos)) => {
                  declarations.push({
                    property,
                    value: @cascade.Value(value),
                    origin: @cascade.Author,
                    importance,
                    specificity: { a: 0, b: 0, c: 0 },
                    source_order: decl_order,
                  })
                  decl_order = decl_order + 1
                  pos = new_pos
                }
                None => pos = pos + 1
              }
          }
        }
        if offsets.length() > 0 {
          blocks.push({ offsets, declarations })
        }
      }
    }
  }
  stylesheet.add_keyframes({ name, blocks })
  pos
}

///|
/// Whether an `@`-rule keyword names a `@keyframes` (incl. vendor-prefixed).
fn is_keyframes_keyword(name : String) -> Bool {
  let n = name.to_lower()
  n == "keyframes" || n == "-webkit-keyframes" || n == "-moz-keyframes"
}

///|
/// Skip an @-rule (until semicolon or matching braces)
fn skip_at_rule(tokens : Array[@token.Token], start : Int) -> Int {
  let mut pos = start + 1
  let mut brace_depth = 0
  while pos < tokens.length() {
    match tokens[pos] {
      @token.Token::LeftBrace => {
        brace_depth = brace_depth + 1
        pos = pos + 1
      }
      @token.Token::RightBrace => {
        brace_depth = brace_depth - 1
        pos = pos + 1
        if brace_depth <= 0 {
          break
        }
      }
      @token.Token::Semicolon => {
        if brace_depth == 0 {
          pos = pos + 1
          break
        }
        pos = pos + 1
      }
      @token.Token::EOF => break
      _ => pos = pos + 1
    }
  }
  pos
}

///|
/// Parse a style rule: selector { declarations }
fn parse_style_rule(
  tokens : Array[@token.Token],
  start : Int,
) -> (
  Array[(String, @selector.ComplexSelector)],
  Array[@cascade.Declaration],
  Int,
)? {
  let mut pos = start

  // Collect selector tokens until '{'
  let selector_start = pos
  while pos < tokens.length() {
    match tokens[pos] {
      @token.Token::LeftBrace => break
      @token.Token::EOF => return None
      _ => pos = pos + 1
    }
  }
  if pos >= tokens.length() {
    return None
  }

  // Parse selectors directly from the already-lexed token stream —
  // avoids stringifying then re-tokenizing each selector.
  let selectors = parse_selector_list_from_tokens(tokens, selector_start, pos)
  if selectors.is_empty() {
    return None
  }

  // Skip '{'
  pos = pos + 1

  // Parse declarations until '}'
  let declarations : Array[@cascade.Declaration] = []
  let mut decl_order = 0
  while pos < tokens.length() {
    pos = skip_ws(tokens, pos)
    match tokens[pos] {
      @token.Token::RightBrace => {
        pos = pos + 1
        break
      }
      @token.Token::EOF => break
      @token.Token::Delim('&') =>
        match skip_nested_rule_block(tokens, pos) {
          Some(next_pos) => pos = next_pos
          None => pos = pos + 1
        }
      _ =>
        // Parse declaration
        match parse_rule_declaration(tokens, pos) {
          Some((property, value, importance, new_pos)) => {
            let decl : @cascade.Declaration = {
              property,
              value: @cascade.Value(value),
              origin: @cascade.Author,
              importance,
              specificity: { a: 0, b: 0, c: 0 }, // Will be set during cascade
              source_order: decl_order,
            }
            declarations.push(decl)
            decl_order = decl_order + 1
            pos = new_pos
          }
          None => pos = pos + 1
        }
    }
  }
  Some((selectors, declarations, pos))
}

///|
/// Parse a declaration within a rule block
fn parse_rule_declaration(
  tokens : Array[@token.Token],
  start : Int,
) -> (String, String, @cascade.Importance, Int)? {
  let mut pos = skip_ws(tokens, start)

  // Get property name
  let property = match tokens[pos] {
    @token.Token::Ident(name) => name
    _ => return None
  }
  pos = pos + 1
  pos = skip_ws(tokens, pos)

  // Expect colon
  match tokens[pos] {
    @token.Token::Colon => pos = pos + 1
    _ => return None
  }
  pos = skip_ws(tokens, pos)

  // Collect value tokens until ';' or '}'
  let value_start = pos
  let mut has_important = false
  while pos < tokens.length() {
    match tokens[pos] {
      @token.Token::Semicolon | @token.Token::RightBrace => break
      @token.Token::EOF => break
      @token.Token::Delim('!') => {
        // Check for !important
        let next_pos = skip_ws(tokens, pos + 1)
        match tokens[next_pos] {
          @token.Token::Ident(s) if s.to_lower() == "important" => {
            has_important = true
            pos = next_pos + 1
            continue
          }
          _ => pos = pos + 1
        }
      }
      _ => pos = pos + 1
    }
  }

  // Extract value (excluding !important)
  let value_end = if has_important {
    // Find where !important starts
    let mut end = value_start
    while end < pos {
      match tokens[end] {
        @token.Token::Delim('!') => break
        _ => end = end + 1
      }
    }
    end
  } else {
    pos
  }
  let value = tokens_to_string(tokens, value_start, value_end).trim().to_owned()

  // Skip semicolon if present
  if pos < tokens.length() {
    match tokens[pos] {
      @token.Token::Semicolon => pos = pos + 1
      _ => ()
    }
  }
  let importance = if has_important {
    @cascade.Important
  } else {
    @cascade.Normal
  }
  Some((property, value, importance, pos))
}

///|
/// Find matching right brace index for a left brace token.
fn find_matching_right_brace(
  tokens : Array[@token.Token],
  left_brace_pos : Int,
  limit : Int,
) -> Int? {
  if left_brace_pos < 0 || left_brace_pos >= tokens.length() {
    return None
  }
  match tokens[left_brace_pos] {
    @token.Token::LeftBrace => ()
    _ => return None
  }
  let mut depth = 1
  let mut pos = left_brace_pos + 1
  while pos < tokens.length() && pos < limit {
    match tokens[pos] {
      @token.Token::LeftBrace => depth = depth + 1
      @token.Token::RightBrace => {
        depth = depth - 1
        if depth == 0 {
          return Some(pos)
        }
      }
      @token.Token::EOF => return None
      _ => ()
    }
    pos = pos + 1
  }
  None
}

///|
/// Skip a nested ampersand rule block like `&::foo { ... }`.
fn skip_nested_rule_block(tokens : Array[@token.Token], start : Int) -> Int? {
  let mut pos = start
  while pos < tokens.length() {
    match tokens[pos] {
      @token.Token::LeftBrace =>
        match find_matching_right_brace(tokens, pos, tokens.length()) {
          Some(close) => return Some(close + 1)
          None => return None
        }
      @token.Token::RightBrace | @token.Token::EOF => return None
      _ => pos = pos + 1
    }
  }
  None
}

///|
fn replace_ampersand(
  nested_selector : String,
  parent_selector : String,
) -> String {
  let out = StringBuilder::new()
  for i = 0; i < nested_selector.length(); i = i + 1 {
    let c = nested_selector[i].to_int().unsafe_to_char()
    if c == '&' {
      out.write_string(parent_selector)
    } else {
      out.write_char(c)
    }
  }
  out.to_string()
}

///|
/// Extract one-level nested `&... { ... }` rules as parsed selector/declaration entries.
fn extract_ampersand_nested_rules(
  tokens : Array[@token.Token],
  rule_start : Int,
  rule_end : Int,
  selectors : Array[(String, @selector.ComplexSelector)],
) -> Array[(String, @selector.ComplexSelector, Array[@cascade.Declaration])] {
  let rules : Array[
    (String, @selector.ComplexSelector, Array[@cascade.Declaration]),
  ] = []
  let mut body_start = -1
  let mut i = rule_start
  while i < rule_end {
    match tokens[i] {
      @token.Token::LeftBrace => {
        body_start = i
        break
      }
      _ => i = i + 1
    }
  }
  if body_start < 0 {
    return rules
  }

  let mut pos = body_start + 1
  while pos < rule_end - 1 {
    pos = skip_ws(tokens, pos)
    if pos >= rule_end - 1 {
      break
    }
    match tokens[pos] {
      @token.Token::Delim('&') => {
        let nested_selector_start = pos
        while pos < rule_end - 1 {
          match tokens[pos] {
            @token.Token::LeftBrace
            | @token.Token::RightBrace
            | @token.Token::EOF => break
            _ => pos = pos + 1
          }
        }
        if pos >= rule_end - 1 {
          break
        }
        match tokens[pos] {
          @token.Token::LeftBrace => {
            let nested_selector_text = tokens_to_string(
                tokens, nested_selector_start, pos,
              )
              .trim()
              .to_owned()
            match find_matching_right_brace(tokens, pos, rule_end) {
              Some(nested_close) => {
                let nested_decls : Array[@cascade.Declaration] = []
                let mut decl_order = 0
                let mut decl_pos = pos + 1
                while decl_pos < nested_close {
                  decl_pos = skip_ws(tokens, decl_pos)
                  if decl_pos >= nested_close {
                    break
                  }
                  match parse_rule_declaration(tokens, decl_pos) {
                    Some((property, value, importance, new_decl_pos)) => {
                      let decl : @cascade.Declaration = {
                        property,
                        value: @cascade.Value(value),
                        origin: @cascade.Author,
                        importance,
                        specificity: { a: 0, b: 0, c: 0 },
                        source_order: decl_order,
                      }
                      nested_decls.push(decl)
                      decl_order = decl_order + 1
                      if new_decl_pos > decl_pos {
                        decl_pos = new_decl_pos
                      } else {
                        decl_pos = decl_pos + 1
                      }
                    }
                    None => decl_pos = decl_pos + 1
                  }
                }
                if !nested_selector_text.is_empty() && !nested_decls.is_empty() {
                  let nested_parts = split_by_comma(nested_selector_text)
                  for parent_item in selectors {
                    let parent_selector = parent_item.0.trim().to_owned()
                    if parent_selector.is_empty() {
                      continue
                    }
                    for nested_part in nested_parts {
                      let nested_trim = nested_part.trim().to_owned()
                      if nested_trim.is_empty() {
                        continue
                      }
                      let expanded = replace_ampersand(
                        nested_trim, parent_selector,
                      )
                      match @selector.parse_selector_text(expanded) {
                        Some(sel) =>
                          rules.push((expanded, sel, nested_decls.copy()))
                        None => ()
                      }
                    }
                  }
                }
                pos = nested_close + 1
                continue
              }
              None => break
            }
          }
          _ => pos = pos + 1
        }
      }
      @token.Token::LeftBrace =>
        match find_matching_right_brace(tokens, pos, rule_end) {
          Some(close) => pos = close + 1
          None => break
        }
      @token.Token::RightBrace | @token.Token::EOF => break
      _ => pos = pos + 1
    }
  }
  rules
}

///|
/// Convert token range to string
fn tokens_to_string(
  tokens : Array[@token.Token],
  start : Int,
  end : Int,
) -> String {
  let buf = StringBuilder::new()
  for i = start; i < end; i = i + 1 {
    match tokens[i] {
      @token.Token::Ident(s) => buf.write_string(s)
      @token.Token::Hash(s, _) => {
        buf.write_char('#')
        buf.write_string(s)
      }
      @token.Token::Delim(c) => buf.write_char(c)
      @token.Token::Colon => buf.write_char(':')
      @token.Token::Comma => buf.write_char(',')
      @token.Token::Whitespace => buf.write_char(' ')
      @token.Token::Dimension(n, u) => {
        buf.write_string(n.to_string())
        buf.write_string(u)
      }
      @token.Token::Number(n, _) => buf.write_string(n.to_string())
      @token.Token::Percentage(n) => {
        buf.write_string(n.to_string())
        buf.write_char('%')
      }
      @token.Token::String(s) => {
        buf.write_char('"')
        buf.write_string(s)
        buf.write_char('"')
      }
      @token.Token::LeftParen => buf.write_char('(')
      @token.Token::RightParen => buf.write_char(')')
      @token.Token::LeftBracket => buf.write_char('[')
      @token.Token::RightBracket => buf.write_char(']')
      @token.Token::Function(name) => {
        buf.write_string(name)
        buf.write_char('(')
      }
      _ => ()
    }
  }
  buf.to_string()
}

///|
/// Parse comma-separated selector list directly from a token slice.
/// Saves the round-trip through `tokens_to_string` +
/// `@token.tokenize` that the previous string-based path required.
fn parse_selector_list_from_tokens(
  tokens : Array[@token.Token],
  start : Int,
  end : Int,
) -> Array[(String, @selector.ComplexSelector)] {
  let result : Array[(String, @selector.ComplexSelector)] = []
  // Find top-level commas; each segment becomes one (text, selector).
  let mut depth = 0
  let mut seg_start = start
  let mut i = start
  while i < end {
    match tokens[i] {
      @token.Token::EOF => break
      @token.Token::Function(_)
      | @token.Token::LeftParen
      | @token.Token::LeftBracket => depth = depth + 1
      @token.Token::RightParen | @token.Token::RightBracket =>
        if depth > 0 {
          depth = depth - 1
        }
      @token.Token::Comma =>
        if depth == 0 {
          push_selector(result, tokens, seg_start, i)
          seg_start = i + 1
        }
      _ => ()
    }
    i = i + 1
  }
  push_selector(result, tokens, seg_start, end)
  result
}

///|
fn push_selector(
  result : Array[(String, @selector.ComplexSelector)],
  tokens : Array[@token.Token],
  start : Int,
  end : Int,
) -> Unit {
  // Use the token-direct selector parser (no re-tokenization).
  // Note `from_tokens` returns Array[ComplexSelector] over each
  // top-level comma; here we feed it a slice that we already know
  // contains a single selector (no top-level comma), so we expect 0 or 1.
  let parsed = @selector.parse_selector_list_from_tokens(tokens, start, end)
  if parsed.is_empty() {
    return
  }
  // Reconstruct the text for the rule's `selector_text` debug field.
  // It's not on the cascade hot path, but downstream code expects a
  // non-empty string here.
  let text = tokens_to_string(tokens, start, end).trim().to_owned()
  for sel in parsed {
    result.push((text, sel))
  }
}

///|
/// Split string by comma
fn split_by_comma(s : String) -> Array[String] {
  let result : Array[String] = []
  let current = StringBuilder::new()
  let mut paren_depth = 0
  let mut bracket_depth = 0
  let mut in_single_quote = false
  let mut in_double_quote = false
  let mut escaped = false
  for i = 0; i < s.length(); i = i + 1 {
    let c = s[i].to_int().unsafe_to_char()
    if escaped {
      current.write_char(c)
      escaped = false
      continue
    }
    if c == '\\' && (in_single_quote || in_double_quote) {
      current.write_char(c)
      escaped = true
      continue
    }
    if in_single_quote {
      current.write_char(c)
      if c == '\'' {
        in_single_quote = false
      }
      continue
    }
    if in_double_quote {
      current.write_char(c)
      if c == '"' {
        in_double_quote = false
      }
      continue
    }

    if c == '\'' {
      in_single_quote = true
      current.write_char(c)
      continue
    }
    if c == '"' {
      in_double_quote = true
      current.write_char(c)
      continue
    }
    if c == '(' {
      paren_depth += 1
      current.write_char(c)
      continue
    }
    if c == ')' {
      if paren_depth > 0 {
        paren_depth -= 1
      }
      current.write_char(c)
      continue
    }
    if c == '[' {
      bracket_depth += 1
      current.write_char(c)
      continue
    }
    if c == ']' {
      if bracket_depth > 0 {
        bracket_depth -= 1
      }
      current.write_char(c)
      continue
    }
    if c == ',' && paren_depth == 0 && bracket_depth == 0 {
      result.push(current.to_string())
      current.reset()
    } else {
      current.write_char(c)
    }
  }
  if current.to_string().length() > 0 {
    result.push(current.to_string())
  }
  result
}

///|
/// Parse result with diagnostics
pub(all) struct ParseResult {
  stylesheet : @cascade.Stylesheet
  diagnostics : @diagnostics.DiagnosticsCollector
}

///|
/// Parse a stylesheet and collect diagnostics
pub fn parse_stylesheet_with_diagnostics(css : String) -> ParseResult {
  let stylesheet = @cascade.Stylesheet::new(@cascade.Author)
  let diagnostics = @diagnostics.DiagnosticsCollector::new()
  let tokens = @token.tokenize(css)
  let mut pos = 0
  while pos < tokens.length() {
    // Skip whitespace
    pos = skip_ws(tokens, pos)
    if pos >= tokens.length() {
      break
    }
    match tokens[pos] {
      @token.Token::EOF => break
      @token.Token::AtKeyword(name) =>
        if name.to_lower() == "media" {
          // Parse @media rule
          pos = parse_media_rule_with_diagnostics(
            stylesheet, diagnostics, tokens, pos,
          )
        } else if name.to_lower() == "supports" {
          // Parse @supports rule (treat condition as always true)
          pos = parse_supports_rule_with_diagnostics(
            stylesheet, diagnostics, tokens, pos,
          )
        } else if is_keyframes_keyword(name) {
          // Parse @keyframes rule
          pos = parse_keyframes_rule(stylesheet, tokens, pos)
        } else {
          // Skip other @-rules (e.g., @import, @font-face)
          pos = skip_at_rule(tokens, pos)
        }
      _ =>
        // Try to parse a style rule
        match parse_style_rule_with_diagnostics(tokens, pos, diagnostics) {
          Some((selectors, declarations, new_pos)) => {
            // Add rules for each selector
            for item in selectors {
              let selector_text = item.0
              let selector = item.1
              stylesheet.add_rule(selector_text, selector, declarations.copy())
            }
            pos = new_pos
          }
          None => pos = pos + 1
        }
    }
  }
  { stylesheet, diagnostics }
}

///|
/// Parse an @media rule with diagnostics
fn parse_media_rule_with_diagnostics(
  stylesheet : @cascade.Stylesheet,
  diagnostics : @diagnostics.DiagnosticsCollector,
  tokens : Array[@token.Token],
  start : Int,
) -> Int {
  let mut pos = start + 1 // Skip @media token

  // Collect media query tokens until '{'
  let query_start = pos
  while pos < tokens.length() {
    match tokens[pos] {
      @token.Token::LeftBrace => break
      @token.Token::EOF => return pos
      _ => pos = pos + 1
    }
  }
  if pos >= tokens.length() {
    return pos
  }

  // Extract and parse media query
  let query_text = tokens_to_string(tokens, query_start, pos).trim().to_owned()
  let media_query = @media.parse_media_query_list(query_text)

  // Skip '{'
  pos = pos + 1

  // Parse rules inside @media block until '}'
  let mut brace_depth = 1
  while pos < tokens.length() && brace_depth > 0 {
    pos = skip_ws(tokens, pos)
    if pos >= tokens.length() {
      break
    }
    match tokens[pos] {
      @token.Token::RightBrace => {
        brace_depth = brace_depth - 1
        pos = pos + 1
        if brace_depth <= 0 {
          break
        }
      }
      @token.Token::LeftBrace => {
        brace_depth = brace_depth + 1
        pos = pos + 1
      }
      @token.Token::EOF => break
      @token.Token::AtKeyword(at_name) =>
        if at_name.to_lower() == "supports" {
          pos = parse_supports_rule_with_media(
            stylesheet, tokens, pos, media_query,
          )
        } else {
          // Skip other nested @-rules inside @media
          pos = skip_at_rule(tokens, pos)
        }
      _ =>
        // Try to parse a style rule
        match parse_style_rule_with_diagnostics(tokens, pos, diagnostics) {
          Some((selectors, declarations, new_pos)) => {
            // Add rules with media query for each selector
            for item in selectors {
              let selector_text = item.0
              let selector = item.1
              stylesheet.add_rule_with_media(
                selector_text,
                selector,
                declarations.copy(),
                media_query,
              )
            }
            pos = new_pos
          }
          None => pos = pos + 1
        }
    }
  }
  pos
}

///|
/// Parse an @supports rule with diagnostics.
/// The condition is treated as always true (minimal implementation).
fn parse_supports_rule_with_diagnostics(
  stylesheet : @cascade.Stylesheet,
  diagnostics : @diagnostics.DiagnosticsCollector,
  tokens : Array[@token.Token],
  start : Int,
) -> Int {
  let mut pos = start + 1 // Skip @supports token

  // Skip condition tokens until '{' (handling nested parens)
  let condition_start = pos
  pos = skip_supports_condition_to_block(tokens, pos)
  if pos >= tokens.length() {
    return pos
  }
  let condition_text = tokens_to_string(tokens, condition_start, pos)
  if !supports_condition_matches(condition_text) {
    return skip_at_rule(tokens, start)
  }

  // Skip '{'
  pos = pos + 1

  // Parse rules inside @supports block until '}'
  let mut brace_depth = 1
  while pos < tokens.length() && brace_depth > 0 {
    pos = skip_ws(tokens, pos)
    if pos >= tokens.length() {
      break
    }
    match tokens[pos] {
      @token.Token::RightBrace => {
        brace_depth -= 1
        pos = pos + 1
        if brace_depth <= 0 {
          break
        }
      }
      @token.Token::EOF => break
      @token.Token::AtKeyword(name) =>
        if name.to_lower() == "media" {
          pos = parse_media_rule_with_diagnostics(
            stylesheet, diagnostics, tokens, pos,
          )
        } else if name.to_lower() == "supports" {
          pos = parse_supports_rule_with_diagnostics(
            stylesheet, diagnostics, tokens, pos,
          )
        } else {
          pos = skip_at_rule(tokens, pos)
        }
      _ =>
        match parse_style_rule_with_diagnostics(tokens, pos, diagnostics) {
          Some((selectors, declarations, new_pos)) => {
            for item in selectors {
              let selector_text = item.0
              let selector = item.1
              stylesheet.add_rule(selector_text, selector, declarations.copy())
            }
            pos = new_pos
          }
          None => pos = pos + 1
        }
    }
  }
  pos
}

///|
/// Parse a style rule with diagnostics
fn parse_style_rule_with_diagnostics(
  tokens : Array[@token.Token],
  start : Int,
  diagnostics : @diagnostics.DiagnosticsCollector,
) -> (
  Array[(String, @selector.ComplexSelector)],
  Array[@cascade.Declaration],
  Int,
)? {
  let mut pos = start

  // Collect selector tokens until '{'
  let selector_start = pos
  while pos < tokens.length() {
    match tokens[pos] {
      @token.Token::LeftBrace => break
      @token.Token::EOF => return None
      _ => pos = pos + 1
    }
  }
  if pos >= tokens.length() {
    return None
  }

  // Parse selectors directly from the already-lexed token stream —
  // avoids stringifying then re-tokenizing each selector.
  let selectors = parse_selector_list_from_tokens(tokens, selector_start, pos)
  if selectors.is_empty() {
    return None
  }

  // Get first selector for diagnostic context
  let selector_context = if selectors.length() > 0 {
    selectors[0].0
  } else {
    ""
  }

  // Skip '{'
  pos = pos + 1

  // Parse declarations until '}'
  let declarations : Array[@cascade.Declaration] = []
  let mut decl_order = 0
  while pos < tokens.length() {
    pos = skip_ws(tokens, pos)
    match tokens[pos] {
      @token.Token::RightBrace => {
        pos = pos + 1
        break
      }
      @token.Token::EOF => break
      _ =>
        // Parse declaration
        match parse_rule_declaration(tokens, pos) {
          Some((property, value, importance, new_pos)) => {
            // Add diagnostic
            diagnostics.add_property(property, value, selector_context)
            let decl : @cascade.Declaration = {
              property,
              value: @cascade.Value(value),
              origin: @cascade.Author,
              importance,
              specificity: { a: 0, b: 0, c: 0 },
              source_order: decl_order,
            }
            declarations.push(decl)
            decl_order = decl_order + 1
            pos = new_pos
          }
          None => pos = pos + 1
        }
    }
  }
  Some((selectors, declarations, pos))
}