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

use std::{
    collections::HashMap,
    os::unix::process::ExitStatusExt,
    path::{Path, PathBuf},
    process::Stdio,
    time::Duration,
};

use async_trait::async_trait;
use enumflags2::BitFlag;
use tokio::process::Child;
use tracing::{debug, info};
use zbus::{Connection, fdo, message::Header, object_server::SignalEmitter};
use zbus_polkit::policykit1::{AuthorityProxy, CheckAuthorizationFlags, Subject};

use crate::io_handles::{IoHandles, IoHandlesSerial};
use brew_proxy_utils::{
    BREW_HOME, BrewCommandLine, BusType, DBUS_DAEMON_NAME, DBUS_DAEMON_PATH, PolkitAction,
    call_with_brew_command_idents, idle,
};

#[async_trait]
pub trait AuthChecker {
    async fn check_authorization(
        &self,
        conn: &Connection,
        subject: &Subject,
        cmdline: BrewCommandLine<'_>,
        interactive: bool,
    ) -> fdo::Result<()>;
}

#[derive(Clone, Debug)]
pub struct PolkitAuth;

#[async_trait]
impl AuthChecker for PolkitAuth {
    async fn check_authorization(
        &self,
        conn: &Connection,
        subject: &Subject,
        cmdline: BrewCommandLine<'_>,
        interactive: bool,
    ) -> fdo::Result<()> {
        let action = PolkitAction::from(cmdline);
        debug!(
            "{}: checking Polkit authorization for action {} for message with subject {:?}",
            cmdline,
            action.id(),
            subject
        );
        let auth_proxy = AuthorityProxy::new(conn).await?;
        let flags = if interactive {
            CheckAuthorizationFlags::AllowUserInteraction.into()
        } else {
            CheckAuthorizationFlags::empty()
        };
        let result = auth_proxy
            .check_authorization(subject, action.id(), &HashMap::new(), flags, "")
            .await?;
        if result.is_authorized {
            Ok(())
        } else {
            Err(fdo::Error::AccessDenied(action.auth_message().to_string()))
        }
    }
}

fn spawn_command_in_pty(
    brew_path: &Path,
    cmdline: BrewCommandLine<'_>,
) -> pty_process::Result<(IoHandles, Child)> {
    debug!("{cmdline}: spawning command in PTY");
    let (pty, pts) = pty_process::open()?;
    pty.resize(pty_process::Size::new(24, 80))?;
    let child = pty_process::Command::new(brew_path)
        .arg(cmdline.command.command())
        .args(cmdline.args)
        .env("HOME", BREW_HOME)
        .stdin(Stdio::null())
        .kill_on_drop(true)
        .spawn(pts)?;
    Ok((IoHandles::from_pty(pty), child))
}

fn spawn_command_piped(
    brew_path: &Path,
    cmdline: BrewCommandLine<'_>,
) -> std::io::Result<(IoHandles, Child)> {
    debug!("{cmdline}: spawning command with piped stdio");
    let (stdin_reader, stdin_writer) = std::io::pipe()?;
    let (stdout_reader, stdout_writer) = std::io::pipe()?;
    let (stderr_reader, stderr_writer) = std::io::pipe()?;
    let child = tokio::process::Command::new(brew_path)
        .arg(cmdline.command.command())
        .args(cmdline.args)
        .env("HOME", BREW_HOME)
        .stdin(Stdio::from(stdin_reader))
        .stdout(Stdio::from(stdout_writer))
        .stderr(Stdio::from(stderr_writer))
        .kill_on_drop(true)
        .spawn()?;
    let io_handles = IoHandles::from_stdio(stdin_writer, stdout_reader, stderr_reader);
    Ok((io_handles, child))
}

fn spawn_command(
    brew_path: &Path,
    cmdline: BrewCommandLine<'_>,
    piped: bool,
) -> zbus::Result<(IoHandles, Child)> {
    if piped {
        spawn_command_piped(brew_path, cmdline).map_err(|err| err.into())
    } else {
        spawn_command_in_pty(brew_path, cmdline)
            .map_err(|err| zbus::Error::Failure(err.to_string()))
    }
}

async fn wait_for_child_exit(
    _active: idle::ActiveClient,
    mut child: Child,
    pid: u32,
    emitter: SignalEmitter<'static>,
    cmdline: String,
) {
    info!("{cmdline} [PID {pid}]: Waiting for command completion");
    match child.wait().await {
        Ok(status) => {
            let exit_code = if let Some(code) = status.code() {
                info!("{cmdline} [PID {pid}]: exited with status {code}");
                code
            } else if let Some(signal) = status.signal() {
                info!("{cmdline} [PID {pid}]: killed by signal {signal}");
                signal + 128
            } else {
                info!("{cmdline} [PID {pid}]: exited with unknown status");
                255
            };
            let _ = emitter.exit_status(pid, exit_code).await;
        }
        Err(err) => {
            info!("{cmdline} [PID {pid}]: failed with error: {err}");
            let _ = emitter.exit_status(pid, 1).await;
        }
    }
}

