//! Bounded bridge between synchronous Component Host calls and async Toasty.
//!
//! Wasmtime calls [`wasmeld_runtime::KvBackend`] synchronously on an Actor
//! thread, while Toasty and libSQL are asynchronous. This adapter moves every
//! database operation onto one dedicated current-thread Tokio runtime. The
//! bounded channel prevents a slow database from accumulating unbounded work
//! or blocking every Actor while waiting for queue capacity.
//!
//! # Timeout semantics
//!
//! A timeout only stops the caller from waiting. It does not remove a command
//! that is already queued or cancel a libSQL operation that has started.
//! Consequently, `set` and `delete` may commit after the Component observes
//! `KvBackendError::Timeout`. Components should make retries idempotent and
//! must not interpret a timeout as proof that no mutation occurred.

use std::{
    fmt, io,
    sync::{
        Arc, Mutex,
        mpsc::{self, Receiver, SyncSender, TrySendError},
    },
    thread,
    time::Duration,
};

use wasmeld_runtime::{KvBackend, KvBackendError};

use crate::persistence::Persistence;

const COMMAND_CAPACITY: usize = 64;
// Keep Host calls responsive even when a Component requests a larger timeout.
// The Component-facing deadline is an upper bound, not permission to occupy an
// Actor thread indefinitely.
const MAX_RESPONSE_TIMEOUT: Duration = Duration::from_millis(400);

pub(crate) struct ToastyKvBackend {
    inner: Arc<BackendInner>,
}

struct BackendInner {
    sender: SyncSender<KvCommand>,
    worker: Mutex<Option<thread::JoinHandle<()>>>,
}

enum KvCommand {
    Get {
        service_id: String,
        key: String,
        response: SyncSender<Result<Option<Vec<u8>>, KvBackendError>>,
    },
    Set {
        service_id: String,
        key: String,
        value: Vec<u8>,
        response: SyncSender<Result<(), KvBackendError>>,
    },
    Delete {
        service_id: String,
        key: String,
        response: SyncSender<Result<(), KvBackendError>>,
    },
    Shutdown,
}

impl ToastyKvBackend {
    /// Starts the database worker and waits until its Tokio runtime is ready.
    ///
    /// Waiting for the ready signal makes construction fail deterministically
    /// instead of returning a backend whose first operation discovers that
    /// the worker could not initialize.
    pub(crate) fn start(persistence: Persistence) -> io::Result<Self> {
        let (sender, receiver) = mpsc::sync_channel(COMMAND_CAPACITY);
        let (ready_sender, ready_receiver) = mpsc::sync_channel(1);
        let worker = thread::Builder::new()
            .name("wasmeld-kv-backend".to_owned())
            .spawn(move || run_worker(persistence, receiver, ready_sender))?;

        match ready_receiver.recv() {
            Ok(Ok(())) => Ok(Self {
                inner: Arc::new(BackendInner {
                    sender,
                    worker: Mutex::new(Some(worker)),
                }),
            }),
            Ok(Err(error)) => {
                let _ = worker.join();
                Err(error)
            }
            Err(_) => {
                let _ = worker.join();
                Err(io::Error::other("KV backend worker stopped during startup"))
            }
        }
    }

    fn request<T>(
        &self,
        timeout: Duration,
        build: impl FnOnce(SyncSender<Result<T, KvBackendError>>) -> KvCommand,
    ) -> Result<T, KvBackendError> {
        let (response, receiver) = mpsc::sync_channel(1);
        // `try_send` is intentional: waiting for queue capacity here would
        // stall the Actor and defeat the Runtime's invocation deadline.
        match self.inner.sender.try_send(build(response)) {
            Ok(()) => {}
            Err(TrySendError::Full(_)) => return Err(KvBackendError::Busy),
            Err(TrySendError::Disconnected(_)) => {
                return Err(KvBackendError::Operation(
                    "backend worker is unavailable".to_owned(),
                ));
            }
        }
        receiver
            .recv_timeout(timeout.min(MAX_RESPONSE_TIMEOUT))
            .map_err(|_| KvBackendError::Timeout)?
    }
}

impl Clone for ToastyKvBackend {
    fn clone(&self) -> Self {
        Self {
            inner: Arc::clone(&self.inner),
        }
    }
}

impl fmt::Debug for ToastyKvBackend {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter
            .debug_struct("ToastyKvBackend")
            .finish_non_exhaustive()
    }
}

impl KvBackend for ToastyKvBackend {
    fn get(
        &self,
        service_id: &str,
        key: &str,
        timeout: Duration,
    ) -> Result<Option<Vec<u8>>, KvBackendError> {
        self.request(timeout, |response| KvCommand::Get {
            service_id: service_id.to_owned(),
            key: key.to_owned(),
            response,
        })
    }

    fn set(
        &self,
        service_id: &str,
        key: &str,
        value: &[u8],
        timeout: Duration,
    ) -> Result<(), KvBackendError> {
        self.request(timeout, |response| KvCommand::Set {
            service_id: service_id.to_owned(),
            key: key.to_owned(),
            value: value.to_vec(),
            response,
        })
    }

    fn delete(&self, service_id: &str, key: &str, timeout: Duration) -> Result<(), KvBackendError> {
        self.request(timeout, |response| KvCommand::Delete {
            service_id: service_id.to_owned(),
            key: key.to_owned(),
            response,
        })
    }
}

impl Drop for BackendInner {
    fn drop(&mut self) {
        // Only the final Arc owner reaches this Drop. Joining guarantees the
        // worker no longer holds the Persistence handle when Console shuts
        // down. If commands precede Shutdown, the FIFO channel drains them.
        let _ = self.sender.send(KvCommand::Shutdown);
        if let Ok(worker) = self.worker.get_mut()
            && let Some(worker) = worker.take()
        {
            let _ = worker.join();
        }
    }
}

fn run_worker(
    persistence: Persistence,
    receiver: Receiver<KvCommand>,
    ready: SyncSender<io::Result<()>>,
) {
    // Toasty is driven from one thread so synchronous Host calls never need to
    // enter or block the Console server's multi-thread Tokio runtime.
    let runtime = match tokio::runtime::Builder::new_current_thread()
        .enable_all()
        .build()
    {
        Ok(runtime) => runtime,
        Err(error) => {
            let _ = ready.send(Err(error));
            return;
        }
    };
    let _ = ready.send(Ok(()));

    // A single consumer also defines the ordering of mutations submitted to
    // this backend. Ordering across different Wasmeld processes is delegated
    // to libSQL and is not guaranteed by this queue.
    while let Ok(command) = receiver.recv() {
        match command {
            KvCommand::Get {
                service_id,
                key,
                response,
            } => {
                let result = runtime
                    .block_on(persistence.kv_get(&service_id, &key))
                    .map_err(operation_failed);
                let _ = response.send(result);
            }
            KvCommand::Set {
                service_id,
                key,
                value,
                response,
            } => {
                let result = runtime
                    .block_on(persistence.kv_set(service_id, key, value))
                    .map_err(operation_failed);
                let _ = response.send(result);
            }
            KvCommand::Delete {
                service_id,
                key,
                response,
            } => {
                let result = runtime
                    .block_on(persistence.kv_delete(&service_id, &key))
                    .map_err(operation_failed);
                let _ = response.send(result);
            }
            KvCommand::Shutdown => return,
        }
    }
}

fn operation_failed(error: toasty::Error) -> KvBackendError {
    tracing::error!(%error, "persistent KV operation failed");
    KvBackendError::Operation("persistent storage failure".to_owned())
}
