use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use std::time::Duration;

use rig::completion::ToolDefinition;
use rig::tool::Tool;
use serde::Deserialize;
use serde_json::json;
use tauri::Runtime;
use tokio::time::timeout;

use crate::agent::panel_context::{compact_panel_payload, panel_type};
use crate::agent::stream_events::AgentStreamEmitter;
use crate::error::AppError;
use crate::state::PendingAgentToolApprovals;
use crate::terminal::{
    ExecuteTerminalCommandRequest, TerminalCommandResponse, TerminalCommandService,
};

const APPROVAL_TIMEOUT: Duration = Duration::from_secs(60);

pub trait AgentCommandExecutor: Send + Sync {
    fn execute<'a>(
        &'a self,
        request: ExecuteTerminalCommandRequest,
    ) -> Pin<Box<dyn Future<Output = TerminalCommandResponse> + Send + 'a>>;
}

impl AgentCommandExecutor for TerminalCommandService {
    fn execute<'a>(
        &'a self,
        request: ExecuteTerminalCommandRequest,
    ) -> Pin<Box<dyn Future<Output = TerminalCommandResponse> + Send + 'a>> {
        Box::pin(async move { self.execute(request).await })
    }
}

#[derive(Clone)]
pub struct RunTerminalCommandTool<R: Runtime> {
    pub stream_emitter: Arc<AgentStreamEmitter<R>>,
    pub command_executor: Arc<dyn AgentCommandExecutor>,
    pub pending_approvals: Arc<PendingAgentToolApprovals>,
    pub request_id: String,
    pub workspace_id: String,
}

#[derive(Debug, Deserialize)]
pub struct RunTerminalCommandArgs {
    pub command: String,
}

#[derive(Debug, thiserror::Error)]
pub enum RunTerminalCommandToolError {
    #[error("{0}")]
    App(#[from] AppError),
    #[error("failed to serialize tool result: {0}")]
    Serialize(String),
}

impl<R: Runtime> Tool for RunTerminalCommandTool<R> {
    const NAME: &'static str = "run_terminal_command";

    type Error = RunTerminalCommandToolError;
    type Args = RunTerminalCommandArgs;
    type Output = String;

    async fn definition(&self, _prompt: String) -> ToolDefinition {
        ToolDefinition {
            name: Self::NAME.to_string(),
            description: "Run a MosaicIQ terminal slash command. Supported read-only commands: /search, /news, /analyze, /fa, /em, /bs, /cf, /dvd, /concepts, /mappings, /formula validate, /formula list, /portfolio. Supported write commands requiring user approval: /buy, /sell, /cash, /mappings add, /mappings update, /mappings delete, and /formula create. /clear and /help are forbidden.".to_string(),
            parameters: json!({
                "type": "object",
                "properties": {
                    "command": {
                        "type": "string",
                        "description": "A full MosaicIQ slash command such as /search AAPL or /fa AAPL annual. Must start with /."
                    }
                },
                "required": ["command"],
                "additionalProperties": false
            }),
        }
    }

    async fn call(&self, args: Self::Args) -> Result<Self::Output, Self::Error> {
        let command = normalize_command(&args.command);
        let Some(command_name) = command_name(&command) else {
            return serialize_status_only_tool_result(
                &command,
                "rejected",
                "Commands must start with '/'.",
            );
        };

        if !is_allowed_agent_command(command_name) {
            return serialize_status_only_tool_result(
                &command,
                "rejected",
                "This command is not available to the agent.",
            );
        }

        self.emit_command(&command)?;

        if requires_approval(&command) {
            let approved = self.await_approval(&command).await?;
            if !approved {
                return serialize_tool_result(&json!({
                    "command": command,
                    "status": "denied",
                    "summary": "The user denied approval for this portfolio-changing command."
                }));
            }
        }

        let response = self
            .command_executor
            .execute(ExecuteTerminalCommandRequest {
                workspace_id: self.workspace_id.clone(),
                input: command.clone(),
            })
            .await;

        self.emit_result(&command, response.clone())?;
        serialize_response_tool_result(command, response)
    }
}

impl<R: Runtime> RunTerminalCommandTool<R> {
    fn emit_command(&self, command: &str) -> Result<(), RunTerminalCommandToolError> {
        self.stream_emitter.tool_command(command.to_string())?;
        Ok(())
    }