#[non_exhaustive]
struct RunBrewCommandOpts<'a> {
    pub conn: &'a Connection,
    pub header: Header<'a>,
    pub emitter: SignalEmitter<'a>,
    pub auth_checker: &'a (dyn AuthChecker + Send + Sync),
    pub brew_path: &'a Path,
    pub cmdline: BrewCommandLine<'a>,
    pub piped: bool,
    pub interactive_auth: bool,
}

async fn run_brew_command(opts: RunBrewCommandOpts<'_>) -> fdo::Result<(IoHandles, u32)> {
    let active = idle::ActiveClient::new();
    info!("Received request to run: {}", opts.cmdline);

    let subject = match Subject::new_for_message_header(&opts.header) {
        Ok(subject) => subject,
        Err(err) => return Err(fdo::Error::AuthFailed(err.to_string())),
    };
    opts.auth_checker
        .check_authorization(opts.conn, &subject, opts.cmdline, opts.interactive_auth)
        .await?;

    let (io_handles, child) = spawn_command(opts.brew_path, opts.cmdline, opts.piped)?;
    let pid = child.id().unwrap_or(0);
    tokio::spawn(wait_for_child_exit(
        active,
        child,
        pid,
        opts.emitter.into_owned(),
        opts.cmdline.to_string(),
    ));
    Ok((io_handles, pid))
}

pub struct BrewInterface {
    brew_path: PathBuf,
    auth_checker: Box<dyn AuthChecker + Send + Sync>,
}

macro_rules! impl_brew_proxy_interface {
    ($($cmd:ident)*) => {
        #[allow(dead_code, non_snake_case)]
        #[zbus::interface(
            name = "sh.brew.BrewProxy",
            proxy(
                default_service = "sh.brew.BrewProxy",
                default_path = "/sh/brew/BrewProxy",
            ),
        )]
        impl BrewInterface {
            $(
                #[zbus(out_args("io", "pid"))]
                async fn $cmd(
                    &self,
                    #[zbus(connection)] conn: &Connection,
                    #[zbus(header)] header: Header<'_>,
                    #[zbus(signal_emitter)] emitter: SignalEmitter<'_>,
                    args: Vec<String>,
                    piped: bool,
                    interactive_auth: bool,
                ) -> fdo::Result<(IoHandlesSerial, u32)> {
                    let cmdline = BrewCommandLine {
                        command: ::brew_proxy_utils::BrewCommand::$cmd,
                        args: &args,
                    };
                    let (io_handles, pid) = run_brew_command(
                        RunBrewCommandOpts {
                            conn,
                            header,
                            emitter,
                            auth_checker: &*self.auth_checker,
                            brew_path: &self.brew_path,
                            cmdline,
                            piped,
                            interactive_auth,
                        }
                    ).await?;
                    Ok((IoHandlesSerial::from(io_handles), pid))
                }
            )*

            #[zbus(signal)]
            async fn exit_status(
                emitter: &SignalEmitter<'_>,
                pid: u32,
                status: i32,
            ) -> zbus::Result<()>;
        }
    };
}

call_with_brew_command_idents!(impl_brew_proxy_interface);

pub async fn run(
    brew_path: PathBuf,
    bus_type: BusType,
    auth_checker: Box<dyn AuthChecker + Send + Sync>,
) -> zbus::Result<Connection> {
    let conn_builder = match bus_type {
        BusType::System => zbus::connection::Builder::system()?,
        BusType::Session => zbus::connection::Builder::session()?,
    };

    let iface = BrewInterface {
        brew_path,
        auth_checker,
    };
    let connection = conn_builder
        .name(DBUS_DAEMON_NAME)?
        .serve_at(DBUS_DAEMON_PATH, iface)?
        .build()
        .await?;

    info!("Listening at {DBUS_DAEMON_NAME}...");
    Ok(connection)
}

pub async fn run_with_idle_timeout(
    brew_path: PathBuf,
    bus_type: BusType,
    auth_checker: Box<dyn AuthChecker + Send + Sync>,
    idle_timeout: Duration,
) -> zbus::Result<()> {
    let connection = run(brew_path, bus_type, auth_checker).await?;

    idle::init_idle_tracker();
    let check_frequency = Duration::from_secs(60).min(idle_timeout / 2);
    loop {
        tokio::time::sleep(check_frequency).await;
        if let Some(idle_time) = idle::idle_time() {
            if idle_time >= idle_timeout {
                break;
            } else if idle_time >= idle_timeout - check_frequency {
                if check_frequency >= Duration::from_secs(1) {
                    info!(
                        "Daemon is idle and will auto-exit in {} seconds.",
                        check_frequency.as_secs()
                    );
                } else {
                    info!("Daemon is idle and will auto-exit in {check_frequency:?}.",);
                }
            }
        }
    }
    info!("Daemon auto-exiting due to idleness.");
    connection.graceful_shutdown().await;
    Ok(())
}
