use std::sync::Arc;

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

use soul_core::error::SoulResult;
use soul_core::tool::{Tool, ToolOutput};
use soul_core::types::ToolDefinition;

use crate::client::GoogleApiClient;

const CALENDAR_EVENTS_URL: &str = "https://www.googleapis.com/calendar/v3/calendars";

pub struct CalendarListTool {
    client: Arc<GoogleApiClient>,
}

impl CalendarListTool {
    pub fn new(client: Arc<GoogleApiClient>) -> Self {
        Self { client }
    }
}

#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
impl Tool for CalendarListTool {
    fn name(&self) -> &str {
        "google_calendar_list"
    }

    fn definition(&self) -> ToolDefinition {
        ToolDefinition {
            name: "google_calendar_list".into(),
            description: "List upcoming events from a Google Calendar. Returns event summaries, times, and locations.".into(),
            input_schema: json!({
                "type": "object",
                "properties": {
                    "calendar_id": {
                        "type": "string",
                        "description": "Calendar ID (default: 'primary')"
                    },
                    "time_min": {
                        "type": "string",
                        "description": "Start time in RFC3339 format (default: now)"
                    },
                    "time_max": {
                        "type": "string",
                        "description": "End time in RFC3339 format (default: 7 days from now)"
                    },
                    "max_results": {
                        "type": "integer",
                        "description": "Maximum number of events to return (default: 50)"
                    }
                },
                "required": []
            }),
        }
    }

