use std::sync::Arc;

use futures::{future::BoxFuture, StreamExt};
use rig::{
    agent::MultiTurnStreamItem,
    client::completion::CompletionClient,
    completion::{Message, Prompt},
    message::ToolChoice,
    providers::openai,
    streaming::{StreamedAssistantContent, StreamingPrompt},
};

use crate::agent::artifact::ArtifactStore;
use crate::agent::prompts::system_prompt_for_profile;
use crate::agent::stream_events::AgentStreamEmitter;
use crate::agent::tools::artifact::ManageArtifactTool;
use crate::agent::tools::terminal_command::{AgentCommandExecutor, RunTerminalCommandTool};
use crate::agent::{
    AgentProviderKind, AgentRuntimeConfig, TaskProfile, DEFAULT_OLLAMA_COMPAT_API_KEY,
};
use crate::error::AppError;
use crate::state::PendingAgentToolApprovals;

const MAX_TOOL_TURNS: usize = 10;

#[derive(Clone)]
pub struct AgentToolRuntimeContext {
    pub stream_emitter: Arc<AgentStreamEmitter<tauri::Wry>>,
    pub command_executor: Arc<dyn AgentCommandExecutor>,
    pub pending_approvals: Arc<PendingAgentToolApprovals>,
    pub artifact_store: Arc<ArtifactStore>,
    pub request_id: String,
    pub workspace_id: String,
}

/// Trait used by the agent service so tests can inject a deterministic gateway.
pub trait ChatGateway: Clone + Send + Sync + 'static {
    /// Start a streaming chat turn for the given config, prompt, and prior history.
    fn stream_chat(
        &self,
        runtime: AgentRuntimeConfig,
        prompt: String,
        context_messages: Vec<Message>,
        history: Vec<Message>,
        tool_runtime: AgentToolRuntimeContext,
        task_profile: TaskProfile,
    ) -> BoxFuture<'static, Result<String, AppError>>;

    fn summarize_history(
        &self,
        runtime: AgentRuntimeConfig,
        existing_summary: Option<String>,
        messages: Vec<Message>,
    ) -> BoxFuture<'static, Result<String, AppError>>;
}

/// Production Rig-backed gateway using the OpenAI-compatible chat completions API.
#[derive(Debug, Clone, Default)]
pub struct RigChatGateway;

impl ChatGateway for RigChatGateway {
    fn stream_chat(
        &self,
        runtime: AgentRuntimeConfig,
        prompt: String,
        context_messages: Vec<Message>,
        history: Vec<Message>,
        tool_runtime: AgentToolRuntimeContext,
        task_profile: TaskProfile,
    ) -> BoxFuture<'static, Result<String, AppError>> {
        Box::pin(async move {
            let history = compose_request_messages(context_messages, history);
            let terminal_tool = RunTerminalCommandTool {
                stream_emitter: tool_runtime.stream_emitter.clone(),
                command_executor: tool_runtime.command_executor.clone(),
                pending_approvals: tool_runtime.pending_approvals.clone(),
                request_id: tool_runtime.request_id.clone(),
                workspace_id: tool_runtime.workspace_id.clone(),
            };
            let artifact_tool = ManageArtifactTool {
                store: tool_runtime.artifact_store.clone(),
                stream_emitter: tool_runtime.stream_emitter.clone(),
                pending_approvals: tool_runtime.pending_approvals.clone(),
                request_id: tool_runtime.request_id.clone(),
                _workspace_id: tool_runtime.workspace_id.clone(),
            };
            let client = build_openai_client(&runtime)?;
            let system_prompt = system_prompt_for_profile(task_profile);
            let rig_stream = client
                .agent(runtime.model)
                .preamble(system_prompt)
                .temperature(0.2)
                .tool(terminal_tool)
                .tool(artifact_tool)
                .tool_choice(ToolChoice::Auto)
                .default_max_turns(MAX_TOOL_TURNS)
                .build()
                .stream_prompt(prompt)
                .with_history(history)
                .multi_turn(MAX_TOOL_TURNS)
                .await;

            consume_stream(rig_stream, &tool_runtime).await
        })
    }

    fn summarize_history(
        &self,
        runtime: AgentRuntimeConfig,
        existing_summary: Option<String>,
        messages: Vec<Message>,
    ) -> BoxFuture<'static, Result<String, AppError>> {
        Box::pin(async move {
            let mut history = Vec::new();

            if let Some(summary) = existing_summary.filter(|value| !value.trim().is_empty()) {
                history.push(Message::system(format!(
                    "Existing rolling summary:\n{}",
                    summary.trim()
                )));
            }
            history.extend(messages);

            let system_prompt = system_prompt_for_profile(TaskProfile::Summarization);
            let client = build_openai_client(&runtime)?;
            let response = client
                .agent(runtime.model)
                .preamble(system_prompt)
                .temperature(0.1)
                .build()
                .prompt("Update the rolling summary using the conversation history.")
                .with_history(history)
                .await
                .map_err(map_prompt_error)?;

            Ok(response.trim().to_string())
        })
    }
}

