//! Handler for `mcptest workbench` (WOR-1429).
//!
//! Parse whatever artifacts the user passed, optionally capture a live target
//! (only after explicit `--allow-stdio` consent), gather them into the
//! workbench model, and write one self-contained HTML page. No network, no
//! account, no telemetry.

use std::collections::BTreeMap;
use std::path::Path;

use anyhow::{Context, Result};
use serde_json::{json, Value};

use mcptest_core::connector::connect_server;
use mcptest_core::suite::ServerSpec;
use mcptest_core::workbench::{build_workbench, render_html, WorkbenchInputs};

use crate::cli::args::WorkbenchArgs;
use crate::cli::exit_codes::{EXIT_INVALID_INVOCATION, EXIT_OK};

/// Build the workbench and write it.
pub(crate) fn workbench_command(args: &WorkbenchArgs) -> Result<i32> {
    let mut inputs = WorkbenchInputs {
        snapshot: read_opt(args.snapshot.as_deref())?,
        cassette: read_opt(args.cassette.as_deref())?,
        report: read_opt(args.report.as_deref())?,
        security: read_opt(args.security.as_deref())?,
        evidence: read_opt(args.evidence.as_deref())?,
    };

    let (source_label, live) = match &args.target {
        Some(command) => {
            // The arg parser already requires --allow-stdio alongside --target;
            // this is the defensive second check before spawning a process.
            if !args.allow_stdio {
                eprintln!("mcptest workbench: --target requires --allow-stdio consent");
                return Ok(EXIT_INVALID_INVOCATION);
            }
            eprintln!("launching live target (consented): {command}");
            inputs.snapshot = Some(capture_live(command)?);
            (command.clone(), true)
        }
        None => {
            if no_artifacts(args) {
                eprintln!(
                    "mcptest workbench: pass at least one artifact \
                     (--snapshot/--cassette/--report/--security/--evidence) or --target"
                );
                return Ok(EXIT_INVALID_INVOCATION);
            }
            (artifact_label(args), false)
        }
    };

    let data = build_workbench(&inputs, &source_label, live);
    let html = render_html(&data);
    super::overwrite::ensure_overwritable(&args.out, args.force)?;
    std::fs::write(&args.out, &html).with_context(|| format!("writing {}", args.out.display()))?;
    eprintln!("wrote the workbench to {}", args.out.display());

    if args.open {
        // Best-effort: a failure to open is not a command failure.
        if let Err(e) = webbrowser::open(&args.out.to_string_lossy()) {
            eprintln!("could not open a browser: {e}");
        }
    }
    Ok(EXIT_OK)
}

/// Whether no artifact inputs were provided.
fn no_artifacts(args: &WorkbenchArgs) -> bool {
    args.snapshot.is_none()
        && args.cassette.is_none()
        && args.report.is_none()
        && args.security.is_none()
        && args.evidence.is_none()
}

/// A label naming the artifacts opened.
fn artifact_label(args: &WorkbenchArgs) -> String {
    let mut parts = Vec::new();
    for (flag, present) in [
        ("snapshot", args.snapshot.is_some()),
        ("cassette", args.cassette.is_some()),
        ("report", args.report.is_some()),
        ("security", args.security.is_some()),
        ("evidence", args.evidence.is_some()),
    ] {
        if present {
            parts.push(flag);
        }
    }
    parts.join(", ")
}

/// Read and parse an optional JSON artifact.
fn read_opt(path: Option<&Path>) -> Result<Option<Value>> {
    match path {
        None => Ok(None),
        Some(p) => {
            let text =
                std::fs::read_to_string(p).with_context(|| format!("reading {}", p.display()))?;
            let value = serde_json::from_str(&text)
                .with_context(|| format!("parsing {} as JSON", p.display()))?;
            Ok(Some(value))
        }
    }
}

/// Capture a live target's tools, resources, and prompts into a snapshot value.
fn capture_live(command: &str) -> Result<Value> {
    let mut parts = command.split_whitespace().map(str::to_string);
    let program = parts.next().context("--target command is empty")?;
    let spec = ServerSpec::Stdio {
        command: program,
        args: parts.collect(),
        env: BTreeMap::new(),
    };
    let runtime = tokio::runtime::Builder::new_multi_thread()
        .worker_threads(2)
        .enable_all()
        .build()
        .context("failed to build tokio runtime")?;
    runtime.block_on(capture_async(&spec))
}

/// The async half of [`capture_live`]: connect, list, shut down.
async fn capture_async(spec: &ServerSpec) -> Result<Value> {
    let live = connect_server(
        "workbench",
        spec,
        "mcptest",
        mcptest_core::version::semver(),
        &mcptest_core::network::ProxyConfig::default(),
        &[],
    )
    .await
    .context("connecting to the target failed")?;

    // Request each list method; any error leaves that pane empty rather than
    // failing the whole capture.
    let tools = list_array(&live, "tools/list", "tools").await;
    let resources = list_array(&live, "resources/list", "resources").await;
    let prompts = list_array(&live, "prompts/list", "prompts").await;
    let _ = live.shutdown.shutdown().await;

    Ok(json!({ "tools": tools, "resources": resources, "prompts": prompts }))
}

/// Request a list method on a live server and pull out its array, or an empty
/// array on any error.
async fn list_array(live: &mcptest_core::connector::LiveServer, method: &str, key: &str) -> Value {
    match live.client.request(method, json!({})).await {
        Ok(value) => value.get(key).cloned().unwrap_or_else(|| json!([])),
        Err(_) => json!([]),
    }
}
