use std::path::Path;
use std::sync::{Arc, Mutex};

use zeroize::Zeroizing;

use crate::channel::{channel_id_from_name, parse_channel_name, Channel};
use crate::mls_engine::MlsEngine;
use crate::transport::{validate_grpc_endpoint, GrpcTransport, Transport};
use crate::SdkError;

/// Top-level client for the Skytale SDK.
///
/// Manages a single MLS identity, a gRPC transport connection, and a tokio
/// runtime. Channels created from this client share the runtime and transport.
///
/// # Example
///
/// ```no_run
/// use skytale_sdk::client::SkytaleClient;
///
/// let mut client = SkytaleClient::new(
///     "https://relay.skytale.sh:5000",
///     "/tmp/agent-data",
///     b"agent-1",
/// ).unwrap();
///
/// let mut ch = client.create_channel("org/team/general").unwrap();
/// ch.send(b"hello").unwrap();
/// ```
pub struct SkytaleClient {
    runtime: Arc<tokio::runtime::Runtime>,
    mls_engine: Arc<Mutex<MlsEngine>>,
    transport: Arc<dyn Transport>,
}

impl SkytaleClient {
    /// Create a new client connected to the supernode.
    ///
    /// * `endpoint` — gRPC endpoint, e.g. `"https://relay.skytale.sh:5000"`
    /// * `data_dir` — directory for MLS state persistence
    /// * `identity` — unique agent identity bytes (e.g. agent name or UUID)
    pub fn new(endpoint: &str, data_dir: &str, identity: &[u8]) -> Result<Self, SdkError> {
        validate_grpc_endpoint(endpoint)?;

        let data_path = Path::new(data_dir);

        let runtime = tokio::runtime::Builder::new_multi_thread()
            .enable_all()
            .build()
            .map_err(|e| SdkError::Runtime(e.to_string()))?;

        let engine = MlsEngine::new(data_path, identity.to_vec()).map_err(SdkError::MlsEngine)?;

        let transport = runtime
            .block_on(GrpcTransport::connect(endpoint, None))
            .map_err(SdkError::Transport)?;

        Ok(Self {
            runtime: Arc::new(runtime),
            mls_engine: Arc::new(Mutex::new(engine)),
            transport: Arc::new(transport) as Arc<dyn Transport>,
        })
    }

    /// Create a new client authenticated via API key.
    ///
    /// Exchanges the API key for a JWT via the API server, then connects
    /// to the supernode with the JWT for authentication.
    ///
    /// * `api_key` — API key (e.g. `"sk_live_..."`)
    /// * `api_url` — API server URL (e.g. `"https://api.skytale.sh"`)
    /// * `endpoint` — supernode gRPC endpoint (e.g. `"https://relay.skytale.sh:5000"`)
    /// * `data_dir` — directory for MLS state persistence
    /// * `identity` — unique agent identity bytes
    pub fn new_with_api_key(
        api_key: &str,
        api_url: &str,
        endpoint: &str,
        data_dir: &str,
        identity: &[u8],
    ) -> Result<Self, SdkError> {
        validate_grpc_endpoint(endpoint)?;

        let data_path = Path::new(data_dir);

        let runtime = tokio::runtime::Builder::new_multi_thread()
            .enable_all()
            .build()
            .map_err(|e| SdkError::Runtime(e.to_string()))?;

        let engine = MlsEngine::new(data_path, identity.to_vec()).map_err(SdkError::MlsEngine)?;

        // Exchange API key for JWT.
        let jwt = runtime
            .block_on(exchange_api_key_for_jwt(api_key, api_url))
            .map_err(|e| SdkError::Auth(e.to_string()))?;

        let transport = runtime
            .block_on(GrpcTransport::connect(endpoint, Some(Zeroizing::new(jwt))))
            .map_err(SdkError::Transport)?;

        Ok(Self {
            runtime: Arc::new(runtime),
            mls_engine: Arc::new(Mutex::new(engine)),
            transport: Arc::new(transport) as Arc<dyn Transport>,
        })
    }

    /// Create a new client using a pre-built transport (e.g. `MockTransport`).
    ///
    /// Skips gRPC connection — useful for local development and testing.
    ///
    /// * `transport` — any `Transport` implementation (typically `MockTransport`)
    /// * `data_dir` — directory for MLS state persistence
    /// * `identity` — unique agent identity bytes
    pub fn new_mock(
        transport: Arc<dyn Transport>,
        data_dir: &str,
        identity: &[u8],
    ) -> Result<Self, SdkError> {
        let data_path = Path::new(data_dir);

        let runtime = tokio::runtime::Builder::new_multi_thread()
            .enable_all()
            .build()
            .map_err(|e| SdkError::Runtime(e.to_string()))?;

        let engine = MlsEngine::new(data_path, identity.to_vec()).map_err(SdkError::MlsEngine)?;

        Ok(Self {
            runtime: Arc::new(runtime),
            mls_engine: Arc::new(Mutex::new(engine)),
            transport,
        })
    }