fn build_openai_client(
    runtime: &AgentRuntimeConfig,
) -> Result<openai::CompletionsClient, AppError> {
    let api_key = match runtime.provider_kind {
        AgentProviderKind::Remote => runtime
            .api_key
            .clone()
            .filter(|value| !value.trim().is_empty())
            .ok_or(AppError::RemoteApiKeyMissing)?,
        AgentProviderKind::Ollama => runtime
            .api_key
            .clone()
            .unwrap_or_else(|| DEFAULT_OLLAMA_COMPAT_API_KEY.to_string()),
    };

    openai::CompletionsClient::builder()
        .api_key(api_key)
        .base_url(&runtime.base_url)
        .build()
        .map_err(|error| AppError::ProviderInit(error.to_string()))
}

fn compose_request_messages(context_messages: Vec<Message>, history: Vec<Message>) -> Vec<Message> {
    context_messages.into_iter().chain(history).collect()
}

async fn consume_stream<R, S>(
    mut rig_stream: S,
    tool_runtime: &AgentToolRuntimeContext,
) -> Result<String, AppError>
where
    S: futures::Stream<Item = Result<MultiTurnStreamItem<R>, rig::agent::StreamingError>> + Unpin,
{
    let mut reply = String::new();
    let mut saw_text = false;
    let mut saw_reasoning_delta = false;

    while let Some(item) = rig_stream.next().await {
        match item {
            Ok(MultiTurnStreamItem::StreamAssistantItem(StreamedAssistantContent::Text(text))) => {
                saw_text = true;
                reply.push_str(&text.text);
                tool_runtime.stream_emitter.text_delta(text.text)?;
            }
            Ok(MultiTurnStreamItem::StreamAssistantItem(StreamedAssistantContent::Reasoning(
                reasoning,
            ))) => {
                if saw_reasoning_delta {
                    continue;
                }

                let text = reasoning_text(&reasoning);
                if text.is_empty() {
                    continue;
                }

                tool_runtime.stream_emitter.reasoning_delta(text)?;
            }
            Ok(MultiTurnStreamItem::StreamAssistantItem(
                StreamedAssistantContent::ReasoningDelta { reasoning, .. },
            )) => {
                saw_reasoning_delta = true;
                tool_runtime.stream_emitter.reasoning_delta(reasoning)?;
            }
            Ok(MultiTurnStreamItem::StreamAssistantItem(StreamedAssistantContent::ToolCall {
                ..
            })) => {}
            Ok(MultiTurnStreamItem::StreamAssistantItem(
                StreamedAssistantContent::ToolCallDelta { .. },
            )) => {}
            Ok(MultiTurnStreamItem::StreamUserItem(_)) => {}
            Ok(MultiTurnStreamItem::FinalResponse(final_response)) => {
                if !saw_text && !final_response.response().is_empty() {
                    reply.push_str(final_response.response());
                    tool_runtime
                        .stream_emitter
                        .text_delta(final_response.response().to_string())?;
                }
            }
            Ok(_) => {}
            Err(error) => return Err(map_streaming_error(error)),
        }
    }

    Ok(reply)
}

fn reasoning_text(reasoning: &rig::message::Reasoning) -> String {
    use rig::message::ReasoningContent;

    reasoning
        .content
        .iter()
        .filter_map(|block| match block {
            ReasoningContent::Text { text, .. } => Some(text.as_str()),
            ReasoningContent::Summary(text) => Some(text.as_str()),
            ReasoningContent::Encrypted(_) | ReasoningContent::Redacted { .. } => None,
            _ => None,
        })
        .collect::<Vec<_>>()
        .join("\n")
}

fn map_streaming_error(error: rig::agent::StreamingError) -> AppError {
    AppError::ProviderRequest(error.to_string())
}

fn map_prompt_error(error: rig::completion::PromptError) -> AppError {
    AppError::ProviderRequest(error.to_string())
}

#[cfg(test)]
mod tests {
    use rig::completion::Message;

    use super::compose_request_messages;

    #[test]
    fn prepends_context_messages_before_history() {
        let messages = compose_request_messages(
            vec![Message::system("panel context")],
            vec![Message::user("previous"), Message::assistant("reply")],
        );

        assert_eq!(messages[0], Message::system("panel context"));
        assert_eq!(messages[1], Message::user("previous"));
        assert_eq!(messages[2], Message::assistant("reply"));
    }
}
