use std::sync::Arc;

use async_trait::async_trait;
use reqwest::Url;
use serde_json::{json, Map, Value};
use zeroclaw::tools::{Tool, ToolResult};

use crate::browser::BrowserBackend;
use crate::pipe::{Action, ExecutionSurfaceMetadata};

pub const BROWSER_ACTION_TOOL_NAME: &str = "browser_action";
pub const SUPERRPA_BROWSER_TOOL_NAME: &str = "superrpa_browser";
const BROWSER_ACTION_TOOL_DESCRIPTION: &str =
    "Execute browser actions in SuperRPA through the existing sgClaw pipe protocol.";
const SUPERRPA_BROWSER_TOOL_DESCRIPTION: &str =
    "Use SuperRPA's dedicated privileged browser interface for page navigation, DOM reading, clicking, and typing inside the protected browser host.";
const MAX_DATA_STRING_CHARS: usize = 2048;
const MAX_AOM_STRING_CHARS: usize = 128;
const MAX_DATA_ARRAY_ITEMS: usize = 12;
const MAX_DATA_OBJECT_FIELDS: usize = 24;
const MAX_DATA_RECURSION_DEPTH: usize = 4;

pub struct ZeroClawBrowserTool {
    browser_tool: Arc<dyn BrowserBackend>,
    tool_name: &'static str,
    description: &'static str,
}

impl ZeroClawBrowserTool {
    pub fn new(browser_tool: Arc<dyn BrowserBackend>) -> Self {
        Self::named(
            browser_tool,
            BROWSER_ACTION_TOOL_NAME,
            BROWSER_ACTION_TOOL_DESCRIPTION,
        )
    }

    pub fn new_superrpa(browser_tool: Arc<dyn BrowserBackend>) -> Self {
        Self::named(
            browser_tool,
            SUPERRPA_BROWSER_TOOL_NAME,
            SUPERRPA_BROWSER_TOOL_DESCRIPTION,
        )
    }

    fn named(
        browser_tool: Arc<dyn BrowserBackend>,
        tool_name: &'static str,
        description: &'static str,
    ) -> Self {
        Self {
            browser_tool,
            tool_name,
            description,
        }
    }

    pub fn surface_metadata(&self) -> ExecutionSurfaceMetadata {
        self.browser_tool.surface_metadata()
    }
}

#[async_trait]
impl Tool for ZeroClawBrowserTool {
    fn name(&self) -> &str {
        self.tool_name
    }

    fn description(&self) -> &str {
        self.description
    }

    fn parameters_schema(&self) -> Value {
        json!({
            "type": "object",
            "required": ["action", "expected_domain"],
            "properties": {
                "action": {
                    "type": "string",
                    "enum": ["click", "type", "navigate", "getText"]
                },
                "expected_domain": {
                    "type": "string"
                },
                "selector": {
                    "type": "string"
                },
                "text": {
                    "type": "string"
                },
                "url": {
                    "type": "string"
                },
                "clear_first": {
                    "type": "boolean"
                }
            }
        })
    }

    async fn execute(&self, args: Value) -> anyhow::Result<ToolResult> {
        let request = match parse_browser_action_request(args) {
            Ok(request) => request,
            Err(err) => return Ok(failed_tool_result(err.to_string())),
        };

        let result =
            match self
                .browser_tool
                .invoke(request.action, request.params, &request.expected_domain)
            {
                Ok(result) => result,
                Err(err) => return Ok(failed_tool_result(err.to_string())),
            };

        let output = serde_json::to_string(&json!({
            "seq": result.seq,
            "success": result.success,
            "data": compact_json_value(&result.data, 0),
            "aom_snapshot": compact_aom_snapshot(&result.aom_snapshot),
            "aom_snapshot_count": result.aom_snapshot.len(),
            "timing": result.timing
        }))?;

        Ok(ToolResult {
            success: result.success,
            output,
            error: (!result.success).then(|| format_browser_action_error(&result.data)),
        })
    }
}

struct BrowserActionRequest {
    action: Action,
    expected_domain: String,
    params: Value,
}

