//! Runtime client for communication with the host.

use crate::runtime::{NodeInfo, Result, RobomotionError};
use once_cell::sync::OnceCell;
use parking_lot::RwLock;
use prost_types::Struct;
use serde_json::Value;
use std::collections::HashMap;
use std::sync::Arc;
use tonic::transport::Channel;

// Import the generated proto types
// These will be generated by tonic-build

/// Global runtime client instance.
static CLIENT: OnceCell<Arc<RwLock<Option<RuntimeClient>>>> = OnceCell::new();

/// Initialize the client storage.
fn get_client_storage() -> &'static Arc<RwLock<Option<RuntimeClient>>> {
    CLIENT.get_or_init(|| Arc::new(RwLock::new(None)))
}

/// Runtime client wrapper.
pub struct RuntimeClient {
    // client: proto::runtime_helper_client::RuntimeHelperClient<Channel>,
    channel: Channel,
}

impl RuntimeClient {
    /// Create a new runtime client connected to the given address.
    pub async fn connect(addr: String) -> Result<Self> {
        let channel = Channel::from_shared(addr)
            .map_err(|e| RobomotionError::Variable(e.to_string()))?
            .connect()
            .await?;

        Ok(Self { channel })
    }
}

/// Set the global runtime client.
pub fn set_client(client: RuntimeClient) {
    *get_client_storage().write() = Some(client);
}

/// Check if the client is initialized.
pub fn is_initialized() -> bool {
    get_client_storage().read().is_some()
}

/// Get robot info from the host.
pub async fn get_robot_info() -> Result<HashMap<String, Value>> {
    get_robot_info_internal().await
}

/// Internal function to get robot info.
pub async fn get_robot_info_internal() -> Result<HashMap<String, Value>> {
    // TODO: Implement actual gRPC call
    // For now, return empty map if client not initialized
    if !is_initialized() {
        return Err(RobomotionError::NotInitialized);
    }

    Ok(HashMap::new())
}

/// Get the robot ID.
pub async fn get_robot_id() -> Result<String> {
    let info = get_robot_info().await?;
    Ok(info
        .get("id")
        .and_then(|v| v.as_str())
        .unwrap_or("unknown")
        .to_string())
}

/// Check if LMO is capable.
pub async fn is_lmo_capable() -> bool {
    match get_robot_info().await {
        Ok(info) => {
            if let Some(caps) = info.get("capabilities") {
                if let Some(caps_obj) = caps.as_object() {
                    return caps_obj.get("lmo").and_then(|v| v.as_bool()).unwrap_or(false);
                }
            }
            false
        }
        Err(_) => false,
    }
}

/// Get a variable from the host.
pub async fn get_variable(scope: &str, name: &str, payload: &[u8]) -> Result<Value> {
    if !is_initialized() {
        return Err(RobomotionError::NotInitialized);
    }

    // TODO: Implement actual gRPC call
    Ok(Value::Null)
}

/// Set a variable on the host.
pub async fn set_variable(scope: &str, name: &str, value: Value) -> Result<()> {
    if !is_initialized() {
        return Err(RobomotionError::NotInitialized);
    }

    // TODO: Implement actual gRPC call
    Ok(())
}

/// Get a vault item.
pub async fn get_vault_item(vault_id: &str, item_id: &str) -> Result<HashMap<String, Value>> {
    if !is_initialized() {
        return Err(RobomotionError::NotInitialized);
    }

    // TODO: Implement actual gRPC call
    Ok(HashMap::new())
}

/// Set a vault item.
pub async fn set_vault_item(
    vault_id: &str,
    item_id: &str,
    data: &[u8],
) -> Result<HashMap<String, Value>> {
    if !is_initialized() {
        return Err(RobomotionError::NotInitialized);
    }

    // TODO: Implement actual gRPC call
    Ok(HashMap::new())
}

/// Emit debug message.
pub async fn debug(guid: &str, name: &str, message: &Value) -> Result<()> {
    if !is_initialized() {
        return Err(RobomotionError::NotInitialized);
    }

    // TODO: Implement actual gRPC call
    Ok(())
}

/// Emit flow event.
pub async fn emit_flow_event(guid: &str, name: &str) -> Result<()> {
    if !is_initialized() {
        return Err(RobomotionError::NotInitialized);
    }

    // TODO: Implement actual gRPC call
    Ok(())
}

/// Emit input to a node.
pub async fn emit_input(guid: &str, input: &[u8]) -> Result<()> {
    if !is_initialized() {
        return Err(RobomotionError::NotInitialized);
    }

    // TODO: Implement actual gRPC call
    Ok(())
}

/// Emit output from a node.
pub async fn emit_output(guid: &str, output: &[u8], port: i32) -> Result<()> {
    if !is_initialized() {
        return Err(RobomotionError::NotInitialized);
    }

    // TODO: Implement actual gRPC call
    Ok(())
}

/// Emit error.
pub async fn emit_error(guid: &str, name: &str, message: &str) -> Result<()> {
    if !is_initialized() {
        return Err(RobomotionError::NotInitialized);
    }

    // TODO: Implement actual gRPC call
    Ok(())
}

/// App request (sync with timeout).
pub async fn app_request(request: &[u8], timeout: i32) -> Result<Vec<u8>> {
    if !is_initialized() {
        return Err(RobomotionError::NotInitialized);
    }

    // TODO: Implement actual gRPC call
    Ok(Vec::new())
}

/// App request V2.
pub async fn app_request_v2(request: &[u8]) -> Result<Vec<u8>> {
    if !is_initialized() {
        return Err(RobomotionError::NotInitialized);
    }

    // TODO: Implement actual gRPC call
    Ok(Vec::new())
}