    fn emit_result(
        &self,
        command: &str,
        response: TerminalCommandResponse,
    ) -> Result<(), RunTerminalCommandToolError> {
        self.stream_emitter
            .tool_result(command.to_string(), response)?;
        Ok(())
    }

    async fn await_approval(&self, command: &str) -> Result<bool, RunTerminalCommandToolError> {
        let (approval_id, receiver) = self
            .pending_approvals
            .register_for_request(Some(self.request_id.clone()))?;
        let (title, message) = approval_prompt(command);

        self.stream_emitter.approval_required(
            approval_id.clone(),
            command.to_string(),
            title,
            message,
        )?;

        let result = timeout(APPROVAL_TIMEOUT, receiver).await;
        match result {
            Ok(Ok(approved)) => Ok(approved),
            Ok(Err(_)) => Ok(false),
            Err(_) => {
                self.pending_approvals.cancel(&approval_id);
                Ok(false)
            }
        }
    }
}

pub(crate) fn normalize_command(input: &str) -> String {
    input.split_whitespace().collect::<Vec<_>>().join(" ")
}

pub(crate) fn command_name(command: &str) -> Option<&str> {
    let command = command.trim();
    if !command.starts_with('/') {
        return None;
    }

    command.split_whitespace().next()
}

pub(crate) fn requires_approval(command: &str) -> bool {
    match command_name(command) {
        Some("/buy" | "/sell" | "/cash") => true,
        Some("/mappings") => command.split_whitespace().nth(1).is_some_and(|value| {
            matches!(
                value.to_ascii_lowercase().as_str(),
                "add" | "update" | "delete"
            )
        }),
        Some("/formula") => command
            .split_whitespace()
            .nth(1)
            .is_some_and(|value| value.eq_ignore_ascii_case("create")),
        _ => false,
    }
}

pub(crate) fn is_allowed_agent_command(command_name: &str) -> bool {
    const ALLOWED: &[&str] = &[
        "/search",
        "/news",
        "/analyze",
        "/fa",
        "/bs",
        "/cf",
        "/dvd",
        "/concepts",
        "/mappings",
        "/formula",
        "/em",
        "/portfolio",
        "/buy",
        "/sell",
        "/cash",
    ];
    ALLOWED
        .iter()
        .any(|cmd| cmd.eq_ignore_ascii_case(command_name))
}

fn approval_prompt(command: &str) -> (String, String) {
    if command.split_whitespace().nth(1).is_some_and(|value| {
        matches!(
            value.to_ascii_lowercase().as_str(),
            "add" | "update" | "delete" | "create"
        )
    }) && matches!(command_name(command), Some("/mappings" | "/formula"))
    {
        return (
            "Approve terminal command".to_string(),
            format!(
                "The agent wants to persist a custom mapping through the terminal:\n\n{}\n\nApprove this action to continue.",
                command
            ),
        );
    }

    (
        "Approve terminal command".to_string(),
        format!(
            "The agent wants to run a terminal command that changes application state:\n\n{}\n\nApprove this action to continue.",
            command
        ),
    )
}

fn serialize_response_tool_result(
    command: String,
    response: TerminalCommandResponse,
) -> Result<String, RunTerminalCommandToolError> {
    match response {
        TerminalCommandResponse::Text { content, .. } => serialize_tool_result(&json!({
            "command": command,
            "status": "executed",
            "responseKind": "text",
            "summary": content,
        })),
        TerminalCommandResponse::Panel { panel } => serialize_tool_result(&json!({
            "command": command,
            "status": "executed",
            "responseKind": "panel",
            "panelType": panel_type(&panel),
            "summary": compact_panel_payload(&panel),
        })),
    }
}

fn serialize_tool_result(value: &serde_json::Value) -> Result<String, RunTerminalCommandToolError> {
    serde_json::to_string(value)
        .map_err(|error| RunTerminalCommandToolError::Serialize(error.to_string()))
}

fn serialize_status_only_tool_result(
    command: &str,
    status: &str,
    summary: &str,
) -> Result<String, RunTerminalCommandToolError> {
    serialize_tool_result(&json!({
        "command": command,
        "status": status,
        "summary": summary,
    }))
}

#[cfg(test)]
mod tests {
    use std::future::Future;
    use std::pin::Pin;
    use std::sync::atomic::{AtomicUsize, Ordering};
    use std::sync::Arc;