fn parse_browser_action_request(
    args: Value,
) -> Result<BrowserActionRequest, BrowserActionAdapterError> {
    let mut args = match args {
        Value::Object(args) => args,
        other => {
            return Err(BrowserActionAdapterError::InvalidArguments(format!(
                "expected object arguments, got {other}"
            )))
        }
    };

    let action_name = take_required_string(&mut args, "action")?;
    let raw_expected_domain = take_required_string(&mut args, "expected_domain")?;
    let action = parse_action(&action_name)?;
    validate_action_params(&action_name, &args)?;
    let expected_domain = normalize_expected_domain(&action, &raw_expected_domain, &args)?;

    Ok(BrowserActionRequest {
        action,
        expected_domain,
        params: Value::Object(args),
    })
}

fn parse_action(action_name: &str) -> Result<Action, BrowserActionAdapterError> {
    match action_name {
        "click" => Ok(Action::Click),
        "type" => Ok(Action::Type),
        "navigate" => Ok(Action::Navigate),
        "getText" => Ok(Action::GetText),
        other => Err(BrowserActionAdapterError::UnsupportedAction(
            other.to_string(),
        )),
    }
}

fn take_required_string(
    args: &mut Map<String, Value>,
    key: &'static str,
) -> Result<String, BrowserActionAdapterError> {
    match args.remove(key) {
        Some(Value::String(value)) if !value.trim().is_empty() => Ok(value),
        Some(other) => Err(BrowserActionAdapterError::InvalidArguments(format!(
            "{key} must be a non-empty string, got {other}"
        ))),
        None => Err(BrowserActionAdapterError::MissingField(key)),
    }
}

fn failed_tool_result(error: String) -> ToolResult {
    ToolResult {
        success: false,
        output: String::new(),
        error: Some(error),
    }
}

fn validate_action_params(
    action_name: &str,
    args: &Map<String, Value>,
) -> Result<(), BrowserActionAdapterError> {
    match action_name {
        "click" | "getText" => require_non_empty_string(args, "selector", action_name),
        "type" => {
            require_non_empty_string(args, "selector", action_name)?;
            require_non_empty_string(args, "text", action_name)
        }
        "navigate" => require_non_empty_string(args, "url", action_name),
        _ => Ok(()),
    }
}

fn require_non_empty_string(
    args: &Map<String, Value>,
    key: &'static str,
    action_name: &str,
) -> Result<(), BrowserActionAdapterError> {
    match args.get(key) {
        Some(Value::String(value)) if !value.trim().is_empty() => Ok(()),
        Some(other) => Err(BrowserActionAdapterError::InvalidArguments(format!(
            "{action_name} requires a non-empty {key}, got {other}"
        ))),
        None => Err(BrowserActionAdapterError::InvalidArguments(format!(
            "{action_name} requires {key}"
        ))),
    }
}

fn normalize_expected_domain(
    action: &Action,
    raw_expected_domain: &str,
    args: &Map<String, Value>,
) -> Result<String, BrowserActionAdapterError> {
    if matches!(action, Action::Navigate) {
        if let Some(url) = args.get("url").and_then(Value::as_str) {
            if let Some(host) = host_from_url(url) {
                return Ok(host);
            }
        }
    }

    normalize_domain_like(raw_expected_domain).ok_or_else(|| {
        BrowserActionAdapterError::InvalidArguments(format!(
            "expected_domain must resolve to a hostname, got {raw_expected_domain:?}"
        ))
    })
}

fn host_from_url(raw: &str) -> Option<String> {
    Url::parse(raw)
        .ok()?
        .host_str()
        .map(|host| host.to_ascii_lowercase())
}

fn normalize_domain_like(raw: &str) -> Option<String> {
    let trimmed = raw.trim();
    if trimmed.is_empty() {
        return None;
    }

    if let Some(host) = host_from_url(trimmed) {
        return Some(host);
    }

    let without_scheme = trimmed
        .trim_start_matches("https://")
        .trim_start_matches("http://");
    let host = without_scheme
        .split(['/', '?', '#'])
        .next()
        .unwrap_or_default()
        .split(':')
        .next()
        .unwrap_or_default()
        .trim()
        .to_ascii_lowercase();

    (!host.is_empty()).then_some(host)
}