    async fn execute(
        &self,
        _call_id: &str,
        arguments: serde_json::Value,
        _partial_tx: Option<mpsc::UnboundedSender<String>>,
    ) -> SoulResult<ToolOutput> {
        let calendar_id = arguments
            .get("calendar_id")
            .and_then(|v| v.as_str())
            .unwrap_or("primary");

        let now = chrono::Utc::now();
        let default_min = now.to_rfc3339();
        let default_max = (now + chrono::Duration::days(7)).to_rfc3339();

        let time_min = arguments
            .get("time_min")
            .and_then(|v| v.as_str())
            .unwrap_or(&default_min);

        let time_max = arguments
            .get("time_max")
            .and_then(|v| v.as_str())
            .unwrap_or(&default_max);

        let max_results = arguments
            .get("max_results")
            .and_then(|v| v.as_u64())
            .unwrap_or(50)
            .to_string();

        let url = format!("{}/{}/events", CALENDAR_EVENTS_URL, calendar_id);

        let result = self
            .client
            .get(
                &url,
                &[
                    ("timeMin", time_min),
                    ("timeMax", time_max),
                    ("maxResults", &max_results),
                    ("singleEvents", "true"),
                    ("orderBy", "startTime"),
                ],
            )
            .await;

        match result {
            Ok(data) => {
                let events = data["items"].as_array();
                let mut output = String::new();

                match events {
                    Some(items) if !items.is_empty() => {
                        output.push_str(&format!("Found {} events:\n\n", items.len()));
                        for (i, event) in items.iter().enumerate() {
                            let summary = event["summary"].as_str().unwrap_or("(No title)");
                            let start = event["start"]["dateTime"]
                                .as_str()
                                .or_else(|| event["start"]["date"].as_str())
                                .unwrap_or("unknown");
                            let end = event["end"]["dateTime"]
                                .as_str()
                                .or_else(|| event["end"]["date"].as_str())
                                .unwrap_or("unknown");
                            let location = event["location"].as_str();

                            output.push_str(&format!("{}. {}\n", i + 1, summary));
                            output.push_str(&format!("   Start: {}\n", start));
                            output.push_str(&format!("   End: {}\n", end));
                            if let Some(loc) = location {
                                output.push_str(&format!("   Location: {}\n", loc));
                            }
                            output.push('\n');
                        }
                    }
                    _ => {
                        output.push_str("No events found in the specified time range.");
                    }
                }

                Ok(ToolOutput::success(output).with_metadata(json!({
                    "calendar_id": calendar_id,
                    "event_count": events.map(|e| e.len()).unwrap_or(0),
                })))
            }
            Err(e) => Ok(ToolOutput::error(format!("Calendar list failed: {}", e))),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::auth::MockGoogleAuth;
    use wiremock::matchers::{method, path};
    use wiremock::{Mock, MockServer, ResponseTemplate};

    async fn setup() -> (MockServer, CalendarListTool) {
        let server = MockServer::start().await;
        let auth = Arc::new(MockGoogleAuth::new("test-token"));
        let client = Arc::new(GoogleApiClient::new(auth).with_http(
            reqwest::Client::builder()
                .build()
                .unwrap(),
        ));
        let tool = CalendarListTool::new(client);
        (server, tool)
    }

    #[tokio::test]
    async fn tool_name_and_definition() {
        let (_server, tool) = setup().await;
        assert_eq!(tool.name(), "google_calendar_list");
        let def = tool.definition();
        assert_eq!(def.name, "google_calendar_list");
        assert!(def.description.contains("Calendar"));
        assert_eq!(def.input_schema["type"], "object");
    }

    #[tokio::test]
    async fn schema_is_valid_object() {
        let (_server, tool) = setup().await;
        let def = tool.definition();
        assert!(def.input_schema["properties"].is_object());
        assert!(def.input_schema["properties"]["calendar_id"].is_object());
        assert!(def.input_schema["properties"]["time_min"].is_object());
        assert!(def.input_schema["properties"]["time_max"].is_object());
        assert!(def.input_schema["properties"]["max_results"].is_object());
    }

    #[tokio::test]
    async fn formats_events_as_text() {
        let (server, _tool) = setup().await;

        Mock::given(method("GET"))
            .and(path("/calendar/v3/calendars/primary/events"))
            .respond_with(ResponseTemplate::new(200).set_body_json(json!({
                "items": [
                    {
                        "summary": "Team standup",
                        "start": {"dateTime": "2026-01-15T09:00:00Z"},
                        "end": {"dateTime": "2026-01-15T09:30:00Z"},
                        "location": "Room A"
                    },
                    {
                        "summary": "Lunch",
                        "start": {"dateTime": "2026-01-15T12:00:00Z"},
                        "end": {"dateTime": "2026-01-15T13:00:00Z"}
                    }
                ]
            })))
            .mount(&server)
            .await;

        // Create tool with server URL override
        let auth = Arc::new(MockGoogleAuth::new("test-token"));
        let client = Arc::new(GoogleApiClient::new(auth));
        // We need to test with the mock server, so we use client directly
        let result = client
            .get(
                &format!("{}/calendar/v3/calendars/primary/events", server.uri()),
                &[("singleEvents", "true")],
            )
            .await
            .unwrap();

        let items = result["items"].as_array().unwrap();
        assert_eq!(items.len(), 2);
        assert_eq!(items[0]["summary"], "Team standup");
    }

    #[tokio::test]
    async fn handles_empty_events() {
        let (server, _tool) = setup().await;

        Mock::given(method("GET"))
            .and(path("/calendar/v3/calendars/primary/events"))
            .respond_with(ResponseTemplate::new(200).set_body_json(json!({
                "items": []
            })))
            .mount(&server)
            .await;

        let auth = Arc::new(MockGoogleAuth::new("test-token"));
        let client = Arc::new(GoogleApiClient::new(auth));
        let result = client
            .get(
                &format!("{}/calendar/v3/calendars/primary/events", server.uri()),
                &[],
            )
            .await
            .unwrap();
        let items = result["items"].as_array().unwrap();
        assert!(items.is_empty());
    }

    #[tokio::test]
    async fn handles_api_error() {
        let (server, _tool) = setup().await;

        Mock::given(method("GET"))
            .and(path("/calendar/v3/calendars/primary/events"))
            .respond_with(ResponseTemplate::new(401).set_body_json(json!({
                "error": {"code": 401, "message": "Invalid token"}
            })))
            .mount(&server)
            .await;

        let auth = Arc::new(MockGoogleAuth::new("test-token"));
        let client = Arc::new(GoogleApiClient::new(auth));
        let err = client
            .get(
                &format!("{}/calendar/v3/calendars/primary/events", server.uri()),
                &[],
            )
            .await
            .unwrap_err();
        assert!(matches!(err, crate::error::GoogleError::Api { .. }));
    }
}
