//! Handler for `mcptest interop` (WOR-1431).
//!
//! Reuses `benchmark_import` to render runnable suites and `interop` for the
//! export and starter-dataset directions. Lossy conversions print warnings to
//! stderr so a dropped construct is never silent.

use std::path::Path;

use anyhow::{Context, Result};

use mcptest_core::benchmark_import::{
    import_tasks, to_suite_yaml, BenchmarkKind, ImportManifest, ImportedTask,
    IMPORT_TRANSFORM_VERSION,
};
use mcptest_core::interop::{export_suite, from_registry, from_tools_list, unsupported_warnings};

use crate::cli::args::{
    InteropArgs, InteropCommand, InteropDatasetArgs, InteropExportArgs, InteropImportArgs,
    InteropSource,
};
use crate::cli::exit_codes::{EXIT_INVALID_INVOCATION, EXIT_OK};

/// Route `interop` to its subcommand.
pub(crate) fn interop_command(args: InteropArgs) -> Result<i32> {
    match args.command {
        InteropCommand::Import(import) => import_suite(import),
        InteropCommand::Export(export) => export_dataset(export),
        InteropCommand::Dataset(dataset) => starter_dataset(dataset),
    }
}

/// Convert a competitor artifact into a runnable mcptest suite.
fn import_suite(args: InteropImportArgs) -> Result<i32> {
    let doc = read_doc(&args.file)?;
    let kind = match args.from {
        InteropSource::Promptfoo => BenchmarkKind::Promptfoo,
        InteropSource::DeepEval => BenchmarkKind::DeepEval,
        InteropSource::McpEval => BenchmarkKind::McpEval,
    };
    let (tasks, skipped) = import_tasks(&doc, kind);
    if tasks.is_empty() {
        eprintln!(
            "mcptest interop import: no importable cases in {} ({} skipped); check --from",
            args.file.display(),
            skipped.len()
        );
        return Ok(EXIT_INVALID_INVOCATION);
    }

    for warning in unsupported_warnings(&doc, kind) {
        eprintln!("warning: {warning}");
    }
    for skip in &skipped {
        eprintln!("warning: skipped {} ({})", skip.id, skip.reason);
    }

    let servers = servers_or_default(args.servers);
    let manifest = ImportManifest {
        source: source_label(args.from),
        source_url: None,
        license: None,
        transform_version: IMPORT_TRANSFORM_VERSION.to_string(),
        imported: tasks.len(),
        skipped,
    };
    let yaml = to_suite_yaml(&tasks, &manifest, &servers, &args.model)
        .context("rendering the imported suite")?;
    write_out(args.out.as_deref(), &yaml, args.force)?;
    eprintln!(
        "imported {} case(s) from {}",
        tasks.len(),
        source_label(args.from)
    );
    Ok(EXIT_OK)
}

/// Export a mcptest suite into a portable dataset.
fn export_dataset(args: InteropExportArgs) -> Result<i32> {
    let raw = std::fs::read_to_string(&args.suite)
        .with_context(|| format!("reading suite {}", args.suite.display()))?;
    let suite: serde_yaml::Value = serde_yaml::from_str(&raw)
        .with_context(|| format!("parsing suite {}", args.suite.display()))?;
    let (dataset, skipped) = export_suite(&suite, &args.suite.display().to_string());
    if dataset.cases.is_empty() {
        eprintln!(
            "mcptest interop export: {} has no agents to export",
            args.suite.display()
        );
        return Ok(EXIT_INVALID_INVOCATION);
    }
    for name in &skipped {
        eprintln!("warning: skipped agent {name} (no prompt)");
    }
    let json = serde_json::to_string_pretty(&dataset)?;
    write_out(args.out.as_deref(), &format!("{json}\n"), args.force)?;
    eprintln!("exported {} case(s)", dataset.cases.len());
    Ok(EXIT_OK)
}

/// Build a starter suite from a tools/list snapshot or a registry export.
fn starter_dataset(args: InteropDatasetArgs) -> Result<i32> {
    let (tasks, source): (Vec<ImportedTask>, &str) = match (&args.tools_list, &args.registry) {
        (Some(path), _) => (from_tools_list(&read_doc(path)?), "tools/list snapshot"),
        (None, Some(path)) => (from_registry(&read_doc(path)?), "registry export"),
        (None, None) => {
            eprintln!("mcptest interop dataset: pass --tools-list <FILE> or --registry <FILE>");
            return Ok(EXIT_INVALID_INVOCATION);
        }
    };
    if tasks.is_empty() {
        eprintln!("mcptest interop dataset: no usable entries in the {source}");
        return Ok(EXIT_INVALID_INVOCATION);
    }
    let servers = servers_or_default(args.servers);
    let manifest = ImportManifest {
        source: source.to_string(),
        source_url: None,
        license: None,
        transform_version: IMPORT_TRANSFORM_VERSION.to_string(),
        imported: tasks.len(),
        skipped: Vec::new(),
    };
    let yaml = to_suite_yaml(&tasks, &manifest, &servers, &args.model)
        .context("rendering the starter suite")?;
    write_out(args.out.as_deref(), &yaml, args.force)?;
    eprintln!(
        "built a starter suite with {} case(s) from the {source}",
        tasks.len()
    );
    Ok(EXIT_OK)
}

/// Read a JSON-or-YAML document into a `serde_json::Value`.
fn read_doc(path: &Path) -> Result<serde_json::Value> {
    let text =
        std::fs::read_to_string(path).with_context(|| format!("reading {}", path.display()))?;
    if let Ok(v) = serde_json::from_str::<serde_json::Value>(&text) {
        return Ok(v);
    }
    serde_yaml::from_str::<serde_json::Value>(&text)
        .with_context(|| format!("parsing {} as JSON or YAML", path.display()))
}

/// Use the given servers or fall back to a single placeholder.
fn servers_or_default(servers: Vec<String>) -> Vec<String> {
    if servers.is_empty() {
        vec!["server".to_string()]
    } else {
        servers
    }
}

/// The provenance label for a source ecosystem.
fn source_label(from: InteropSource) -> String {
    match from {
        InteropSource::Promptfoo => "promptfoo",
        InteropSource::DeepEval => "deepeval",
        InteropSource::McpEval => "mcp-eval",
    }
    .to_string()
}

/// Write text to a file, or stdout when no path is given. Refuses to overwrite
/// an existing file unless `force` is set, so a re-run cannot silently clobber a
/// tracked or hand-edited suite.
fn write_out(out: Option<&Path>, body: &str, force: bool) -> Result<()> {
    match out {
        Some(path) => {
            super::overwrite::ensure_overwritable(path, force)?;
            std::fs::write(path, body).with_context(|| format!("writing {}", path.display()))?;
        }
        None => print!("{body}"),
    }
    Ok(())
}
