//! Advanced normalized command data structures.
//!
//! This module is mainly useful for tools that need to inspect command-mode
//! decisions, flag normalization, or positional argument classification after
//! semantic analysis.

use mel_ast::ShellWord;
use mel_syntax::{SourceView, TextRange, range_end, range_start, text_range};
use std::sync::Arc;

use crate::{
    CommandModeMask, CommandSchema, Diagnostic, DiagnosticSeverity, FlagArity, FlagArityByMode,
    PositionalSchema, PositionalSourcePolicy, PositionalTailSchema, ScopeId,
    ValidatedCommandSchema, ValueShape, command_schema::CommandKind,
};

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CommandMode {
    Create,
    Edit,
    Query,
    Unknown,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PositionalArg {
    pub word: ShellWord,
    pub range: TextRange,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NormalizedFlag {
    pub source_range: TextRange,
    pub canonical_name: Option<Arc<str>>,
    pub args: Vec<PositionalArg>,
    pub range: TextRange,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum NormalizedCommandItem {
    Flag(NormalizedFlag),
    Positional(PositionalArg),
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NormalizedCommandInvoke {
    pub range: TextRange,
    pub scope: ScopeId,
    pub head_range: TextRange,
    pub schema_name: Arc<str>,
    pub kind: CommandKind,
    pub mode: CommandMode,
    pub items: Vec<NormalizedCommandItem>,
}

#[derive(Debug, Clone, Copy)]
struct BorrowedPositionalArg<'a> {
    word: &'a ShellWord,
    range: TextRange,
}

#[derive(Debug, Clone, Copy)]
struct SyntheticFlagSchema {
    long_name: &'static str,
    mode_mask: CommandModeMask,
    arity_by_mode: FlagArityByMode,
    allows_multiple: bool,
}

#[derive(Debug, Clone, Copy)]
enum ResolvedFlagSchema<'a> {
    Borrowed(&'a crate::FlagSchema),
    Synthetic(SyntheticFlagSchema),
}

impl ResolvedFlagSchema<'_> {
    fn long_name(&self) -> &str {
        match self {
            Self::Borrowed(schema) => &schema.long_name,
            Self::Synthetic(schema) => schema.long_name,
        }
    }

    fn canonical_name(&self) -> Arc<str> {
        match self {
            Self::Borrowed(schema) => schema.long_name.clone(),
            Self::Synthetic(schema) => Arc::from(schema.long_name),
        }
    }

    fn mode_mask(self) -> CommandModeMask {
        match self {
            Self::Borrowed(schema) => schema.mode_mask,
            Self::Synthetic(schema) => schema.mode_mask,
        }
    }

    fn arity_by_mode(self) -> FlagArityByMode {
        match self {
            Self::Borrowed(schema) => schema.arity_by_mode,
            Self::Synthetic(schema) => schema.arity_by_mode,
        }
    }

    fn allows_multiple(self) -> bool {
        match self {
            Self::Borrowed(schema) => schema.allows_multiple,
            Self::Synthetic(schema) => schema.allows_multiple,
        }
    }
}

fn push_primary_diagnostic(
    diagnostics: &mut Vec<Diagnostic>,
    severity: DiagnosticSeverity,
    range: TextRange,
    message: impl Into<Arc<str>>,
) {
    push_primary_diagnostic_filtered(diagnostics, severity, range, message, true);
}

fn push_primary_diagnostic_filtered(
    diagnostics: &mut Vec<Diagnostic>,
    severity: DiagnosticSeverity,
    range: TextRange,
    message: impl Into<Arc<str>>,
    include_warnings: bool,
) {
    if severity == DiagnosticSeverity::Warning && !include_warnings {
        return;
    }
    let message = message.into();
    diagnostics.push(Diagnostic {
        severity,
        message: message.clone(),
        range,
        labels: vec![crate::DiagnosticLabel {
            range,
            message,
            is_primary: true,
        }],
    });
}

pub(crate) fn normalize_shell_like_invoke(
    command: &ValidatedCommandSchema,
    scope: ScopeId,
    head_range: TextRange,
    words: &[ShellWord],
    range: TextRange,
    source: SourceView<'_>,
) -> (NormalizedCommandInvoke, Vec<Diagnostic>) {
    let mut diagnostics = Vec::new();
    let mut items = Vec::new();
    let mut seen_flags = Vec::<ResolvedFlagSchema<'_>>::new();
    let mut positional_args = Vec::<BorrowedPositionalArg<'_>>::new();
    let (create_ranges, edit_ranges, query_ranges) =
        collect_mode_flag_ranges(command, words, source);
    let active_mode_count = usize::from(!create_ranges.is_empty())
        + usize::from(!edit_ranges.is_empty())
        + usize::from(!query_ranges.is_empty());
    let mode = match active_mode_count {
        0 => CommandMode::Create,
        1 if !create_ranges.is_empty() => CommandMode::Create,
        1 if !edit_ranges.is_empty() => CommandMode::Edit,
        1 if !query_ranges.is_empty() => CommandMode::Query,
        _ => {
            diagnostics.push(Diagnostic {
                severity: DiagnosticSeverity::Error,
                message: format!(
                    "command \"{}\" cannot combine create/edit/query mode flags",
                    command.name
                )
                .into(),
                range,
                labels: vec![crate::DiagnosticLabel {
                    range,
                    message: format!(
                        "command \"{}\" cannot combine create/edit/query mode flags",
                        command.name
                    )
                    .into(),
                    is_primary: true,
                }],
            });
            CommandMode::Unknown
        }
    };
    let mut index = 0;

    while index < words.len() {
        match &words[index] {
            ShellWord::Flag {
                text,
                range: flag_range,
            } => {
                let flag_text = source.slice(*text);
                let Some(schema) = find_flag_schema(command, flag_text) else {
                    diagnostics.push(Diagnostic {
                        severity: DiagnosticSeverity::Warning,
                        message: format!(
                            "unknown flag \"{flag_text}\" for command \"{}\"",
                            command.name
                        )
                        .into(),
                        range: *flag_range,
                        labels: vec![crate::DiagnosticLabel {
                            range: *flag_range,
                            message: format!(
                                "unknown flag \"{flag_text}\" for command \"{}\"",
                                command.name
                            )
                            .into(),
                            is_primary: true,
                        }],
                    });
                    items.push(NormalizedCommandItem::Flag(NormalizedFlag {
                        source_range: *flag_range,
                        canonical_name: None,
                        args: Vec::new(),
                        range: *flag_range,
                    }));
                    index += 1;
                    continue;
                };

                if !schema.allows_multiple()
                    && seen_flags
                        .iter()
                        .any(|seen_schema| seen_schema.long_name() == schema.long_name())
                {
                    diagnostics.push(Diagnostic {
                        severity: DiagnosticSeverity::Error,
                        message: format!(
                            "flag \"-{0}\" cannot be repeated for command \"{1}\"",
                            schema.long_name(),
                            command.name
                        )
                        .into(),
                        range: *flag_range,
                        labels: vec![crate::DiagnosticLabel {
                            range: *flag_range,
                            message: format!(
                                "flag \"-{0}\" cannot be repeated for command \"{1}\"",
                                schema.long_name(),
                                command.name
                            )
                            .into(),
                            is_primary: true,
                        }],
                    });
                } else {
                    seen_flags.push(schema);
                }

                let expected_arity = arity_for_mode(schema.arity_by_mode(), mode);
                let (min_arity, max_arity) = arity_bounds(expected_arity);
                let mut args = Vec::new();
                let mut consumed = 0;
                while consumed < max_arity {
                    let next_index = index + 1 + consumed;
                    let Some(next_word) = words.get(next_index) else {
                        break;
                    };
                    if matches!(next_word, ShellWord::Flag { .. }) {
                        break;
                    }
                    args.push(BorrowedPositionalArg {
                        word: next_word,
                        range: shell_word_range(next_word),
                    });
                    consumed += 1;
                }

                let owned_args = args
                    .iter()
                    .map(|arg| PositionalArg {
                        word: arg.word.clone(),
                        range: arg.range,
                    })
                    .collect::<Vec<_>>();

                if args.len() < min_arity {
                    diagnostics.push(Diagnostic {
                        severity: if matches!(mode, CommandMode::Query) {
                            DiagnosticSeverity::Warning
                        } else {
                            DiagnosticSeverity::Error
                        },
                        message: format!(
                            "flag \"-{0}\" expects {1} argument(s) for command \"{2}\"",
                            schema.long_name(),
                            format_arity(expected_arity),
                            command.name
                        )
                        .into(),
                        range: *flag_range,
                        labels: vec![crate::DiagnosticLabel {
                            range: *flag_range,
                            message: format!(
                                "flag \"-{0}\" expects {1} argument(s) for command \"{2}\"",
                                schema.long_name(),
                                format_arity(expected_arity),
                                command.name
                            )
                            .into(),
                            is_primary: true,
                        }],
                    });
                }

                let item_range = args.last().map_or(*flag_range, |arg| {
                    text_range(range_start(*flag_range), range_end(arg.range))
                });
                items.push(NormalizedCommandItem::Flag(NormalizedFlag {
                    source_range: *flag_range,
                    canonical_name: Some(schema.canonical_name()),
                    args: owned_args,
                    range: item_range,
                }));
                index += 1 + consumed;
            }
            word => {
                let positional_arg = BorrowedPositionalArg {
                    word,
                    range: shell_word_range(word),
                };
                positional_args.push(positional_arg);
                items.push(NormalizedCommandItem::Positional(PositionalArg {
                    word: word.clone(),
                    range: positional_arg.range,
                }));
                index += 1;
            }
        }
    }

    if !mode_allows(command.mode_mask, mode) {
        diagnostics.push(Diagnostic {
            severity: if matches!(mode, CommandMode::Query) {
                DiagnosticSeverity::Warning
            } else {
                DiagnosticSeverity::Error
            },
            message: format!(
                "command \"{}\" is not available in {} mode",
                command.name,
                mode_label(mode)
            )
            .into(),
            range,
            labels: vec![crate::DiagnosticLabel {
                range,
                message: format!(
                    "command \"{}\" is not available in {} mode",
                    command.name,
                    mode_label(mode)
                )
                .into(),
                is_primary: true,
            }],
        });
    }

    for item in &items {
        let NormalizedCommandItem::Flag(flag) = item else {
            continue;
        };
        let Some(canonical_name) = flag.canonical_name.as_deref() else {
            continue;
        };
        let Some(schema) = find_flag_schema_by_canonical_name(command, canonical_name) else {
            continue;
        };
        if !mode_allows(schema.mode_mask(), mode) {
            diagnostics.push(Diagnostic {
                severity: if matches!(mode, CommandMode::Query) {
                    DiagnosticSeverity::Warning
                } else {
                    DiagnosticSeverity::Error
                },
                message: format!(
                    "flag \"-{0}\" is not available in {1} mode for command \"{2}\"",
                    schema.long_name(),
                    mode_label(mode),
                    command.name
                )
                .into(),
                range: flag.source_range,
                labels: vec![crate::DiagnosticLabel {
                    range: flag.source_range,
                    message: format!(
                        "flag \"-{0}\" is not available in {1} mode for command \"{2}\"",
                        schema.long_name(),
                        mode_label(mode),
                        command.name
                    )
                    .into(),
                    is_primary: true,
                }],
            });
        }
    }

    let positional_args = positional_args.iter().collect::<Vec<_>>();
    validate_positionals(
        command,
        &command.positionals,
        &positional_args,
        range,
        &mut diagnostics,
        source,
    );

    (
        NormalizedCommandInvoke {
            range,
            scope,
            head_range,
            schema_name: command.name.clone(),
            kind: command.kind,
            mode,
            items,
        },
        diagnostics,
    )
}

pub(crate) fn collect_command_diagnostics(
    command: &CommandSchema,
    words: &[ShellWord],
    range: TextRange,
    source: SourceView<'_>,
    include_warnings: bool,
) -> Vec<Diagnostic> {
    let mut diagnostics = Vec::new();
    let mut seen_flags = Vec::<ResolvedFlagSchema<'_>>::new();
    let mut positional_args = Vec::<BorrowedPositionalArg<'_>>::new();
    let mut seen_flag_instances = Vec::<(TextRange, Option<ResolvedFlagSchema<'_>>)>::new();
    let (create_ranges, edit_ranges, query_ranges) =
        collect_mode_flag_ranges(command, words, source);
    let active_mode_count = usize::from(!create_ranges.is_empty())
        + usize::from(!edit_ranges.is_empty())
        + usize::from(!query_ranges.is_empty());
    let mode = match active_mode_count {
        0 => CommandMode::Create,
        1 if !create_ranges.is_empty() => CommandMode::Create,
        1 if !edit_ranges.is_empty() => CommandMode::Edit,
        1 if !query_ranges.is_empty() => CommandMode::Query,
        _ => {
            push_primary_diagnostic(
                &mut diagnostics,
                DiagnosticSeverity::Error,
                range,
                format!(
                    "command \"{}\" cannot combine create/edit/query mode flags",
                    command.name
                ),
            );
            CommandMode::Unknown
        }
    };
    let mut index = 0;

    while index < words.len() {
        match &words[index] {
            ShellWord::Flag {
                text,
                range: flag_range,
            } => {
                let flag_text = source.slice(*text);
                let Some(schema) = find_flag_schema(command, flag_text) else {
                    push_primary_diagnostic_filtered(
                        &mut diagnostics,
                        DiagnosticSeverity::Warning,
                        *flag_range,
                        format!(
                            "unknown flag \"{flag_text}\" for command \"{}\"",
                            command.name
                        ),
                        include_warnings,
                    );
                    seen_flag_instances.push((*flag_range, None));
                    index += 1;
                    continue;
                };

                if !schema.allows_multiple()
                    && seen_flags
                        .iter()
                        .any(|seen_schema| seen_schema.long_name() == schema.long_name())
                {
                    push_primary_diagnostic(
                        &mut diagnostics,
                        DiagnosticSeverity::Error,
                        *flag_range,
                        format!(
                            "flag \"-{0}\" cannot be repeated for command \"{1}\"",
                            schema.long_name(),
                            command.name
                        ),
                    );
                } else {
                    seen_flags.push(schema);
                }

                let expected_arity = arity_for_mode(schema.arity_by_mode(), mode);
                let (min_arity, max_arity) = arity_bounds(expected_arity);
                let mut args = Vec::new();
                let mut consumed = 0;
                while consumed < max_arity {
                    let next_index = index + 1 + consumed;
                    let Some(next_word) = words.get(next_index) else {
                        break;
                    };
                    if matches!(next_word, ShellWord::Flag { .. }) {
                        break;
                    }
                    args.push(BorrowedPositionalArg {
                        word: next_word,
                        range: shell_word_range(next_word),
                    });
                    consumed += 1;
                }

                if args.len() < min_arity {
                    push_primary_diagnostic_filtered(
                        &mut diagnostics,
                        if matches!(mode, CommandMode::Query) {
                            DiagnosticSeverity::Warning
                        } else {
                            DiagnosticSeverity::Error
                        },
                        *flag_range,
                        format!(
                            "flag \"-{0}\" expects {1} argument(s) for command \"{2}\"",
                            schema.long_name(),
                            format_arity(expected_arity),
                            command.name
                        ),
                        include_warnings,
                    );
                }
                seen_flag_instances.push((*flag_range, Some(schema)));
                index += 1 + consumed;
            }
            word => {
                positional_args.push(BorrowedPositionalArg {
                    word,
                    range: shell_word_range(word),
                });
                index += 1;
            }
        }
    }

    if !mode_allows(command.mode_mask, mode) {
        push_primary_diagnostic_filtered(
            &mut diagnostics,
            if matches!(mode, CommandMode::Query) {
                DiagnosticSeverity::Warning
            } else {
                DiagnosticSeverity::Error
            },
            range,
            format!(
                "command \"{}\" is not available in {} mode",
                command.name,
                mode_label(mode)
            ),
            include_warnings,
        );
    }

    for (flag_range, schema) in seen_flag_instances {
        let Some(schema) = schema else {
            continue;
        };
        if !mode_allows(schema.mode_mask(), mode) {
            push_primary_diagnostic_filtered(
                &mut diagnostics,
                if matches!(mode, CommandMode::Query) {
                    DiagnosticSeverity::Warning
                } else {
                    DiagnosticSeverity::Error
                },
                flag_range,
                format!(
                    "flag \"-{0}\" is not available in {1} mode for command \"{2}\"",
                    schema.long_name(),
                    mode_label(mode),
                    command.name
                ),
                include_warnings,
            );
        }
    }

    let positional_args = positional_args.iter().collect::<Vec<_>>();
    validate_positionals(
        command,
        &command.positionals,
        &positional_args,
        range,
        &mut diagnostics,
        source,
    );

    diagnostics
}

fn find_flag_schema<'a>(command: &'a CommandSchema, text: &str) -> Option<ResolvedFlagSchema<'a>> {
    let normalized = text.strip_prefix('-').unwrap_or(text);
    command
        .flags
        .iter()
        .find(|flag| {
            normalized == flag.long_name.as_ref()
                || flag
                    .short_name
                    .as_deref()
                    .is_some_and(|short| short == normalized)
        })
        .map(ResolvedFlagSchema::Borrowed)
        .or_else(|| synthetic_mode_flag_for_name(command, normalized))
}

fn find_flag_schema_by_canonical_name<'a>(
    command: &'a CommandSchema,
    canonical_name: &str,
) -> Option<ResolvedFlagSchema<'a>> {
    command
        .flags
        .iter()
        .find(|flag| flag.long_name.as_ref() == canonical_name)
        .map(ResolvedFlagSchema::Borrowed)
        .or_else(|| synthetic_mode_flag_for_name(command, canonical_name))
}

