//! Socket.IO event routing and protocol handlers.
//!
//! Thin transport layer: parses incoming Socket.IO events and publishes them
//! to the event bus for domain-specific handling. Webhook routing lives in
//! `webhooks::bus`, channel inbound processing lives in `channels::bus`.

use std::sync::Arc;

use serde_json::json;
use tokio::sync::mpsc;

use crate::api::models::socket::ConnectionStatus;
use crate::core::event_bus::{publish_global, DomainEvent};
use crate::openhuman::webhooks::WebhookRequest;

use super::manager::{emit_server_event, emit_state_change, SharedState};

// ---------------------------------------------------------------------------
// Main event dispatcher
// ---------------------------------------------------------------------------

/// Route a Socket.IO event to the appropriate handler based on its name.
pub(super) fn handle_sio_event(
    event_name: &str,
    data: serde_json::Value,
    _emit_tx: &mpsc::UnboundedSender<String>,
    shared: &Arc<SharedState>,
) {
    // Log every incoming event for observability.
    log::info!(
        "[socket] event received: name={} data_bytes={}",
        event_name,
        data.to_string().len()
    );
    log::debug!(
        "[socket] event payload: name={} data={}",
        event_name,
        &data.to_string()[..data.to_string().len().min(500)]
    );

    match event_name {
        "ready" => {
            log::info!("[socket] Server ready — auth successful");
            *shared.status.write() = ConnectionStatus::Connected;
            emit_state_change(shared);
        }
        "error" => {
            log::error!("[socket] Server error event: {}", data);
            *shared.status.write() = ConnectionStatus::Error;
            emit_state_change(shared);
        }
        // Webhook tunnel — publish to event bus for routing by WebhookRequestSubscriber
        "webhook:request" => {
            log::info!("[socket] Publishing webhook:request to event bus");
            match serde_json::from_value::<WebhookRequest>(data.clone()) {
                Ok(request) => {
                    publish_global(DomainEvent::WebhookIncomingRequest {
                        request,
                        raw_data: data,
                    });
                }
                Err(e) => {
                    log::error!("[socket] Failed to parse webhook:request payload: {e}");
                    // Publish with a minimal request so the subscriber can still
                    // emit an error response. Build a request from what we can parse.
                    let cid = data
                        .get("correlationId")
                        .and_then(|v| v.as_str())
                        .unwrap_or("unknown")
                        .to_string();
                    let _tunnel_uuid = data
                        .get("tunnelUuid")
                        .and_then(|v| v.as_str())
                        .unwrap_or("")
                        .to_string();

                    // Record parse error in router debug log if available
                    if let Some(router) = shared.webhook_router.read().clone() {
                        router.record_parse_error(
                            cid.clone(),
                            data.get("tunnelUuid")
                                .and_then(|v| v.as_str())
                                .map(|v| v.to_string()),
                            data.get("method")
                                .and_then(|v| v.as_str())
                                .map(|v| v.to_string()),
                            data.get("path")
                                .and_then(|v| v.as_str())
                                .map(|v| v.to_string()),
                            data.clone(),
                            format!("bad request: {e}"),
                        );
                    }

                    // Emit error response directly via socket manager
                    if let Some(mgr) = crate::openhuman::socket::global_socket_manager() {
                        let err_json = json!({ "error": format!("Bad request: {e}") });
                        let body = base64_encode(&err_json.to_string());
                        let response_data = json!({
                            "correlationId": cid,
                            "statusCode": 400,
                            "headers": {},
                            "body": body,
                        });
                        let mgr = mgr.clone();
                        tokio::spawn(async move {
                            if let Err(e) = mgr.emit("webhook:response", response_data).await {
                                log::error!("[socket] Failed to emit webhook error response: {e}");
                            }
                        });
                    }
                }
            }
        }
        // Composio trigger webhook — backend emits this after HMAC-verifying
        // an incoming Composio webhook. Deserialize into the canonical
        // `ComposioTriggerEvent` DTO so shape mismatches fail fast with a
        // clear log line instead of being silently coerced to empty strings.
        "composio:trigger" => {
            log::info!("[socket] Publishing composio:trigger to event bus");
            match serde_json::from_value::<crate::openhuman::composio::ComposioTriggerEvent>(
                data.clone(),
            ) {
                Ok(event) => {
                    if event.toolkit.is_empty() || event.trigger.is_empty() {
                        log::warn!(
                            "[socket] composio:trigger missing toolkit/trigger; dropping event"
                        );
                    } else {
                        log::info!(
                            "[socket] Publishing composio:trigger to event bus: toolkit={}, trigger={}, metadata_id={}, metadata_uuid={}",
                            event.toolkit,
                            event.trigger,
                            event.metadata.id,
                            event.metadata.uuid
                        );
                        publish_global(DomainEvent::ComposioTriggerReceived {
                            toolkit: event.toolkit,
                            trigger: event.trigger,
                            metadata_id: event.metadata.id,
                            metadata_uuid: event.metadata.uuid,
                            payload: event.payload,
                        });
                    }
                }
                Err(e) => {
                    log::warn!(
                        "[socket] failed to parse composio:trigger payload: {e}; dropping event"
                    );
                }
            }
        }
        // Channel inbound message — publish to event bus for ChannelInboundSubscriber
        _ if event_name.ends_with(":message") => {
            log::info!(
                "[socket] Publishing inbound channel message '{}' to event bus",
                event_name
            );

            let channel = data
                .get("channel")
                .and_then(|v| v.as_str())
                .unwrap_or("")
                .to_string();
            let message = data
                .get("message")
                .and_then(|v| v.as_str())
                .unwrap_or("")
                .trim()
                .to_string();

            if channel.is_empty() {
                log::warn!("[socket] channel:message missing 'channel' field");
                return;
            }
            if message.is_empty() {
                log::debug!("[socket] channel:message empty or missing 'message'");
                return;
            }

            publish_global(DomainEvent::ChannelInboundMessage {
                event_name: event_name.to_string(),
                channel,
                message,
                raw_data: data,
            });
        }
        _ => {
            log::debug!("[socket] Unhandled event '{}' — logging only", event_name);
            emit_server_event(shared, event_name, data);
        }
    }
}

