//! Handler functions for Phase 1 MCP tools.
//!
//! Each `handle_*` function corresponds to exactly one Phase 1 tool declared in
//! [`crate::mcp::tools`].  They are pure functions of their arguments and the
//! shared [`crate::mcp::tools::AppRegistry`] — no global mutable state.
//!
//! Internal helpers (`parse_app_identifier`, `extract_app_query`,
//! `parse_action_mode`, `click_at_coordinates`) are also defined here because
//! they exist solely to support these handlers.

use std::sync::Arc;

use serde_json::{json, Value};

use crate::app::AXApp;
use crate::mcp::protocol::ToolCallResult;
use crate::mcp::tools::AppRegistry;

// ---------------------------------------------------------------------------
// Handlers
// ---------------------------------------------------------------------------

pub(crate) fn handle_is_accessible() -> ToolCallResult {
    let enabled = crate::accessibility::check_accessibility_enabled();
    let result = if enabled {
        json!({ "enabled": true })
    } else {
        json!({
            "enabled": false,
            "suggestion": "Open System Settings > Privacy & Security > Accessibility and enable the terminal app."
        })
    };
    ToolCallResult {
        content: vec![crate::mcp::protocol::ContentItem::text(result.to_string())],
        is_error: !enabled,
    }
}

pub(crate) fn handle_connect(args: &Value, registry: &Arc<AppRegistry>) -> ToolCallResult {
    let Some(app_id) = args["app"].as_str() else {
        return ToolCallResult::error("Missing required field: app");
    };
    let alias = args["alias"].as_str().unwrap_or(app_id);

    let (name, bundle_id, pid) = parse_app_identifier(app_id);
    match AXApp::connect_native(name.as_deref(), bundle_id.as_deref(), pid) {
        Ok(app) => {
            #[allow(clippy::cast_sign_loss)]
            let connected_pid = app.pid as u32;
            let bundle = app.bundle_id.clone();
            registry.insert(alias.to_string(), app);
            ToolCallResult::ok(
                json!({
                    "connected": true,
                    "alias": alias,
                    "pid": connected_pid,
                    "bundle_id": bundle
                })
                .to_string(),
            )
        }
        Err(e) => ToolCallResult::error(format!("Failed to connect to '{app_id}': {e}")),
    }
}

pub(crate) fn handle_find(args: &Value, registry: &Arc<AppRegistry>) -> ToolCallResult {
    let (app_name, query) = match extract_app_query(args) {
        Ok(v) => v,
        Err(e) => return ToolCallResult::error(e),
    };
    let timeout_ms = args["timeout_ms"].as_u64().unwrap_or(5000);

    registry
        .with_app(&app_name, |app| {
            match app.find_native(&query, Some(timeout_ms)) {
                Ok(el) => {
                    let bounds_arr = el.bounds().map(|(x, y, w, h)| {
                        serde_json::Value::Array(vec![
                            serde_json::json!(x),
                            serde_json::json!(y),
                            serde_json::json!(w),
                            serde_json::json!(h),
                        ])
                    });
                    ToolCallResult::ok(
                        json!({
                            "found": true,
                            "role": el.role(),
                            "title": el.title(),
                            "value": el.value(),
                            "enabled": el.enabled(),
                            "bounds": bounds_arr
                        })
                        .to_string(),
                    )
                }
                Err(_) => ToolCallResult::error(format!(
                    "Element not found: '{query}' (timeout: {timeout_ms}ms)"
                )),
            }
        })
        .unwrap_or_else(ToolCallResult::error)
}

pub(crate) fn handle_click(args: &Value, registry: &Arc<AppRegistry>) -> ToolCallResult {
    let (app_name, query) = match extract_app_query(args) {
        Ok(v) => v,
        Err(e) => return ToolCallResult::error(e),
    };
    let mode_str = args["mode"].as_str().unwrap_or("background");
    let click_type = args["click_type"].as_str().unwrap_or("single");
    let mode = parse_action_mode(mode_str);

    registry
        .with_app(&app_name, |app| match app.find_native(&query, Some(100)) {
            Ok(el) => {
                let click_result = match click_type {
                    "double" => el.double_click_native(mode),
                    "right" => el.right_click_native(mode),
                    _ => el.click_native(mode),
                };
                match click_result {
                    Ok(()) => {
                        ToolCallResult::ok(json!({"clicked": true, "query": query}).to_string())
                    }
                    Err(e) => ToolCallResult::error(format!("Click failed: {e}")),
                }
            }
            Err(_) => ToolCallResult::error(format!("Element not found: '{query}'")),
        })
        .unwrap_or_else(ToolCallResult::error)
}

