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

//! Constants and utility functions for use in both brew-proxy-client and brew-proxy-daemon.
//!
//! This crate should not be considered a public API and its contents might not be stable.

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

use tokio::{
    fs::File,
    io::{AsyncRead, AsyncWrite},
};

pub mod bundle;
pub mod env;
pub mod idle;

pub const BREW_PROXY_HOMEPAGE: &str = "https://codeberg.org/HastD/brew-proxy";
pub const BREW_PROXY_ISSUES_PAGE: &str = "https://codeberg.org/HastD/brew-proxy/issues";

pub const BREW_USER: &str = "linuxbrew";
pub const BREW_HOME: &str = "/home/linuxbrew";
pub const BREW_PREFIX: &str = "/home/linuxbrew/.linuxbrew";
pub const BREW_EXECUTABLE: &str = "/home/linuxbrew/.linuxbrew/bin/brew";

pub const DBUS_DAEMON_NAME: &str = "sh.brew.BrewProxy";
pub const DBUS_DAEMON_PATH: &str = "/sh/brew/BrewProxy";
pub const DBUS_DAEMON_INTERFACE: &str = "sh.brew.BrewProxy";

#[derive(Clone, Copy, Debug)]
pub enum BusType {
    System,
    Session,
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub enum InputSource {
    Null,
    Stdin,
    Path(PathBuf),
}

impl InputSource {
    pub async fn open(&self) -> std::io::Result<Option<Pin<Box<dyn AsyncRead>>>> {
        match self {
            Self::Null => Ok(None),
            Self::Stdin => Ok(Some(Box::pin(tokio::io::stdin()))),
            Self::Path(path) => Ok(Some(Box::pin(File::open(path).await?))),
        }
    }
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub enum OutputDestination {
    Stdout,
    Path(PathBuf),
}

impl OutputDestination {
    pub async fn open(&self) -> std::io::Result<Pin<Box<dyn AsyncWrite>>> {
        match self {
            Self::Stdout => Ok(Box::pin(tokio::io::stdout())),
            Self::Path(path) => Ok(Box::pin(File::create(path).await?)),
        }
    }
}

#[macro_export]
macro_rules! call_with_brew_command_idents {
    ($callback:ident) => {
        $callback!(
            Cache Caskroom Cellar Env Prefix Repository Taps Version Alias Analytics Autoremove
            Bundle Casks Cleanup Command CommandNotFoundInit Commands Completions Config Deps
            Desc Developer Doctor Fetch Formulae GistLogs Help Info Install Leaves Link List Log
            Migrate Missing NodenvSync Options Outdated Pin Postinstall PyenvSync RbenvSync
            Readall Reinstall Search Services SetupRuby Shellenv Tab Tap TapInfo Trust Unalias
            Uninstall Unlink Unpin Untap Untrust Update UpdateIfNeeded UpdateReset Upgrade Uses
            VendorInstall VersionInstall WhichFormula
        );
    };
}

#[derive(Clone, Copy, Debug)]
pub struct BrewCommandLine<'a> {
    pub command: BrewCommand,
    pub args: &'a [String],
}

impl<'a> fmt::Display for BrewCommandLine<'a> {
    /// Produce a formatted representation of a brew command line.
    ///
    /// # Examples
    /// ```rust
    /// # use brew_proxy_utils::*;
    /// let cmdline = BrewCommandLine {
    ///     command: BrewCommand::Update,
    ///     args: &[]
    /// };
    /// assert_eq!(cmdline.to_string(), "brew update");
    ///
    /// let cmdline = BrewCommandLine {
    ///     command: BrewCommand::Info,
    ///     args: &["hello".to_owned()]
    /// };
    /// assert_eq!(cmdline.to_string(), "brew info hello");
    ///
    /// let cmdline = BrewCommandLine {
    ///     command: BrewCommand::Search,
    ///     args: &["with space".to_owned()]
    /// };
    /// assert_eq!(cmdline.to_string(), "brew search \"with space\"");
    /// ```
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "brew {}", self.command.command())?;
        for arg in self.args {
            f.write_str(" ")?;
            if arg.contains(' ') {
                write!(f, "\"{}\"", arg.replace('"', "\\\""))?;
            } else {
                f.write_str(arg)?;
            }
        }
        Ok(())
    }
}

