//! Polkit authentication agent, split by concern:
//! - `service`: the D-Bus agent (registration, `AuthenticationAgentResponse2`).
//! - `view`: the auth-dialog rendering and hit-testing.
//! - this module: the model and controller (request queue, password buffer,
//!   PAM verification off-thread, and the outcome plumbing).

use std::collections::{HashMap, VecDeque};
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::{Duration, Instant};

use calloop::channel::{self, Sender};
use calloop::LoopHandle;
use tracing::error;
use zeroize::{Zeroize, Zeroizing};

use crate::drawing::{RenderTexture, Texture};
use crate::forge::Forge;

mod service;
mod view;

#[derive(Debug, Clone, Copy)]
pub enum AuthOutcome {
    Success,
    Failed,
    Cancelled,
}

pub(crate) type PendingMap = Arc<Mutex<HashMap<String, async_channel::Sender<AuthOutcome>>>>;

// Dialog background colours (consumed by `new`); layout and text colours live
// alongside the rendering in `view`.
const OVERLAY_BG: [u8; 4] = [0, 0, 0, 160];
const DIALOG_BG: [u8; 4] = [30, 30, 30, 245];
const INPUT_BG: [u8; 4] = [50, 50, 50, 255];
const BUTTON_BG: [u8; 4] = [60, 100, 180, 255];
const CANCEL_BG: [u8; 4] = [80, 80, 80, 255];
const MAX_ATTEMPTS: u32 = 3;
const MAX_PASSWORD_BYTES: usize = 128;

/// Cap on queued authorization requests, so a client cannot flood the agent.
const MAX_POLKIT_QUEUE: usize = 8;

#[derive(Debug, Clone)]
pub struct PolkitRequest {
    pub message: String,
    #[allow(dead_code)]
    pub icon: String,
    pub cookie: String,
    pub target_uid: u32,
    pub attempts: u32,
}

#[derive(Debug)]
pub enum PolkitEvent {
    AuthRequest(PolkitRequest),
    AuthCancel(String),
    AuthResult { cookie: String, ok: bool },
}

pub enum PolkitAction {
    Authenticate,
    Cancel,
    None,
}

pub struct PolkitAgent {
    pub visible: bool,
    queue: VecDeque<PolkitRequest>,
    password_buffer: Zeroizing<String>,
    error: Option<String>,
    authenticating: bool,
    dirty: bool,
    last_scale: f64,

    pending: PendingMap,
    event_sender: Option<Sender<PolkitEvent>>,

    overlay_bg: RenderTexture,
    dialog_bg: RenderTexture,
    input_bg: RenderTexture,
    button_bg: RenderTexture,
    cancel_bg: RenderTexture,
    title_texture: Option<RenderTexture>,
    message_texture: Option<RenderTexture>,
    dots_texture: Option<RenderTexture>,
    error_texture: Option<RenderTexture>,
    auth_label: Option<RenderTexture>,
    cancel_label: Option<RenderTexture>,
}

impl PolkitAgent {
    pub fn new( event_sender: Sender<PolkitEvent>, pending: PendingMap ) -> Self {
        Self {
            visible: false,
            queue: VecDeque::new(),
            password_buffer: Zeroizing::new( String::with_capacity( MAX_PASSWORD_BYTES + 8 ) ),
            error: None,
            authenticating: false,
            dirty: true,
            last_scale: 0.0,
            pending,
            event_sender: Some( event_sender ),
            overlay_bg: RenderTexture::new(Texture::from_color(OVERLAY_BG, false)),
            dialog_bg: RenderTexture::new(Texture::from_color(DIALOG_BG, true)),
            input_bg: RenderTexture::new(Texture::from_color(INPUT_BG, true)),
            button_bg: RenderTexture::new(Texture::from_color(BUTTON_BG, true)),
            cancel_bg: RenderTexture::new(Texture::from_color(CANCEL_BG, true)),
            title_texture: None,
            message_texture: None,
            dots_texture: None,
            error_texture: None,
            auth_label: None,
            cancel_label: None,
        }
    }

