//! POSIX-style argument parser.
//!
//! Handles: flag bundling (`-abc`), stuck values (`-oval`),
//! separated values (`-o val`), `--` terminator, and `+` polarity.

use crate::error::Error;
use crate::polarity::Polarity;

// ── Public types ────────────────────────────────────────────────

/// How to handle unrecognized flag characters.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[expect(clippy::exhaustive_enums, reason = "two-state behavioral switch")]
pub enum OnUnknown {
    /// Return an error (standard POSIX behavior).
    #[default]
    Reject,
    /// Treat the entire arg as an operand (echo-style).
    PassThrough,
}

/// Classification of a declared flag for the parser.
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum FlagKind {
    /// Boolean toggle — presence means true.
    Bool,
    /// Takes a value (next arg or inline remainder).
    Value,
    /// Polarity-aware — tracks `-x` vs `+x`.
    Polar,
    /// Polarity-aware and takes a value.
    PolarValue,
    /// Accepted but ignored (no-op).
    Noop,
}

/// A declared flag that the parser recognizes.
#[derive(Debug, Clone)]
#[expect(clippy::exhaustive_structs, reason = "static config generated by derive")]
pub struct FlagDef {
    /// The single-character flag (e.g., `'v'` for `-v`).
    pub ch: char,
    /// What kind of flag this is.
    pub kind: FlagKind,
    /// Flags cleared when this one fires (last-wins mutex).
    pub clears: &'static [char],
}

/// Result of parsing a single flag occurrence.
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum Parsed {
    /// A boolean flag was set.
    Bool(char),
    /// A valued flag with its argument.
    Value(char, String),
    /// A polarity flag was set.
    Polar(char, Polarity),
    /// A polarity flag with a value.
    PolarValue(char, Polarity, String),
}

/// Output of a successful `scan` call.
#[derive(Debug, Clone, PartialEq, Eq)]
#[expect(clippy::exhaustive_structs, reason = "result of scan")]
pub struct ScanResult {
    /// Parsed flag occurrences in order.
    pub flags: Vec<Parsed>,
    /// Remaining positional operands.
    pub operands: Vec<String>,
}

// ── Main entry point ────────────────────────────────────────────

/// Parse arguments according to declared flags.
///
/// # Errors
///
/// Returns an error if an unknown flag is encountered (when
/// `on_unknown` is `Reject`) or a valued flag is missing its argument.
pub fn scan(
    args: &[&str],
    flags: &[FlagDef],
    on_unknown: OnUnknown,
) -> Result<ScanResult, Error> {
    let mut result = ScanResult { flags: Vec::new(), operands: Vec::new() };
    let has_polarity = flags.iter().any(|f| is_polar(&f.kind));
    let mut cursor = Cursor::new(args);

    while let Some(arg) = cursor.peek() {
        if arg == "--" {
            cursor.advance();
            break;
        }
        match classify(arg, has_polarity) {
            ArgClass::Flags(polarity, chars) => {
                process_cluster(
                    chars, polarity, &mut cursor, flags, on_unknown,
                    &mut result,
                )?;
            }
            ArgClass::Operand => break,
        }
    }

    result.operands.extend(cursor.rest().iter().map(|s| (*s).to_owned()));
    Ok(result)
}

// ── Cursor ──────────────────────────────────────────────────────

struct Cursor<'a> {
    args: &'a [&'a str],
    pos: usize,
}

impl<'a> Cursor<'a> {
    const fn new(args: &'a [&'a str]) -> Self {
        Self { args, pos: 0 }
    }

    fn peek(&self) -> Option<&'a str> {
        self.args.get(self.pos).copied()
    }

    const fn advance(&mut self) {
        self.pos = self.pos.saturating_add(1);
    }

    fn next_value(&mut self, flag_ch: char) -> Result<String, Error> {
        self.advance();
        let val = self.peek()
            .map(ToOwned::to_owned)
            .ok_or_else(|| Error::MissingValue(format!("-{flag_ch}")))?;
        self.advance();
        Ok(val)
    }

    fn rest(&self) -> &'a [&'a str] {
        self.args.get(self.pos..).unwrap_or_default()
    }
}

// ── Classification ──────────────────────────────────────────────

const fn is_polar(kind: &FlagKind) -> bool {
    matches!(kind, FlagKind::Polar | FlagKind::PolarValue)
}

