// SPDX-FileCopyrightText: 2026 Contributors to brew-proxy <https://codeberg.org/HastD/brew-proxy>
//
// SPDX-License-Identifier: Apache-2.0 OR MIT

use std::{fmt, path::PathBuf, str::FromStr};

/// Subcommands for `brew bundle`. For reference, see:
/// <https://docs.brew.sh/Brew-Bundle-and-Brewfile>
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[non_exhaustive]
pub enum BundleSubcommand {
    Add,
    Check,
    Cleanup,
    Dump,
    Env,
    #[default]
    Install,
    List,
    Remove,
    Upgrade,
}

impl BundleSubcommand {
    /// Returns the representation of the subcommand as a string.
    #[must_use]
    pub const fn as_str(self) -> &'static str {
        match self {
            Self::Add => "add",
            Self::Check => "check",
            Self::Cleanup => "cleanup",
            Self::Dump => "dump",
            Self::Env => "env",
            Self::Install => "install",
            Self::List => "list",
            Self::Remove => "remove",
            Self::Upgrade => "upgrade",
        }
    }

    /// Returns whether the subcommand reads from a Brewfile, writes to a Brewfile, or both.
    #[must_use]
    pub const fn io_direction(self) -> IoDirection {
        match self {
            Self::Dump => IoDirection::Write,
            Self::Add | Self::Remove => IoDirection::ReadWrite,
            Self::Check
            | Self::Cleanup
            | Self::Env
            | Self::Install
            | Self::List
            | Self::Upgrade => IoDirection::Read,
        }
    }
}

impl FromStr for BundleSubcommand {
    type Err = BundleSubcommandError;

    fn from_str(cmd: &str) -> Result<Self, Self::Err> {
        match cmd {
            "add" => Ok(Self::Add),
            "check" => Ok(Self::Check),
            "cleanup" => Ok(Self::Cleanup),
            "dump" => Ok(Self::Dump),
            "env" => Ok(Self::Env),
            "install" => Ok(Self::Install),
            "list" => Ok(Self::List),
            "remove" => Ok(Self::Remove),
            "upgrade" => Ok(Self::Upgrade),
            "edit" | "exec" | "sh" => Err(BundleSubcommandError {
                kind: BundleSubcommandErrorKind::Unsupported,
                cmd: cmd.to_owned(),
            }),
            _ => Err(BundleSubcommandError {
                kind: BundleSubcommandErrorKind::Unknown,
                cmd: cmd.to_owned(),
            }),
        }
    }
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum IoDirection {
    Read,
    Write,
    ReadWrite,
}

#[derive(Clone, Debug)]
pub struct BundleSubcommandError {
    cmd: String,
    kind: BundleSubcommandErrorKind,
}

#[derive(Clone, Copy, Debug)]
#[non_exhaustive]
pub enum BundleSubcommandErrorKind {
    /// Known brew bundle subcommand, but unsupported by brew-proxy.
    Unsupported,

    /// Not a known brew bundle subcommand.
    Unknown,
}

impl fmt::Display for BundleSubcommandError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self.kind {
            BundleSubcommandErrorKind::Unsupported => {
                write!(f, "Unsupported brew bundle subcommand: {}", self.cmd)
            }
            BundleSubcommandErrorKind::Unknown => {
                write!(f, "Unknown brew bundle subcommand: {}", self.cmd)
            }
        }
    }
}

#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub enum Brewfile {
    #[default]
    Default,
    Path(PathBuf),
    Global,
    Stdin,
}

impl Brewfile {
    fn from_arg(arg: &str) -> Self {
        if arg == "-" {
            Self::Stdin
        } else {
            Self::Path(PathBuf::from(arg))
        }
    }
}

#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct BundleCommandLine {
    subcommand: BundleSubcommand,
    brewfile: Brewfile,
    args: Vec<String>,
}

