//! The polkit D-Bus authentication agent: registration on the system bus and
//! delivering `AuthenticationAgentResponse2` once PAM has verified the user.

use std::collections::HashMap;
use std::sync::OnceLock;
use std::thread;
use std::time::Duration;

use calloop::channel::Sender;
use tracing::{error, info, warn};
use zbus::zvariant::{OwnedValue, Value};

use super::{AuthOutcome, PendingMap, PolkitEvent, PolkitRequest};

static AGENT_CONN: OnceLock<zbus::blocking::Connection> = OnceLock::new();

struct AgentInterface {
    sender: Sender<PolkitEvent>,
    pending: PendingMap,
}

#[zbus::interface(name = "org.freedesktop.PolicyKit1.AuthenticationAgent")]
impl AgentInterface {
    async fn begin_authentication(
        &self,
        _action_id:   String,
        message:       String,
        icon_name:     String,
        _details:      HashMap<String, String>,
        cookie:        String,
        identities:    Vec<(String, HashMap<String, OwnedValue>)>,
    ) -> zbus::fdo::Result<()>
    {
        let target_uid = select_target_uid( &identities );
        let ( tx, rx ) = async_channel::bounded( 1 );
        {
            self.pending.lock().unwrap_or_else( |e| e.into_inner() ).insert( cookie.clone(), tx );
        }

        // A failed send means the compositor loop is gone: nothing will ever
        // resolve this cookie, so drop the orphan entry instead of awaiting a
        // reply that can never arrive.
        if self.sender.send( PolkitEvent::AuthRequest( PolkitRequest {
            message,
            icon:       icon_name,
            cookie:     cookie.clone(),
            target_uid,
            attempts:   0,
        })).is_err()
        {
            self.pending.lock().unwrap_or_else( |e| e.into_inner() ).remove( &cookie );
            return Err( zbus::fdo::Error::Failed( "compositor unavailable".into() ) );
        }

        match rx.recv().await.unwrap_or( AuthOutcome::Cancelled ) {
            AuthOutcome::Success   => Ok(()),
            AuthOutcome::Failed    => Err( zbus::fdo::Error::Failed( "authentication failed".into() ) ),
            AuthOutcome::Cancelled => Err( zbus::fdo::Error::Failed( "authentication cancelled".into() ) ),
        }
    }

    fn cancel_authentication( &self, cookie: &str )
    {
        let _ = self.sender.send( PolkitEvent::AuthCancel( cookie.to_string() ) );
    }
}

fn select_target_uid( identities: &[(String, HashMap<String, OwnedValue>)] ) -> u32
{
    let self_uid = unsafe { libc::getuid() };
    let mut first_user: Option<u32> = None;

    for ( kind, details ) in identities {
        if kind != "unix-user" {
            continue;
        }
        if let Some( uid ) = details.get( "uid" ).and_then( |v| u32::try_from( v ).ok() ) {
            if uid == self_uid {
                return uid;
            }
            first_user.get_or_insert( uid );
        }
    }

    first_user.unwrap_or( self_uid )
}

/// Deliver the authenticated identity to polkit for `cookie`. Returns whether
/// the bus call succeeded.
pub(super) fn send_response( cookie: &str, target_uid: u32 ) -> bool
{
    let Some( conn ) = AGENT_CONN.get() else {
        error!( "Polkit: agent connection not ready" );
        return false;
    };

    let details: HashMap<&str, Value> = HashMap::from([ ("uid", Value::U32( target_uid )) ]);
    let identity = ( "unix-user", details );
    let agent_uid = unsafe { libc::getuid() };

    match conn.call_method(
        Some( "org.freedesktop.PolicyKit1" ),
        "/org/freedesktop/PolicyKit1/Authority",
        Some( "org.freedesktop.PolicyKit1.Authority" ),
        "AuthenticationAgentResponse2",
        &( agent_uid, cookie, identity ),
    ) {
        Ok( _ )  => true,
        Err( e ) => {
            error!( "AuthenticationAgentResponse2 failed: {e}" );
            false
        }
    }
}

fn session_id( conn: &zbus::blocking::Connection ) -> Option<String>
{
    if let Ok( id ) = std::env::var( "XDG_SESSION_ID" ) {
        if !id.is_empty() {
            return Some( id );
        }
    }

    let reply = conn.call_method(
        Some( "org.freedesktop.login1" ),
        "/org/freedesktop/login1",
        Some( "org.freedesktop.login1.Manager" ),
        "GetSessionByPID",
        &( std::process::id(), ),
    ).ok()?;

    let path: zbus::zvariant::OwnedObjectPath = reply.body().deserialize().ok()?;
    path.as_str().rsplit( '/' ).next().map( str::to_owned )
}

pub(super) fn run_polkit_agent(
    sender: Sender<PolkitEvent>,
    pending: PendingMap,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>>
{
    info!("Polkit agent: connecting to system bus");

    let conn = zbus::blocking::Connection::system()?;
    let _ = AGENT_CONN.set( conn.clone() );

    // Export the authentication agent interface so polkit can call us back.
    conn.object_server().at(
        "/org/freedesktop/PolicyKit1/AuthenticationAgent",
        AgentInterface { sender, pending },
    )?;

    let Some( session ) = session_id( &conn ) else {
        warn!( "Polkit agent: no session id — not registering" );
        loop {
            thread::sleep( Duration::from_secs( 60 ) );
        }
    };

    let subject_details: HashMap<&str, Value<'_>> =
        HashMap::from([ ("session-id", Value::new( session.as_str() )) ]);

    match conn.call_method(
        Some( "org.freedesktop.PolicyKit1" ),
        "/org/freedesktop/PolicyKit1/Authority",
        Some( "org.freedesktop.PolicyKit1.Authority" ),
        "RegisterAuthenticationAgent",
        &(
            ( "unix-session", &subject_details ),
            "en_US",
            "/org/freedesktop/PolicyKit1/AuthenticationAgent",
        ),
    ) {
        Ok( _ )  => info!( "Polkit agent registered (session {session})" ),
        Err( e ) => warn!( "Polkit agent registration failed: {e} — auth dialogs may not appear" ),
    }

    // Hold the connection alive; zbus dispatches incoming calls on its
    // internal executor as long as `conn` is live.
    loop {
        thread::sleep( Duration::from_secs( 60 ) );
    }
}