enum ArgClass<'a> {
    Flags(Polarity, &'a str),
    Operand,
}

fn classify(arg: &str, has_polarity: bool) -> ArgClass<'_> {
    let Some((&first, rest)) = arg.as_bytes().split_first() else {
        return ArgClass::Operand;
    };
    if rest.is_empty() {
        return ArgClass::Operand;
    }
    let chars = arg.get(1..).unwrap_or_default();
    match first {
        b'-' => ArgClass::Flags(Polarity::On, chars),
        b'+' if has_polarity => ArgClass::Flags(Polarity::Off, chars),
        _ => ArgClass::Operand,
    }
}

fn find_flag(ch: char, flags: &[FlagDef]) -> Option<&FlagDef> {
    flags.iter().find(|f| f.ch == ch)
}

// ── Cluster processing ──────────────────────────────────────────

fn process_cluster(
    chars: &str,
    polarity: Polarity,
    cursor: &mut Cursor<'_>,
    flags: &[FlagDef],
    on_unknown: OnUnknown,
    result: &mut ScanResult,
) -> Result<(), Error> {
    if try_passthrough(chars, cursor, flags, on_unknown, &mut result.operands) {
        return Ok(());
    }
    parse_known_cluster(chars, polarity, cursor, flags, result)
}

fn try_passthrough(
    chars: &str,
    cursor: &mut Cursor<'_>,
    flags: &[FlagDef],
    on_unknown: OnUnknown,
    operands: &mut Vec<String>,
) -> bool {
    if on_unknown != OnUnknown::PassThrough {
        return false;
    }
    if !has_unknown_flag(chars, flags) {
        return false;
    }
    if let Some(full_arg) = cursor.peek() {
        operands.push(full_arg.to_owned());
    }
    cursor.advance();
    true
}

fn has_unknown_flag(chars: &str, flags: &[FlagDef]) -> bool {
    for &b in chars.as_bytes() {
        match find_flag(char::from(b), flags) {
            None => return true,
            Some(def) if matches!(def.kind, FlagKind::Value | FlagKind::PolarValue) => {
                return false;
            }
            _ => {}
        }
    }
    false
}

fn parse_known_cluster(
    chars: &str,
    polarity: Polarity,
    cursor: &mut Cursor<'_>,
    flags: &[FlagDef],
    result: &mut ScanResult,
) -> Result<(), Error> {
    for (bi, &byte) in chars.as_bytes().iter().enumerate() {
        let ch = char::from(byte);
        let def = find_flag(ch, flags)
            .ok_or_else(|| Error::UnknownFlag(format!("-{ch}")))?;

        match def.kind {
            FlagKind::Bool => result.flags.push(Parsed::Bool(ch)),
            FlagKind::Noop => {}
            FlagKind::Polar => result.flags.push(Parsed::Polar(ch, polarity)),
            FlagKind::Value | FlagKind::PolarValue => {
                let value = extract_value(chars, bi, cursor, ch)?;
                let parsed = match def.kind {
                    FlagKind::Value => Parsed::Value(ch, value),
                    _ => Parsed::PolarValue(ch, polarity, value),
                };
                result.flags.push(parsed);
                return Ok(());
            }
        }
    }
    cursor.advance();
    Ok(())
}

fn extract_value(
    chars: &str,
    byte_pos: usize,
    cursor: &mut Cursor<'_>,
    flag_ch: char,
) -> Result<String, Error> {
    let after = byte_pos.saturating_add(1);
    let remainder = chars.get(after..).unwrap_or_default();
    if !remainder.is_empty() {
        cursor.advance();
        return Ok(remainder.to_owned());
    }
    cursor.next_value(flag_ch)
}

#[cfg(test)]
#[expect(clippy::unwrap_used, reason = "tests verify success paths")]
mod tests {
    use pretty_assertions::assert_eq;

    use super::{FlagDef, FlagKind, OnUnknown, Parsed, scan};
    use crate::error::Error;
    use crate::polarity::Polarity;

    fn bool_flag(ch: char) -> FlagDef {
        FlagDef { ch, kind: FlagKind::Bool, clears: &[] }
    }

    fn value_flag(ch: char) -> FlagDef {
        FlagDef { ch, kind: FlagKind::Value, clears: &[] }
    }

    fn polar_flag(ch: char) -> FlagDef {
        FlagDef { ch, kind: FlagKind::Polar, clears: &[] }
    }

