//! `colons` rule: limit spaces around `:` (and explicit `?`) in mappings.
//!
//! Alias-key special case: a YAML anchor/alias name may legally contain `:` (only the
//! flow indicators `,[]{}` are excluded — YAML 1.2.2 §6.9.2 "Node Anchors", §7.1 "Alias
//! Nodes"), so `*foo:` is the alias to an anchor named `foo:`. Using an alias as a
//! mapping key therefore *requires* one separating space — `*foo : bar` — which must not
//! be reported. We take alias extents from the parser (`collect_alias_ends`) rather than
//! guessing from raw text, so the exemption fires exactly when an alias node ends one
//! space before the colon — matching yamllint's `colons.py` `AliasToken` rule (see
//! adrienverge/yamllint#226).
use std::collections::HashSet;

use crate::config::YamlLintConfig;
use crate::rules::support::punctuation::{
    build_line_starts, collect_alias_ends, collect_scalar_ranges, line_and_column,
    skip_comment,
};
use crate::rules::support::span_utils::{CharPos, containing_scalar_range};

pub const ID: &str = "colons";
const TOO_MANY_BEFORE: &str = "too many spaces before colon";
const TOO_MANY_AFTER: &str = "too many spaces after colon";
const TOO_MANY_AFTER_QUESTION: &str = "too many spaces after question mark";

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Config {
    max_spaces_before: i64,
    max_spaces_after: i64,
}

impl Config {
    const DEFAULT_MAX_BEFORE: i64 = 0;
    const DEFAULT_MAX_AFTER: i64 = 1;

    #[must_use]
    pub fn resolve(cfg: &YamlLintConfig) -> Self {
        Self {
            max_spaces_before: cfg.rule_option_int(
                ID,
                "max-spaces-before",
                Self::DEFAULT_MAX_BEFORE,
            ),
            max_spaces_after: cfg.rule_option_int(
                ID,
                "max-spaces-after",
                Self::DEFAULT_MAX_AFTER,
            ),
        }
    }

    #[must_use]
    pub const fn new_for_tests(max_spaces_before: i64, max_spaces_after: i64) -> Self {
        Self {
            max_spaces_before,
            max_spaces_after,
        }
    }

    #[must_use]
    pub const fn max_spaces_before(&self) -> i64 {
        self.max_spaces_before
    }

