use std::future::Future;
use std::pin::Pin;

use crate::model::TokenUsage;
use crate::tool::ToolOutput;
use serde_json::Value;

/// Streaming content-block index offset for `tool_calls` entries.
///
/// Used by `OpenAI`-compatible providers (`DeepSeek` in particular) to
/// encode `tool_calls` entries. Tool-call chunks arrive with indices
/// `>= TOOL_ARG_INDEX_BASE` so they can be multiplexed with text `content`
/// blocks (indices `0..TOOL_ARG_INDEX_BASE`) in a single streaming event.
/// Consumers of `Event::ContentDelta` that render text should filter
/// indices `>= TOOL_ARG_INDEX_BASE` to avoid leaking raw tool-call JSON
/// into the message body.
pub const TOOL_ARG_INDEX_BASE: usize = 1000;

/// Domain events for agent-to-UI communication.
///
/// All variants are `Clone + Send + 'static` to support
/// `tokio::sync::broadcast` channels.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub enum Event {
    // ─── Message lifecycle ───
    /// A new conversation turn has started.
    TurnStart { turn_index: usize },

    /// API response message started, with a unique message ID.
    MessageStart { id: String },

    /// Incremental text content from the model.
    ContentDelta { index: usize, delta: String },

    /// Incremental thinking content from extended thinking mode.
    ThinkingDelta { index: usize, delta: String },

    /// A content block has finished streaming.
    ContentBlockStop { index: usize },

    /// The full message has completed.
    MessageEnd { usage: TokenUsage },

    // ─── Tool execution ───
    /// A tool call has started.
    ToolUseStart {
        id: String,
        name: String,
        /// Tool input parameters (for rendering hooks).
        #[serde(default)]
        input: Value,
    },

    /// Incremental tool input (streaming).
    ToolUseInput { id: String, input: Value },

    /// Incremental output from a running tool (e.g. bash stdout line).
    ToolOutputDelta { id: String, delta: String },

    /// Real-time progress from a running tool (elapsed time, output size, tail).
    ToolProgress {
        id: String,
        progress: crate::tool::ToolProgress,
    },

    /// Tool execution has completed.
    ToolResult { id: String, output: ToolOutput },

    // ─── Permission interaction ───
    /// Request user confirmation for a tool execution.
    PermissionRequest {
        tool_name: String,
        input_summary: String,
        request_id: String,
        /// Raw tool input parameters, available to UI for richer rendering
        /// (e.g. show the bash command, edit diff, or write target).
        #[serde(default)]
        tool_input: Value,
    },

    /// User's response to a permission request.
    ///
    /// `feedback` carries an optional free-text note from the user (typically
    /// only present on a deny) that downstream consumers forward to the
    /// model so it can adjust its next move.
    PermissionResponse {
        request_id: String,
        allowed: bool,
        #[serde(default, skip_serializing_if = "Option::is_none")]
        feedback: Option<String>,
    },

    // ─── Context compaction ───
    /// Context compaction has started.
    CompactStart {
        strategy: String,
        before_tokens: u64,
    },

    /// Context compaction has completed.
    CompactEnd {
        after_tokens: u64,
        removed_messages: usize,
    },

    // ─── Token warnings ───
    /// Token usage has exceeded a threshold.
    TokenWarning {
        usage_pct: f32,
        used: u64,
        limit: u64,
    },

    /// The active model was swapped to a larger-context variant to avoid
    /// compaction. Emitted before the next LLM call uses `to`.
    ContextUpgraded {
        /// Previously active model ID.
        from: String,
        /// Newly active model ID (extended-context variant).
        to: String,
        /// Old context window size in tokens.
        old_window: u64,
        /// New context window size in tokens.
        new_window: u64,
    },

    // ─── Memory ───
    /// Memory files were loaded at session start.
    MemoryLoaded { count: usize },

    /// A memory file was saved during the session.
    MemorySaved { filename: String },

    // ─── Session history ───
    /// Session was saved to disk.
    SessionSaved { session_id: String },

    /// Session was resumed from disk.
    SessionResumed {
        session_id: String,
        message_count: usize,
    },

    // ─── User interaction ───
    /// The `AskUserQuestion` tool is requesting input from the user.
    ///
    /// The UI should display the question/options and send back a
    /// `UserPromptResponse` with the matching `request_id`.
    UserPromptRequest {
        request_id: String,
        question: String,
        options: Vec<String>,
        multi_select: bool,
    },

    /// The user's answer to an `AskUserQuestion` prompt.
    UserPromptResponse {
        request_id: String,
        response: String,
    },

    // ─── Sub-agent workers ───
    /// A sub-agent worker has started.
    AgentWorkerStarted {
        worker_id: String,
        task_prompt: String,
    },

    /// A sub-agent worker has completed.
    AgentWorkerCompleted {
        worker_id: String,
        /// Final text output from the worker, if any.
        result: Option<String>,
        /// Whether the worker completed successfully.
        success: bool,
        /// Total tokens used by this worker.
        usage: TokenUsage,
    },

    // ─── Errors ───
    /// An error occurred during processing.
    Error { message: String },

    /// The current streaming response was aborted (e.g. model overloaded,
    /// falling back to alternate model). TUI should discard partial content.
    StreamAborted { reason: String },
}

