use anyhow::{Result, anyhow, bail};
use serde_json::Value;

#[derive(Debug, Clone, PartialEq)]
pub(super) enum BrowserAction {
    Open {
        url: String,
    },
    Snapshot {
        interactive_only: bool,
        compact: bool,
        depth: Option<u64>,
    },
    Click {
        selector: String,
    },
    Fill {
        selector: String,
        value: String,
    },
    Type {
        selector: Option<String>,
        text: String,
    },
    GetText {
        selector: String,
    },
    GetTitle,
    GetUrl,
    Screenshot {
        filename: Option<String>,
        full_page: bool,
        annotate: bool,
        present_to_user: bool,
    },
    Focus {
        selector: String,
    },
    Wait {
        selector: Option<String>,
        ms: Option<u64>,
        text: Option<String>,
    },
    Press {
        key: String,
    },
    Hover {
        selector: String,
    },
    ClickAt {
        x: u32,
        y: u32,
    },
    Scroll {
        direction: String,
        pixels: Option<u32>,
    },
    Close,
}

impl BrowserAction {
    pub(super) fn parse(args: &Value) -> Result<Self> {
        let action = required_str(args, "action")?;
        match action {
            "open" => Ok(Self::Open {
                url: required_str(args, "url")?.to_string(),
            }),
            "snapshot" => Ok(Self::Snapshot {
                interactive_only: args
                    .get("interactive_only")
                    .and_then(Value::as_bool)
                    .unwrap_or(true),
                compact: args.get("compact").and_then(Value::as_bool).unwrap_or(true),
                depth: args.get("depth").and_then(Value::as_u64),
            }),
            "click" => Ok(Self::Click {
                selector: required_str(args, "selector")?.to_string(),
            }),
            "fill" => Ok(Self::Fill {
                selector: required_str(args, "selector")?.to_string(),
                value: required_str(args, "value")?.to_string(),
            }),
            "type" => Ok(Self::Type {
                selector: optional_str(args, "selector"),
                text: required_str(args, "text")?.to_string(),
            }),
            "get_text" => Ok(Self::GetText {
                selector: required_str(args, "selector")?.to_string(),
            }),
            "get_title" => Ok(Self::GetTitle),
            "get_url" => Ok(Self::GetUrl),
            "screenshot" => Ok(Self::Screenshot {
                filename: optional_str(args, "path"),
                full_page: args
                    .get("full_page")
                    .and_then(Value::as_bool)
                    .unwrap_or(false),
                annotate: args
                    .get("annotate")
                    .and_then(Value::as_bool)
                    .unwrap_or(false),
                present_to_user: args
                    .get("present_to_user")
                    .and_then(Value::as_bool)
                    .unwrap_or(true),
            }),
            "focus" => Ok(Self::Focus {
                selector: required_str(args, "selector")?.to_string(),
            }),
            "wait" => {
                let wait = Self::Wait {
                    selector: optional_str(args, "selector"),
                    ms: args.get("ms").and_then(Value::as_u64),
                    text: optional_str(args, "text"),
                };
                if matches!(
                    wait,
                    Self::Wait {
                        selector: None,
                        ms: None,
                        text: None
                    }
                ) {
                    bail!("wait requires one of selector, ms, or text");
                }
                Ok(wait)
            }
            "press" => Ok(Self::Press {
                key: required_str(args, "key")?.to_string(),
            }),
            "hover" => Ok(Self::Hover {
                selector: required_str(args, "selector")?.to_string(),
            }),
            "click_at" => Ok(Self::ClickAt {
                x: required_u32(args, "x")?,
                y: required_u32(args, "y")?,
            }),
            "scroll" => {
                let direction = required_str(args, "direction")?;
                if !matches!(direction, "up" | "down" | "left" | "right") {
                    bail!("direction must be one of up, down, left, right");
                }
                Ok(Self::Scroll {
                    direction: direction.to_string(),
                    pixels: args
                        .get("pixels")
                        .and_then(Value::as_u64)
                        .map(|value| u32::try_from(value).unwrap_or(u32::MAX)),
                })
            }
            "close" => Ok(Self::Close),
            other => bail!("unsupported browser action: {other}"),
        }
    }

