//! Hydrogen webview tab lifecycle helpers.
//!
//! Every webapp-backed subcommand wants the same three things on
//! startup/shutdown: pick a good target pane (NEVER the VS Code
//! pane — see the rule in the adom-workspace-control skill), spawn
//! a webview tab pointed at our local HTTP server, and (on shutdown)
//! remove the tab by name.
//!
//! The pane-picking rule: walk the workspace layout tree, find every
//! leaf, filter to leaves whose tabs do NOT include a VS Code tab
//! (panelType `adom/a1b2c3d4-eeee-4000-a000-00000000000e`), and pick
//! one of those. Only fall back to the currently-focused pane when
//! every pane has VS Code in it (single-pane layout). Never ever
//! target the VS Code pane — it hosts the Claude Code chat, and
//! stacking a webview over it hides the user's primary interaction
//! surface. This bug was shipped for everyone before the rule was
//! documented; don't regress it.

use std::process::Command;

/// Canonical VS Code panelType. Any tab carrying this type is a
/// VS Code / code-server instance hosting the Claude Code chat.
const VSCODE_PANEL_TYPE: &str = "adom/a1b2c3d4-eeee-4000-a000-00000000000e";

/// Open a Hydrogen webview tab pointed at `url` with the given
/// display name and MDI icon. Uses `panel_id_override` if set,
/// otherwise picks a target pane that does NOT contain VS Code.
///
/// If a tab with `name` already exists (from a prior session, a
/// crashed instance, or a leftover from the legacy Python pipeline),
/// it is removed first so the new tab can land cleanly. This makes
/// every restart self-healing — the user doesn't have to manually
/// close stale tabs to recover.
pub fn open(
    name: &str,
    url: &str,
    display_icon: &str,
    panel_id_override: Option<&str>,
) -> Result<(), String> {
    // Remove any pre-existing tab with the same name. The remove call
    // returns 404 if no such tab exists, which we ignore — both "no
    // stale tab" and "successfully removed stale tab" are good
    // outcomes here.
    let _ = Command::new("adom-cli")
        .args(["hydrogen", "workspace", "remove-tab", "--name", name])
        .output();

    let panel_id = match panel_id_override {
        Some(p) => p.to_string(),
        None => pick_non_vscode_panel_id()?,
    };

    let initial_state = format!(r#"{{"url":"{}"}}"#, url);
    let result = Command::new("adom-cli")
        .args([
            "hydrogen",
            "workspace",
            "add-tab",
            "--panel-id",
            &panel_id,
            "--panel-type",
            "adom/a1b2c3d4-0031-4000-a000-000000000031",
            "--display-name",
            name,
            "--display-icon",
            display_icon,
            "--initial-state",
            &initial_state,
        ])
        .output()
        .map_err(|e| format!("adom-cli spawn failed: {}", e))?;
    if !result.status.success() {
        return Err(format!(
            "failed to add webview tab: {}",
            String::from_utf8_lossy(&result.stderr)
        ));
    }
    // NB: intentionally NOT calling `adom-cli hydrogen workspace
    // active-tab --name <same name>` here. Doing that right after
    // add-tab silently wipes the displayIcon out of the just-added
    // tab's panelState (verified live on 2026-04-15). add-tab
    // already activates the new tab by default (its own _hint
    // output says "Tab ... created and active"), so the follow-up
    // call is redundant anyway. This is a latent bug in adom-cli
    // or Hydrogen that should be filed separately.
    Ok(())
}

/// Remove the webview tab with the given display name.
pub fn remove(name: &str) -> Result<(), String> {
    let result = Command::new("adom-cli")
        .args(["hydrogen", "workspace", "remove-tab", "--name", name])
        .output()
        .map_err(|e| format!("adom-cli spawn failed: {}", e))?;
    if !result.status.success() {
        return Err(format!(
            "failed to remove webview tab: {}",
            String::from_utf8_lossy(&result.stderr)
        ));
    }
    Ok(())
}

/// Resolve a server port to the correct URL for the Hydrogen webview
/// to fetch. Inside Coder/Gallia containers the webview iframe is
/// same-origin with hydrogen.adom.inc and can't load `http://127.0.0.1:...`
/// directly — `VSCODE_PROXY_URI` is set to a template like
/// `https://<slug>.adom.cloud/proxy/{{port}}/` which the webview CAN
/// reach. Falls back to the loopback URL when the env var is unset.
pub fn resolve_url(port: u16) -> String {
    match std::env::var("VSCODE_PROXY_URI") {
        Ok(template) => template.replace("{{port}}", &port.to_string()),
        Err(_) => format!("http://127.0.0.1:{}/", port),
    }
}

/// Query the workspace layout and pick a target pane that does
/// NOT contain a VS Code tab. Preference order:
///   1. The currently-focused pane IF it is not VS Code.
///   2. The first non-VS-Code pane in tree order.
///   3. The focused pane (fallback, single-pane layout — caller
///      will land a tab on top of VS Code and should hopefully
///      have already warned the user).
///
/// See the adom-workspace-control skill, "NEVER place a new tab on
/// the same pane that hosts VS Code" for the reasoning.
pub fn pick_non_vscode_panel_id() -> Result<String, String> {
    let result = Command::new("adom-cli")
        .args(["hydrogen", "workspace", "get"])
        .output()
        .map_err(|e| format!("adom-cli spawn failed: {}", e))?;
    if !result.status.success() {
        return Err(format!(
            "failed to query workspace: {}",
            String::from_utf8_lossy(&result.stderr)
        ));
    }
    let body = String::from_utf8_lossy(&result.stdout);
    let parsed: serde_json::Value =
        serde_json::from_str(&body).map_err(|e| format!("failed to parse workspace JSON: {}", e))?;

    let focused = parsed
        .get("focusedPanelId")
        .and_then(|v| v.as_str())
        .map(|s| s.to_string());

    let root = parsed
        .get("root")
        .ok_or_else(|| "workspace response has no root".to_string())?;
    let mut leaves: Vec<&serde_json::Value> = Vec::new();
    collect_leaves(root, &mut leaves);

    // Filter out any leaf that has a VS Code tab.
    let non_vscode: Vec<&serde_json::Value> = leaves
        .iter()
        .copied()
        .filter(|leaf| !leaf_contains_vscode(leaf))
        .collect();

    if non_vscode.is_empty() {
        // Single-pane layout — we have no safe choice. Log a warning
        // and return the focused pane so the caller at least doesn't
        // fail outright.
        eprintln!(
            "warning: every workspace pane contains a VS Code tab; falling back to focused pane. \
             The new webview will land on top of VS Code. Split a pane first to avoid this."
        );
        return focused.ok_or_else(|| "no focusedPanelId in workspace response".to_string());
    }

    // Prefer the focused pane IF it is itself non-VS-Code.
    if let Some(f) = &focused {
        for leaf in &non_vscode {
            if leaf.get("id").and_then(|v| v.as_str()) == Some(f.as_str()) {
                return Ok(f.clone());
            }
        }
    }
    // Otherwise take the first non-VS-Code pane in tree order.
    non_vscode[0]
        .get("id")
        .and_then(|v| v.as_str())
        .map(|s| s.to_string())
        .ok_or_else(|| "non-VS-Code leaf had no id".to_string())
}

/// Recursive helper: walk the workspace tree and append every leaf
/// (node with a `tabs` array) to `out`.
fn collect_leaves<'a>(node: &'a serde_json::Value, out: &mut Vec<&'a serde_json::Value>) {
    if node.get("tabs").is_some() {
        out.push(node);
        return;
    }
    if let Some(first) = node.get("first") {
        collect_leaves(first, out);
    }
    if let Some(second) = node.get("second") {
        collect_leaves(second, out);
    }
}

/// Return true if any tab in the leaf carries the VS Code panelType.
fn leaf_contains_vscode(leaf: &serde_json::Value) -> bool {
    leaf.get("tabs")
        .and_then(|t| t.as_array())
        .map(|tabs| {
            tabs.iter().any(|tab| {
                tab.get("panelType").and_then(|v| v.as_str()) == Some(VSCODE_PANEL_TYPE)
            })
        })
        .unwrap_or(false)
}