use std::process::Command;

const WEBVIEW_PANEL_TYPE: &str = "adom/a1b2c3d4-0031-4000-a000-000000000031";

/// Open a webview tab in the Adom workspace for a shotlog channel.
///
/// `skinny` puts it in a slim right-side panel (the preferred shotlog layout):
/// if the workspace already has a right pane, the tab is added there and that
/// split is resized so the shotlog panel is ~`ratio` of the width; if there's
/// only one pane, it's split to create the right panel. Multiple `open --skinny`
/// calls stack their tabs in the same panel.
pub fn open_webview(
    channel: &str,
    display_name: Option<&str>,
    icon: Option<&str>,
    port: u16,
    skinny: bool,
    ratio: f64,
) {
    let default_name = "Shotlog".to_string();
    let name = display_name.unwrap_or(&default_name);
    let icon = icon.unwrap_or("mdi:camera-burst");

    let proxy_uri = match std::env::var("VSCODE_PROXY_URI") {
        Ok(uri) => uri.replace("{{port}}", &port.to_string()),
        Err(_) => {
            eprintln!("VSCODE_PROXY_URI not set — cannot determine proxy URL.");
            eprintln!("Falling back to http://127.0.0.1:{port}");
            format!("http://127.0.0.1:{port}")
        }
    };
    // The SINGLE-WINDOW app: /log/ hosts every channel as in-app tabs;
    // ?channel deep-links the initially active tab.
    let url = format!("{proxy_uri}log/?surface=webview&channel={channel}");
    let initial_state = serde_json::json!({ "url": url }).to_string();

    // Fetch the layout once.
    let layout_json = Command::new("adom-cli")
        .args(["hydrogen", "workspace", "get"])
        .output()
        .ok()
        .map(|o| String::from_utf8_lossy(&o.stdout).to_string())
        .unwrap_or_default();
    let layout: serde_json::Value = serde_json::from_str(&layout_json).unwrap_or(serde_json::Value::Null);
    let root = layout.get("root").unwrap_or(&layout);

    // No split yet + skinny → split the single pane to create the right panel.
    if skinny {
        if let Some(leaf_id) = single_leaf_id(root) {
            // first child keeps (1 - ratio); the new right pane is `ratio` wide.
            let first_ratio = (1.0 - ratio).clamp(0.1, 0.9);
            let out = Command::new("adom-cli")
                .args([
                    "hydrogen", "workspace", "split",
                    "--panel-id", &leaf_id,
                    "--direction", "horizontal",
                    "--position", "after",
                    "--ratio", &format!("{first_ratio}"),
                    "--panel-type", WEBVIEW_PANEL_TYPE,
                    "--display-name", name,
                    "--display-icon", icon,
                    "--initial-state", &initial_state,
                ])
                .output();
            report(out, name, channel, &url);
            return;
        }
    }

    // Otherwise: add the tab to the preferred (right/second) leaf.
    let Some(leaf_id) = find_leaf_id(root) else {
        eprintln!("Could not find a leaf pane in workspace layout.");
        eprintln!("URL: {url}");
        return;
    };
    // Idempotent create-or-refresh: with --panel-id it CREATES the tab if the
    // name doesn't exist, else refreshes it. (Without --panel-id it can only
    // refresh — the bug that made auto-open silently fail.)
    let out = Command::new("adom-cli")
        .args([
            "hydrogen", "webview", "open-or-refresh",
            "--name", name,
            "--url", &url,
            "--panel-id", &leaf_id,
            "--display-icon", icon,
        ])
        .output();
    report(out, name, channel, &url);

    // Skinny + an existing split → resize so the shotlog pane is ~`ratio` wide.
    if skinny {
        if let Some((split_id, side)) = find_enclosing_split(root, &leaf_id) {
            // ratio is the FIRST child's fraction. If shotlog is the second
            // (right) child, first gets (1 - ratio); if it's the first, it gets ratio.
            let first_ratio = if side == "second" { 1.0 - ratio } else { ratio };
            let first_ratio = first_ratio.clamp(0.1, 0.9);
            let _ = Command::new("adom-cli")
                .args(["hydrogen", "workspace", "resize", "--split-id", &split_id, "--ratio", &format!("{first_ratio}")])
                .stdout(std::process::Stdio::null())
                .stderr(std::process::Stdio::null())
                .status();
        }
    }
}