    use rig::tool::Tool;
    use tauri::test::{mock_builder, mock_context, noop_assets, MockRuntime};

    use super::{
        command_name, is_allowed_agent_command, normalize_command, requires_approval,
        AgentCommandExecutor, RunTerminalCommandArgs, RunTerminalCommandTool,
    };
    use crate::agent::stream_events::AgentStreamEmitter;
    use crate::state::PendingAgentToolApprovals;
    use crate::terminal::{
        Company, ExecuteTerminalCommandRequest, PanelPayload, TerminalCommandResponse,
    };

    struct FakeExecutor {
        response: TerminalCommandResponse,
        calls: AtomicUsize,
    }

    impl FakeExecutor {
        fn new(response: TerminalCommandResponse) -> Self {
            Self {
                response,
                calls: AtomicUsize::new(0),
            }
        }
    }

    impl AgentCommandExecutor for FakeExecutor {
        fn execute<'a>(
            &'a self,
            _request: ExecuteTerminalCommandRequest,
        ) -> Pin<Box<dyn Future<Output = TerminalCommandResponse> + Send + 'a>> {
            self.calls.fetch_add(1, Ordering::Relaxed);
            let response = self.response.clone();
            Box::pin(async move { response })
        }
    }

    #[test]
    fn classifies_commands_correctly() {
        assert!(is_allowed_agent_command("/search"));
        assert!(is_allowed_agent_command("/bs"));
        assert!(is_allowed_agent_command("/mappings"));
        assert!(is_allowed_agent_command("/formula"));
        assert!(requires_approval("/buy AAPL 1"));
        assert!(requires_approval(
            "/mappings add {\"targetField\":\"total_revenues\"}"
        ));
        assert!(requires_approval(
            "/mappings update abc {\"targetField\":\"total_revenues\"}"
        ));
        assert!(requires_approval("/mappings delete abc"));
        assert!(requires_approval(
            "/formula create {\"targetField\":\"total_revenues\",\"expression\":\"$gross_profit\"}"
        ));
        assert!(!requires_approval("/mappings AAPL"));
        assert!(!requires_approval("/formula validate \"$gross_profit\""));
        assert!(!requires_approval("/formula list AAPL"));
        assert!(!is_allowed_agent_command("/clear"));
        assert_eq!(command_name("/search AAPL"), Some("/search"));
        assert_eq!(normalize_command("  /search   AAPL  "), "/search AAPL");
    }

    #[tokio::test]
    async fn read_only_command_executes_and_returns_panel_summary() {
        let app = build_test_app();
        let executor = Arc::new(FakeExecutor::new(TerminalCommandResponse::panel(
            PanelPayload::Company {
                data: Company {
                    symbol: "AAPL".to_string(),
                    name: "Apple Inc.".to_string(),
                    price: 200.0,
                    change: 1.0,
                    change_percent: 0.5,
                    market_cap: 1.0,
                    volume: None,
                    volume_label: None,
                    pe: None,
                    eps: None,
                    high52_week: None,
                    low52_week: None,
                    profile: None,
                    price_chart: None,
                    price_chart_ranges: None,
                },
            },
        )));

        let tool = RunTerminalCommandTool {
            stream_emitter: Arc::new(AgentStreamEmitter::new(
                app.handle().clone(),
                "workspace-1".to_string(),
                "request-1".to_string(),
                "session-1".to_string(),
            )),
            command_executor: executor.clone(),
            pending_approvals: Arc::new(PendingAgentToolApprovals::new()),
            request_id: "request-1".to_string(),
            workspace_id: "workspace-1".to_string(),
        };

        let result = tool
            .call(RunTerminalCommandArgs {
                command: "/search AAPL".to_string(),
            })
            .await
            .expect("tool result");

        assert!(result.contains("\"status\":\"executed\""));
        assert!(result.contains("\"panelType\":\"company\""));
        assert_eq!(executor.calls.load(Ordering::Relaxed), 1);
    }

    #[tokio::test]
    async fn denied_write_command_does_not_execute() {
        let app = build_test_app();
        let approvals = Arc::new(PendingAgentToolApprovals::new());
        let executor = Arc::new(FakeExecutor::new(TerminalCommandResponse::Text {
            content: "ok".to_string(),
            portfolio: None,
        }));

        let tool = RunTerminalCommandTool {
            stream_emitter: Arc::new(AgentStreamEmitter::new(
                app.handle().clone(),
                "workspace-1".to_string(),
                "request-1".to_string(),
                "session-1".to_string(),
            )),
            command_executor: executor.clone(),
            pending_approvals: approvals.clone(),
            request_id: "request-1".to_string(),
            workspace_id: "workspace-1".to_string(),
        };

        let approvals_for_task = approvals.clone();
        let handle = tokio::spawn(async move {
            tool.call(RunTerminalCommandArgs {
                command: "/buy AAPL 1".to_string(),
            })
            .await
            .expect("tool result")
        });

        tokio::task::yield_now().await;
        approvals_for_task.resolve("approval-1", false).unwrap();

        let result = handle.await.unwrap();
        assert!(result.contains("\"status\":\"denied\""));
        assert_eq!(executor.calls.load(Ordering::Relaxed), 0);
    }

    #[tokio::test]
    async fn approved_write_command_executes_once() {
        let app = build_test_app();
        let approvals = Arc::new(PendingAgentToolApprovals::new());
        let executor = Arc::new(FakeExecutor::new(TerminalCommandResponse::Text {
            content: "Bought 1 share".to_string(),
            portfolio: None,
        }));

        let tool = RunTerminalCommandTool {
            stream_emitter: Arc::new(AgentStreamEmitter::new(
                app.handle().clone(),
                "workspace-1".to_string(),
                "request-1".to_string(),
                "session-1".to_string(),
            )),
            command_executor: executor.clone(),
            pending_approvals: approvals.clone(),
            request_id: "request-1".to_string(),
            workspace_id: "workspace-1".to_string(),
        };

        let approvals_for_task = approvals.clone();
        let handle = tokio::spawn(async move {
            tool.call(RunTerminalCommandArgs {
                command: "/buy AAPL 1".to_string(),
            })
            .await
            .expect("tool result")
        });

        tokio::task::yield_now().await;
        approvals_for_task.resolve("approval-1", true).unwrap();

        let result = handle.await.unwrap();
        assert!(result.contains("\"status\":\"executed\""));
        assert_eq!(executor.calls.load(Ordering::Relaxed), 1);
    }

    #[tokio::test]
    async fn approved_mappings_add_command_executes_once() {
        let app = build_test_app();
        let approvals = Arc::new(PendingAgentToolApprovals::new());
        let executor = Arc::new(FakeExecutor::new(TerminalCommandResponse::Text {
            content: "Created custom mapping".to_string(),
            portfolio: None,
        }));

        let tool = RunTerminalCommandTool {
            stream_emitter: Arc::new(AgentStreamEmitter::new(
                app.handle().clone(),
                "workspace-1".to_string(),
                "request-1".to_string(),
                "session-1".to_string(),
            )),
            command_executor: executor.clone(),
            pending_approvals: approvals.clone(),
            request_id: "request-1".to_string(),
            workspace_id: "workspace-1".to_string(),
        };

        let approvals_for_task = approvals.clone();
        let handle = tokio::spawn(async move {
            tool.call(RunTerminalCommandArgs {
                command: "/mappings add {\"targetField\":\"total_revenues\"}".to_string(),
            })
            .await
            .expect("tool result")
        });

        tokio::task::yield_now().await;
        approvals_for_task.resolve("approval-1", true).unwrap();

        let result = handle.await.unwrap();
        assert!(result.contains("\"status\":\"executed\""));
        assert_eq!(executor.calls.load(Ordering::Relaxed), 1);
    }

    fn build_test_app() -> tauri::App<MockRuntime> {
        mock_builder()
            .plugin(tauri_plugin_store::Builder::new().build())
            .build(mock_context(noop_assets()))
            .unwrap()
    }
}