macro_rules! define_brew_command_enum {
    ($($cmd:ident)*) => {
        #[derive(Clone, Copy, Debug)]
        pub enum BrewCommand {
            $($cmd,)*
        }
    };
}

call_with_brew_command_idents!(define_brew_command_enum);

impl BrewCommand {
    #[must_use]
    pub const fn command(self) -> &'static str {
        match self {
            Self::Cache => "--cache",
            Self::Caskroom => "--caskroom",
            Self::Cellar => "--cellar",
            Self::Env => "--env",
            Self::Prefix => "--prefix",
            Self::Repository => "--repository",
            Self::Taps => "--taps",
            Self::Version => "--version",
            Self::Alias => "alias",
            Self::Analytics => "analytics",
            Self::Autoremove => "autoremove",
            Self::Bundle => "bundle",
            Self::Casks => "casks",
            Self::Cleanup => "cleanup",
            Self::Command => "command",
            Self::CommandNotFoundInit => "command-not-found-init",
            Self::Commands => "commands",
            Self::Completions => "completions",
            Self::Config => "config",
            Self::Deps => "deps",
            Self::Desc => "desc",
            Self::Developer => "developer",
            Self::Doctor => "doctor",
            Self::Fetch => "fetch",
            Self::Formulae => "formulae",
            Self::GistLogs => "gist-logs",
            Self::Help => "help",
            Self::Info => "info",
            Self::Install => "install",
            Self::Leaves => "leaves",
            Self::Link => "link",
            Self::List => "list",
            Self::Log => "log",
            Self::Migrate => "migrate",
            Self::Missing => "missing",
            Self::NodenvSync => "nodenv-sync",
            Self::Options => "options",
            Self::Outdated => "outdated",
            Self::Pin => "pin",
            Self::Postinstall => "postinstall",
            Self::PyenvSync => "pyenv-sync",
            Self::RbenvSync => "rbenv-sync",
            Self::Readall => "readall",
            Self::Reinstall => "reinstall",
            Self::Search => "search",
            Self::Services => "services",
            Self::SetupRuby => "setup-ruby",
            Self::Shellenv => "shellenv",
            Self::Tab => "tab",
            Self::Tap => "tap",
            Self::TapInfo => "tap-info",
            Self::Trust => "trust",
            Self::Unalias => "unalias",
            Self::Uninstall => "uninstall",
            Self::Unlink => "unlink",
            Self::Unpin => "unpin",
            Self::Untap => "untap",
            Self::Untrust => "untrust",
            Self::Update => "update",
            Self::UpdateIfNeeded => "update-if-needed",
            Self::UpdateReset => "update-reset",
            Self::Upgrade => "upgrade",
            Self::Uses => "uses",
            Self::VendorInstall => "vendor-install",
            Self::VersionInstall => "version-install",
            Self::WhichFormula => "which-formula",
        }
    }

    #[must_use]
    pub const fn is_info_command(self) -> bool {
        matches!(
            self,
            Self::Cache
                | Self::Caskroom
                | Self::Cellar
                | Self::Env
                | Self::Prefix
                | Self::Repository
                | Self::Taps
                | Self::Version
                | Self::Casks
                | Self::Command
                | Self::CommandNotFoundInit
                | Self::Commands
                | Self::Config
                | Self::Deps
                | Self::Desc
                | Self::Doctor
                | Self::Formulae
                | Self::Help
                | Self::Info
                | Self::Leaves
                | Self::List
                | Self::Log
                | Self::Missing
                | Self::Options
                | Self::Outdated
                | Self::Search
                | Self::Shellenv
                | Self::TapInfo
                | Self::Uses
                | Self::WhichFormula
        )
    }

    /// Commands that should be run as the user who invoked them, rather than being forwarded over
    /// DBus to the `linuxbrew` user.
    ///
    /// For example, `brew services` is used for setting up user services, so it's much more akin to
    /// running a brew-installed program than, say, installing a brew formula. Other commands like
    /// `brew --prefix` just output some fixed strings and there's no need to send them through
    /// brew-proxy.
    #[must_use]
    pub const fn runs_as_user(self) -> bool {
        matches!(
            self,
            Self::Cache
                | Self::Caskroom
                | Self::Cellar
                | Self::Prefix
                | Self::Repository
                | Self::Taps
                | Self::Shellenv
                | Self::Services
        )
    }
}

