use std::time::{Duration, Instant};

use night_fury_core::{BrowserSession, NightFuryError, Selector};
use serde_json::json;

use night_fury_cli_core::output::{output_err, output_ok, OutputMode};
use night_fury_cli_core::selector_parse;

pub async fn click(session: &BrowserSession, parts: &[&str], mode: OutputMode) {
    let rest = parts.join(" ");
    match selector_parse::parse_selector(rest.trim()) {
        Ok(sel) => match session.click(sel).await {
            Ok(msg) => output_ok(mode, &msg, json!({"action": "click"})),
            Err(e) => output_err(mode, &e.to_string()),
        },
        Err(e) => output_err(mode, &e),
    }
}

pub async fn type_text(session: &BrowserSession, line: &str, parts: &[&str], mode: OutputMode) {
    // parts are the args after the command name (e.g. ["@e2", "hello world"])
    // or ["css", "#search", "hello world"] when splitn(4) is used
    if parts.len() < 2 {
        output_err(
            mode,
            "Usage: type @e<N> <text>  or  type css <selector> <text>",
        );
        return;
    }

    let target = parts[0];

    if target.eq_ignore_ascii_case("css") {
        // type css <selector> <text>
        // Re-split from the original line for correct handling
        let raw: Vec<&str> = line.splitn(4, ' ').collect();
        if raw.len() < 4 {
            output_err(mode, "Usage: type css <selector> <text>");
            return;
        }
        let css_sel = raw[2];
        let text = raw[3];
        match session
            .type_text(Selector::Css(css_sel.to_string()), text)
            .await
        {
            Ok(msg) => output_ok(mode, &msg, json!({"action": "type"})),
            Err(e) => output_err(mode, &e.to_string()),
        }
        return;
    }

    // @e<N> form — text is everything after the selector
    // Since we used splitn(4,' ') in dispatch, parts[1] is the text
    let sel = if let Some(rest) = target.strip_prefix("@e") {
        rest.parse::<usize>()
            .map(Selector::Ref)
            .map_err(|_| NightFuryError::InvalidArgument(format!("bad @ref: {target}")))
    } else {
        Err(NightFuryError::InvalidArgument(
            "first arg must be @e<N> or 'css'".to_string(),
        ))
    };

    match sel {
        Ok(sel) => {
            let text = parts[1..].join(" ");
            match session.type_text(sel, &text).await {
                Ok(msg) => output_ok(mode, &msg, json!({"action": "type"})),
                Err(e) => output_err(mode, &e.to_string()),
            }
        }
        Err(e) => output_err(mode, &e.to_string()),
    }
}

pub async fn scroll(session: &BrowserSession, parts: &[&str], mode: OutputMode) {
    let px: i32 = parts.first().and_then(|s| s.parse().ok()).unwrap_or(600);
    match session.scroll(px).await {
        Ok(msg) => output_ok(mode, &msg, json!({"action": "scroll", "pixels": px})),
        Err(e) => output_err(mode, &e.to_string()),
    }
}

pub async fn hover(session: &BrowserSession, parts: &[&str], mode: OutputMode) {
    let rest = parts.join(" ");
    match selector_parse::parse_selector(rest.trim()) {
        Ok(sel) => match session.hover(sel).await {
            Ok(msg) => output_ok(mode, &msg, json!({"action": "hover"})),
            Err(e) => output_err(mode, &e.to_string()),
        },
        Err(e) => output_err(mode, &e),
    }
}

pub async fn select(session: &BrowserSession, parts: &[&str], mode: OutputMode) {
    // select <selector> <value>
    // parts[0] is selector (or "css"/"@e<N>"), last part is value
    if parts.len() < 2 {
        output_err(mode, "Usage: select <selector> <value>");
        return;
    }
    let value = parts[parts.len() - 1];
    let sel_str = parts[..parts.len() - 1].join(" ");
    match selector_parse::parse_selector(sel_str.trim()) {
        Ok(sel) => match session.select(sel, value).await {
            Ok(msg) => output_ok(mode, &msg, json!({"action": "select", "value": value})),
            Err(e) => output_err(mode, &e.to_string()),
        },
        Err(e) => output_err(mode, &e),
    }
}

