//! CLI argument parsing.
//!
//! Walking-skeleton scope is small enough that a hand-rolled
//! parser is simpler than pulling in clap. When the CLI grows
//! (project loading per L1, L2 etc.) we will revisit.

use std::env;
use std::path::PathBuf;

use crate::mode::Mode;
use crate::theme::Theme;

#[derive(Debug, Clone)]
pub struct Args {
    pub theme: Theme,
    pub log_path: Option<PathBuf>,
    /// `--data-dir <PATH>`: replace the OS-standard data root
    /// for the duration of this run (ADR-0015 §1).
    pub data_dir: Option<PathBuf>,
    /// Positional path argument: open an existing project at
    /// this path (L1, ADR-0015 §1). Mutually exclusive with
    /// `--resume`.
    pub project_path: Option<PathBuf>,
    /// `--resume`: open the most-recently-used project at
    /// startup (L1a, ADR-0015 §7). Reads the path from
    /// `<data-root>/last_project`. Mutually exclusive with
    /// `<project-path>` — supplying both is an error rather
    /// than silently picking one.
    pub resume: bool,
    /// `--help` / `-h`: print usage to stdout and exit. The
    /// runtime checks this flag before doing any other work.
    pub help: bool,
    /// `--version` / `-V`: print the version (`CARGO_PKG_VERSION`,
    /// the single source of truth — ADR-0054) and exit. Checked
    /// alongside `--help` before any other work.
    pub version: bool,
    /// `--no-undo`: disable the auto-snapshot / undo machinery for
    /// this run (ADR-0006 Amendment 1). When set, no snapshots are
    /// taken — zero per-command overhead — and `undo` / `redo`
    /// report that undo is turned off. The escape hatch for small
    /// hardware where per-command snapshotting is too heavy.
    pub no_undo: bool,
    /// `--mode simple|advanced`: start in this input mode,
    /// overriding the project's stored mode (ADR-0015 mode-restore
    /// amendment, issue #14). Precedence: `--mode` > stored project
    /// mode > the default (`simple`). Combines with `--resume` and
    /// a positional path; on collision the flag wins.
    pub mode: Option<Mode>,
    /// `--demo` (or `RDBMS_PLAYGROUND_DEMO` set truthy): enter
    /// **demonstration mode** (ADR-0047, issue #22). Off by default,
    /// zero footprint when off. When on, the app shows transient
    /// on-screen badges for otherwise-invisible keys (Tab, Enter, …)
    /// and enables the `Ctrl+]` stealth step-caption buffer — for
    /// screencasts and live teaching. The flag wins over the env var.
    pub demo: bool,
}

/// Usage banner printed by `--help`.
///
/// Wraps the catalog lookup (`help.cli_banner`) so callers
/// don't have to spell out the key. The catalog body is the
/// single source of truth — see
/// `src/friendly/strings/en-US.yaml`.
#[must_use]
pub fn help_text() -> String {
    crate::t!("help.cli_banner")
}

/// Version line printed by `--version` / `-V` and the in-app `version`
/// command (ADR-0054).
///
/// `CARGO_PKG_VERSION` is the single source of truth — it equals the `v*`
/// release tag (the release CI guards that), so what the binary reports
/// always matches the downloaded artifact.
#[must_use]
pub fn version_text() -> String {
    crate::t!("cli.version_line", version = env!("CARGO_PKG_VERSION"))
}

#[derive(Debug)]
pub enum ArgsError {
    MissingValue(&'static str),
    InvalidValue {
        flag: &'static str,
        value: String,
        expected: &'static str,
    },
    Unknown(String),
    MultiplePaths {
        first: String,
        second: String,
    },
    ResumeWithPath,
}

impl std::fmt::Display for ArgsError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::MissingValue(flag) => f.write_str(&crate::t!("cli.missing_value", flag = flag,)),
            Self::InvalidValue {
                flag,
                value,
                expected,
            } => f.write_str(&crate::t!(
                "cli.invalid_value",
                flag = flag,
                value = value,
                expected = expected,
            )),
            Self::Unknown(arg) => f.write_str(&crate::t!("cli.unknown_argument", arg = arg,)),
            Self::MultiplePaths { first, second } => f.write_str(&crate::t!(
                "cli.multiple_paths",
                first = first,
                second = second,
            )),
            Self::ResumeWithPath => f.write_str(&crate::t!("cli.resume_with_path")),
        }
    }
}

impl std::error::Error for ArgsError {}

impl Args {
    /// Parse `Args` from the process command line.
    pub fn from_env() -> Result<Self, ArgsError> {
        Self::parse(env::args().skip(1))
    }