fn report(out: std::io::Result<std::process::Output>, name: &str, channel: &str, url: &str) {
    match out {
        Ok(output) if output.status.success() => {
            println!("Opened shotlog viewer: {name}");
            println!("Channel: {channel}");
            println!("URL: {url}");
            surface_tab(name);
        }
        Ok(output) => eprintln!("adom-cli failed: {}", String::from_utf8_lossy(&output.stderr)),
        Err(e) => eprintln!("Failed to run adom-cli: {e}"),
    }
}

/// After opening/refreshing a webview tab, make sure the user actually LANDS on
/// it: bring it to the front in its own pane (workspace active-tab - never a
/// tab move) and paint the orange update highlight. Without this, a pup->webview
/// switch left the new tab buried behind whatever was active in the panel.
fn surface_tab(name: &str) {
    let _ = Command::new("adom-cli")
        .args(["hydrogen", "workspace", "active-tab", "--name", name])
        .stdout(std::process::Stdio::null()).stderr(std::process::Stdio::null())
        .status();
    let _ = Command::new("adom-cli")
        .args(["hydrogen", "alert", "set", "--name", name, "-d", "6000"])
        .stdout(std::process::Stdio::null()).stderr(std::process::Stdio::null())
        .status();
}

/// The id of the single leaf, iff the layout is exactly one leaf (no split).
fn single_leaf_id(root: &serde_json::Value) -> Option<String> {
    if root.get("type").and_then(|t| t.as_str()) == Some("leaf") {
        return root.get("id").and_then(|id| id.as_str()).map(String::from);
    }
    None
}

/// First leaf id, preferring the second (right/bottom) child of a split.
fn find_leaf_id(root: &serde_json::Value) -> Option<String> {
    find_leaf_recursive(root)
}

fn find_leaf_recursive(val: &serde_json::Value) -> Option<String> {
    match val.get("type").and_then(|t| t.as_str()) {
        Some("leaf") => val.get("id").and_then(|id| id.as_str()).map(String::from),
        Some("split") => val
            .get("second").and_then(find_leaf_recursive)
            .or_else(|| val.get("first").and_then(find_leaf_recursive)),
        _ => None,
    }
}

/// Find the split directly containing the given leaf, and whether the leaf is its
/// "first" or "second" child.
fn find_enclosing_split(val: &serde_json::Value, leaf_id: &str) -> Option<(String, String)> {
    if val.get("type").and_then(|t| t.as_str()) != Some("split") {
        return None;
    }
    for side in ["first", "second"] {
        if let Some(child) = val.get(side) {
            if child.get("type").and_then(|t| t.as_str()) == Some("leaf")
                && child.get("id").and_then(|i| i.as_str()) == Some(leaf_id)
            {
                let sid = val.get("id").and_then(|i| i.as_str())?.to_string();
                return Some((sid, side.to_string()));
            }
            if let Some(found) = find_enclosing_split(child, leaf_id) {
                return Some(found);
            }
        }
    }
    None
}