fn synthetic_mode_flag_for_name(
    command: &CommandSchema,
    name: &str,
) -> Option<ResolvedFlagSchema<'static>> {
    match name {
        "create" | "c" if command.mode_mask.create => Some(ResolvedFlagSchema::Synthetic(
            synthetic_mode_flag("create", "c"),
        )),
        "edit" | "e" if command.mode_mask.edit => Some(ResolvedFlagSchema::Synthetic(
            synthetic_mode_flag("edit", "e"),
        )),
        "query" | "q" if command.mode_mask.query => Some(ResolvedFlagSchema::Synthetic(
            synthetic_mode_flag("query", "q"),
        )),
        _ => None,
    }
}

fn synthetic_mode_flag(long_name: &'static str, short_name: &'static str) -> SyntheticFlagSchema {
    let _ = short_name;
    SyntheticFlagSchema {
        long_name,
        mode_mask: CommandModeMask {
            create: true,
            edit: true,
            query: true,
        },
        arity_by_mode: FlagArityByMode {
            create: FlagArity::None,
            edit: FlagArity::None,
            query: FlagArity::None,
        },
        allows_multiple: false,
    }
}

fn collect_mode_flag_ranges(
    command: &CommandSchema,
    words: &[ShellWord],
    source: SourceView<'_>,
) -> (Vec<TextRange>, Vec<TextRange>, Vec<TextRange>) {
    let mut create_ranges = Vec::new();
    let mut edit_ranges = Vec::new();
    let mut query_ranges = Vec::new();

    for word in words {
        let ShellWord::Flag { text, range } = word else {
            continue;
        };
        let Some(schema) = find_flag_schema(command, source.slice(*text)) else {
            continue;
        };
        match schema.long_name() {
            "create" => create_ranges.push(*range),
            "edit" => edit_ranges.push(*range),
            "query" => query_ranges.push(*range),
            _ => {}
        }
    }

    (create_ranges, edit_ranges, query_ranges)
}

