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

use std::{io::Write, process::ExitCode};

use nix::errno::Errno;
use pty_process::Pty;
use tokio::{
    io::{AsyncWrite, AsyncWriteExt},
    join,
};
use tokio_stream::StreamExt;
use tracing::debug;
use zbus::{Connection, fdo};

use crate::daemon::BrewInterfaceProxy;
use brew_proxy_utils::{
    BREW_PROXY_HOMEPAGE, BREW_PROXY_ISSUES_PAGE, BrewCommand, BusType,
    call_with_brew_command_idents,
};

pub fn print_usage(mut writer: impl Write) -> std::io::Result<()> {
    let usage = r#"brew (via brew-proxy)

Example usage:
  brew search TEXT|/REGEX/
  brew info [FORMULA|CASK...]
  brew install FORMULA|CASK...
  brew update
  brew upgrade [FORMULA|CASK...]
  brew uninstall FORMULA|CASK...
  brew list [FORMULA|CASK...]

Troubleshooting:
  brew config
  brew doctor
  brew install --verbose --debug FORMULA|CASK

Further help:
  brew commands
  brew help [COMMAND]
  man brew
  https://docs.brew.sh

Note: brew-proxy does not support Homebrew developer commands.
"#;
    write!(writer, "{}", usage)
}

pub fn print_brew_proxy_info(cmd: BrewCommand, mut writer: impl Write) -> std::io::Result<()> {
    match cmd {
        BrewCommand::Config => {
            writeln!(writer, "brew-proxy version: {}", env!("CARGO_PKG_VERSION"))
        }
        BrewCommand::Doctor => {
            writeln!(
                writer,
                r#"Note: You are running brew via brew-proxy. For more info, see:
  {BREW_PROXY_HOMEPAGE}
If you are experiencing issues with Homebrew, please ensure the issue still
occurs with a standard Homebrew installation before reporting it to Homebrew.
If the issue is specific to brew-proxy, you can report it here:
  {BREW_PROXY_ISSUES_PAGE}
"#
            )
        }
        BrewCommand::Version => {
            writeln!(writer, "brew-proxy {}", env!("CARGO_PKG_VERSION"))
        }
        _ => Ok(()),
    }
}

macro_rules! match_brew_cmd_to_method {
    ($($cmd:ident)*) => {
        async move |proxy: &BrewInterfaceProxy,
                    cmd: BrewCommand,
                    args: Vec<String>,
                    interactive: bool| {
            match cmd {
                $(
                    BrewCommand::$cmd => proxy.$cmd(args, interactive).await,
                )*
            }
        }
    };
}

pub async fn run(
    bus_type: BusType,
    writer: Box<dyn AsyncWrite + Send + Sync>,
    cmd: BrewCommand,
    args: Vec<String>,
    interactive: bool,
) -> fdo::Result<ExitCode> {
    let conn = match bus_type {
        BusType::System => Connection::system().await?,
        BusType::Session => Connection::session().await?,
    };
    let proxy = BrewInterfaceProxy::new(&conn).await?;
    let all_exit_status_stream = proxy.receive_exit_status().await?;

    debug!(
        "Requesting brew command '{}' with args {:?}",
        cmd.command(),
        args
    );
    let (pty_fd, pid) =
        call_with_brew_command_idents!(match_brew_cmd_to_method)(&proxy, cmd, args, interactive)
            .await?;

    if pid == 0 {
        return Err(fdo::Error::SpawnFailed(
            "received invalid PID for child process".to_owned(),
        ));
    }

    // SAFETY: `pty_fd` is a valid file descriptor belonging to a pty created by brew-proxy-daemon
    // in nonblocking mode.
    let mut pty =
        unsafe { Pty::from_fd(pty_fd.into()).map_err(|err| fdo::Error::Failed(err.to_string()))? };

    let mut exit_status_stream = all_exit_status_stream.filter_map(move |msg| {
        if let Ok(msg_args) = msg.args()
            && msg_args.pid == pid
        {
            let status = msg_args.status;
            debug!("Received exit status {status} from PID {pid}");
            Some(msg_args.status)
        } else {
            None
        }
    });

    debug!("Copying pty output and waiting for exit status on PID {pid}");
    let mut writer = Box::into_pin(writer);
    let (copy_result, exit_status) = join!(
        tokio::io::copy(&mut pty, &mut writer),
        exit_status_stream.next()
    );
    match copy_result {
        Ok(_) => {}
        // This error is expected when the subsidiary end of the pty closes.
        Err(err) if err.raw_os_error() == Some(Errno::EIO as _) => {}
        // Other errors are not expected.
        Err(err) => return Err(fdo::Error::IOError(err.to_string())),
    }
    let _ = writer.shutdown().await;

    let exit_code = exit_status
        .map(|status| ExitCode::from(status as u8))
        .unwrap_or(ExitCode::FAILURE);
    Ok(exit_code)
}