fn format_browser_action_error(data: &Value) -> String {
    if let Some(error) = data.get("error") {
        if let Some(message) = error.get("message").and_then(Value::as_str) {
            return message.to_string();
        }
        return format!("browser action failed: {error}");
    }

    if data.is_null() {
        return "browser action returned success=false".to_string();
    }

    format!("browser action failed: {data}")
}

fn compact_json_value(value: &Value, depth: usize) -> Value {
    compact_json_value_with_string_limit(value, depth, MAX_DATA_STRING_CHARS)
}

fn compact_aom_snapshot(snapshot: &[Value]) -> Value {
    Value::Array(
        snapshot
            .iter()
            .take(MAX_DATA_ARRAY_ITEMS)
            .map(|item| compact_aom_value(item, 0))
            .collect(),
    )
}

fn compact_aom_value(value: &Value, depth: usize) -> Value {
    if depth >= MAX_DATA_RECURSION_DEPTH {
        return Value::String("[truncated nested value]".to_string());
    }

    match value {
        Value::Object(map) => {
            let mut compacted = Map::new();
            for (key, item) in map.iter().take(MAX_DATA_OBJECT_FIELDS) {
                if matches!(key.as_str(), "text" | "value" | "html") {
                    let summary = item
                        .as_str()
                        .map(|text| format!("[{} chars omitted]", text.chars().count()))
                        .unwrap_or_else(|| "[omitted]".to_string());
                    compacted.insert(key.clone(), Value::String(summary));
                    continue;
                }

                compacted.insert(key.clone(), compact_aom_value(item, depth + 1));
            }
            Value::Object(compacted)
        }
        Value::Array(items) => Value::Array(
            items
                .iter()
                .take(MAX_DATA_ARRAY_ITEMS)
                .map(|item| compact_aom_value(item, depth + 1))
                .collect(),
        ),
        _ => compact_json_value_with_string_limit(value, depth, MAX_AOM_STRING_CHARS),
    }
}

fn compact_json_value_with_string_limit(
    value: &Value,
    depth: usize,
    max_string_chars: usize,
) -> Value {
    if depth >= MAX_DATA_RECURSION_DEPTH {
        return Value::String("[truncated nested value]".to_string());
    }

    match value {
        Value::Null | Value::Bool(_) | Value::Number(_) => value.clone(),
        Value::String(text) => Value::String(truncate_string(text, max_string_chars)),
        Value::Array(items) => {
            let mut compacted: Vec<Value> = items
                .iter()
                .take(MAX_DATA_ARRAY_ITEMS)
                .map(|item| compact_json_value_with_string_limit(item, depth + 1, max_string_chars))
                .collect();
            if items.len() > MAX_DATA_ARRAY_ITEMS {
                compacted.push(Value::String(format!(
                    "[{} more items omitted]",
                    items.len() - MAX_DATA_ARRAY_ITEMS
                )));
            }
            Value::Array(compacted)
        }
        Value::Object(map) => {
            let mut compacted = Map::new();
            for (key, item) in map.iter().take(MAX_DATA_OBJECT_FIELDS) {
                compacted.insert(
                    key.clone(),
                    compact_json_value_with_string_limit(item, depth + 1, max_string_chars),
                );
            }
            if map.len() > MAX_DATA_OBJECT_FIELDS {
                compacted.insert(
                    "_truncated_fields".to_string(),
                    Value::String(format!(
                        "{} additional fields omitted",
                        map.len() - MAX_DATA_OBJECT_FIELDS
                    )),
                );
            }
            Value::Object(compacted)
        }
    }
}

fn truncate_string(text: &str, max_chars: usize) -> String {
    let total_chars = text.chars().count();
    if total_chars <= max_chars {
        return text.to_string();
    }

    let prefix: String = text.chars().take(max_chars).collect();
    format!("{prefix}...[truncated {} chars]", total_chars - max_chars)
}

#[derive(Debug, thiserror::Error)]
enum BrowserActionAdapterError {
    #[error("unsupported action: {0}")]
    UnsupportedAction(String),
    #[error("missing required field: {0}")]
    MissingField(&'static str),
    #[error("invalid tool arguments: {0}")]
    InvalidArguments(String),
}