    fn noop_flag(ch: char) -> FlagDef {
        FlagDef { ch, kind: FlagKind::Noop, clears: &[] }
    }

    #[test]
    fn empty_args_returns_empty() {
        let r = scan(&[], &[], OnUnknown::Reject).unwrap();
        assert!(r.flags.is_empty());
        assert!(r.operands.is_empty());
    }

    #[test]
    fn single_bool_flag() {
        let flags = [bool_flag('v')];
        let r = scan(&["-v"], &flags, OnUnknown::Reject).unwrap();
        assert_eq!(r.flags, [Parsed::Bool('v')]);
        assert!(r.operands.is_empty());
    }

    #[test]
    fn bundled_bool_flags() {
        let flags = [bool_flag('a'), bool_flag('b'), bool_flag('c')];
        let r = scan(&["-abc"], &flags, OnUnknown::Reject).unwrap();
        assert_eq!(r.flags, [
            Parsed::Bool('a'),
            Parsed::Bool('b'),
            Parsed::Bool('c'),
        ]);
    }

    #[test]
    fn double_dash_terminates_flags() {
        let flags = [bool_flag('v')];
        let r = scan(&["--", "-v"], &flags, OnUnknown::Reject).unwrap();
        assert!(r.flags.is_empty());
        assert_eq!(r.operands, ["-v"]);
    }

    #[test]
    fn operands_after_flags() {
        let flags = [bool_flag('v')];
        let r = scan(&["-v", "file.txt"], &flags, OnUnknown::Reject).unwrap();
        assert_eq!(r.flags, [Parsed::Bool('v')]);
        assert_eq!(r.operands, ["file.txt"]);
    }

    #[test]
    fn valued_flag_separate() {
        let flags = [value_flag('o')];
        let r = scan(&["-o", "file"], &flags, OnUnknown::Reject).unwrap();
        assert_eq!(r.flags, [Parsed::Value('o', "file".into())]);
        assert!(r.operands.is_empty());
    }

    #[test]
    fn valued_flag_separate_with_trailing() {
        let flags = [value_flag('o')];
        let r = scan(&["-o", "file", "rest"], &flags, OnUnknown::Reject).unwrap();
        assert_eq!(r.flags, [Parsed::Value('o', "file".into())]);
        assert_eq!(r.operands, ["rest"]);
    }

    #[test]
    fn valued_flag_stuck() {
        let flags = [value_flag('o')];
        let r = scan(&["-ofile"], &flags, OnUnknown::Reject).unwrap();
        assert_eq!(r.flags, [Parsed::Value('o', "file".into())]);
        assert!(r.operands.is_empty());
    }

    #[test]
    fn valued_flag_missing_arg_errors() {
        let flags = [value_flag('o')];
        let r = scan(&["-o"], &flags, OnUnknown::Reject);
        assert!(matches!(r, Err(Error::MissingValue(_))));
    }

    #[test]
    fn bundled_bool_then_valued() {
        let flags = [bool_flag('v'), value_flag('o')];
        let r = scan(&["-vo", "file"], &flags, OnUnknown::Reject).unwrap();
        assert_eq!(r.flags, [
            Parsed::Bool('v'),
            Parsed::Value('o', "file".into()),
        ]);
        assert!(r.operands.is_empty());
    }

    #[test]
    fn bundled_bool_then_valued_stuck() {
        let flags = [bool_flag('v'), value_flag('o')];
        let r = scan(&["-vofile"], &flags, OnUnknown::Reject).unwrap();
        assert_eq!(r.flags, [
            Parsed::Bool('v'),
            Parsed::Value('o', "file".into()),
        ]);
        assert!(r.operands.is_empty());
    }

    #[test]
    fn multiple_valued_flags_in_sequence() {
        let flags = [value_flag('o'), value_flag('d')];
        let r = scan(&["-o", "out", "-d", "dir"], &flags, OnUnknown::Reject).unwrap();
        assert_eq!(r.flags, [
            Parsed::Value('o', "out".into()),
            Parsed::Value('d', "dir".into()),
        ]);
        assert!(r.operands.is_empty());
    }

    #[test]
    fn unknown_flag_errors() {
        let flags = [bool_flag('v')];
        let r = scan(&["-x"], &flags, OnUnknown::Reject);
        assert!(matches!(r, Err(Error::UnknownFlag(_))));
    }