impl FromStr for BrewCommand {
    type Err = ParseBrewCommandError;

    fn from_str(cmd: &str) -> Result<Self, Self::Err> {
        match cmd {
            "--cache" => Ok(Self::Cache),
            "--caskroom" => Ok(Self::Caskroom),
            "--cellar" => Ok(Self::Cellar),
            "--env" | "environment" => Ok(Self::Env),
            "--prefix" => Ok(Self::Prefix),
            "--repository" | "--repo" => Ok(Self::Repository),
            "--taps" => Ok(Self::Taps),
            "--version" | "-v" => Ok(Self::Version),
            "alias" => Ok(Self::Alias),
            "analytics" => Ok(Self::Analytics),
            "autoremove" => Ok(Self::Autoremove),
            "bundle" => Ok(Self::Bundle),
            "casks" => Ok(Self::Casks),
            "cleanup" => Ok(Self::Cleanup),
            "command" => Ok(Self::Command),
            "command-not-found-init" => Ok(Self::CommandNotFoundInit),
            "commands" => Ok(Self::Commands),
            "completions" => Ok(Self::Completions),
            "config" | "--config" => Ok(Self::Config),
            "deps" => Ok(Self::Deps),
            "desc" => Ok(Self::Desc),
            "developer" => Ok(Self::Developer),
            "doctor" | "dr" => Ok(Self::Doctor),
            "fetch" => Ok(Self::Fetch),
            "formulae" => Ok(Self::Formulae),
            "gist-logs" => Ok(Self::GistLogs),
            "help" | "-h" | "--help" => Ok(Self::Help),
            "info" | "abv" => Ok(Self::Info),
            "install" => Ok(Self::Install),
            "leaves" => Ok(Self::Leaves),
            "link" | "ln" => Ok(Self::Link),
            "list" | "ls" => Ok(Self::List),
            "log" => Ok(Self::Log),
            "migrate" => Ok(Self::Migrate),
            "missing" => Ok(Self::Missing),
            "nodenv-sync" => Ok(Self::NodenvSync),
            "options" => Ok(Self::Options),
            "outdated" => Ok(Self::Outdated),
            "pin" => Ok(Self::Pin),
            "postinstall" | "post_install" => Ok(Self::Postinstall),
            "pyenv-sync" => Ok(Self::PyenvSync),
            "rbenv-sync" => Ok(Self::RbenvSync),
            "readall" => Ok(Self::Readall),
            "reinstall" => Ok(Self::Reinstall),
            "search" | "-S" => Ok(Self::Search),
            "services" => Ok(Self::Services),
            "setup-ruby" => Ok(Self::SetupRuby),
            "shellenv" => Ok(Self::Shellenv),
            "tab" => Ok(Self::Tab),
            "tap" => Ok(Self::Tap),
            "tap-info" => Ok(Self::TapInfo),
            "trust" => Ok(Self::Trust),
            "unalias" => Ok(Self::Unalias),
            "uninstall" | "remove" | "rm" => Ok(Self::Uninstall),
            "unlink" => Ok(Self::Unlink),
            "unpin" => Ok(Self::Unpin),
            "untap" => Ok(Self::Untap),
            "untrust" => Ok(Self::Untrust),
            "update" | "up" => Ok(Self::Update),
            "update-if-needed" => Ok(Self::UpdateIfNeeded),
            "update-reset" => Ok(Self::UpdateReset),
            "upgrade" => Ok(Self::Upgrade),
            "uses" => Ok(Self::Uses),
            "vendor-install" => Ok(Self::VendorInstall),
            "version-install" => Ok(Self::VersionInstall),
            "which-formula" => Ok(Self::WhichFormula),
            "as-console-user" | "docs" | "home" | "mcp-server" | "source" | "update-report" => {
                Err(ParseBrewCommandError {
                    kind: ParseBrewCommandErrorKind::Unsupported,
                    cmd: cmd.to_owned(),
                })
            }
            _ if BREW_DEVELOPER_COMMANDS.contains(&cmd) => Err(ParseBrewCommandError {
                kind: ParseBrewCommandErrorKind::Developer,
                cmd: cmd.to_owned(),
            }),
            _ => Err(ParseBrewCommandError {
                kind: ParseBrewCommandErrorKind::Unknown,
                cmd: cmd.to_owned(),
            }),
        }
    }
}