/// Open (or add a tab to) the ONE shared shotlog pup window for a channel.
/// The full-browser canvas: real zoom, full-size images. The URL carries
/// `?surface=pup&session=shotlog`, so the broker flashes this window's taskbar
/// orange automatically on every update. `swap` also closes the channel's
/// skinny webview tab so pup becomes the channel's primary canvas.
pub fn open_pup(channel: &str, port: u16, swap: bool) {
    let proxy_uri = match std::env::var("VSCODE_PROXY_URI") {
        Ok(uri) => uri.replace("{{port}}", &port.to_string()),
        Err(_) => {
            eprintln!("ERROR: VSCODE_PROXY_URI not set - cannot build the proxy URL for pup.");
            std::process::exit(1);
        }
    };
    let url = format!("{proxy_uri}log/?surface=pup&session=shotlog&channel={channel}");

    let ad = |args: &[&str]| -> (bool, String) {
        let mut cmd = Command::new("adom-desktop");
        if let Ok(t) = std::env::var("SHOTLOG_AD_TARGET") {
            cmd.args(["--target", &t]);
        }
        match cmd.args(args).output() {
            Ok(o) => {
                let out = format!("{}{}", String::from_utf8_lossy(&o.stdout), String::from_utf8_lossy(&o.stderr));
                (o.status.success() && !out.contains("\"error\""), out)
            }
            Err(e) => (false, e.to_string()),
        }
    };

    // One shotlog pup window for ALL channels, one tab PER channel (idempotent).
    // SPEED: each `ad` call is a cloud->laptop round-trip (~1s), so branch off
    // the ONE list_tabs result instead of try-and-fallback chains:
    //   session missing -> open_window (2 trips total)
    //   tab exists      -> switch_tab   (2 trips)
    //   session, no tab -> open_tab     (2 trips)
    let _ = &channel;
    let needle = "/log/".to_string();
    // The bridge's tab REGISTRY can outlive the actual Chrome window (a closed
    // window leaves GHOST tab records; verbs then fail "browser is not
    // connected"). So: try the cheap path, but ANY failure of switch/open_tab
    // falls through to browser_open_window, which revives a dead session.
    let open_window = |ad: &dyn Fn(&[&str]) -> (bool, String)| -> bool {
        let win_json = format!("{{\"sessionId\":\"shotlog\",\"profile\":\"shotlog\",\"url\":\"{url}\"}}");
        let (mut ok, mut out) = ad(&["browser_open_window", &win_json]);
        // A COLD BRIDGE is not a failure: AD auto-starts Puppeteer+Chrome on
        // demand (10-15s, sometimes more after a reboot). The response says
        // status:"bridge_starting"/pending:true - WAIT and retry instead of
        // surrendering (this misread stranded humans on the webview fallback
        // for a whole day while pup was one warm-up away).
        let mut waited = 0u32;
        while !ok && waited < 9
            && (out.contains("bridge_starting") || out.contains("\"pending\":true") || out.contains("\"pending\": true"))
        {
            eprintln!("Hint: pup bridge is cold - Adom Desktop is auto-starting Chrome, waiting 10s (attempt {}/9)...", waited + 1);
            std::thread::sleep(std::time::Duration::from_secs(10));
            waited += 1;
            let r = ad(&["browser_open_window", &win_json]);
            ok = r.0;
            out = r.1;
        }
        ok
    };
    let (listed, list_out) = ad(&["browser_list_tabs", "{\"sessionId\":\"shotlog\"}"]);
    let mut opened = false;
    let mut via_window = false;
    if !listed || list_out.contains("session_not_found") {
        opened = open_window(&ad);
        via_window = true;
    } else {
        let existing = serde_json::from_str::<serde_json::Value>(&list_out).ok()
            .and_then(|v| v.get("tabs").and_then(|t| t.as_array()).and_then(|tabs| {
                tabs.iter().find(|t| t.get("url").and_then(|u| u.as_str()).map(|u| u.contains(&needle)).unwrap_or(false))
                    .and_then(|t| t.get("tabId").or_else(|| t.get("id")).and_then(|x| x.as_str()).map(String::from))
            }));
        if let Some(tid) = existing {
            let sw = format!("{{\"sessionId\":\"shotlog\",\"tabId\":\"{tid}\"}}");
            opened = ad(&["browser_switch_tab", &sw]).0;
        } else {
            let tab_json = format!("{{\"sessionId\":\"shotlog\",\"url\":\"{url}\"}}");
            opened = ad(&["browser_open_tab", &tab_json]).0;
        }
        if !opened {
            opened = open_window(&ad);
            via_window = true;
        }
    }
    // TRUST BUT VERIFY: the bridge's registry can outlive the real Chrome
    // window and fake tab-verb success (switch_tab "ok" into the void). A pup
    // viewer's WebSocket CONNECTING to the broker is the only real proof -
    // poll briefly; if nobody connects, force a fresh window.
    if opened && !via_window {
        let mut connected = false;
        for _ in 0..4 {
            std::thread::sleep(std::time::Duration::from_secs(2));
            if broker_has_pup_viewer(port) {
                connected = true;
                break;
            }
        }
        if !connected {
            eprintln!("Hint: pup tab verbs succeeded but no viewer connected (ghost session) - opening a fresh window.");
            opened = open_window(&ad);
        }
    }

    if !opened {
        eprintln!("ERROR: could not open the pup window (is adom-desktop connected?).");
        eprintln!("Hint: check `adom-desktop targets`; with multiple desktops set SHOTLOG_AD_TARGET=<name>.");
        std::process::exit(1);
    }
    println!("OK: pup canvas open for '{channel}' (tab in the shared 'shotlog' pup window).");
    println!("URL: {url}");
    eprintln!("Hint: the broker will flash this window's taskbar ORANGE automatically on every update (the URL registered session=shotlog). Do NOT foreground or refresh it yourself.");
    eprintln!("Hint: pup is the full-browser canvas: zooming a card now uses nearly the whole screen, so text is readable. The skinny webview is better for ambient watching; switch back any time with `shotlog open -c {channel} --skinny`.");

    if swap {
        let removed = Command::new("adom-cli")
            .args(["hydrogen", "workspace", "remove-tab", "--name", &format!("Shots: {channel}")])
            .output()
            .map(|o| o.status.success())
            .unwrap_or(false);
        if removed {
            println!("OK: closed the skinny webview tab for '{channel}' - pup is now this channel's canvas.");
        } else {
            eprintln!("Hint: no webview tab found to close for '{channel}' (already closed, or named differently).");
        }
    } else {
        eprintln!("Hint: the webview tab (if open) stays connected and will still pull forward on updates. Pass --swap to close it and make pup the ONLY canvas for this channel.");
    }
}