pub async fn check(session: &BrowserSession, parts: &[&str], mode: OutputMode) {
    let css = parts.join(" ");
    let css = css.trim();
    if css.is_empty() {
        output_err(mode, "Usage: check <css-selector>");
        return;
    }
    match session.check(css).await {
        Ok(msg) => output_ok(mode, &msg, json!({"action": "check"})),
        Err(e) => output_err(mode, &e.to_string()),
    }
}

pub async fn uncheck(session: &BrowserSession, parts: &[&str], mode: OutputMode) {
    let css = parts.join(" ");
    let css = css.trim();
    if css.is_empty() {
        output_err(mode, "Usage: uncheck <css-selector>");
        return;
    }
    match session.uncheck(css).await {
        Ok(msg) => output_ok(mode, &msg, json!({"action": "uncheck"})),
        Err(e) => output_err(mode, &e.to_string()),
    }
}

pub async fn wait(session: &BrowserSession, line: &str, parts: &[&str], mode: OutputMode) {
    let sub = parts.first().copied().unwrap_or("");
    match resolve_wait_sub(sub) {
        WaitSub::Element => {
            let rest = line.splitn(3, ' ').nth(2).unwrap_or("").trim();
            let (selector, timeout_ms) = split_tail_timeout(rest, 30_000);
            match session.wait_for_selector(selector, timeout_ms).await {
                Ok(msg) => output_ok(
                    mode,
                    &msg,
                    json!({"action":"wait_element","selector":selector}),
                ),
                Err(e) => output_err(mode, &e.to_string()),
            }
        }
        WaitSub::Load => {
            let timeout_ms = parts.get(1).and_then(|s| s.parse().ok()).unwrap_or(30_000);
            match session.wait_for_navigation(timeout_ms).await {
                Ok(msg) => output_ok(
                    mode,
                    &msg,
                    json!({"action":"wait_load","timeout_ms":timeout_ms}),
                ),
                Err(e) => output_err(mode, &e.to_string()),
            }
        }
        WaitSub::Network => {
            let timeout_ms = parts.get(1).and_then(|s| s.parse().ok()).unwrap_or(30_000);
            match session.wait_for_network_idle(timeout_ms, 500).await {
                Ok(msg) => output_ok(
                    mode,
                    &msg,
                    json!({"action":"wait_network","timeout_ms":timeout_ms}),
                ),
                Err(e) => output_err(mode, &e.to_string()),
            }
        }
        WaitSub::Text => {
            let rest = line.splitn(3, ' ').nth(2).unwrap_or("").trim();
            let (text, timeout_ms) = split_tail_timeout(rest, 30_000);
            wait_for_text(session, text, timeout_ms, mode).await;
        }
        WaitSub::Url => {
            let rest = line.splitn(3, ' ').nth(2).unwrap_or("").trim();
            let (pattern, timeout_ms) = split_tail_timeout(rest, 30_000);
            wait_for_url(session, pattern, timeout_ms, mode).await;
        }
        WaitSub::Ms => {
            let ms: u64 = parts.first().and_then(|s| s.parse().ok()).unwrap_or(1000);
            session.wait(Duration::from_millis(ms)).await;
            output_ok(
                mode,
                &format!("Waited {ms}ms"),
                json!({"action":"wait","ms":ms}),
            );
        }
    }
}

/// Resolved `wait` sub-command. `Ms` is the legacy bare-millisecond form.
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub(super) enum WaitSub {
    Element,
    Load,
    Network,
    Text,
    Url,
    Ms,
}

/// Map the first word after `wait` to a [`WaitSub`]. Anything unknown falls
/// through to the legacy `wait <ms>` form — consistent with the handler's
/// behaviour, which treats unrecognised tokens as an integer parse.
pub(super) fn resolve_wait_sub(sub: &str) -> WaitSub {
    match sub.to_lowercase().as_str() {
        "element" => WaitSub::Element,
        "load" => WaitSub::Load,
        "network" => WaitSub::Network,
        "text" => WaitSub::Text,
        "url" => WaitSub::Url,
        _ => WaitSub::Ms,
    }
}

// Split "some text 5000" → ("some text", 5000). Falls back to default_ms if last token isn't u64.
pub(super) fn split_tail_timeout(input: &str, default_ms: u64) -> (&str, u64) {
    if let Some((head, tail)) = input.rsplit_once(' ') {
        if let Ok(ms) = tail.parse::<u64>() {
            return (head.trim(), ms);
        }
    }
    (input, default_ms)
}