// ---------------------------------------------------------------------------
// Utility functions
// ---------------------------------------------------------------------------

/// Base64-encode a string (for webhook error response bodies).
fn base64_encode(input: &str) -> String {
    use base64::Engine;
    base64::engine::general_purpose::STANDARD.encode(input.as_bytes())
}

/// Send a Socket.IO event through the emit channel.
///
/// Format: `42["eventName", data]`
pub(super) fn emit_via_channel(
    tx: &mpsc::UnboundedSender<String>,
    event: &str,
    data: serde_json::Value,
) {
    let payload = serde_json::to_string(&json!([event, data])).unwrap_or_default();
    let msg = format!("42{}", payload);
    if let Err(e) = tx.send(msg) {
        log::error!("[socket] emit_via_channel failed: {e}");
    }
}

// ---------------------------------------------------------------------------
// SIO event parsing
// ---------------------------------------------------------------------------

/// Parse a Socket.IO EVENT payload into an event name and JSON data.
///
/// Format: `["eventName", data]` or `<ackId>["eventName", data]`.
pub(super) fn parse_sio_event(text: &str) -> Option<(String, serde_json::Value)> {
    let json_start = text.find('[')?;
    let json_str = &text[json_start..];
    let arr: Vec<serde_json::Value> = serde_json::from_str(json_str).ok()?;
    let event_name = arr.first()?.as_str()?.to_string();
    let data = arr.get(1).cloned().unwrap_or(serde_json::Value::Null);
    Some((event_name, data))
}

#[cfg(test)]
mod tests {
    use super::*;
    use parking_lot::RwLock;
    use serde_json::json;

    fn make_shared() -> Arc<SharedState> {
        Arc::new(SharedState {
            webhook_router: RwLock::new(None),
            status: RwLock::new(ConnectionStatus::Disconnected),
            socket_id: RwLock::new(None),
        })
    }

    // ── base64_encode ───────────────────────────────────────────────

    #[test]
    fn base64_encode_round_trips_ascii() {
        use base64::Engine;
        let s = "hello world";
        let encoded = base64_encode(s);
        let decoded = base64::engine::general_purpose::STANDARD
            .decode(encoded.as_bytes())
            .unwrap();
        assert_eq!(decoded, s.as_bytes());
    }

    #[test]
    fn base64_encode_handles_empty_string() {
        assert_eq!(base64_encode(""), "");
    }