    /// Create a new encrypted channel.
    ///
    /// `name` must be a 3-component SLIM name as `"org/namespace/service"`.
    #[tracing::instrument(skip(self), fields(channel.name = name))]
    pub fn create_channel(&mut self, name: &str) -> Result<Channel, SdkError> {
        let (org, ns, svc) = parse_channel_name(name)?;
        let channel_id = channel_id_from_name(&org, &ns, &svc);

        // Create the MLS group.
        {
            let mut engine = self.mls_engine.lock().map_err(|_| SdkError::LockPoisoned)?;
            engine
                .create_group(&channel_id)
                .map_err(SdkError::MlsEngine)?;
        }

        // Subscribe on the transport.
        let receiver = self
            .runtime
            .block_on(self.transport.subscribe((&org, &ns, &svc), channel_id))?;

        Ok(Channel::new(
            channel_id,
            (org, ns, svc),
            self.mls_engine.clone(),
            self.transport.clone(),
            self.runtime.clone(),
            receiver,
        ))
    }

    /// Generate a KeyPackage for joining channels.
    ///
    /// Other agents need this to add us to their channels via
    /// `Channel::add_member`.
    pub fn generate_key_package(&mut self) -> Result<Vec<u8>, SdkError> {
        let engine = self.mls_engine.lock().map_err(|_| SdkError::LockPoisoned)?;
        engine.generate_key_package().map_err(SdkError::MlsEngine)
    }

    /// Join a channel from a Welcome message received from another agent.
    ///
    /// `name` must match the channel name used by the creator.
    /// `welcome_bytes` are the bytes returned by `Channel::add_member`.
    #[tracing::instrument(skip(self, welcome_bytes), fields(channel.name = name))]
    pub fn join_channel(&mut self, name: &str, welcome_bytes: &[u8]) -> Result<Channel, SdkError> {
        let (org, ns, svc) = parse_channel_name(name)?;
        let channel_id = channel_id_from_name(&org, &ns, &svc);

        // Join the MLS group from the Welcome.
        {
            let mut engine = self.mls_engine.lock().map_err(|_| SdkError::LockPoisoned)?;
            engine
                .join_from_welcome(welcome_bytes)
                .map_err(SdkError::MlsEngine)?;
        }

        // Subscribe on the transport.
        let receiver = self
            .runtime
            .block_on(self.transport.subscribe((&org, &ns, &svc), channel_id))?;

        Ok(Channel::new(
            channel_id,
            (org, ns, svc),
            self.mls_engine.clone(),
            self.transport.clone(),
            self.runtime.clone(),
            receiver,
        ))
    }
}

/// Validate that a URL uses HTTPS, or HTTP only for localhost/loopback.
///
/// Parses the URL properly to prevent bypasses like `http://localhost.evil.com`
/// or `http://localhost@evil.com`.
pub(crate) fn validate_url_security(url_str: &str) -> Result<(), String> {
    let parsed = url::Url::parse(url_str).map_err(|e| format!("invalid URL: {e}"))?;
    match parsed.scheme() {
        "https" => Ok(()),
        "http" => match parsed.host_str() {
            Some("localhost") | Some("127.0.0.1") | Some("[::1]") => Ok(()),
            _ => Err(format!(
                "non-localhost HTTP endpoints must use HTTPS (got {url_str})"
            )),
        },
        other => Err(format!("unsupported URL scheme: {other}")),
    }
}

/// Exchange an API key for a short-lived JWT via the API server.
async fn exchange_api_key_for_jwt(api_key: &str, api_url: &str) -> Result<String, String> {
    validate_url_security(api_url)?;

    // Disable redirect following to prevent leaking the API key to redirect
    // targets, and set a timeout to avoid hanging indefinitely.
    let client = reqwest::Client::builder()
        .redirect(reqwest::redirect::Policy::none())
        .timeout(std::time::Duration::from_secs(30))
        .build()
        .map_err(|e| format!("failed to build HTTP client: {e}"))?;
    let url = format!("{}/v1/tokens", api_url.trim_end_matches('/'));

    let response = client
        .post(&url)
        .header("Authorization", format!("Bearer {}", api_key))
        .send()
        .await
        .map_err(|e| format!("failed to contact API server: {e}"))?;

    if response.status().is_redirection() {
        return Err(
            "API server returned a redirect — refusing to follow to protect API key".into(),
        );
    }

    if !response.status().is_success() {
        let status = response.status();
        // Do not include the response body — it may echo the API key.
        return Err(format!("token exchange failed (HTTP {status})"));
    }

    #[derive(serde::Deserialize)]
    struct TokenResponse {
        token: String,
    }

    let token_resp: TokenResponse = response
        .json()
        .await
        .map_err(|e| format!("failed to parse token response: {e}"))?;

    Ok(token_resp.token)
}