async fn wait_for_text(session: &BrowserSession, text: &str, timeout_ms: u64, mode: OutputMode) {
    if text.is_empty() {
        output_err(mode, "Usage: wait text <text> [timeout_ms]");
        return;
    }
    let escaped = serde_json::to_string(text).unwrap_or_default();
    let js = format!("document.body.innerText.includes({escaped})");
    let deadline = Duration::from_millis(timeout_ms);
    let start = Instant::now();
    loop {
        if let Ok(v) = session.eval(&js).await {
            if v == serde_json::Value::Bool(true) {
                output_ok(
                    mode,
                    &format!("Text found: {text}"),
                    json!({"action":"wait_text","text":text}),
                );
                return;
            }
        }
        if start.elapsed() >= deadline {
            output_err(mode, &format!("Timeout waiting for text: {text}"));
            return;
        }
        tokio::time::sleep(Duration::from_millis(200)).await;
    }
}

async fn wait_for_url(session: &BrowserSession, pattern: &str, timeout_ms: u64, mode: OutputMode) {
    if pattern.is_empty() {
        output_err(mode, "Usage: wait url <pattern> [timeout_ms]");
        return;
    }
    let deadline = Duration::from_millis(timeout_ms);
    let start = Instant::now();
    loop {
        if let Ok(url) = session.get_url().await {
            if url.contains(pattern) {
                output_ok(
                    mode,
                    &format!("URL matched: {url}"),
                    json!({"action":"wait_url","pattern":pattern,"url":url}),
                );
                return;
            }
        }
        if start.elapsed() >= deadline {
            output_err(mode, &format!("Timeout waiting for URL: {pattern}"));
            return;
        }
        tokio::time::sleep(Duration::from_millis(200)).await;
    }
}

#[cfg(test)]
mod tests {
    use super::{resolve_wait_sub, split_tail_timeout, WaitSub};

    #[test]
    fn split_tail_timeout_extracts_trailing_integer() {
        assert_eq!(
            split_tail_timeout("button.submit 5000", 30_000),
            ("button.submit", 5000)
        );
    }

    #[test]
    fn split_tail_timeout_handles_multiword_head() {
        assert_eq!(
            split_tail_timeout("Login to continue 10000", 30_000),
            ("Login to continue", 10_000)
        );
    }

    #[test]
    fn split_tail_timeout_falls_back_when_tail_not_integer() {
        // last token is not a u64 — treat whole input as body
        assert_eq!(split_tail_timeout("foo bar", 30_000), ("foo bar", 30_000));
    }

    #[test]
    fn split_tail_timeout_single_token_returns_default() {
        // no space at all → default timeout, body unchanged
        assert_eq!(split_tail_timeout("selector", 30_000), ("selector", 30_000));
    }

    #[test]
    fn split_tail_timeout_trims_head() {
        assert_eq!(split_tail_timeout("button   42", 1), ("button", 42));
    }

    #[test]
    fn split_tail_timeout_rejects_negative() {
        // -5 is not a u64 — falls back to default
        assert_eq!(split_tail_timeout("x -5", 1000), ("x -5", 1000));
    }

    #[test]
    fn resolve_wait_sub_routes_known_subcommands() {
        assert_eq!(resolve_wait_sub("element"), WaitSub::Element);
        assert_eq!(resolve_wait_sub("load"), WaitSub::Load);
        assert_eq!(resolve_wait_sub("network"), WaitSub::Network);
        assert_eq!(resolve_wait_sub("text"), WaitSub::Text);
        assert_eq!(resolve_wait_sub("url"), WaitSub::Url);
    }

    #[test]
    fn resolve_wait_sub_is_case_insensitive() {
        assert_eq!(resolve_wait_sub("ELEMENT"), WaitSub::Element);
        assert_eq!(resolve_wait_sub("Load"), WaitSub::Load);
    }

    #[test]
    fn resolve_wait_sub_defaults_to_ms_for_unknown() {
        // Legacy `wait <ms>` form — any non-subcommand word falls through.
        assert_eq!(resolve_wait_sub(""), WaitSub::Ms);
        assert_eq!(resolve_wait_sub("1000"), WaitSub::Ms);
        assert_eq!(resolve_wait_sub("garbage"), WaitSub::Ms);
    }
}
