pub mod channel;
pub mod client;
pub mod mls_engine;

pub mod mock_transport;
pub mod transport;

/// Top-level error type for the SDK, wrapping sub-system errors.
// MLS errors are large by nature; boxing them would add allocation overhead
// on every error path without meaningful benefit.
#[allow(clippy::result_large_err)]
#[derive(Debug, thiserror::Error)]
pub enum SdkError {
    #[error("MLS engine error: {0}")]
    MlsEngine(#[from] mls_engine::MlsEngineError),

    #[error("transport error: {0}")]
    Transport(#[from] transport::TransportError),

    #[error("invalid channel name: {0} (expected org/namespace/service)")]
    InvalidChannelName(String),

    #[error("message too large: {0} bytes exceeds {1} byte limit")]
    MessageTooLarge(usize, usize),

    #[error("key package too large: {0} bytes exceeds {1} byte limit")]
    KeyPackageTooLarge(usize, usize),

    #[error("config error: {0}")]
    Config(String),

    #[error("runtime error: {0}")]
    Runtime(String),

    #[error("auth error: {0}")]
    Auth(String),

    #[error("internal lock poisoned")]
    LockPoisoned,
}

// ---------------------------------------------------------------------------
// PyO3 bindings (behind the "python" feature flag)
// ---------------------------------------------------------------------------

#[cfg(feature = "python")]
use pyo3::prelude::*;

#[cfg(feature = "python")]
use std::collections::HashMap;
#[cfg(feature = "python")]
use std::sync::{LazyLock, Mutex as StdMutex};

/// Global registry of mock transports keyed by group name.
/// Agents sharing the same mock_group get the same transport instance.
#[cfg(feature = "python")]
static MOCK_TRANSPORTS: LazyLock<
    StdMutex<HashMap<String, std::sync::Arc<dyn transport::Transport>>>,
> = LazyLock::new(|| StdMutex::new(HashMap::new()));

/// Map an `SdkError` to the appropriate Python exception from `skytale_sdk.errors`.
///
/// Falls back to `PyRuntimeError` if the errors module is not importable.
#[cfg(feature = "python")]
fn sdk_error_to_py(err: SdkError) -> pyo3::PyErr {
    let class_name = match &err {
        SdkError::Auth(_) => "AuthError",
        SdkError::Transport(_) => "TransportError",
        SdkError::MlsEngine(_) => "MlsError",
        SdkError::InvalidChannelName(_) => "ChannelError",
        SdkError::MessageTooLarge(_, _) => "ChannelError",
        SdkError::KeyPackageTooLarge(_, _) => "ChannelError",
        SdkError::Config(_) | SdkError::Runtime(_) | SdkError::LockPoisoned => "SkytaleError",
    };
    let msg = err.to_string();
    let fallback_msg = msg.clone();

    // The GIL is already held when called from #[pymethods].
    Python::try_attach(|py| {
        if let Ok(module) = PyModule::import(py, "skytale_sdk.errors") {
            if let Ok(cls) = module.getattr(class_name) {
                if let Ok(instance) = cls.call1((msg.clone(),)) {
                    return pyo3::PyErr::from_value(instance.into_any());
                }
            }
        }
        pyo3::exceptions::PyRuntimeError::new_err(msg)
    })
    .unwrap_or_else(|| pyo3::exceptions::PyRuntimeError::new_err(fallback_msg))
}

/// Python-exposed client for creating and managing encrypted MLS channels.
///
/// This is a `#[pyclass]` binding over `SkytaleClient` that surfaces the core
/// lifecycle operations — constructing a client, generating key packages, and
/// opening or joining channels — to Python callers via the `_native` extension module.
#[cfg(feature = "python")]
#[pyclass]
pub struct PySkytaleClient {
    inner: client::SkytaleClient,
}

#[cfg(feature = "python")]
#[pymethods]
impl PySkytaleClient {
    #[new]
    #[pyo3(signature = (endpoint="", data_dir="", identity=vec![], api_key=None, api_url=None, mock=false, mock_group=None))]
    fn new(
        endpoint: &str,
        data_dir: &str,
        identity: Vec<u8>,
        api_key: Option<&str>,
        api_url: Option<&str>,
        mock: bool,
        mock_group: Option<&str>,
    ) -> PyResult<Self> {
        let client = if mock {
            // Mock mode: use in-memory transport.
            let group = mock_group.unwrap_or("default").to_string();
            let transport = {
                let mut registry = MOCK_TRANSPORTS.lock().map_err(|_| {
                    pyo3::exceptions::PyRuntimeError::new_err("mock registry lock poisoned")
                })?;
                registry
                    .entry(group)
                    .or_insert_with(|| std::sync::Arc::new(mock_transport::MockTransport::new()))
                    .clone()
            };
            let dir = if data_dir.is_empty() {
                let id_hex = hex::encode(&identity);
                let tmp = std::env::temp_dir();
                tmp.join(format!("skytale-mock-{}", id_hex))
                    .to_string_lossy()
                    .into_owned()
            } else {
                data_dir.to_string()
            };
            // Symlink check is handled by MlsEngine::new() inside new_mock().
            client::SkytaleClient::new_mock(transport, &dir, &identity)
        } else {
            match (api_key, api_url) {
                (Some(key), Some(url)) => {
                    client::SkytaleClient::new_with_api_key(key, url, endpoint, data_dir, &identity)
                }
                _ => client::SkytaleClient::new(endpoint, data_dir, &identity),
            }
        }
        .map_err(sdk_error_to_py)?;
        Ok(Self { inner: client })
    }

    fn create_channel(&mut self, name: &str) -> PyResult<PyChannel> {
        let ch = self.inner.create_channel(name).map_err(sdk_error_to_py)?;
        Ok(PyChannel { inner: ch })
    }

    fn generate_key_package(&mut self) -> PyResult<Vec<u8>> {
        self.inner.generate_key_package().map_err(sdk_error_to_py)
    }

    fn join_channel(&mut self, name: &str, welcome_bytes: Vec<u8>) -> PyResult<PyChannel> {
        let ch = self
            .inner
            .join_channel(name, &welcome_bytes)
            .map_err(sdk_error_to_py)?;
        Ok(PyChannel { inner: ch })
    }
}

/// Python-exposed handle for an open encrypted channel.
///
/// Wraps a `Channel` and exposes the send, receive, and membership operations
/// as `#[pymethods]` so Python code can add members, publish encrypted payloads,
/// and iterate over incoming messages without touching Rust types directly.
#[cfg(feature = "python")]
#[pyclass]
pub struct PyChannel {
    inner: channel::Channel,
}

#[cfg(feature = "python")]
#[pymethods]
impl PyChannel {
    fn add_member(&mut self, key_package: Vec<u8>) -> PyResult<Vec<u8>> {
        self.inner.add_member(&key_package).map_err(sdk_error_to_py)
    }

    fn send(&mut self, payload: Vec<u8>) -> PyResult<()> {
        self.inner.send(&payload).map_err(sdk_error_to_py)
    }

    fn messages(&self) -> PyResult<PyMessageIterator> {
        let iter = self.inner.messages();
        Ok(PyMessageIterator { inner: iter })
    }
}

/// Python iterator over decrypted messages arriving on a channel.
///
/// Implements the `__iter__` / `__next__` protocol as a `#[pyclass]` so it can
/// be used in a plain Python `for` loop. Each call to `__next__` polls the
/// underlying `MessageIterator` with a short timeout and briefly releases the
/// GIL between polls, allowing other Python threads (e.g. a sender) to run
/// concurrently. Raises `TimeoutError` after 5 minutes of inactivity.
#[cfg(feature = "python")]
#[pyclass]
pub struct PyMessageIterator {
    inner: channel::MessageIterator,
}

#[cfg(feature = "python")]
#[pymethods]
impl PyMessageIterator {
    fn __iter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> {
        slf
    }

    fn __next__(&mut self, py: Python<'_>) -> PyResult<Option<Vec<u8>>> {
        let poll_duration = std::time::Duration::from_millis(50);
        let max_idle = std::time::Duration::from_secs(300);
        let idle_start = std::time::Instant::now();
        loop {
            // Poll with a short timeout so we don't hold the GIL forever.
            match self.inner.next_timeout(poll_duration) {
                Some(msg) => {
                    return Ok(Some(msg.payload));
                }
                None => {
                    if idle_start.elapsed() > max_idle {
                        return Err(pyo3::exceptions::PyTimeoutError::new_err(
                            "no messages received for 5 minutes, connection may be lost",
                        ));
                    }
                    // Release the GIL briefly so other Python threads can run
                    // (e.g., the main thread calling send()).
                    py.detach(|| std::thread::sleep(std::time::Duration::from_millis(1)));
                    // Check for KeyboardInterrupt / signals.
                    py.check_signals()?;
                }
            }
        }
    }
}

#[cfg(feature = "python")]
#[pymodule]
fn _native(m: &Bound<'_, PyModule>) -> PyResult<()> {
    m.add_class::<PySkytaleClient>()?;
    m.add_class::<PyChannel>()?;
    m.add_class::<PyMessageIterator>()?;
    Ok(())
}
