//! Small in-process executor for storage calls. It never launches a process.

use std::{
    sync::{
        Mutex,
        mpsc::{self, Receiver, Sender},
    },
    thread,
    time::Duration,
};

use ironstorage::{
    authentication::{NativeAuthenticationHandle, NativeAuthenticationSession},
    crypto::KeyInfo,
    secret_store::SecretProtectionPolicy,
};

use crate::app::{AsyncPayload, AsyncResult, RequestToken};
use crate::workflow::WorkflowSubmission;

pub struct AsyncExecutor {
    sender: Sender<AsyncResult>,
    receiver: Receiver<AsyncResult>,
    tasks: Mutex<Vec<thread::JoinHandle<()>>>,
}

#[derive(Debug)]
pub enum AuthenticationEvent {
    Granted(AuthenticationTarget),
    Failed { workflow: bool, message: String },
    Expired,
}

#[derive(Debug)]
pub enum AuthenticationTarget {
    Entry(String),
    Workflow(Box<WorkflowSubmission>),
    Git(ironstorage::command::GitRequest),
    Otp(crate::app::OtpUiRequest),
    Show(ironstorage::command::ShowRequest),
}

struct AuthenticationCompletion {
    generation: u64,
    target: AuthenticationTarget,
    result: Result<NativeAuthenticationHandle, String>,
}

pub struct AuthenticationCoordinator {
    session: NativeAuthenticationSession,
    key: KeyInfo,
    handle: Option<NativeAuthenticationHandle>,
    sender: Sender<AuthenticationCompletion>,
    receiver: Receiver<AuthenticationCompletion>,
    generation: u64,
    native_prompt_pending: bool,
}

impl AuthenticationCoordinator {
    pub fn system(
        timeout: ironstorage::authentication::AuthenticationTimeout,
        key: KeyInfo,
    ) -> Result<Self, String> {
        let session =
            NativeAuthenticationSession::system(SecretProtectionPolicy::default(), timeout)
                .map_err(|error| error.to_string())?;
        let (sender, receiver) = mpsc::channel();
        Ok(Self {
            session,
            key,
            handle: None,
            sender,
            receiver,
            generation: 0,
            native_prompt_pending: false,
        })
    }

    pub fn request_entry(&mut self, entry: String) {
        self.request(AuthenticationTarget::Entry(entry));
    }

    pub fn request_workflow(&mut self, submission: Box<WorkflowSubmission>) {
        self.request(AuthenticationTarget::Workflow(submission));
    }

    pub fn request_git(&mut self, request: ironstorage::command::GitRequest) {
        self.request(AuthenticationTarget::Git(request));
    }

    pub fn request_otp(&mut self, request: crate::app::OtpUiRequest) {
        self.request(AuthenticationTarget::Otp(request));
    }

    pub fn request_show(&mut self, request: ironstorage::command::ShowRequest) {
        self.request(AuthenticationTarget::Show(request));
    }

    fn request(&mut self, target: AuthenticationTarget) {
        self.generation = self.generation.wrapping_add(1);
        self.native_prompt_pending = self.handle.is_none();
        let generation = self.generation;
        let session = self.session.clone();
        let key = self.key.clone();
        let sender = self.sender.clone();
        thread::spawn(move || {
            let result = session
                .authenticate(&key)
                .map_err(|error| error.to_string());
            let _ignored = sender.send(AuthenticationCompletion {
                generation,
                target,
                result,
            });
        });
    }

    pub fn completion(&mut self) -> Option<AuthenticationEvent> {
        let completion = self
            .receiver
            .try_iter()
            .filter(|completion| completion.generation == self.generation)
            .last()?;
        self.native_prompt_pending = false;
        match completion.result {
            Ok(handle) => {
                self.handle = Some(handle);
                Some(AuthenticationEvent::Granted(completion.target))
            }
            Err(error) => {
                self.handle = None;
                Some(AuthenticationEvent::Failed {
                    workflow: matches!(completion.target, AuthenticationTarget::Workflow(_)),
                    message: error,
                })
            }
        }
    }

    pub fn touch_user_activity(&mut self) -> Option<AuthenticationEvent> {
        let handle = self.handle.as_ref()?;
        if let Err(error) = handle.touch_user_activity() {
            self.handle = None;
            return Some(AuthenticationEvent::Failed {
                workflow: false,
                message: error.to_string(),
            });
        }
        None
    }