    pub(super) fn command_name(&self) -> &'static str {
        match self {
            Self::Open { .. } => "open",
            Self::Snapshot { .. } => "snapshot",
            Self::Click { .. } => "click",
            Self::Fill { .. } => "fill",
            Self::Type { .. } => "type",
            Self::GetText { .. } => "get_text",
            Self::GetTitle => "get_title",
            Self::GetUrl => "get_url",
            Self::Screenshot { .. } => "screenshot",
            Self::Focus { .. } => "focus",
            Self::Wait { .. } => "wait",
            Self::Press { .. } => "press",
            Self::Hover { .. } => "hover",
            Self::ClickAt { .. } => "click_at",
            Self::Scroll { .. } => "scroll",
            Self::Close => "close",
        }
    }

    pub(super) fn screenshot_filename(&self) -> Option<Option<&str>> {
        match self {
            Self::Screenshot { filename, .. } => Some(filename.as_deref()),
            _ => None,
        }
    }

    pub(super) fn screenshot_present_to_user(&self) -> bool {
        match self {
            Self::Screenshot {
                present_to_user, ..
            } => *present_to_user,
            _ => false,
        }
    }

    pub(super) fn commands(&self, screenshot_path: Option<&str>) -> Vec<Vec<String>> {
        let command = |items: &[&str]| items.iter().map(|item| (*item).to_string()).collect();
        match self {
            Self::Open { url } => vec![vec!["open".into(), url.clone()]],
            Self::Snapshot {
                interactive_only,
                compact,
                depth,
            } => {
                let mut args = vec!["snapshot".to_string()];
                if *interactive_only {
                    args.push("--interactive".into());
                }
                if *compact {
                    args.push("--compact".into());
                }
                if let Some(depth) = depth {
                    args.extend(["--depth".into(), depth.to_string()]);
                }
                vec![args]
            }
            Self::Click { selector } => vec![vec!["click".into(), selector.clone()]],
            Self::Fill { selector, value } => {
                vec![vec!["fill".into(), selector.clone(), value.clone()]]
            }
            Self::Type { selector, text } => match selector {
                Some(selector) => {
                    vec![vec!["type".into(), selector.clone(), text.clone()]]
                }
                None => vec![vec!["keyboard".into(), "type".into(), text.clone()]],
            },
            Self::GetText { selector } => {
                vec![vec!["get".into(), "text".into(), selector.clone()]]
            }
            Self::GetTitle => vec![command(&["get", "title"])],
            Self::GetUrl => vec![command(&["get", "url"])],
            Self::Screenshot {
                full_page,
                annotate,
                ..
            } => {
                let mut args = vec![
                    "screenshot".to_string(),
                    screenshot_path
                        .expect("manager supplies screenshot path")
                        .to_string(),
                ];
                if *full_page {
                    args.push("--full".into());
                }
                if *annotate {
                    args.push("--annotate".into());
                }
                vec![args]
            }
            Self::Focus { selector } => vec![vec!["focus".into(), selector.clone()]],
            Self::Wait { selector, ms, text } => {
                let args = if let Some(selector) = selector {
                    vec!["wait".into(), selector.clone()]
                } else if let Some(text) = text {
                    vec!["wait".into(), "--text".into(), text.clone()]
                } else {
                    vec!["wait".into(), ms.unwrap_or_default().to_string()]
                };
                vec![args]
            }
            Self::Press { key } => vec![vec!["press".into(), key.clone()]],
            Self::Hover { selector } => vec![vec!["hover".into(), selector.clone()]],
            Self::ClickAt { x, y } => vec![
                vec!["mouse".into(), "move".into(), x.to_string(), y.to_string()],
                command(&["mouse", "down", "left"]),
                command(&["mouse", "up", "left"]),
            ],
            Self::Scroll { direction, pixels } => {
                let mut args = vec!["scroll".into(), direction.clone()];
                if let Some(pixels) = pixels {
                    args.push(pixels.to_string());
                }
                vec![args]
            }
            Self::Close => vec![command(&["close"])],
        }
    }

    pub(super) fn is_close(&self) -> bool {
        matches!(self, Self::Close)
    }
}

fn required_str<'a>(args: &'a Value, key: &str) -> Result<&'a str> {
    args.get(key)
        .and_then(Value::as_str)
        .filter(|value| !value.is_empty())
        .ok_or_else(|| anyhow!("missing required parameter: {key}"))
}

fn optional_str(args: &Value, key: &str) -> Option<String> {
    args.get(key)
        .and_then(Value::as_str)
        .filter(|value| !value.is_empty())
        .map(str::to_string)
}

fn required_u32(args: &Value, key: &str) -> Result<u32> {
    let value = args
        .get(key)
        .and_then(Value::as_u64)
        .ok_or_else(|| anyhow!("missing required parameter: {key}"))?;
    u32::try_from(value).map_err(|_| anyhow!("{key} is outside the supported range"))
}

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

    #[test]
    fn maps_snapshot_options_to_native_cli() {
        let action = BrowserAction::parse(&json!({
            "action": "snapshot",
            "interactive_only": true,
            "compact": true,
            "depth": 4
        }))
        .unwrap();
        assert_eq!(
            action.commands(None),
            vec![vec![
                "snapshot",
                "--interactive",
                "--compact",
                "--depth",
                "4"
            ]]
        );
    }

    #[test]
    fn type_without_selector_uses_keyboard_target() {
        let action = BrowserAction::parse(&json!({"action": "type", "text": "hello"})).unwrap();
        assert_eq!(
            action.commands(None),
            vec![vec!["keyboard", "type", "hello"]]
        );
    }

    #[test]
    fn wait_requires_a_condition() {
        assert!(BrowserAction::parse(&json!({"action": "wait"})).is_err());
    }

    #[test]
    fn screenshot_is_user_visible_by_default_and_can_be_model_only() {
        let visible = BrowserAction::parse(&json!({"action": "screenshot"})).unwrap();
        assert!(visible.screenshot_present_to_user());

        let model_only = BrowserAction::parse(&json!({
            "action": "screenshot",
            "present_to_user": false
        }))
        .unwrap();
        assert!(!model_only.screenshot_present_to_user());
    }
}