const BREW_DEVELOPER_COMMANDS: &[&str] = &[
    "audit",
    "bottle",
    "bump",
    "bump-cask-pr",
    "bump-formula-pr",
    "bump-revision",
    "bump-unversioned-casks",
    "cat",
    "contributions",
    "create",
    "debugger",
    "determine-test-runners",
    "dispatch-build-bottle",
    "edit",
    "extract",
    "formula",
    "formula-analytics",
    "generate-analytics-api",
    "generate-cask-api",
    "generate-cask-ci-matrix",
    "generate-formula-api",
    "generate-man-completions",
    "generate-zap",
    "install-bundler-gems",
    "irb",
    "lc",
    "lgtm",
    "linkage",
    "livecheck",
    "pr-automerge",
    "pr-publish",
    "pr-pull",
    "pr-upload",
    "prof",
    "release",
    "rubocop",
    "ruby",
    "rubydoc",
    "sh",
    "style",
    "tap-new",
    "tc",
    "test",
    "test-bot",
    "tests",
    "typecheck",
    "unbottled",
    "unpack",
    "update-license-data",
    "update-maintainers",
    "update-perl-resources",
    "update-python-resources",
    "update-sponsors",
    "update-test",
    "vendor-gems",
    "verify",
    "which-update",
];

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

#[derive(Clone, Copy, Debug)]
pub enum ParseBrewCommandErrorKind {
    /// Known brew command, but unsupported by brew-proxy.
    Unsupported,

    /// Homebrew developer command; these are unsupported by brew-proxy.
    Developer,

    /// Not a known brew command.
    Unknown,
}

impl fmt::Display for ParseBrewCommandError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self.kind {
            ParseBrewCommandErrorKind::Unsupported => {
                write!(f, "Unsupported brew command: {}", self.cmd)
            }
            ParseBrewCommandErrorKind::Developer => {
                write!(f, "brew developer commands are unsupported: {}", self.cmd)
            }
            ParseBrewCommandErrorKind::Unknown => {
                write!(f, "Unknown brew command: {}", self.cmd)
            }
        }
    }
}

#[derive(Clone, Copy, Debug)]
#[non_exhaustive]
pub enum PolkitAction {
    /// Purely informational commands that do not modify the Homebrew installation.
    Info,
    /// Commands that update Homebrew itself.
    Update,
    /// Commands that update Homebrew packages.
    Upgrade,
    /// Commands that do maintenance tasks that do not permit the installation of new packages or
    /// other substantial modification of the Homebrew installation. (Example: `brew cleanup`.)
    Maintain,
    /// Commands that install Homebrew packages.
    Install,
    /// Commands that uninstall Homebrew packages.
    Uninstall,
    /// Catch-all action for commands that modify the Homebrew installation and do not fall into one
    /// of the above categories.
    Admin,
}

