//! Handler for `mcptest ecosystem` (WOR-1427).
//!
//! Thin glue: read the source files, hand the bytes to the offline corpus
//! builder in `mcptest-core`, and write the deterministic manifest. The
//! `select` subcommand reloads a manifest and carves a fixture pack. No
//! network, no telemetry.

use std::path::Path;

use anyhow::{Context, Result};

use mcptest_core::ecosystem::{
    build_corpus, select, EcosystemCorpus, ImportInput, ProjectRole, SelectFilter, SourceKind,
    TransportKind,
};

use crate::cli::args::{
    EcosystemArgs, EcosystemCommand, EcosystemImportArgs, EcosystemSelectArgs, RoleFilter,
    TransportFilter,
};
use crate::cli::exit_codes::{EXIT_INVALID_INVOCATION, EXIT_OK};

/// Route `ecosystem` to its subcommand.
pub(crate) fn ecosystem_command(args: EcosystemArgs) -> Result<i32> {
    match args.command {
        EcosystemCommand::Import(import) => import_corpus(import),
        EcosystemCommand::Select(sel) => select_pack(sel),
    }
}

/// Build a corpus manifest from the named sources.
fn import_corpus(args: EcosystemImportArgs) -> Result<i32> {
    let mut inputs: Vec<ImportInput> = Vec::new();
    collect(&mut inputs, &args.mcpcorpus, SourceKind::McpCorpus)?;
    collect(&mut inputs, &args.registry, SourceKind::Registry)?;
    collect(&mut inputs, &args.repos, SourceKind::Repos)?;

    if inputs.is_empty() {
        eprintln!(
            "mcptest ecosystem import: pass at least one of --mcpcorpus, --registry, --repos"
        );
        return Ok(EXIT_INVALID_INVOCATION);
    }

    let corpus = build_corpus(&inputs).context("building the ecosystem corpus")?;
    let json = serde_json::to_string_pretty(&corpus)?;
    write_out(args.out.as_deref(), &json)?;

    eprintln!(
        "imported {} entries from {} source(s); {} skipped",
        corpus.entries.len(),
        corpus.sources.len(),
        corpus.skipped.len()
    );
    Ok(EXIT_OK)
}

/// Read each path and push it as an [`ImportInput`] of the given kind.
fn collect(
    inputs: &mut Vec<ImportInput>,
    paths: &[std::path::PathBuf],
    kind: SourceKind,
) -> Result<()> {
    for path in paths {
        let bytes =
            std::fs::read(path).with_context(|| format!("reading source {}", path.display()))?;
        inputs.push(ImportInput {
            kind,
            source_ref: path.display().to_string(),
            bytes,
        });
    }
    Ok(())
}

/// Carve a fixture pack out of a saved corpus manifest.
fn select_pack(args: EcosystemSelectArgs) -> Result<i32> {
    let raw = std::fs::read_to_string(&args.corpus)
        .with_context(|| format!("reading corpus {}", args.corpus.display()))?;
    let corpus: EcosystemCorpus = serde_json::from_str(&raw)
        .with_context(|| format!("parsing corpus {}", args.corpus.display()))?;

    let filter = SelectFilter {
        transport: args.transport.map(|t| match t {
            TransportFilter::Stdio => TransportKind::Stdio,
            TransportFilter::Http => TransportKind::Http,
            TransportFilter::Sse => TransportKind::Sse,
        }),
        language: args.language,
        role: args.role.map(|r| match r {
            RoleFilter::Server => ProjectRole::Server,
            RoleFilter::Client => ProjectRole::Client,
        }),
        category: args.category,
        min_tools: args.min_tools,
        maintained_only: args.maintained,
        fixture_ready_only: args.fixture_ready,
    };

    let pack = select(&corpus, &filter, args.limit);
    let json = serde_json::to_string_pretty(&pack)?;
    write_out(args.out.as_deref(), &json)?;

    eprintln!(
        "selected {} of {} matching entries ({})",
        pack.entries.len(),
        pack.matched,
        pack.filter_summary
    );
    Ok(EXIT_OK)
}

/// Write JSON to a file, or stdout when no path is given.
fn write_out(out: Option<&Path>, json: &str) -> Result<()> {
    match out {
        Some(path) => {
            std::fs::write(path, format!("{json}\n"))
                .with_context(|| format!("writing {}", path.display()))?;
        }
        None => println!("{json}"),
    }
    Ok(())
}