impl BundleCommandLine {
    #[must_use]
    pub fn subcommand(&self) -> BundleSubcommand {
        self.subcommand
    }

    /// Given arguments to `brew bundle` command, parse the arguments to get the subcommand, the
    /// brewfile, and the list of remaining arguments.
    pub fn from_args(input_args: Vec<String>) -> Result<Self, BundleSubcommandError> {
        let mut subcommand = None;
        let mut brewfile = Brewfile::Default;
        let mut final_args = Vec::with_capacity(input_args.len());
        let mut is_file_arg = false;
        for arg in input_args {
            if is_file_arg {
                brewfile = Brewfile::from_arg(&arg);
                is_file_arg = false;
            } else if arg == "--file" {
                is_file_arg = true;
            } else if arg == "-g" || arg == "--global" {
                brewfile = Brewfile::Global;
            } else if let Some(file_arg) = arg.strip_prefix("--file=") {
                brewfile = Brewfile::from_arg(file_arg);
            } else if subcommand.is_none() && !arg.starts_with("-") {
                subcommand = Some(arg.parse()?);
            } else {
                final_args.push(arg);
            }
        }
        Ok(Self {
            subcommand: subcommand.unwrap_or_default(),
            brewfile,
            args: final_args,
        })
    }

    /// Get the arguments to be passed over DBus and the Brewfile
    #[must_use]
    pub fn into_args_and_brewfile(mut self) -> (Vec<String>, Brewfile) {
        let mut output_args = Vec::with_capacity(self.args.len() + 2);
        if matches!(self.brewfile, Brewfile::Global) {
            output_args.push("--global".to_owned());
        } else {
            output_args.push("--file=-".to_owned());
        }
        output_args.push(self.subcommand.as_str().to_owned());
        output_args.append(&mut self.args);
        (output_args, self.brewfile)
    }
}

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

    #[test]
    fn empty_bundle_args() {
        let input_args = vec![];
        let cmdline = BundleCommandLine::from_args(input_args).unwrap();
        assert_eq!(cmdline.subcommand, BundleSubcommand::Install);
        assert_eq!(cmdline.brewfile, Brewfile::Default);
        assert!(cmdline.args.is_empty());
        let (output_args, _) = cmdline.into_args_and_brewfile();
        assert_eq!(output_args.len(), 2);
        assert_eq!(output_args[0], "--file=-");
        assert_eq!(output_args[1], "install");
    }

    #[test]
    fn dump_only() {
        let input_args = vec!["dump".to_owned()];
        let cmdline = BundleCommandLine::from_args(input_args).unwrap();
        assert_eq!(cmdline.subcommand, BundleSubcommand::Dump);
        assert_eq!(cmdline.brewfile, Brewfile::Default);
        assert!(cmdline.args.is_empty());
        let (output_args, _) = cmdline.into_args_and_brewfile();
        assert_eq!(output_args.len(), 2);
        assert_eq!(output_args[0], "--file=-");
        assert_eq!(output_args[1], "dump");
    }

    #[test]
    fn stdin_specified() {
        let input_args = vec!["--file=-".to_owned()];
        let cmdline = BundleCommandLine::from_args(input_args).unwrap();
        assert_eq!(cmdline.subcommand, BundleSubcommand::Install);
        assert_eq!(cmdline.brewfile, Brewfile::Stdin);
        assert!(cmdline.args.is_empty());
        let (output_args, _) = cmdline.into_args_and_brewfile();
        assert_eq!(output_args.len(), 2);
        assert_eq!(output_args[0], "--file=-");
        assert_eq!(output_args[1], "install");
    }

    #[test]
    fn file_specified() {
        let input_args = vec!["--file=/path/to/file".to_owned()];
        let cmdline = BundleCommandLine::from_args(input_args).unwrap();
        assert_eq!(cmdline.subcommand, BundleSubcommand::Install);
        assert_eq!(
            cmdline.brewfile,
            Brewfile::Path(PathBuf::from("/path/to/file"))
        );
        assert!(cmdline.args.is_empty());
        let (output_args, _) = cmdline.into_args_and_brewfile();
        assert_eq!(output_args.len(), 2);
        assert_eq!(output_args[0], "--file=-");
        assert_eq!(output_args[1], "install");
    }

    #[test]
    fn file_specified_in_two_args() {
        let input_args = vec!["--file".to_owned(), "/path/to/file".to_owned()];
        let cmdline = BundleCommandLine::from_args(input_args).unwrap();
        assert_eq!(cmdline.subcommand, BundleSubcommand::Install);
        assert_eq!(
            cmdline.brewfile,
            Brewfile::Path(PathBuf::from("/path/to/file"))
        );
        assert!(cmdline.args.is_empty());
        let (output_args, _) = cmdline.into_args_and_brewfile();
        assert_eq!(output_args.len(), 2);
        assert_eq!(output_args[0], "--file=-");
        assert_eq!(output_args[1], "install");
    }

    #[test]
    fn add_with_args() {
        let input_args = vec![
            "add".to_owned(),
            "--file=/path/to/file".to_owned(),
            "hello".to_owned(),
        ];
        let cmdline = BundleCommandLine::from_args(input_args).unwrap();
        assert_eq!(cmdline.subcommand, BundleSubcommand::Add);
        assert_eq!(
            cmdline.brewfile,
            Brewfile::Path(PathBuf::from("/path/to/file"))
        );
        assert_eq!(cmdline.args.len(), 1);
        assert_eq!(cmdline.args[0], "hello");
        let (output_args, _) = cmdline.into_args_and_brewfile();
        assert_eq!(output_args.len(), 3);
        assert_eq!(output_args[0], "--file=-");
        assert_eq!(output_args[1], "add");
        assert_eq!(output_args[2], "hello");
    }

    #[test]
    fn global_specified_short() {
        let input_args = vec!["list".to_owned(), "-g".to_owned(), "whatever".to_owned()];
        let cmdline = BundleCommandLine::from_args(input_args).unwrap();
        assert_eq!(cmdline.subcommand, BundleSubcommand::List);
        assert_eq!(cmdline.brewfile, Brewfile::Global);
        assert_eq!(cmdline.args.len(), 1);
        assert_eq!(cmdline.args[0], "whatever");
        let (output_args, _) = cmdline.into_args_and_brewfile();
        assert_eq!(output_args.len(), 3);
        assert_eq!(output_args[0], "--global");
        assert_eq!(output_args[1], "list");
        assert_eq!(output_args[2], "whatever");
    }

    #[test]
    fn global_specified_long() {
        let input_args = vec!["check".to_owned(), "--global".to_owned()];
        let cmdline = BundleCommandLine::from_args(input_args).unwrap();
        assert_eq!(cmdline.subcommand, BundleSubcommand::Check);
        assert_eq!(cmdline.brewfile, Brewfile::Global);
        assert!(cmdline.args.is_empty());
        let (output_args, _) = cmdline.into_args_and_brewfile();
        assert_eq!(output_args.len(), 2);
        assert_eq!(output_args[0], "--global");
        assert_eq!(output_args[1], "check");
    }

    #[test]
    fn unsupported_subcommand() {
        let input_args = vec!["edit".to_owned(), "install".to_owned()];
        assert!(matches!(
            BundleCommandLine::from_args(input_args),
            Err(BundleSubcommandError {
                kind: BundleSubcommandErrorKind::Unsupported,
                ..
            })
        ));
    }

    #[test]
    fn unknown_subcommand() {
        let input_args = vec!["asdf".to_owned(), "install".to_owned()];
        assert!(matches!(
            BundleCommandLine::from_args(input_args),
            Err(BundleSubcommandError {
                kind: BundleSubcommandErrorKind::Unknown,
                ..
            })
        ));
    }
}