pub(crate) fn handle_type(args: &Value, registry: &Arc<AppRegistry>) -> ToolCallResult {
    let (app_name, query) = match extract_app_query(args) {
        Ok(v) => v,
        Err(e) => return ToolCallResult::error(e),
    };
    let text = match args["text"].as_str() {
        Some(t) => t.to_string(),
        None => return ToolCallResult::error("Missing required field: text"),
    };
    let mode_str = args["mode"].as_str().unwrap_or("focus");
    let mode = parse_action_mode(mode_str);

    let char_count = text.chars().count();
    registry
        .with_app(&app_name, |app| match app.find_native(&query, Some(100)) {
            Ok(el) => match el.type_text_native(&text, mode) {
                Ok(()) => {
                    ToolCallResult::ok(json!({"typed": true, "char_count": char_count}).to_string())
                }
                Err(e) => ToolCallResult::error(format!("Type failed: {e}")),
            },
            Err(_) => ToolCallResult::error(format!("Element not found: '{query}'")),
        })
        .unwrap_or_else(ToolCallResult::error)
}

pub(crate) fn handle_set_value(args: &Value, registry: &Arc<AppRegistry>) -> ToolCallResult {
    let (app_name, query) = match extract_app_query(args) {
        Ok(v) => v,
        Err(e) => return ToolCallResult::error(e),
    };
    let value = match args["value"].as_str() {
        Some(v) => v.to_string(),
        None => return ToolCallResult::error("Missing required field: value"),
    };

    registry
        .with_app(&app_name, |app| match app.find_native(&query, Some(100)) {
            Ok(el) => match el.set_value_native(&value) {
                Ok(()) => ToolCallResult::ok(json!({"set": true, "value": value}).to_string()),
                Err(e) => ToolCallResult::error(format!("set_value failed: {e}")),
            },
            Err(_) => ToolCallResult::error(format!("Element not found: '{query}'")),
        })
        .unwrap_or_else(ToolCallResult::error)
}

pub(crate) fn handle_get_value(args: &Value, registry: &Arc<AppRegistry>) -> ToolCallResult {
    let (app_name, query) = match extract_app_query(args) {
        Ok(v) => v,
        Err(e) => return ToolCallResult::error(e),
    };

    registry
        .with_app(&app_name, |app| match app.find_native(&query, Some(100)) {
            Ok(el) => ToolCallResult::ok(json!({"found": true, "value": el.value()}).to_string()),
            Err(_) => ToolCallResult::error(format!("Element not found: '{query}'")),
        })
        .unwrap_or_else(ToolCallResult::error)
}

pub(crate) fn handle_list_windows(args: &Value, registry: &Arc<AppRegistry>) -> ToolCallResult {
    let app_name = match args["app"].as_str() {
        Some(s) => s.to_string(),
        None => return ToolCallResult::error("Missing required field: app"),
    };

    registry
        .with_app(&app_name, |app| match app.windows_native() {
            Ok(windows) => {
                let items: Vec<Value> = windows
                    .iter()
                    .map(|w: &crate::AXElement| {
                        let bounds_val = w.bounds().map(|(x, y, bw, bh)| {
                            serde_json::Value::Array(vec![
                                serde_json::json!(x),
                                serde_json::json!(y),
                                serde_json::json!(bw),
                                serde_json::json!(bh),
                            ])
                        });
                        json!({ "title": w.title(), "bounds": bounds_val })
                    })
                    .collect();
                ToolCallResult::ok(json!({"windows": items}).to_string())
            }
            Err(e) => ToolCallResult::error(format!("Failed to list windows: {e}")),
        })
        .unwrap_or_else(ToolCallResult::error)
}

