// SPDX-FileCopyrightText: Copyright 2026 Daniel Hast
//
// SPDX-License-Identifier: Apache-2.0 OR MIT

use std::{
    fs::File,
    os::fd::{AsFd, OwnedFd as StdOwnedFd},
    path::PathBuf,
};

use zbus::zvariant::OwnedFd;

use crate::{daemon::BrewInterfaceProxy, error::Result};

const DEFAULT_BREWFILE_PATH: &str = "./Brewfile";

/// `brew bundle` needs special handling because it expects to read a Brewfile that the sandboxed
/// `brew` might not be able to access. We work around this by modifying the arguments to `brew
/// bundle` to read from standard input, and pass the Brewfile as a file descriptor.
pub async fn run_brew_bundle(
    proxy: &BrewInterfaceProxy<'_>,
    mut args: Vec<String>,
    interactive: bool,
) -> Result<(OwnedFd, u32, Option<PathBuf>)> {
    let bundle_options = modify_bundle_args(&mut args);
    let stdout_path = bundle_options.stdout_path();
    let stdin_fd = bundle_options.into_stdin_fd()?;
    let (pty_fd, pid) = proxy.Bundle(args, stdin_fd, interactive).await?;
    Ok((pty_fd, pid, stdout_path))
}

fn modify_bundle_args(args: &mut Vec<String>) -> BundleOptions {
    let bundle_options = BundleOptions::from_args(args);
    match &bundle_options.file {
        // Modify `--file` argument to point to stdin
        Some(Brewfile::Path(_)) => {
            let mut is_file_arg = false;
            for arg in args.iter_mut() {
                if is_file_arg {
                    *arg = "-".to_owned();
                    is_file_arg = false;
                } else if arg == "--file" {
                    is_file_arg = true;
                } else if arg.starts_with("--file=") {
                    *arg = "--file=-".to_owned();
                }
            }
        }
        // No `--file` argument needs to be modified in these cases
        Some(Brewfile::Global | Brewfile::Stdin) => {}
        // Need to add a `--file=-` argument
        None => {
            args.push("--file=-".to_owned());
        }
    }
    bundle_options
}

#[derive(Clone, Debug, PartialEq, Eq)]
struct BundleOptions {
    file: Option<Brewfile>,
    dump: bool,
}

impl BundleOptions {
    fn from_args(args: &[String]) -> Self {
        let dump = args.iter().any(|arg| arg == "dump");
        let mut file = None;
        let mut is_file_arg = false;
        for arg in args {
            if is_file_arg {
                file = Some(Brewfile::from_arg(arg));
                break;
            }
            if arg == "--file" {
                is_file_arg = true;
                continue;
            }
            if arg == "-g" || arg == "--global" {
                file = Some(Brewfile::Global);
                break;
            }
            if let Some(file_arg) = arg.strip_prefix("--file=") {
                file = Some(Brewfile::from_arg(file_arg));
                break;
            }
        }
        Self { file, dump }
    }

    fn stdout_path(&self) -> Option<PathBuf> {
        if !self.dump {
            return None;
        }
        match &self.file {
            Some(Brewfile::Path(brewfile)) => Some(brewfile.to_path_buf()),
            Some(Brewfile::Global | Brewfile::Stdin) => None,
            None => Some(PathBuf::from(DEFAULT_BREWFILE_PATH)),
        }
    }

    fn into_stdin_fd(self) -> Result<Option<OwnedFd>> {
        if self.dump {
            return Ok(None);
        }
        let stdin_fd: Option<StdOwnedFd> = match self.file {
            Some(Brewfile::Global) => None,
            Some(Brewfile::Stdin) => Some(std::io::stdin().as_fd().try_clone_to_owned()?),
            Some(Brewfile::Path(brewfile)) => Some(StdOwnedFd::from(File::open(brewfile)?)),
            None => Some(StdOwnedFd::from(File::open(DEFAULT_BREWFILE_PATH)?)),
        };
        Ok(stdin_fd.map(OwnedFd::from))
    }
}

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

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

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

    #[test]
    fn empty_bundle_args() {
        let mut args = vec![];
        let opts = modify_bundle_args(&mut args);
        assert!(!opts.dump);
        assert!(opts.file.is_none());
        assert_eq!(args.len(), 1);
        assert_eq!(args[0], "--file=-");
    }

    #[test]
    fn dump_only() {
        let mut args = vec!["dump".to_owned()];
        let opts = modify_bundle_args(&mut args);
        assert!(opts.dump);
        assert!(opts.file.is_none());
        assert_eq!(args.len(), 2);
        assert_eq!(args[0], "dump");
        assert_eq!(args[1], "--file=-");
    }

    #[test]
    fn stdin_specified() {
        let mut args = vec!["--file=-".to_owned()];
        let opts = modify_bundle_args(&mut args);
        assert!(!opts.dump);
        assert_eq!(opts.file, Some(Brewfile::Stdin));
        assert_eq!(args.len(), 1);
        assert_eq!(args[0], "--file=-");
    }

    #[test]
    fn file_specified() {
        let mut args = vec!["--file=/path/to/file".to_owned()];
        let opts = modify_bundle_args(&mut args);
        assert!(!opts.dump);
        assert_eq!(opts.file, Some(Brewfile::Path("/path/to/file".into())));
        assert_eq!(args.len(), 1);
        assert_eq!(args[0], "--file=-");
    }

    #[test]
    fn file_specified_in_two_args() {
        let mut args = vec!["--file".to_owned(), "/path/to/file".to_owned()];
        let opts = modify_bundle_args(&mut args);
        assert!(!opts.dump);
        assert_eq!(opts.file, Some(Brewfile::Path("/path/to/file".into())));
        assert_eq!(args.len(), 2);
        assert_eq!(args[0], "--file");
        assert_eq!(args[1], "-");
    }

    #[test]
    fn global_specified_short() {
        let mut args = vec!["-g".to_owned(), "asdf".to_owned()];
        let opts = modify_bundle_args(&mut args);
        assert!(!opts.dump);
        assert_eq!(opts.file, Some(Brewfile::Global));
        assert_eq!(args.len(), 2);
        assert_eq!(args[0], "-g");
        assert_eq!(args[1], "asdf");
    }

    #[test]
    fn global_specified_long() {
        let mut args = vec!["asdf".to_owned(), "--global".to_owned()];
        let opts = modify_bundle_args(&mut args);
        assert!(!opts.dump);
        assert_eq!(opts.file, Some(Brewfile::Global));
        assert_eq!(args.len(), 2);
        assert_eq!(args[0], "asdf");
        assert_eq!(args[1], "--global");
    }
}