    /// Parse `Args` from an arbitrary iterator (used by tests).
    pub fn parse<I, S>(iter: I) -> Result<Self, ArgsError>
    where
        I: IntoIterator<Item = S>,
        S: Into<String>,
    {
        let mut theme = default_theme();
        let mut log_path = env::var_os("RDBMS_PLAYGROUND_LOG_FILE").map(PathBuf::from);
        let mut data_dir: Option<PathBuf> = None;
        let mut project_path: Option<PathBuf> = None;
        let mut resume = false;
        let mut help = false;
        let mut version = false;
        let mut no_undo = false;
        let mut mode: Option<Mode> = None;
        // Demonstration mode (ADR-0047): the env var is the default,
        // the `--demo` flag overrides it to on. Mirrors the
        // env-then-flag layering used for the log file above.
        let mut demo = env::var("RDBMS_PLAYGROUND_DEMO")
            .ok()
            .is_some_and(|v| demo_value_is_truthy(&v));
        let mut iter = iter.into_iter().map(Into::into);
        while let Some(arg) = iter.next() {
            match arg.as_str() {
                "--help" | "-h" => {
                    help = true;
                }
                "--version" | "-V" => {
                    version = true;
                }
                "--resume" => {
                    resume = true;
                }
                "--no-undo" => {
                    no_undo = true;
                }
                "--demo" => {
                    demo = true;
                }
                "--theme" => {
                    let value = iter.next().ok_or(ArgsError::MissingValue("theme"))?;
                    theme = match value.as_str() {
                        "light" => Theme::light(),
                        "dark" => Theme::dark(),
                        other => {
                            return Err(ArgsError::InvalidValue {
                                flag: "theme",
                                value: other.to_string(),
                                expected: "light, dark",
                            });
                        }
                    };
                }
                "--log-file" => {
                    let value = iter.next().ok_or(ArgsError::MissingValue("log-file"))?;
                    log_path = Some(PathBuf::from(value));
                }
                "--data-dir" => {
                    let value = iter.next().ok_or(ArgsError::MissingValue("data-dir"))?;
                    data_dir = Some(PathBuf::from(value));
                }
                "--mode" => {
                    let value = iter.next().ok_or(ArgsError::MissingValue("mode"))?;
                    mode = Some(Mode::from_keyword(&value).ok_or_else(|| {
                        ArgsError::InvalidValue {
                            flag: "mode",
                            value: value.clone(),
                            expected: "simple, advanced",
                        }
                    })?);
                }
                other if other.starts_with("--") => {
                    return Err(ArgsError::Unknown(other.to_string()));
                }
                other => {
                    if let Some(existing) = &project_path {
                        return Err(ArgsError::MultiplePaths {
                            first: existing.display().to_string(),
                            second: other.to_string(),
                        });
                    }
                    project_path = Some(PathBuf::from(other));
                }
            }
        }
        if resume && project_path.is_some() {
            return Err(ArgsError::ResumeWithPath);
        }
        Ok(Self {
            theme,
            log_path,
            data_dir,
            project_path,
            resume,
            help,
            version,
            no_undo,
            mode,
            demo,
        })
    }
}

/// Whether a `RDBMS_PLAYGROUND_DEMO` value enables demo mode.
///
/// Truthy for any value except the conventional "off" set
/// (`0`/`false`/`no`/`off`, case-insensitively, and the empty
/// string). So `RDBMS_PLAYGROUND_DEMO=1` and `=true` enable, while
/// `=0` / `=false` explicitly disable — letting a value of `0` turn
/// it off even if something upstream exported the variable.
fn demo_value_is_truthy(value: &str) -> bool {
    !matches!(
        value.trim().to_ascii_lowercase().as_str(),
        "" | "0" | "false" | "no" | "off"
    )
}