    #[test]
    fn base64_encode_handles_json_body() {
        let encoded = base64_encode(r#"{"error":"nope"}"#);
        assert_eq!(encoded, "eyJlcnJvciI6Im5vcGUifQ==");
    }

    // ── parse_sio_event ─────────────────────────────────────────────

    #[test]
    fn parse_sio_event_accepts_bare_array() {
        let (name, data) = parse_sio_event(r#"["hello",{"x":1}]"#).unwrap();
        assert_eq!(name, "hello");
        assert_eq!(data, json!({"x": 1}));
    }

    #[test]
    fn parse_sio_event_strips_ack_id_prefix() {
        let (name, data) = parse_sio_event(r#"123["hello",{"x":1}]"#).unwrap();
        assert_eq!(name, "hello");
        assert_eq!(data["x"], 1);
    }

    #[test]
    fn parse_sio_event_defaults_data_to_null_when_missing() {
        let (name, data) = parse_sio_event(r#"["ping"]"#).unwrap();
        assert_eq!(name, "ping");
        assert!(data.is_null());
    }

    #[test]
    fn parse_sio_event_returns_none_for_garbage() {
        assert!(parse_sio_event("not an sio event").is_none());
        assert!(parse_sio_event("").is_none());
    }

    #[test]
    fn parse_sio_event_returns_none_when_first_element_is_not_string() {
        assert!(parse_sio_event("[42,{}]").is_none());
    }

    #[test]
    fn parse_sio_event_returns_none_when_json_invalid() {
        assert!(parse_sio_event(r#"[invalid json"#).is_none());
    }

    // ── handle_sio_event dispatch ───────────────────────────────────

    #[test]
    fn handle_sio_event_ready_sets_connected() {
        let shared = make_shared();
        let (tx, _rx) = mpsc::unbounded_channel::<String>();
        handle_sio_event("ready", json!({}), &tx, &shared);
        assert_eq!(*shared.status.read(), ConnectionStatus::Connected);
    }

    #[test]
    fn handle_sio_event_error_sets_error_status() {
        let shared = make_shared();
        let (tx, _rx) = mpsc::unbounded_channel::<String>();
        handle_sio_event("error", json!({"msg":"oops"}), &tx, &shared);
        assert_eq!(*shared.status.read(), ConnectionStatus::Error);
    }

    #[test]
    fn handle_sio_event_unknown_event_is_noop_on_status() {
        let shared = make_shared();
        let (tx, _rx) = mpsc::unbounded_channel::<String>();
        // Start disconnected — an unhandled event must not flip status.
        handle_sio_event("weird.unrelated.event", json!({}), &tx, &shared);
        assert_eq!(*shared.status.read(), ConnectionStatus::Disconnected);
    }

    #[test]
    fn handle_sio_event_channel_message_missing_channel_is_dropped() {
        let shared = make_shared();
        let (tx, _rx) = mpsc::unbounded_channel::<String>();
        // No "channel" field → the dispatcher must return without touching status.
        handle_sio_event("telegram:message", json!({"message": "hi"}), &tx, &shared);
        assert_eq!(*shared.status.read(), ConnectionStatus::Disconnected);
    }

    #[test]
    fn handle_sio_event_channel_message_empty_text_is_dropped() {
        let shared = make_shared();
        let (tx, _rx) = mpsc::unbounded_channel::<String>();
        handle_sio_event(
            "telegram:message",
            json!({"channel": "tg:123", "message": "   "}),
            &tx,
            &shared,
        );
        // Status should still be untouched. The dropped-empty branch is the
        // coverage target — this test validates we hit the early-return path.
        assert_eq!(*shared.status.read(), ConnectionStatus::Disconnected);
    }

    // ── emit_via_channel ────────────────────────────────────────────

    #[test]
    fn emit_via_channel_sends_socketio_event_frame() {
        let (tx, mut rx) = mpsc::unbounded_channel::<String>();
        emit_via_channel(&tx, "hello", json!({"x": 1}));
        let msg = rx.try_recv().expect("message should be sent");
        assert!(
            msg.starts_with("42"),
            "expected SIO EVENT prefix, got: {msg}"
        );
        assert!(msg.contains("\"hello\""));
        assert!(msg.contains("\"x\""));
    }

    #[test]
    fn emit_via_channel_works_with_null_data() {
        let (tx, mut rx) = mpsc::unbounded_channel::<String>();
        emit_via_channel(&tx, "ping", serde_json::Value::Null);
        let msg = rx.try_recv().expect("message should be sent");
        assert_eq!(msg, r#"42["ping",null]"#);
    }

    #[test]
    fn emit_via_channel_logs_but_does_not_panic_on_closed_receiver() {
        let (tx, rx) = mpsc::unbounded_channel::<String>();
        drop(rx); // receiver closed first
                  // Must not panic — error path just logs.
        emit_via_channel(&tx, "ping", json!({}));
    }
}