pub(crate) fn handle_screenshot(args: &Value, registry: &Arc<AppRegistry>) -> ToolCallResult {
    let app_name = match args["app"].as_str() {
        Some(s) => s.to_string(),
        None => return ToolCallResult::error("Missing required field: app"),
    };
    let query = args["query"].as_str().map(str::to_string);

    registry
        .with_app(&app_name, |app| {
            let data_result: Result<Vec<u8>, String> = if let Some(ref q) = query {
                app.find_native(q, Some(100))
                    .map_err(|e| e.to_string())
                    .and_then(|el| el.screenshot_native().map_err(|e| e.to_string()))
            } else {
                app.screenshot_native().map_err(|e| e.to_string())
            };

            match data_result {
                Ok(bytes) => {
                    use base64::Engine as _;
                    let b64 = base64::engine::general_purpose::STANDARD.encode::<&[u8]>(&bytes);
                    let size = bytes.len();
                    ToolCallResult::ok(
                        json!({
                            "captured": true,
                            "base64_png": b64,
                            "size_bytes": size
                        })
                        .to_string(),
                    )
                }
                Err(e) => ToolCallResult::error(format!("Screenshot failed: {e}")),
            }
        })
        .unwrap_or_else(ToolCallResult::error)
}

pub(crate) fn handle_click_at(args: &Value) -> ToolCallResult {
    let Some(x_raw) = args["x"].as_i64() else {
        return ToolCallResult::error("Missing required field: x");
    };
    let Some(y_raw) = args["y"].as_i64() else {
        return ToolCallResult::error("Missing required field: y");
    };
    #[allow(clippy::cast_possible_truncation)]
    let x = x_raw as i32;
    #[allow(clippy::cast_possible_truncation)]
    let y = y_raw as i32;
    let click_type = args["click_type"].as_str().unwrap_or("single");

    match click_at_coordinates(x, y, click_type) {
        Ok(()) => ToolCallResult::ok(json!({"clicked": true, "x": x, "y": y}).to_string()),
        Err(e) => ToolCallResult::error(format!("click_at ({x},{y}) failed: {e}")),
    }
}

pub(crate) fn handle_find_visual(args: &Value, registry: &Arc<AppRegistry>) -> ToolCallResult {
    let Some(app_name) = args["app"].as_str().map(str::to_string) else {
        return ToolCallResult::error("Missing required field: app");
    };
    let Some(description) = args["description"].as_str().map(str::to_string) else {
        return ToolCallResult::error("Missing required field: description");
    };

    // VLM detection is a future capability. We take a screenshot and explain
    // the situation clearly so the MCP client can decide what to do next.
    registry
        .with_app(&app_name, |_app| {
            ToolCallResult::error(format!(
                "VLM visual detection for '{description}' is not yet available in the Rust server. \
                 Configure ANTHROPIC_API_KEY or OPENAI_API_KEY and use ax_screenshot to \
                 capture the app, then call your VLM to locate the element."
            ))
        })
        .unwrap_or_else(ToolCallResult::error)
}

pub(crate) fn handle_wait_idle(args: &Value, registry: &Arc<AppRegistry>) -> ToolCallResult {
    let Some(app_name) = args["app"].as_str().map(str::to_string) else {
        return ToolCallResult::error("Missing required field: app");
    };
    let timeout_ms = args["timeout_ms"].as_u64().unwrap_or(5000);

    let start = std::time::Instant::now();
    registry
        .with_app(&app_name, |app| {
            let idle = app.wait_idle_native(timeout_ms);
            #[allow(clippy::cast_possible_truncation)]
            let elapsed = start.elapsed().as_millis() as u64;
            ToolCallResult::ok(json!({"idle": idle, "elapsed_ms": elapsed}).to_string())
        })
        .unwrap_or_else(ToolCallResult::error)
}

// ---------------------------------------------------------------------------
// Internal helpers
// ---------------------------------------------------------------------------

/// Extract the mandatory `app` and `query` string fields from an argument object.
pub(crate) fn extract_app_query(args: &Value) -> Result<(String, String), String> {
    let app = args["app"]
        .as_str()
        .ok_or_else(|| "Missing required field: app".to_string())?
        .to_string();
    let query = args["query"]
        .as_str()
        .ok_or_else(|| "Missing required field: query".to_string())?
        .to_string();
    Ok((app, query))
}

/// Parse an app identifier string into (name, `bundle_id`, pid) for `AXApp::connect`.
///
/// Heuristics:
/// - All digits → PID
/// - Contains two or more dots → bundle ID
/// - Otherwise → display name
pub(crate) fn parse_app_identifier(id: &str) -> (Option<String>, Option<String>, Option<u32>) {
    if id.chars().all(|c| c.is_ascii_digit()) {
        let pid: u32 = id.parse().unwrap_or(0);
        return (None, None, Some(pid));
    }
    if id.matches('.').count() >= 2 {
        return (None, Some(id.to_string()), None);
    }
    (Some(id.to_string()), None, None)
}