fn arity_for_mode(arity_by_mode: FlagArityByMode, mode: CommandMode) -> FlagArity {
    match mode {
        CommandMode::Create | CommandMode::Unknown => arity_by_mode.create,
        CommandMode::Edit => arity_by_mode.edit,
        CommandMode::Query => arity_by_mode.query,
    }
}

fn arity_bounds(arity: FlagArity) -> (usize, usize) {
    match arity {
        FlagArity::None => (0, 0),
        FlagArity::Exact(value) => {
            let value = usize::from(value);
            (value, value)
        }
        FlagArity::Range { min, max } => (usize::from(min), usize::from(max)),
    }
}

fn format_arity(arity: FlagArity) -> String {
    match arity {
        FlagArity::None => "0".to_owned(),
        FlagArity::Exact(value) => value.to_string(),
        FlagArity::Range { min, max } if min == max => min.to_string(),
        FlagArity::Range { min, max } => format!("{min} to {max}"),
    }
}

fn validate_positionals(
    command: &CommandSchema,
    schema: &PositionalSchema,
    positional_args: &[&BorrowedPositionalArg<'_>],
    command_range: TextRange,
    diagnostics: &mut Vec<Diagnostic>,
    source: SourceView<'_>,
) {
    let prefix_len = schema.prefix.len();
    let required_prefix_len = required_prefix_len(schema.prefix);
    let positional_len = positional_args.len();

    if prefix_len == 0 && matches!(schema.tail, PositionalTailSchema::None) && positional_len > 0 {
        push_primary_diagnostic(
            diagnostics,
            DiagnosticSeverity::Error,
            command_range,
            format!(
                "command \"{}\" does not accept positional arguments",
                command.name
            ),
        );
        return;
    }

    if positional_len < required_prefix_len {
        push_primary_diagnostic(
            diagnostics,
            DiagnosticSeverity::Error,
            command_range,
            format!(
                "command \"{}\" expects {} positional argument(s) but call provides {}",
                command.name, required_prefix_len, positional_len
            ),
        );
        return;
    }

    for (index, slot) in schema.prefix.iter().enumerate() {
        if let Some(actual_shape) = positional_args
            .get(index)
            .and_then(|arg| inferred_value_shape(arg.word, source))
        {
            validate_positional_shape(
                command,
                index,
                positional_args[index].range,
                actual_shape,
                slot.value_shapes,
                diagnostics,
            );
        }
    }

    let tail_args = &positional_args[prefix_len.min(positional_len)..];
    match schema.tail {
        PositionalTailSchema::None => {
            if !tail_args.is_empty() {
                push_primary_diagnostic(
                    diagnostics,
                    DiagnosticSeverity::Error,
                    command_range,
                    format!(
                        "command \"{}\" expects {} positional argument(s) but call provides {}",
                        command.name, prefix_len, positional_len
                    ),
                );
            }
        }
        PositionalTailSchema::Opaque { min, max } => {
            validate_tail_arity(
                command,
                min,
                max,
                tail_args.len(),
                prefix_len,
                command_range,
                diagnostics,
            );
        }
        PositionalTailSchema::Shaped {
            min,
            max,
            value_shapes,
        } => {
            validate_tail_arity(
                command,
                min,
                max,
                tail_args.len(),
                prefix_len,
                command_range,
                diagnostics,
            );
            for (tail_index, arg) in tail_args.iter().enumerate() {
                let Some(actual_shape) = inferred_value_shape(arg.word, source) else {
                    continue;
                };
                validate_positional_shape(
                    command,
                    prefix_len + tail_index,
                    arg.range,
                    actual_shape,
                    value_shapes,
                    diagnostics,
                );
            }
        }
    }
}

fn required_prefix_len(prefix: &[crate::PositionalSlotSchema]) -> usize {
    let mut seen_optional = false;
    let mut required = 0;
    for slot in prefix {
        let is_optional = matches!(
            slot.source_policy,
            PositionalSourcePolicy::ExplicitOrCurrentSelection
        );
        if is_optional {
            seen_optional = true;
        } else if !seen_optional {
            required += 1;
        } else {
            debug_assert!(
                false,
                "validated command schema invariant violated: selection-aware positional slots must form a trailing suffix"
            );
        }
    }
    required
}

fn validate_tail_arity(
    command: &CommandSchema,
    min: u8,
    max: Option<u8>,
    actual_tail_len: usize,
    prefix_len: usize,
    command_range: TextRange,
    diagnostics: &mut Vec<Diagnostic>,
) {
    let min = usize::from(min);
    let max = max.map(usize::from);
    let actual_total = prefix_len + actual_tail_len;
    let min_total = prefix_len + min;
    let max_total = max.map(|max| prefix_len + max);
    let too_few = actual_tail_len < min;
    let too_many = max.is_some_and(|max| actual_tail_len > max);
    if !too_few && !too_many {
        return;
    }

    let expected = match max_total {
        Some(max_total) if min_total == max_total => min_total.to_string(),
        Some(max_total) => format!("{min_total} to {max_total}"),
        None => format!("{min_total}+"),
    };
    let message = format!(
        "command \"{}\" expects {expected} positional argument(s) but call provides {actual_total}",
        command.name
    );
    let message: std::sync::Arc<str> = message.into();
    diagnostics.push(Diagnostic {
        severity: DiagnosticSeverity::Error,
        message: message.clone(),
        range: command_range,
        labels: vec![crate::DiagnosticLabel {
            range: command_range,
            message,
            is_primary: true,
        }],
    });
}

fn validate_positional_shape(
    command: &CommandSchema,
    index: usize,
    arg_range: TextRange,
    actual_shape: ValueShape,
    allowed_shapes: &[ValueShape],
    diagnostics: &mut Vec<Diagnostic>,
) {
    if allowed_shapes.is_empty()
        || allowed_shapes
            .iter()
            .any(|shape| value_shape_matches(*shape, actual_shape))
    {
        return;
    }

    let expected = format_value_shapes(allowed_shapes);
    let actual = format_value_shape(actual_shape);
    let message = format!(
        "positional argument {} for command \"{}\" expects {} but got {}",
        index + 1,
        command.name,
        expected,
        actual
    );
    let message: std::sync::Arc<str> = message.into();
    diagnostics.push(Diagnostic {
        severity: DiagnosticSeverity::Error,
        message: message.clone(),
        range: arg_range,
        labels: vec![crate::DiagnosticLabel {
            range: arg_range,
            message,
            is_primary: true,
        }],
    });
}

fn inferred_value_shape(word: &ShellWord, source: SourceView<'_>) -> Option<ValueShape> {
    match word {
        ShellWord::NumericLiteral { text, .. } => {
            let text = source.slice(*text);
            if text.contains('.') || text.contains('e') || text.contains('E') {
                Some(ValueShape::Float)
            } else {
                Some(ValueShape::Int)
            }
        }
        ShellWord::QuotedString { .. } => Some(ValueShape::String),
        ShellWord::BareWord { text, .. } => {
            let text = source.slice(*text);
            match text {
                "true" | "false" | "on" | "off" | "yes" | "no" => Some(ValueShape::Bool),
                _ => None,
            }
        }
        ShellWord::VectorLiteral { .. } => Some(ValueShape::FloatTuple(3)),
        ShellWord::Flag { .. }
        | ShellWord::Variable { .. }
        | ShellWord::GroupedExpr { .. }
        | ShellWord::BraceList { .. }
        | ShellWord::Capture { .. } => None,
    }
}

fn value_shape_matches(expected: ValueShape, actual: ValueShape) -> bool {
    matches!(expected, ValueShape::Unknown | ValueShape::Script) || expected == actual
}

fn format_value_shapes(shapes: &[ValueShape]) -> String {
    shapes
        .iter()
        .map(|shape| format_value_shape(*shape))
        .collect::<Vec<_>>()
        .join(" or ")
}

fn format_value_shape(shape: ValueShape) -> String {
    match shape {
        ValueShape::Bool => "bool".to_owned(),
        ValueShape::Int => "int".to_owned(),
        ValueShape::Float => "float".to_owned(),
        ValueShape::String => "string".to_owned(),
        ValueShape::Script => "script".to_owned(),
        ValueShape::StringArray => "string[]".to_owned(),
        ValueShape::FloatTuple(size) => format!("float[{size}]"),
        ValueShape::IntTuple(size) => format!("int[{size}]"),
        ValueShape::NodeName => "node name".to_owned(),
        ValueShape::Unknown => "unknown".to_owned(),
    }
}

fn shell_word_range(word: &ShellWord) -> TextRange {
    match word {
        ShellWord::Flag { range, .. }
        | ShellWord::NumericLiteral { range, .. }
        | ShellWord::BareWord { range, .. }
        | ShellWord::QuotedString { range, .. }
        | ShellWord::Variable { range, .. }
        | ShellWord::GroupedExpr { range, .. }
        | ShellWord::BraceList { range, .. }
        | ShellWord::VectorLiteral { range, .. }
        | ShellWord::Capture { range, .. } => *range,
    }
}

fn mode_allows(mask: CommandModeMask, mode: CommandMode) -> bool {
    match mode {
        CommandMode::Create => mask.create,
        CommandMode::Edit => mask.edit,
        CommandMode::Query => mask.query,
        CommandMode::Unknown => true,
    }
}

fn mode_label(mode: CommandMode) -> &'static str {
    match mode {
        CommandMode::Create => "create",
        CommandMode::Edit => "edit",
        CommandMode::Query => "query",
        CommandMode::Unknown => "unknown",
    }
}