// ── Frontend abstraction layer ───────────────────────────────────────

/// Session-level event wrapper — extends [`Event`] with UI-specific
/// events that don't belong in the core domain enum.
#[derive(Debug, Clone)]
pub enum SessionEvent {
    /// A core domain event.
    Core(Event),
}

impl From<Event> for SessionEvent {
    fn from(e: Event) -> Self {
        Self::Core(e)
    }
}

/// Trait for sending session events to a frontend.
///
/// Implementations might write to a `tokio::sync::broadcast` channel,
/// a WebSocket, or an in-process event bus. The workspace avoids
/// `async_trait` — implementors return a boxed future directly.
pub trait EventSink: Send + Sync {
    fn send(
        &self,
        event: SessionEvent,
    ) -> Pin<Box<dyn Future<Output = crate::Result<()>> + Send + '_>>;
}

/// A stream of session events consumed by a frontend.
pub type EventStream = Pin<Box<dyn futures::Stream<Item = SessionEvent> + Send>>;

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn event_is_send() {
        fn assert_send<T: Send>() {}
        assert_send::<Event>();
    }

    #[test]
    fn event_is_clone() {
        let event = Event::ContentDelta {
            index: 0,
            delta: "hello".into(),
        };
        #[allow(clippy::redundant_clone)]
        let cloned = event.clone();
        if let Event::ContentDelta { index, delta } = cloned {
            assert_eq!(index, 0);
            assert_eq!(delta, "hello");
        } else {
            panic!("unexpected variant");
        }
    }

    #[test]
    fn event_is_static() {
        fn assert_static<T: 'static>() {}
        assert_static::<Event>();
    }

    #[test]
    fn turn_start_event() {
        let event = Event::TurnStart { turn_index: 3 };
        if let Event::TurnStart { turn_index } = event {
            assert_eq!(turn_index, 3);
        } else {
            panic!("unexpected variant");
        }
    }

    #[test]
    fn message_start_event() {
        let event = Event::MessageStart {
            id: "msg_123".into(),
        };
        if let Event::MessageStart { id } = event {
            assert_eq!(id, "msg_123");
        } else {
            panic!("unexpected variant");
        }
    }

    #[test]
    fn tool_result_event() {
        let event = Event::ToolResult {
            id: "tu_1".into(),
            output: ToolOutput::success("done"),
        };
        if let Event::ToolResult { id, output } = event {
            assert_eq!(id, "tu_1");
            assert!(!output.is_error);
            assert_eq!(output.text(), "done");
        } else {
            panic!("unexpected variant");
        }
    }

    #[test]
    fn permission_request_event() {
        let event = Event::PermissionRequest {
            tool_name: "bash".into(),
            input_summary: "rm -rf /".into(),
            request_id: "req_1".into(),
            tool_input: serde_json::json!({"command": "rm -rf /"}),
        };
        if let Event::PermissionRequest {
            tool_name,
            input_summary,
            request_id,
            tool_input,
        } = event
        {
            assert_eq!(tool_name, "bash");
            assert_eq!(input_summary, "rm -rf /");
            assert_eq!(request_id, "req_1");
            assert_eq!(tool_input["command"], "rm -rf /");
        } else {
            panic!("unexpected variant");
        }
    }

    #[test]
    fn token_warning_event() {
        let event = Event::TokenWarning {
            usage_pct: 0.85,
            used: 85000,
            limit: 100_000,
        };
        if let Event::TokenWarning {
            usage_pct,
            used,
            limit,
        } = event
        {
            assert!((usage_pct - 0.85).abs() < f32::EPSILON);
            assert_eq!(used, 85000);
            assert_eq!(limit, 100_000);
        } else {
            panic!("unexpected variant");
        }
    }

    #[test]
    fn error_event() {
        let event = Event::Error {
            message: "something broke".into(),
        };
        if let Event::Error { message } = event {
            assert_eq!(message, "something broke");
        } else {
            panic!("unexpected variant");
        }
    }

    #[test]
    fn compact_events() {
        let start = Event::CompactStart {
            strategy: "summarize".into(),
            before_tokens: 90000,
        };
        let end = Event::CompactEnd {
            after_tokens: 40000,
            removed_messages: 15,
        };
        assert!(matches!(start, Event::CompactStart { .. }));
        assert!(matches!(end, Event::CompactEnd { .. }));
    }

    // ─── Serde roundtrip tests for all Event variants ───

    fn serde_roundtrip(event: &Event) {
        let json = serde_json::to_string(event).unwrap();
        let parsed: Event = serde_json::from_str(&json).unwrap();
        // Compare JSON representations since Event doesn't derive PartialEq
        let json2 = serde_json::to_string(&parsed).unwrap();
        assert_eq!(json, json2);
    }

    #[test]
    fn event_serde_turn_start() {
        serde_roundtrip(&Event::TurnStart { turn_index: 5 });
    }

    #[test]
    fn event_serde_message_start() {
        serde_roundtrip(&Event::MessageStart {
            id: "msg_abc".into(),
        });
    }

    #[test]
    fn event_serde_content_delta() {
        serde_roundtrip(&Event::ContentDelta {
            index: 2,
            delta: "hello world".into(),
        });
    }

    #[test]
    fn event_serde_content_block_stop() {
        serde_roundtrip(&Event::ContentBlockStop { index: 0 });
    }

    #[test]
    fn event_serde_message_end() {
        serde_roundtrip(&Event::MessageEnd {
            usage: TokenUsage {
                input_tokens: 100,
                output_tokens: 50,
                cache_read_tokens: 10,
                cache_creation_tokens: 5,
            },
        });
    }

    #[test]
    fn event_serde_tool_use_start() {
        serde_roundtrip(&Event::ToolUseStart {
            id: "tu_1".into(),
            name: "bash".into(),
            input: serde_json::json!({"command": "ls"}),
        });
    }

    #[test]
    fn event_serde_tool_use_input() {
        serde_roundtrip(&Event::ToolUseInput {
            id: "tu_1".into(),
            input: serde_json::json!({"command": "ls"}),
        });
    }

    #[test]
    fn event_serde_tool_result() {
        serde_roundtrip(&Event::ToolResult {
            id: "tu_1".into(),
            output: ToolOutput::success("file1.txt"),
        });
    }

    #[test]
    fn event_serde_tool_result_error() {
        serde_roundtrip(&Event::ToolResult {
            id: "tu_2".into(),
            output: ToolOutput::error("command failed"),
        });
    }

    #[test]
    fn event_serde_permission_request() {
        serde_roundtrip(&Event::PermissionRequest {
            tool_name: "bash".into(),
            input_summary: "rm -rf /tmp/cache".into(),
            request_id: "req_42".into(),
            tool_input: serde_json::json!({"command": "rm -rf /tmp/cache"}),
        });
    }

    #[test]
    fn event_serde_permission_response() {
        serde_roundtrip(&Event::PermissionResponse {
            request_id: "req_42".into(),
            allowed: true,
            feedback: None,
        });
        serde_roundtrip(&Event::PermissionResponse {
            request_id: "req_43".into(),
            allowed: false,
            feedback: None,
        });
        serde_roundtrip(&Event::PermissionResponse {
            request_id: "req_44".into(),
            allowed: false,
            feedback: Some("please ask before deleting files".into()),
        });
    }

    #[test]
    fn permission_response_feedback_omitted_when_none() {
        // Round-trips that round-tripped before (no feedback) must keep
        // the on-the-wire shape stable so older consumers don't see a new
        // null field. `skip_serializing_if = Option::is_none` is what
        // guarantees this.
        let event = Event::PermissionResponse {
            request_id: "req_50".into(),
            allowed: true,
            feedback: None,
        };
        let json = serde_json::to_string(&event).unwrap();
        assert!(!json.contains("feedback"), "serialized form: {json}");
    }

    #[test]
    fn permission_response_feedback_serialized_when_some() {
        let event = Event::PermissionResponse {
            request_id: "req_51".into(),
            allowed: false,
            feedback: Some("use Read tool instead".into()),
        };
        let json = serde_json::to_string(&event).unwrap();
        assert!(json.contains("\"feedback\":\"use Read tool instead\""));
    }

    #[test]
    fn permission_response_feedback_default_when_absent_from_input() {
        let raw = r#"{"PermissionResponse":{"request_id":"req_60","allowed":true}}"#;
        let event: Event = serde_json::from_str(raw).unwrap();
        match event {
            Event::PermissionResponse {
                request_id,
                allowed,
                feedback,
            } => {
                assert_eq!(request_id, "req_60");
                assert!(allowed);
                assert!(feedback.is_none());
            }
            other => panic!("unexpected variant: {other:?}"),
        }
    }

    #[test]
    fn event_serde_compact_start() {
        serde_roundtrip(&Event::CompactStart {
            strategy: "summarize".into(),
            before_tokens: 95000,
        });
    }

    #[test]
    fn event_serde_compact_end() {
        serde_roundtrip(&Event::CompactEnd {
            after_tokens: 40000,
            removed_messages: 12,
        });
    }

    #[test]
    fn event_serde_token_warning() {
        serde_roundtrip(&Event::TokenWarning {
            usage_pct: 0.92,
            used: 92000,
            limit: 100_000,
        });
    }

    #[test]
    fn event_serde_error() {
        serde_roundtrip(&Event::Error {
            message: "rate limit exceeded".into(),
        });
    }

    #[test]
    fn event_serde_memory_loaded() {
        serde_roundtrip(&Event::MemoryLoaded { count: 5 });
    }

    #[test]
    fn event_serde_memory_saved() {
        serde_roundtrip(&Event::MemorySaved {
            filename: "user_role.md".into(),
        });
    }

    #[test]
    fn event_serde_session_saved() {
        serde_roundtrip(&Event::SessionSaved {
            session_id: "sess_abc".into(),
        });
    }

    #[test]
    fn event_serde_session_resumed() {
        serde_roundtrip(&Event::SessionResumed {
            session_id: "sess_abc".into(),
            message_count: 42,
        });
    }

    #[test]
    fn thinking_delta_event() {
        let event = Event::ThinkingDelta {
            index: 0,
            delta: "Let me reason...".into(),
        };
        if let Event::ThinkingDelta { index, delta } = &event {
            assert_eq!(*index, 0);
            assert_eq!(delta, "Let me reason...");
        } else {
            panic!("unexpected variant");
        }
    }

    #[test]
    fn tool_output_delta_event() {
        let event = Event::ToolOutputDelta {
            id: "tu_1".into(),
            delta: "line of output\n".into(),
        };
        if let Event::ToolOutputDelta { id, delta } = &event {
            assert_eq!(id, "tu_1");
            assert!(delta.contains("output"));
        } else {
            panic!("unexpected variant");
        }
    }

    #[test]
    fn event_serde_tool_output_delta() {
        serde_roundtrip(&Event::ToolOutputDelta {
            id: "tu_5".into(),
            delta: "streaming line".into(),
        });
    }

    #[test]
    fn event_serde_thinking_delta() {
        serde_roundtrip(&Event::ThinkingDelta {
            index: 0,
            delta: "Step 1: analyze the problem".into(),
        });
    }

    #[test]
    fn event_serde_user_prompt_request() {
        serde_roundtrip(&Event::UserPromptRequest {
            request_id: "ask_1".into(),
            question: "Pick a branch".into(),
            options: vec!["main".into(), "dev".into()],
            multi_select: false,
        });
    }

    #[test]
    fn event_serde_user_prompt_response() {
        serde_roundtrip(&Event::UserPromptResponse {
            request_id: "ask_1".into(),
            response: "main".into(),
        });
    }

    #[test]
    fn tool_arg_index_base_is_stable() {
        // Locks the constant to 1000. If a future change raises this value,
        // it must be a conscious review — the constant is load-bearing for
        // both the OpenAI-compatible streaming producer (crab-api) and the
        // text-rendering consumers (crab-tui).
        assert_eq!(TOOL_ARG_INDEX_BASE, 1000);
    }

    // ── SessionEvent tests ───

    #[test]
    fn session_event_from_core() {
        let core = Event::TurnStart { turn_index: 1 };
        let se: SessionEvent = core.into();
        assert!(matches!(
            se,
            SessionEvent::Core(Event::TurnStart { turn_index: 1 })
        ));
    }

    #[test]
    fn session_event_is_send() {
        fn assert_send<T: Send>() {}
        assert_send::<SessionEvent>();
    }

    #[test]
    fn session_event_is_clone() {
        let se = SessionEvent::Core(Event::Error {
            message: "test".into(),
        });
        #[allow(clippy::redundant_clone)]
        let cloned = se.clone();
        assert!(matches!(cloned, SessionEvent::Core(Event::Error { .. })));
    }

    #[test]
    fn event_sink_is_object_safe() {
        fn assert_object_safe(_: &dyn EventSink) {}
        let _ = assert_object_safe;
    }
}
