// SPDX-FileCopyrightText: 2023 Gleb Smirnov <glebsmirnov0708@gmail.com>
// SPDX-FileCopyrightText: 2025 Stanislav Alekseev <stas.ale66@gmail.com>
//
// SPDX-License-Identifier: MIT

//! This crate provides action provider type for function-based actions.
//!
//! If you want to use it, make sure you always call [`init`] in the beginning
//! of the program before doing anything else. If you use `textpieces-core`
//! crate, you can use `textpieces-core::init` that calls [`init`].

#![warn(
    missing_docs,
    clippy::missing_docs_in_private_items,
    clippy::missing_panics_doc
)]

mod action;
mod parameters;

pub use action::*;
use futures::channel::oneshot;
pub use parameters::*;
use thiserror::Error;

use std::{
    collections::HashMap,
    marker::PhantomData,
    sync::{
        atomic::{AtomicBool, AtomicU64, Ordering},
        Arc, Mutex, TryLockError,
    },
    time::Duration,
};

// use crossmist::{KillHandle, StaticRef};
use textpieces_foundations::{Action, ActionProvider, ActionProviderStop, ParameterValue};

pub use anyhow;
pub use procspawn::SpawnError;

/// This function must be called before doing anything that have
/// observable effects.
pub fn init() {
    procspawn::init();
}

/// Trait representing a collection of function-based actions.
///
/// It's used because it allows to require the collection to
/// be defined as `const`. It guarantees that the collection
/// is available by the pointer at any moment. It's required
/// because
pub trait FunctionCollection: Send + Sync {
    /// Array of function-based actions.
    const COLLECTION: &'static [DynFunctionAction];
}

/// Action provider for function-based actions.
#[derive(Debug)]
pub struct Functions<C: FunctionCollection> {
    /// Counter for generating task IDs.
    next_task_id: AtomicU64,

    /// Cancellers for running actions.
    cancellers: Mutex<HashMap<u64, Arc<AtomicBool>>>,

    /// Needed to use `C::COLLECTION`.
    _phantom_collection: PhantomData<C>,
}

impl<C: FunctionCollection> Functions<C> {
    /// Creates new action provider.
    pub fn new() -> Self {
        Self {
            next_task_id: AtomicU64::new(0),
            cancellers: Mutex::default(),
            _phantom_collection: PhantomData,
        }
    }
}

impl<C: FunctionCollection> Default for Functions<C> {
    fn default() -> Self {
        Self::new()
    }
}

impl<C: FunctionCollection> ActionProvider for Functions<C> {
    type LoadActionsError = std::convert::Infallible;

    async fn load_actions(&self) -> Result<HashMap<String, Action>, Self::LoadActionsError> {
        Ok(C::COLLECTION
            .iter()
            .map(|action| {
                (
                    action.id().to_owned(),
                    Action {
                        name: action.name().to_owned(),
                        description: action.description().to_owned(),
                        parameters: action.parameters(),
                    },
                )
            })
            .collect())
    }

    async fn perform_action(
        &self,
        action_id: &str,
        parameters: &[ParameterValue],
        input: &str,
    ) -> Result<String, String> {
        let parameters = parameters.to_vec();
        let input = input.to_owned();

        let data = (
            action_id.to_string(),
            parameters.to_vec(),
            input.to_string(),
        );

        let task_id = self.next_task_id.fetch_add(1, Ordering::SeqCst);

        let (send, recv) = oneshot::channel::<Result<String, String>>();
        let is_cancelled = Arc::new(AtomicBool::new(false));

        self.cancellers
            .lock()
            .map_err(|e| e.to_string())?
            .insert(task_id, is_cancelled.clone());
        let _guard = scopeguard::guard((), |()| {
            if let Some(canceller) = self
                .cancellers
                .lock()
                .expect("Failed to lock cancellers table")
                .remove(&task_id)
            {
                canceller.store(true, Ordering::SeqCst);
            }
        });

        let mut handle = procspawn::spawn(data, |(action_id, parameters, input)| {
            let action = C::COLLECTION
                .iter()
                .find(|action| action.id() == action_id)
                .expect("unknown action");

            action
                .call_function(&input, &parameters)
                .map_err(|e| e.to_string())
        });

        std::thread::spawn(move || {
            let result = loop {
                match handle.join_timeout(Duration::from_secs(1)) {
                    Ok(result) => break Some(result),
                    Err(err) if err.is_timeout() => (),
                    Err(err) => break Some(Err(err.to_string())),
                }
                if let Ok(result) = handle.join_timeout(Duration::from_secs(1)) {
                    break Some(result);
                }

                if is_cancelled.load(Ordering::SeqCst) || send.is_canceled() {
                    _ = handle.kill();
                    break None;
                }
            };

            if let Some(result) = result {
                _ = send.send(result);
            }
        });

        recv.await
            .unwrap_or_else(|_| Err(String::from("Action was stopped")))
    }
}

/// Error type for stopping running actions.
#[allow(missing_docs, reason = "they have messages")]
#[derive(Debug, Error)]
pub enum StopError {
    #[error(transparent)]
    Procspawn(#[from] SpawnError),

    #[error("cancellers table is locked")]
    LockedMutex,

    #[error("cancellers table mutex is poisoned")]
    PoisonedMutex,
}

impl<C: FunctionCollection> ActionProviderStop for Functions<C> {
    type StopError = StopError;

    fn stop(&self) -> Result<(), Self::StopError> {
        self.cancellers
            .try_lock()
            .map_err(|err| match err {
                TryLockError::Poisoned(_) => StopError::PoisonedMutex,
                TryLockError::WouldBlock => StopError::LockedMutex,
            })?
            .drain()
            .for_each(|(_, is_cancelled)| is_cancelled.store(true, Ordering::SeqCst));

        Ok(())
    }
}