/// Does the broker currently see ANY pup viewer connected (the real proof a
/// pup page is alive)? Minimal HTTP GET, no external deps.
pub fn broker_has_pup_viewer(port: u16) -> bool {
    use std::io::{Read, Write};
    let Ok(mut st) = std::net::TcpStream::connect(("127.0.0.1", port)) else { return false };
    let _ = st.set_read_timeout(Some(std::time::Duration::from_secs(3)));
    let req = format!("GET /api/viewer-status?channel=_all HTTP/1.1\r\nHost: 127.0.0.1:{port}\r\nConnection: close\r\n\r\n");
    if st.write_all(req.as_bytes()).is_err() { return false; }
    let mut buf = String::new();
    let _ = st.read_to_string(&mut buf);
    buf.contains("\"pup\"")
}

/// Close this channel's tab in the shared shotlog pup window (if present).
/// Used when swapping a channel back to the webview canvas.
pub fn close_pup_tab(channel: &str) -> bool {
    let ad = |args: &[&str]| -> (bool, String) {
        let mut cmd = Command::new("adom-desktop");
        if let Ok(t) = std::env::var("SHOTLOG_AD_TARGET") {
            cmd.args(["--target", &t]);
        }
        match cmd.args(args).output() {
            Ok(o) => (o.status.success(),
                format!("{}{}", String::from_utf8_lossy(&o.stdout), String::from_utf8_lossy(&o.stderr))),
            Err(e) => (false, e.to_string()),
        }
    };
    let _ = &channel;
    let needle = "/log/".to_string();
    let (ok, out) = ad(&["browser_list_tabs", "{\"sessionId\":\"shotlog\"}"]);
    if !ok { return false; }
    let Ok(v) = serde_json::from_str::<serde_json::Value>(&out) else { return false };
    let Some(tab) = v.get("tabs").and_then(|t| t.as_array()).and_then(|tabs| {
        tabs.iter().find(|t| t.get("url").and_then(|u| u.as_str()).map(|u| u.contains(&needle)).unwrap_or(false))
    }) else { return false };
    let Some(tid) = tab.get("tabId").or_else(|| tab.get("id")).and_then(|x| x.as_str()) else { return false };
    ad(&["browser_close_tab", &format!("{{\"sessionId\":\"shotlog\",\"tabId\":\"{tid}\"}}")]).0
}

/// Switch the shared shotlog pup window's ACTIVE tab to this channel's tab
/// (no create, no window state change). The pup mirror of the webview
/// pull-to-front: a new shot should be what the user sees when they look.
pub fn switch_pup_tab(channel: &str) -> bool {
    let ad = |args: &[&str]| -> (bool, String) {
        let mut cmd = Command::new("adom-desktop");
        if let Ok(t) = std::env::var("SHOTLOG_AD_TARGET") {
            cmd.args(["--target", &t]);
        }
        match cmd.args(args).output() {
            Ok(o) => (o.status.success() && !String::from_utf8_lossy(&o.stdout).contains("\"error\""),
                String::from_utf8_lossy(&o.stdout).to_string()),
            Err(e) => (false, e.to_string()),
        }
    };
    let needle = format!("/log/{channel}/");
    let (ok, out) = ad(&["browser_list_tabs", "{\"sessionId\":\"shotlog\"}"]);
    if !ok { return false; }
    let Ok(v) = serde_json::from_str::<serde_json::Value>(&out) else { return false };
    let tabs = v.get("tabs").and_then(|t| t.as_array());
    let Some(tabs) = tabs else { return false };
    let Some(tab) = tabs.iter().find(|t| t.get("url").and_then(|u| u.as_str()).map(|u| u.contains(&needle)).unwrap_or(false)) else { return false };
    // already front?
    let tid = tab.get("tabId").or_else(|| tab.get("id")).and_then(|x| x.as_str()).unwrap_or("");
    if v.get("activeTabId").and_then(|a| a.as_str()) == Some(tid) { return true; }
    ad(&["browser_switch_tab", &format!("{{\"sessionId\":\"shotlog\",\"tabId\":\"{tid}\"}}")]).0
}