/// Map mode string to `ActionMode`.
pub(crate) fn parse_action_mode(mode: &str) -> crate::ActionMode {
    if mode == "focus" {
        crate::ActionMode::Focus
    } else {
        crate::ActionMode::Background
    }
}

/// Perform a coordinate click via `CGEvent` posted to the HID event tap.
///
/// # Safety
/// Calls macOS Core Graphics APIs. The coordinates must be within display bounds.
pub(crate) fn click_at_coordinates(x: i32, y: i32, click_type: &str) -> Result<(), String> {
    use core_graphics::event::{CGEvent, CGEventType, CGMouseButton};
    use core_graphics::event_source::{CGEventSource, CGEventSourceStateID};
    use core_graphics::geometry::CGPoint;

    let source = CGEventSource::new(CGEventSourceStateID::HIDSystemState)
        .map_err(|()| "Failed to create CGEventSource".to_string())?;

    let point = CGPoint::new(f64::from(x), f64::from(y));

    let post_click = |down_type: CGEventType, up_type: CGEventType, btn: CGMouseButton| {
        let down = CGEvent::new_mouse_event(source.clone(), down_type, point, btn)
            .map_err(|()| "Failed to create mouse event".to_string())?;
        down.post(core_graphics::event::CGEventTapLocation::HID);

        let up = CGEvent::new_mouse_event(source.clone(), up_type, point, btn)
            .map_err(|()| "Failed to create mouse event".to_string())?;
        up.post(core_graphics::event::CGEventTapLocation::HID);
        Ok::<(), String>(())
    };

    match click_type {
        "right" => post_click(
            CGEventType::RightMouseDown,
            CGEventType::RightMouseUp,
            CGMouseButton::Right,
        )?,
        "double" => {
            post_click(
                CGEventType::LeftMouseDown,
                CGEventType::LeftMouseUp,
                CGMouseButton::Left,
            )?;
            std::thread::sleep(std::time::Duration::from_millis(80));
            post_click(
                CGEventType::LeftMouseDown,
                CGEventType::LeftMouseUp,
                CGMouseButton::Left,
            )?;
        }
        _ => post_click(
            CGEventType::LeftMouseDown,
            CGEventType::LeftMouseUp,
            CGMouseButton::Left,
        )?,
    }

    Ok(())
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

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

    #[test]
    fn parse_app_identifier_digits_returns_pid() {
        // GIVEN: pure digit string
        // WHEN: parsed
        let (name, bundle, pid) = parse_app_identifier("12345");
        // THEN: pid branch
        assert_eq!(pid, Some(12345));
        assert!(name.is_none());
        assert!(bundle.is_none());
    }

    #[test]
    fn parse_app_identifier_bundle_id_detected() {
        // GIVEN: bundle ID with two dots
        let (name, bundle, pid) = parse_app_identifier("com.apple.Safari");
        // THEN: bundle branch
        assert_eq!(bundle.as_deref(), Some("com.apple.Safari"));
        assert!(name.is_none());
        assert!(pid.is_none());
    }

    #[test]
    fn parse_app_identifier_name_fallback() {
        // GIVEN: plain name
        let (name, bundle, pid) = parse_app_identifier("Safari");
        // THEN: name branch
        assert_eq!(name.as_deref(), Some("Safari"));
        assert!(bundle.is_none());
        assert!(pid.is_none());
    }

    #[test]
    fn extract_app_query_succeeds_with_valid_args() {
        let args = json!({"app": "Safari", "query": "Save"});
        let (app, query) = extract_app_query(&args).unwrap();
        assert_eq!(app, "Safari");
        assert_eq!(query, "Save");
    }

    #[test]
    fn extract_app_query_fails_without_app() {
        let args = json!({"query": "Save"});
        assert!(extract_app_query(&args).is_err());
    }

    #[test]
    fn extract_app_query_fails_without_query() {
        let args = json!({"app": "Safari"});
        assert!(extract_app_query(&args).is_err());
    }

    #[test]
    fn parse_action_mode_background_is_default() {
        assert_eq!(
            parse_action_mode("background"),
            crate::ActionMode::Background
        );
        assert_eq!(parse_action_mode("unknown"), crate::ActionMode::Background);
    }

    #[test]
    fn parse_action_mode_focus_recognised() {
        assert_eq!(parse_action_mode("focus"), crate::ActionMode::Focus);
    }
}