impl From<BrewCommandLine<'_>> for PolkitAction {
    fn from(cmdline: BrewCommandLine) -> Self {
        match cmdline.command {
            _ if cmdline.args == ["-h"] || cmdline.args == ["--help"] => Self::Info,
            BrewCommand::Update | BrewCommand::UpdateIfNeeded => Self::Update,
            BrewCommand::Upgrade => {
                // `brew upgrade` has a lot of options, some of which do complicated things like
                // opening an interactive debugging session. To reduce the risk that some such
                // option could be used for privilege escalation, if any of these options except
                // the below basic ones are used, classify the command as an admin command, not a
                // maintainence command.
                let upgrade_basic_args = [
                    "-h",
                    "--help",
                    "-q",
                    "--quiet",
                    "-g",
                    "--greedy",
                    "-n",
                    "--dry-run",
                    "-v",
                    "--verbose",
                    "--display-times",
                ];
                if cmdline
                    .args
                    .iter()
                    .all(|arg| upgrade_basic_args.contains(&&**arg))
                {
                    Self::Upgrade
                } else {
                    Self::Admin
                }
            }
            BrewCommand::Autoremove | BrewCommand::Cleanup => Self::Maintain,
            BrewCommand::Install => Self::Install,
            BrewCommand::Uninstall => Self::Uninstall,
            cmd if cmd.is_info_command() => Self::Info,
            _ => Self::Admin,
        }
    }
}

impl PolkitAction {
    pub fn id(self) -> &'static str {
        match self {
            Self::Info => "sh.brew.BrewProxy.info",
            Self::Update => "sh.brew.BrewProxy.update",
            Self::Upgrade => "sh.brew.BrewProxy.upgrade",
            Self::Maintain => "sh.brew.BrewProxy.maintain",
            Self::Install => "sh.brew.BrewProxy.install",
            Self::Uninstall => "sh.brew.BrewProxy.uninstall",
            Self::Admin => "sh.brew.BrewProxy.admin",
        }
    }

    pub fn description(self) -> &'static str {
        match self {
            Self::Info => "Get information about the Homebrew installation",
            Self::Update => "Update Homebrew",
            Self::Upgrade => "Update Homebrew-installed packages",
            Self::Maintain => "Run Homebrew maintenance tasks",
            Self::Install => "Install Homebrew packages",
            Self::Uninstall => "Uninstall Homebrew packages",
            Self::Admin => "Modify the Homebrew installation",
        }
    }

    pub fn auth_message(self) -> &'static str {
        match self {
            Self::Info => {
                "Authentication is required to get information about the Homebrew installation"
            }
            Self::Update => "Authentication is required to update Homebrew",
            Self::Upgrade => "Authentication is required to update Homebrew-installed packages",
            Self::Maintain => "Authentication is required to run Homebrew maintenance tasks",
            Self::Install => "Authentication is required to install Homebrew packages",
            Self::Uninstall => "Authentication is required to uninstall Homebrew packages",
            Self::Admin => "Authentication is required to modify the Homebrew installation",
        }
    }
}

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

    #[test]
    fn upgrade_polkit_actions() {
        let cmdline = BrewCommandLine {
            command: BrewCommand::Upgrade,
            args: &[],
        };
        assert!(matches!(PolkitAction::from(cmdline), PolkitAction::Upgrade));

        let cmdline = BrewCommandLine {
            command: BrewCommand::Upgrade,
            args: &["-n".to_owned(), "--verbose".to_owned()],
        };
        assert!(matches!(PolkitAction::from(cmdline), PolkitAction::Upgrade));

        let cmdline = BrewCommandLine {
            command: BrewCommand::Upgrade,
            args: &["--debug".to_owned()],
        };
        assert!(matches!(PolkitAction::from(cmdline), PolkitAction::Admin));
    }

    #[test]
    fn help_polkit_actions() {
        let cmdline = BrewCommandLine {
            command: BrewCommand::Install,
            args: &["--help".to_owned()],
        };
        assert!(matches!(PolkitAction::from(cmdline), PolkitAction::Info));

        let cmdline = BrewCommandLine {
            command: BrewCommand::Install,
            args: &["help".to_owned()],
        };
        assert!(matches!(PolkitAction::from(cmdline), PolkitAction::Install));

        let cmdline = BrewCommandLine {
            command: BrewCommand::Install,
            args: &["hello".to_owned()],
        };
        assert!(matches!(PolkitAction::from(cmdline), PolkitAction::Install));
    }
}