    #[test]
    fn passthrough_unknown_becomes_operand() {
        let flags = [bool_flag('n')];
        let r = scan(&["-nea"], &flags, OnUnknown::PassThrough).unwrap();
        assert!(r.flags.is_empty());
        assert_eq!(r.operands, ["-nea"]);
    }

    #[test]
    fn passthrough_all_known_still_parses() {
        let flags = [bool_flag('n'), bool_flag('e')];
        let r = scan(&["-ne", "hello"], &flags, OnUnknown::PassThrough).unwrap();
        assert_eq!(r.flags, [Parsed::Bool('n'), Parsed::Bool('e')]);
        assert_eq!(r.operands, ["hello"]);
    }

    #[test]
    fn passthrough_first_char_unknown() {
        let flags = [bool_flag('n')];
        let r = scan(&["-xyz", "rest"], &flags, OnUnknown::PassThrough).unwrap();
        assert!(r.flags.is_empty());
        assert_eq!(r.operands, ["-xyz", "rest"]);
    }

    #[test]
    fn passthrough_value_flag_stuck_still_parses() {
        let flags = [value_flag('o')];
        let r = scan(&["-ofile"], &flags, OnUnknown::PassThrough).unwrap();
        assert_eq!(r.flags, [Parsed::Value('o', "file".into())]);
        assert!(r.operands.is_empty());
    }

    #[test]
    fn polarity_on() {
        let flags = [polar_flag('x')];
        let r = scan(&["-x"], &flags, OnUnknown::Reject).unwrap();
        assert_eq!(r.flags, [Parsed::Polar('x', Polarity::On)]);
    }

    #[test]
    fn polarity_off() {
        let flags = [polar_flag('x')];
        let r = scan(&["+x"], &flags, OnUnknown::Reject).unwrap();
        assert_eq!(r.flags, [Parsed::Polar('x', Polarity::Off)]);
    }

    #[test]
    fn plus_prefix_only_when_polarity_flags_exist() {
        let flags = [bool_flag('x')];
        let r = scan(&["+x"], &flags, OnUnknown::Reject).unwrap();
        assert_eq!(r.operands, ["+x"]);
    }

    #[test]
    fn noop_flag_accepted_silently() {
        let flags = [bool_flag('r'), noop_flag('e')];
        let r = scan(&["-re"], &flags, OnUnknown::Reject).unwrap();
        assert_eq!(r.flags, [Parsed::Bool('r')]);
    }

    #[test]
    fn bare_dash_is_operand() {
        let flags = [bool_flag('v')];
        let r = scan(&["-"], &flags, OnUnknown::Reject).unwrap();
        assert_eq!(r.operands, ["-"]);
    }

    #[test]
    fn multiple_flag_groups() {
        let flags = [bool_flag('a'), bool_flag('b')];
        let r = scan(&["-a", "-b", "file"], &flags, OnUnknown::Reject).unwrap();
        assert_eq!(r.flags, [Parsed::Bool('a'), Parsed::Bool('b')]);
        assert_eq!(r.operands, ["file"]);
    }

    #[test]
    fn polar_value_on() {
        let flags = [FlagDef { ch: 'o', kind: FlagKind::PolarValue, clears: &[] }];
        let r = scan(&["-o", "errexit"], &flags, OnUnknown::Reject).unwrap();
        assert_eq!(r.flags, [Parsed::PolarValue('o', Polarity::On, "errexit".into())]);
        assert!(r.operands.is_empty());
    }

    #[test]
    fn polar_value_off() {
        let flags = [FlagDef { ch: 'o', kind: FlagKind::PolarValue, clears: &[] }];
        let r = scan(&["+o", "verbose"], &flags, OnUnknown::Reject).unwrap();
        assert_eq!(r.flags, [Parsed::PolarValue('o', Polarity::Off, "verbose".into())]);
        assert!(r.operands.is_empty());
    }

    #[test]
    fn polar_value_stuck() {
        let flags = [FlagDef { ch: 'o', kind: FlagKind::PolarValue, clears: &[] }];
        let r = scan(&["-oerrexit"], &flags, OnUnknown::Reject).unwrap();
        assert_eq!(r.flags, [Parsed::PolarValue('o', Polarity::On, "errexit".into())]);
        assert!(r.operands.is_empty());
    }
}