/// App publish.
pub async fn app_publish(request: &[u8]) -> Result<()> {
    if !is_initialized() {
        return Err(RobomotionError::NotInitialized);
    }

    // TODO: Implement actual gRPC call
    Ok(())
}

/// App download.
pub async fn app_download(id: &str, dir: &str, file: &str) -> Result<String> {
    if !is_initialized() {
        return Err(RobomotionError::NotInitialized);
    }

    // TODO: Implement actual gRPC call
    Ok(String::new())
}

/// App upload.
pub async fn app_upload(id: &str, path: &str) -> Result<String> {
    if !is_initialized() {
        return Err(RobomotionError::NotInitialized);
    }

    // TODO: Implement actual gRPC call
    Ok(String::new())
}

/// Gateway request.
pub async fn gateway_request(
    method: &str,
    endpoint: &str,
    body: &str,
    headers: HashMap<String, String>,
) -> Result<GatewayResponse> {
    if !is_initialized() {
        return Err(RobomotionError::NotInitialized);
    }

    // TODO: Implement actual gRPC call
    Ok(GatewayResponse {
        status_code: 0,
        body: Vec::new(),
        headers: HashMap::new(),
    })
}

/// Gateway response.
#[derive(Debug, Clone)]
pub struct GatewayResponse {
    pub status_code: i32,
    pub body: Vec<u8>,
    pub headers: HashMap<String, String>,
}

/// HTTP request for proxy.
#[derive(Debug, Clone)]
pub struct HttpRequest {
    pub method: String,
    pub url: String,
    pub headers: HashMap<String, String>,
    pub body: Vec<u8>,
}

/// HTTP response from proxy.
#[derive(Debug, Clone)]
pub struct HttpResponse {
    pub status_code: i32,
    pub body: Vec<u8>,
    pub headers: HashMap<String, String>,
}

/// Proxy request.
pub async fn proxy_request(req: HttpRequest) -> Result<HttpResponse> {
    if !is_initialized() {
        return Err(RobomotionError::NotInitialized);
    }

    // TODO: Implement actual gRPC call
    Ok(HttpResponse {
        status_code: 0,
        body: Vec::new(),
        headers: HashMap::new(),
    })
}

/// Check if the flow is running.
pub async fn is_running() -> Result<bool> {
    if !is_initialized() {
        return Err(RobomotionError::NotInitialized);
    }

    // TODO: Implement actual gRPC call
    Ok(true)
}

/// Get port connections.
pub async fn get_port_connections(guid: &str, port: i32) -> Result<Vec<NodeInfo>> {
    if !is_initialized() {
        return Err(RobomotionError::NotInitialized);
    }

    // TODO: Implement actual gRPC call
    Ok(Vec::new())
}

/// Instance access info.
#[derive(Debug, Clone)]
pub struct InstanceAccess {
    pub amq_endpoint: String,
    pub api_endpoint: String,
    pub access_token: String,
}

/// Get instance access.
pub async fn get_instance_access() -> Result<InstanceAccess> {
    if !is_initialized() {
        return Err(RobomotionError::NotInitialized);
    }

    // TODO: Implement actual gRPC call
    Ok(InstanceAccess {
        amq_endpoint: String::new(),
        api_endpoint: String::new(),
        access_token: String::new(),
    })
}

/// Close the runtime.
pub async fn close() -> Result<()> {
    if !is_initialized() {
        return Err(RobomotionError::NotInitialized);
    }

    // TODO: Implement actual gRPC call
    Ok(())
}

/// Convert protobuf Struct to serde_json Value.
pub fn struct_to_value(s: &Struct) -> Value {
    let mut map = serde_json::Map::new();
    for (key, value) in &s.fields {
        map.insert(key.clone(), prost_value_to_json(value));
    }
    Value::Object(map)
}

/// Convert protobuf Value to serde_json Value.
fn prost_value_to_json(v: &prost_types::Value) -> Value {
    use prost_types::value::Kind;

    match &v.kind {
        Some(Kind::NullValue(_)) => Value::Null,
        Some(Kind::NumberValue(n)) => {
            if n.fract() == 0.0 && *n >= i64::MIN as f64 && *n <= i64::MAX as f64 {
                Value::Number(serde_json::Number::from(*n as i64))
            } else {
                serde_json::Number::from_f64(*n)
                    .map(Value::Number)
                    .unwrap_or(Value::Null)
            }
        }
        Some(Kind::StringValue(s)) => Value::String(s.clone()),
        Some(Kind::BoolValue(b)) => Value::Bool(*b),
        Some(Kind::StructValue(s)) => struct_to_value(s),
        Some(Kind::ListValue(l)) => {
            Value::Array(l.values.iter().map(prost_value_to_json).collect())
        }
        None => Value::Null,
    }
}

/// Convert serde_json Value to protobuf Value.
pub fn json_to_prost_value(v: &Value) -> prost_types::Value {
    use prost_types::value::Kind;

    let kind = match v {
        Value::Null => Some(Kind::NullValue(0)),
        Value::Bool(b) => Some(Kind::BoolValue(*b)),
        Value::Number(n) => {
            let f = n.as_f64().unwrap_or(0.0);
            Some(Kind::NumberValue(f))
        }
        Value::String(s) => Some(Kind::StringValue(s.clone())),
        Value::Array(arr) => Some(Kind::ListValue(prost_types::ListValue {
            values: arr.iter().map(json_to_prost_value).collect(),
        })),
        Value::Object(obj) => {
            let fields = obj
                .iter()
                .map(|(k, v)| (k.clone(), json_to_prost_value(v)))
                .collect();
            Some(Kind::StructValue(Struct { fields }))
        }
    };

    prost_types::Value { kind }
}