    pub fn poll_lease(&mut self) -> Option<AuthenticationEvent> {
        match self.session.expire() {
            Ok(true) => {
                self.handle = None;
                Some(AuthenticationEvent::Expired)
            }
            Ok(false) => None,
            Err(error) => {
                self.handle = None;
                Some(AuthenticationEvent::Failed {
                    workflow: false,
                    message: error.to_string(),
                })
            }
        }
    }

    pub fn remaining_time(&self) -> Option<Duration> {
        self.handle
            .as_ref()
            .and_then(|handle| handle.remaining_time().ok())
    }

    pub fn handle(&self) -> Option<NativeAuthenticationHandle> {
        self.handle.clone()
    }

    pub const fn native_prompt_pending(&self) -> bool {
        self.native_prompt_pending
    }

    pub fn lock(&mut self) -> Result<(), String> {
        self.generation = self.generation.wrapping_add(1);
        self.native_prompt_pending = false;
        self.handle = None;
        self.session
            .manual_lock()
            .map_err(|error| error.to_string())
    }
}

impl Default for AsyncExecutor {
    fn default() -> Self {
        Self::new()
    }
}

impl AsyncExecutor {
    pub fn new() -> Self {
        let (sender, receiver) = mpsc::channel();
        Self {
            sender,
            receiver,
            tasks: Mutex::new(Vec::new()),
        }
    }

    pub fn submit<F>(&self, token: RequestToken, work: F)
    where
        F: FnOnce() -> Result<AsyncPayload, String> + Send + 'static,
    {
        let sender = self.sender.clone();
        let task = thread::spawn(move || {
            let _ignored = sender.send(AsyncResult {
                token,
                payload: work(),
            });
        });
        self.tasks
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
            .push(task);
    }

    pub fn progress_reporter(
        &self,
        token: RequestToken,
    ) -> impl Fn(ironstorage::git::GitProgressPhase) + Send + Sync + 'static {
        let sender = self.sender.clone();
        move |phase| {
            let _ignored = sender.send(AsyncResult {
                token,
                payload: Ok(AsyncPayload::GitProgress(phase)),
            });
        }
    }

    pub fn clipboard_started_reporter(
        &self,
        token: RequestToken,
    ) -> impl Fn(crate::app::ClipboardPresentationId, std::time::Instant) + Send + 'static {
        let sender = self.sender.clone();
        move |presentation, deadline| {
            let _ignored = sender.send(AsyncResult {
                token,
                payload: Ok(AsyncPayload::ClipboardStarted {
                    presentation,
                    deadline,
                }),
            });
        }
    }

    pub fn drain(&self) -> impl Iterator<Item = AsyncResult> + '_ {
        self.receiver.try_iter()
    }
}

impl Drop for AsyncExecutor {
    fn drop(&mut self) {
        let tasks = self
            .tasks
            .get_mut()
            .unwrap_or_else(std::sync::PoisonError::into_inner);
        for task in tasks.drain(..) {
            let _ignored = task.join();
        }
    }
}

#[cfg(test)]
mod tests {
    use std::time::Duration;

    use super::*;
    use crate::app::{App, ResultDisposition};

    #[test]
    fn work_completes_off_the_input_thread() {
        let executor = AsyncExecutor::new();
        let mut app = App::new();
        let token = app.begin_request();
        executor.submit(token, || Err("test result".to_owned()));

        let result = executor
            .receiver
            .recv_timeout(Duration::from_secs(2))
            .expect("worker should return a typed result");
        assert_eq!(app.apply_result(result), ResultDisposition::Applied);
    }

    #[test]
    fn blocked_storage_work_never_blocks_interaction_state() {
        let executor = AsyncExecutor::new();
        let mut app = App::new();
        let token = app.begin_request();
        let (release, blocked) = std::sync::mpsc::channel();
        executor.submit(token, move || {
            blocked.recv().expect("release slow storage");
            Err("slow result".to_owned())
        });

        app.resize(60, 12);
        app.tick();
        assert!(app.transition(crate::app::Transition::OpenHelp));
        assert_eq!(app.mode(), crate::app::Mode::Help);

        release.send(()).expect("release worker");
        let result = executor
            .receiver
            .recv_timeout(Duration::from_secs(2))
            .expect("worker result");
        assert_eq!(app.apply_result(result), ResultDisposition::Applied);
    }
}