    #[must_use]
    pub const fn max_spaces_after(&self) -> i64 {
        self.max_spaces_after
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Violation {
    pub line: usize,
    pub column: usize,
    pub message: String,
}

enum BeforeResult {
    SameLine { spaces: usize },
    Ignored,
}

enum AfterResult {
    SameLine { spaces: usize, next_char: usize },
    Ignored,
}

#[must_use]
pub fn check(buffer: &str, cfg: &Config) -> Vec<Violation> {
    if buffer.is_empty() {
        return Vec::new();
    }

    let scalar_ranges = collect_scalar_ranges(buffer);
    let alias_ends: HashSet<usize> = collect_alias_ends(buffer)
        .iter()
        .map(|pos| pos.get())
        .collect();
    let chars: Vec<(usize, char)> = buffer.char_indices().collect();
    let line_starts = build_line_starts(&chars);

    let mut scalar_idx = 0usize;
    let mut idx = 0usize;
    let mut violations = Vec::new();

    while idx < chars.len() {
        if let Some(range) =
            containing_scalar_range(&scalar_ranges, &mut scalar_idx, idx)
        {
            idx = range.end.get();
            continue;
        }

        match chars[idx].1 {
            '#' => {
                idx = skip_comment(&chars, idx);
                continue;
            }
            ':' => {
                evaluate_colon(
                    cfg,
                    &mut violations,
                    &chars,
                    idx,
                    &line_starts,
                    &alias_ends,
                );
            }
            '?' => {
                evaluate_question_mark(cfg, &mut violations, &chars, idx, &line_starts);
            }
            _ => {}
        }

        idx += 1;
    }

    violations
}

fn evaluate_colon(
    cfg: &Config,
    violations: &mut Vec<Violation>,
    chars: &[(usize, char)],
    colon_idx: usize,
    line_starts: &[CharPos],
    alias_ends: &HashSet<usize>,
) {
    // An alias used as a mapping key needs exactly one separating space (`*foo : bar`);
    // skip both checks when an alias node ends one char before this colon.
    if colon_idx
        .checked_sub(1)
        .is_some_and(|prev| alias_ends.contains(&prev))
    {
        return;
    }

    if cfg.max_spaces_before >= 0
        && let BeforeResult::SameLine { spaces } =
            compute_spaces_before(chars, colon_idx)
    {
        let spaces_i64 = i64::try_from(spaces).unwrap_or(i64::MAX);
        if spaces_i64 > cfg.max_spaces_before {
            let (line, column) = line_and_column(line_starts, CharPos::new(colon_idx));
            let highlight_column = column.saturating_sub(1).max(1);
            violations.push(Violation {
                line,
                column: highlight_column,
                message: TOO_MANY_BEFORE.to_string(),
            });
        }
    }

    if cfg.max_spaces_after >= 0
        && let AfterResult::SameLine { spaces, next_char } =
            compute_spaces_after(chars, colon_idx)
    {
        if chars[next_char].1 == '#' {
            return;
        }
        let spaces_i64 = i64::try_from(spaces).unwrap_or(i64::MAX);
        if spaces_i64 > cfg.max_spaces_after {
            let (line, column) = line_and_column(line_starts, CharPos::new(next_char));
            let highlight_column = column.saturating_sub(1).max(1);
            violations.push(Violation {
                line,
                column: highlight_column,
                message: TOO_MANY_AFTER.to_string(),
            });
        }
    }
}

fn evaluate_question_mark(
    cfg: &Config,
    violations: &mut Vec<Violation>,
    chars: &[(usize, char)],
    question_idx: usize,
    line_starts: &[CharPos],
) {
    if cfg.max_spaces_after >= 0
        && is_explicit_question_mark(chars, question_idx)
        && let AfterResult::SameLine { spaces, next_char } =
            compute_spaces_after(chars, question_idx)
    {
        let spaces_i64 = i64::try_from(spaces).unwrap_or(i64::MAX);
        if spaces_i64 > cfg.max_spaces_after {
            let (line, column) = line_and_column(line_starts, CharPos::new(next_char));
            let highlight_column = column.saturating_sub(1).max(1);
            violations.push(Violation {
                line,
                column: highlight_column,
                message: TOO_MANY_AFTER_QUESTION.to_string(),
            });
        }
    }
}

fn compute_spaces_before(chars: &[(usize, char)], colon_idx: usize) -> BeforeResult {
    let mut spaces = 0usize;
    let mut idx = colon_idx;

    while let Some(prev) = idx.checked_sub(1) {
        match chars[prev].1 {
            ' ' | '\t' => {
                spaces += 1;
                idx = prev;
            }
            '\n' | '\r' => return BeforeResult::Ignored,
            _ => return BeforeResult::SameLine { spaces },
        }
    }
    BeforeResult::SameLine { spaces }
}

fn compute_spaces_after(chars: &[(usize, char)], start_idx: usize) -> AfterResult {
    let mut spaces = 0usize;
    let mut idx = start_idx + 1;

    while idx < chars.len() {
        let ch = chars[idx].1;
        match ch {
            ' ' | '\t' => {
                spaces += 1;
                idx += 1;
            }
            '\n' => return AfterResult::Ignored,
            '\r' => {
                if idx + 1 < chars.len() && chars[idx + 1].1 == '\n' {
                    return AfterResult::Ignored;
                }
                return AfterResult::Ignored;
            }
            _ => {
                return AfterResult::SameLine {
                    spaces,
                    next_char: idx,
                };
            }
        }
    }

    AfterResult::Ignored
}

fn is_explicit_question_mark(chars: &[(usize, char)], idx: usize) -> bool {
    let next = chars.get(idx + 1).map_or('\0', |(_, ch)| *ch);
    if !(matches!(next, ' ' | '\t' | '\n' | '\r')) {
        return false;
    }

    match prev_non_ws_same_line(chars, idx) {
        None => true,
        Some((prev_idx, prev_ch)) => {
            matches!(prev_ch, '[' | '{' | ',' | '?')
                || (prev_ch == '-' && is_sequence_indicator(chars, prev_idx))
        }
    }
}

fn prev_non_ws_same_line(chars: &[(usize, char)], idx: usize) -> Option<(usize, char)> {
    let mut cursor = idx;
    while let Some(prev) = cursor.checked_sub(1) {
        let ch = chars[prev].1;
        match ch {
            ' ' | '\t' => {
                cursor = prev;
            }
            '\n' | '\r' => return None,
            _ => return Some((prev, ch)),
        }
    }
    None
}

fn is_sequence_indicator(chars: &[(usize, char)], hyphen_idx: usize) -> bool {
    let mut cursor = hyphen_idx;
    while let Some(prev) = cursor.checked_sub(1) {
        let ch = chars[prev].1;
        match ch {
            ' ' | '\t' => cursor = prev,
            '\n' | '\r' => return true,
            _ => return false,
        }
    }
    true
}

#[doc(hidden)]
#[must_use]
pub fn coverage_is_explicit_question_mark(chars: &[(usize, char)], idx: usize) -> bool {
    is_explicit_question_mark(chars, idx)
}

#[doc(hidden)]
#[must_use]
pub fn coverage_is_sequence_indicator(chars: &[(usize, char)], idx: usize) -> bool {
    is_sequence_indicator(chars, idx)
}

#[doc(hidden)]
#[must_use]
pub fn coverage_evaluate_question_mark(buffer: &str, cfg: &Config) -> Vec<Violation> {
    let chars: Vec<(usize, char)> = buffer.char_indices().collect();
    let mut violations = Vec::new();
    let line_starts = build_line_starts(&chars);
    if let Some((idx, _)) = chars.iter().enumerate().find(|(_, (_, ch))| *ch == '?') {
        evaluate_question_mark(cfg, &mut violations, &chars, idx, &line_starts);
    } else {
        // explicit branch to ensure coverage marks the absence case
        let () = ();
    }
    violations
}

#[doc(hidden)]
#[must_use]
pub fn coverage_skip_comment(buffer: &str) -> bool {
    let chars: Vec<(usize, char)> = buffer.char_indices().collect();
    let idx = skip_comment(&chars, 0);
    chars.get(idx).is_some_and(|(_, ch)| *ch == '\n')
}