fn default_theme() -> Theme {
    // NFR-7: support both backgrounds. For the walking skeleton we
    // honour an explicit `--theme` flag and the COLORFGBG env var
    // (which xterm/Konsole/iTerm export in the form `<fg>;<bg>`).
    // True OSC-11 background querying is a later improvement.
    if let Ok(value) = env::var("COLORFGBG")
        && let Some(bg) = value.split(';').next_back()
        && let Ok(code) = bg.trim().parse::<u8>()
    {
        // Standard convention: 0..=6 and 8 are dark backgrounds,
        // 7 and 9..=15 are light. ITerm emits 15 for white-ish.
        let is_dark = matches!(code, 0..=6 | 8);
        return if is_dark {
            Theme::dark()
        } else {
            Theme::light()
        };
    }
    Theme::default()
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::theme::Background;

    #[test]
    fn no_args_yields_default_theme() {
        let args = Args::parse(std::iter::empty::<&str>()).unwrap();
        // The default depends on environment; we only assert it parsed.
        let _ = args.theme;
    }

    #[test]
    fn theme_flag_light() {
        let args = Args::parse(["--theme", "light"]).unwrap();
        assert_eq!(args.theme.background, Background::Light);
    }

    #[test]
    fn theme_flag_dark() {
        let args = Args::parse(["--theme", "dark"]).unwrap();
        assert_eq!(args.theme.background, Background::Dark);
    }

    #[test]
    fn theme_flag_invalid() {
        let err = Args::parse(["--theme", "neon"]).unwrap_err();
        assert!(matches!(err, ArgsError::InvalidValue { flag: "theme", .. }));
    }

    #[test]
    fn theme_flag_missing_value() {
        let err = Args::parse(["--theme"]).unwrap_err();
        assert!(matches!(err, ArgsError::MissingValue("theme")));
    }

    // ---- ADR-0015 mode-restore amendment (issue #14): --mode ----

    #[test]
    fn no_mode_flag_yields_none() {
        // Absent `--mode` is "no startup override" — the runtime
        // then falls back to the project's stored mode.
        let args = Args::parse(std::iter::empty::<&str>()).unwrap();
        assert_eq!(args.mode, None);
    }

    #[test]
    fn mode_flag_simple_and_advanced() {
        assert_eq!(
            Args::parse(["--mode", "simple"]).unwrap().mode,
            Some(Mode::Simple)
        );
        assert_eq!(
            Args::parse(["--mode", "advanced"]).unwrap().mode,
            Some(Mode::Advanced)
        );
        // Case-insensitive, like the `mode` command.
        assert_eq!(
            Args::parse(["--mode", "ADVANCED"]).unwrap().mode,
            Some(Mode::Advanced)
        );
    }

    #[test]
    fn mode_flag_invalid_value() {
        let err = Args::parse(["--mode", "expert"]).unwrap_err();
        assert!(matches!(err, ArgsError::InvalidValue { flag: "mode", .. }));
    }

    #[test]
    fn mode_flag_missing_value() {
        let err = Args::parse(["--mode"]).unwrap_err();
        assert!(matches!(err, ArgsError::MissingValue("mode")));
    }

    #[test]
    fn mode_flag_combines_with_resume() {
        // `--mode` is not mutually exclusive with `--resume`; the
        // flag is the startup override, resume picks the project.
        let args = Args::parse(["--resume", "--mode", "advanced"]).unwrap();
        assert!(args.resume);
        assert_eq!(args.mode, Some(Mode::Advanced));
    }

    #[test]
    fn unknown_flag_errors() {
        let err = Args::parse(["--bogus"]).unwrap_err();
        assert!(matches!(err, ArgsError::Unknown(s) if s == "--bogus"));
    }

    #[test]
    fn data_dir_flag_parses() {
        let args = Args::parse(["--data-dir", "/tmp/playground-data"]).unwrap();
        assert_eq!(
            args.data_dir.as_deref(),
            Some(std::path::Path::new("/tmp/playground-data"))
        );
    }

    #[test]
    fn data_dir_flag_missing_value() {
        let err = Args::parse(["--data-dir"]).unwrap_err();
        assert!(matches!(err, ArgsError::MissingValue("data-dir")));
    }

    #[test]
    fn positional_path_parses() {
        let args = Args::parse(["/home/me/projects/MyProject"]).unwrap();
        assert_eq!(
            args.project_path.as_deref(),
            Some(std::path::Path::new("/home/me/projects/MyProject"))
        );
    }

    #[test]
    fn data_dir_and_positional_can_coexist() {
        let args = Args::parse(["--data-dir", "/tmp/data", "/home/me/MyProject"]).unwrap();
        assert_eq!(
            args.data_dir.as_deref(),
            Some(std::path::Path::new("/tmp/data"))
        );
        assert_eq!(
            args.project_path.as_deref(),
            Some(std::path::Path::new("/home/me/MyProject"))
        );
    }

    #[test]
    fn two_positional_paths_error() {
        let err = Args::parse(["/a", "/b"]).unwrap_err();
        assert!(
            matches!(err, ArgsError::MultiplePaths { .. }),
            "got: {err:?}"
        );
    }

    #[test]
    fn help_flag_long_form_sets_help() {
        let args = Args::parse(["--help"]).unwrap();
        assert!(args.help);
    }

    #[test]
    fn help_flag_short_form_sets_help() {
        let args = Args::parse(["-h"]).unwrap();
        assert!(args.help);
    }

    #[test]
    fn resume_flag_parses() {
        let args = Args::parse(["--resume"]).unwrap();
        assert!(args.resume);
        assert!(args.project_path.is_none());
    }

    #[test]
    fn resume_with_positional_path_errors() {
        let err = Args::parse(["--resume", "/some/path"]).unwrap_err();
        assert!(matches!(err, ArgsError::ResumeWithPath), "got: {err:?}");
    }

    #[test]
    fn positional_path_with_resume_errors_in_either_order() {
        let err = Args::parse(["/some/path", "--resume"]).unwrap_err();
        assert!(matches!(err, ArgsError::ResumeWithPath), "got: {err:?}");
    }

    #[test]
    fn no_undo_flag_parses() {
        let args = Args::parse(["--no-undo"]).unwrap();
        assert!(args.no_undo);
    }

    #[test]
    fn no_undo_defaults_off() {
        let args = Args::parse(std::iter::empty::<&str>()).unwrap();
        assert!(!args.no_undo, "undo is enabled unless --no-undo is given");
    }

    #[test]
    fn no_undo_coexists_with_positional_path() {
        let args = Args::parse(["--no-undo", "/home/me/MyProject"]).unwrap();
        assert!(args.no_undo);
        assert_eq!(
            args.project_path.as_deref(),
            Some(std::path::Path::new("/home/me/MyProject"))
        );
    }

    // ---- ADR-0047 (issue #22): --demo demonstration mode ----

    #[test]
    fn demo_flag_parses() {
        let args = Args::parse(["--demo"]).unwrap();
        assert!(args.demo);
    }

    #[test]
    fn demo_defaults_off() {
        // Absent `--demo` (and absent env var in the test runner),
        // demo mode is off — zero footprint for real users.
        let args = Args::parse(std::iter::empty::<&str>()).unwrap();
        assert!(
            !args.demo,
            "demo is off unless --demo or the env var is given"
        );
    }

    #[test]
    fn demo_flag_coexists_with_positional_path() {
        let args = Args::parse(["--demo", "/home/me/MyProject"]).unwrap();
        assert!(args.demo);
        assert_eq!(
            args.project_path.as_deref(),
            Some(std::path::Path::new("/home/me/MyProject"))
        );
    }

    #[test]
    fn demo_flag_combines_with_resume_and_mode() {
        let args = Args::parse(["--resume", "--demo", "--mode", "advanced"]).unwrap();
        assert!(args.demo);
        assert!(args.resume);
        assert_eq!(args.mode, Some(Mode::Advanced));
    }

    #[test]
    fn demo_env_value_truthiness() {
        // Enabling values.
        for v in ["1", "true", "TRUE", "yes", "on", "anything", " 1 "] {
            assert!(demo_value_is_truthy(v), "{v:?} should enable demo mode");
        }
        // Disabling values.
        for v in ["", "  ", "0", "false", "False", "no", "off", "OFF"] {
            assert!(
                !demo_value_is_truthy(v),
                "{v:?} should not enable demo mode"
            );
        }
    }

    #[test]
    fn unknown_double_dash_flag_errors_even_with_positional() {
        // Make sure the path-vs-flag distinction is robust:
        // unknown flags don't get silently swallowed as paths.
        let err = Args::parse(["--bogus", "/some/path"]).unwrap_err();
        assert!(
            matches!(&err, ArgsError::Unknown(s) if s == "--bogus"),
            "got: {err:?}"
        );
    }

    // ---- ADR-0054: --version / -V ----

    #[test]
    fn version_long_flag_parses() {
        assert!(Args::parse(["--version"]).unwrap().version);
    }

    #[test]
    fn version_short_flag_parses() {
        assert!(Args::parse(["-V"]).unwrap().version);
    }

    #[test]
    fn version_defaults_off() {
        assert!(!Args::parse(std::iter::empty::<&str>()).unwrap().version);
    }

    #[test]
    fn version_text_carries_the_cargo_version() {
        // The binary's self-reported version IS Cargo.toml's (the
        // single source of truth, ADR-0054) — and the release CI guards
        // that the `v*` tag equals it.
        let text = version_text();
        assert!(
            text.contains(env!("CARGO_PKG_VERSION")),
            "version line should embed CARGO_PKG_VERSION; got {text:?}"
        );
    }
}
