//! Handler for `mcptest record` (WOR-1264): the stdio proxy recorder.
//!
//! Lowers the clap struct into [`mcptest_core::proxy::ProxyOptions`] and
//! drives [`mcptest_core::proxy::run_stdio_front`] over the process's real
//! stdin/stdout. Everything human-facing goes to stderr so stdout stays
//! clean NDJSON framing for the MCP client on the other side.

use std::path::PathBuf;

use anyhow::{anyhow, bail, Result};
use mcptest_core::proxy::{run_stdio_front, LiveTarget, ProxyOptions};
use tokio::io::BufReader;

use crate::cli::args::RecordArgs;
use crate::cli::GlobalArgs;

/// Handle `mcptest record --target <COMMAND|URL> [--output FILE]`.
pub(crate) fn record_command(global: &GlobalArgs, args: &RecordArgs) -> Result<i32> {
    let target = parse_target(&args.target)?;
    if args.front != "stdio" {
        bail!(
            "--front {} is not available; the HTTP front is tracked by WOR-1349. \
             Use the stdio front, which also records traffic to an HTTP --target.",
            args.front
        );
    }
    let output = absolute_output(&args.output)?;
    if !global.quiet {
        print_startup(&args.target, &output);
    }
    let opts = ProxyOptions {
        target,
        output,
        proxy: global.proxy_config(),
    };
    let rt = tokio::runtime::Builder::new_current_thread()
        .enable_all()
        .build()?;
    let summary = rt.block_on(run_stdio_front(
        &opts,
        BufReader::new(tokio::io::stdin()),
        tokio::io::stdout(),
    ))?;
    if !global.quiet {
        eprintln!(
            "mcptest record: {} exchange(s) on tape at {}",
            summary.exchanges,
            summary.path.display()
        );
    }
    Ok(0)
}

/// Decide whether `--target` is a streamable-http URL or a stdio command
/// line. A scheme prefix wins; everything else is shell-split into argv so
/// quoting behaves like `--server-command`.
fn parse_target(raw: &str) -> Result<LiveTarget> {
    let trimmed = raw.trim();
    if trimmed.is_empty() {
        bail!("--target is empty; pass a server command line or an http(s):// URL");
    }
    if trimmed.starts_with("http://") || trimmed.starts_with("https://") {
        return Ok(LiveTarget::Url(trimmed.to_string()));
    }
    let argv = shell_words::split(trimmed)
        .map_err(|e| anyhow!("could not parse --target as a command line: {e}"))?;
    if argv.is_empty() {
        bail!("--target is empty; pass a server command line or an http(s):// URL");
    }
    Ok(LiveTarget::Command(argv))
}

/// Render `--output` absolute so the printed client snippet works from any
/// working directory the client happens to spawn the proxy in.
fn absolute_output(output: &std::path::Path) -> Result<PathBuf> {
    if output.is_absolute() {
        return Ok(output.to_path_buf());
    }
    Ok(std::env::current_dir()?.join(output))
}

/// Startup banner on stderr: where the tape goes, a ready-to-paste
/// `mcpServers` entry for `.mcp.json` / `.cursor/mcp.json`, and the distill
/// handoff, so the record-to-test loop needs no doc lookup.
fn print_startup(target_raw: &str, output: &std::path::Path) {
    let output_str = output.display().to_string();
    let snippet = serde_json::json!({
        "mcpServers": {
            "recorded": {
                "command": "mcptest",
                "args": ["record", "--target", target_raw, "--output", output_str],
            }
        }
    });
    eprintln!("mcptest record: proxying stdio to `{target_raw}`");
    eprintln!("mcptest record: writing {output_str} after each exchange");
    eprintln!();
    eprintln!("Point your MCP client (.mcp.json, .cursor/mcp.json) at the proxy:");
    // The snippet always serializes (literal keys, string values); an empty
    // fallback would only ever hide a serde_json bug.
    match serde_json::to_string_pretty(&snippet) {
        Ok(rendered) => eprintln!("{rendered}"),
        Err(err) => eprintln!("(snippet unavailable: {err})"),
    }
    eprintln!();
    eprintln!("When you are done, turn the tape into a suite:");
    eprintln!("  mcptest distill {output_str}");
}

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

    #[test]
    fn record_front_http_is_rejected_with_followup_pointer() {
        let args = RecordArgs {
            target: "node server.js".to_string(),
            output: PathBuf::from("tape.json"),
            front: "http".to_string(),
        };
        let err = record_command(&GlobalArgs::default(), &args).unwrap_err();
        let message = err.to_string();
        assert!(message.contains("WOR-1349"), "{message}");
        assert!(message.contains("stdio front"), "{message}");
    }

    #[test]
    fn target_with_scheme_parses_as_url_and_command_shell_splits() {
        assert_eq!(
            parse_target("https://mcp.example.com/v1").unwrap(),
            LiveTarget::Url("https://mcp.example.com/v1".to_string())
        );
        assert_eq!(
            parse_target("node 'my server.js' --port 3000").unwrap(),
            LiveTarget::Command(vec![
                "node".to_string(),
                "my server.js".to_string(),
                "--port".to_string(),
                "3000".to_string(),
            ])
        );
    }

    #[test]
    fn empty_target_is_rejected_with_a_clear_message() {
        let err = parse_target("   ").unwrap_err();
        assert!(err.to_string().contains("--target is empty"), "{err}");
    }
}