    /// Start polkit agent listener. Returns the event sender and the shared
    /// pending-response map to hand to [`PolkitAgent::new`].
    pub fn start_agent( event_loop: &LoopHandle<'static, Forge> ) -> ( Sender<PolkitEvent>, PendingMap ) {
        let ( sender, channel ) = channel::channel::<PolkitEvent>();
        let pending: PendingMap = Arc::new( Mutex::new( HashMap::new() ) );

        event_loop
            .insert_source(channel, |event, _, forge| {
                if let channel::Event::Msg(evt) = event {
                    match evt {
                        PolkitEvent::AuthRequest(req) => {
                            let q = &forge.shell.polkit.queue;
                            let dup = q.iter().any(|r| r.cookie == req.cookie);
                            if !dup && q.len() < MAX_POLKIT_QUEUE {
                                forge.shell.polkit.queue.push_back(req);
                                forge.shell.polkit.visible = true;
                                forge.shell.polkit.authenticating = false;
                                forge.shell.polkit.error = None;
                                forge.shell.polkit.reset_buffer();
                                forge.shell.polkit.dirty = true;
                            } else {
                                // Rejected (queue full or duplicate cookie):
                                // answer the pending D-Bus call instead of
                                // stranding it on its channel forever. Not
                                // `resolve` — that would also reset the
                                // password buffer of the dialog on screen.
                                let tx = forge
                                    .shell
                                    .polkit
                                    .pending
                                    .lock()
                                    .unwrap_or_else(|e| e.into_inner())
                                    .remove(&req.cookie);
                                if let Some(tx) = tx {
                                    let _ = tx.try_send(AuthOutcome::Cancelled);
                                }
                            }
                        }
                        PolkitEvent::AuthCancel(cookie) => {
                            forge.shell.polkit.resolve(&cookie, AuthOutcome::Cancelled);
                        }
                        PolkitEvent::AuthResult { cookie, ok } => {
                            forge.shell.polkit.on_auth_result(&cookie, ok);
                        }
                    }
                    forge.sync_modal_focus();
                    forge.windows.set_dirty();
                    forge.unstall();
                }
            })
            .expect("polkit channel");

        let thread_pending = pending.clone();
        let thread_sender = sender.clone();
        thread::Builder::new()
            .name("forge-polkit".into())
            .spawn(move || {
                if let Err(e) = service::run_polkit_agent(thread_sender, thread_pending) {
                    error!("Polkit agent failed: {e}");
                }
            })
            .expect("polkit thread");

        ( sender, pending )
    }

    fn reset_buffer( &mut self ) {
        self.password_buffer.zeroize();
        self.password_buffer.clear();
    }

    pub fn push_char(&mut self, c: char) {
        if self.authenticating {
            return;
        }
        if self.password_buffer.len() + c.len_utf8() <= MAX_PASSWORD_BYTES {
            self.password_buffer.push(c);
            self.dirty = true;
        }
    }

    pub fn pop_char(&mut self) {
        if self.authenticating {
            return;
        }
        self.password_buffer.pop();
        self.dirty = true;
    }

    /// Resolve `cookie` towards polkit and drop its request from the queue.
    fn resolve(&mut self, cookie: &str, outcome: AuthOutcome) {
        if let Some(tx) = self.pending.lock().unwrap_or_else(|e| e.into_inner()).remove(cookie) {
            let _ = tx.try_send(outcome);
        }
        self.queue.retain(|r| r.cookie != cookie);
        self.authenticating = false;
        self.error = None;
        self.reset_buffer();
        self.visible = !self.queue.is_empty();
        self.dirty = true;
    }

    fn on_auth_result(&mut self, cookie: &str, ok: bool) {
        self.authenticating = false;
        if ok {
            self.resolve(cookie, AuthOutcome::Success);
            return;
        }
        let give_up = match self.queue.front_mut() {
            Some(req) if req.cookie == cookie => {
                req.attempts += 1;
                req.attempts >= MAX_ATTEMPTS
            }
            _ => false,
        };
        if give_up {
            self.resolve(cookie, AuthOutcome::Failed);
        } else {
            self.error = Some("Authentication failed".into());
            self.reset_buffer();
            self.dirty = true;
        }
    }

    pub fn cancel(&mut self) {
        if self.authenticating {
            return;
        }
        match self.queue.front() {
            Some(req) => {
                let cookie = req.cookie.clone();
                self.resolve(&cookie, AuthOutcome::Cancelled);
            }
            None => {
                self.visible = false;
                self.dirty = true;
            }
        }
    }

    /// Begin verifying the entered password off the compositor thread.
    pub fn begin_authenticate(&mut self) {
        if self.authenticating || self.password_buffer.is_empty() {
            return;
        }
        let Some(req) = self.queue.front() else {
            return;
        };
        let cookie = req.cookie.clone();
        let uid = req.target_uid;

        let password = std::mem::replace(
            &mut self.password_buffer,
            Zeroizing::new(String::with_capacity(MAX_PASSWORD_BYTES + 8)),
        );
        self.authenticating = true;
        self.error = None;
        self.dirty = true;

        let sender = self.event_sender.clone();
        let _ = thread::Builder::new()
            .name("forge-polkit-auth".into())
            .spawn(move || {
                let start = Instant::now();
                let pam_ok = crate::pam_verify::verify_password(uid, &password);
                let ok = pam_ok && service::send_response(&cookie, uid);
                drop(password);

                if !ok {
                    let elapsed = start.elapsed();
                    if elapsed < Duration::from_secs(1) {
                        thread::sleep(Duration::from_secs(1) - elapsed);
                    }
                }

                if let Some(sender) = sender {
                    let _ = sender.send(PolkitEvent::AuthResult { cookie, ok });
                }
            });
    }

    pub fn invalidate(&mut self) {
        self.dirty = true;
    }
}
