//! Help-output inspector. Output is always `TrustCeiling::Inferred`.

use indexmap::IndexMap;
use mcpact_core::{AuthorityClass, TrustCeiling};
use mcpact_manifest::{
    ArgSpec, ArgType, AuditSpec, CliSpec, EnvSpec, Manifest, OutputSpec, PackageSpec, PolicySpec,
    ToolSpec, SCHEMA_VERSION,
};
use serde::Deserialize;
use thiserror::Error;

/// Inspector error.
#[derive(Debug, Error)]
pub enum InspectError {
    /// Empty help text.
    #[error("help text is empty")]
    Empty,
    /// JSON schema parse failed.
    #[error("schema JSON parse failed: {0}")]
    Json(#[from] serde_json::Error),
}

/// Parse CLI help text into an inferred manifest draft.
pub fn inspect_help(cli_name: &str, binary: &str, help: &str) -> Result<Manifest, InspectError> {
    if help.trim().is_empty() {
        return Err(InspectError::Empty);
    }
    let args = parse_options(help);
    Ok(Manifest {
        schema_version: SCHEMA_VERSION.into(),
        package: PackageSpec {
            name: format!("mcp-{cli_name}"),
            version: "0.1.0".into(),
            description: format!("Inferred MCP adapter for {cli_name}"),
            trust: TrustCeiling::Inferred,
            signature: None,
        },
        cli: CliSpec {
            name: cli_name.into(),
            binary: binary.into(),
            version_command: vec!["--version".into()],
            default_cwd: None,
        },
        audit: AuditSpec::default(),
        tools: vec![ToolSpec {
            name: format!("{}_run_inferred", sanitize(cli_name)),
            title: Some(format!("{cli_name} inferred command")),
            description: "Draft tool inferred from help output. Review before use.".into(),
            command: Vec::new(),
            args,
            output: OutputSpec::default(),
            policy: PolicySpec {
                authority: AuthorityClass::Observe,
                read_only: false,
                network: false,
                writes_files: false,
                approval: mcpact_core::ApprovalMode::Never,
                requires_approval: false,
                timeout_seconds: 60,
            },
            env: EnvSpec::default(),
        }],
    })
}

fn parse_options(help: &str) -> IndexMap<String, ArgSpec> {
    let mut args = IndexMap::new();
    for line in help.lines() {
        let trimmed = line.trim();
        if !trimmed.starts_with('-') {
            continue;
        }
        let Some(flag) = trimmed
            .split_whitespace()
            .find(|part| part.starts_with("--"))
        else {
            continue;
        };
        let flag = flag.trim_end_matches(',').to_string();
        let key = sanitize(flag.trim_start_matches("--"));
        if key == "help" || key == "version" {
            continue;
        }
        let takes_value = trimmed.contains(" <")
            || trimmed.contains("=")
            || trimmed.contains("VALUE")
            || trimmed.contains("PATH");
        args.insert(
            key,
            ArgSpec {
                arg_type: if takes_value {
                    ArgType::String
                } else {
                    ArgType::Bool
                },
                required: false,
                description: trimmed.to_string(),
                flag: Some(flag),
                position: None,
                values: Vec::new(),
                default: None,
                multiple: false,
                must_exist: false,
                within_workspace: false,
                redact: false,
            },
        );
    }
    args
}

fn sanitize(s: &str) -> String {
    let mut out = String::new();
    for c in s.chars() {
        if c.is_ascii_alphanumeric() {
            out.push(c.to_ascii_lowercase());
        } else if c == '-' || c == '_' {
            out.push('_');
        }
    }
    while out.contains("__") {
        out = out.replace("__", "_");
    }
    out.trim_matches('_').to_string()
}

/// Minimal clap schema JSON accepted by the scaffold inspector.
///
/// Real clap exports vary; this type intentionally captures the stable subset McPact needs:
/// command name, binary, and a list of command surfaces. Unknown fields are ignored.
#[derive(Debug, Clone, Deserialize)]
pub struct ClapSchemaDraft {
    /// CLI name.
    pub name: String,
    /// Binary name/path.
    #[serde(default)]
    pub binary: Option<String>,
    /// Commands to expose as draft tools.
    #[serde(default)]
    pub commands: Vec<ClapCommandDraft>,
}

/// Draft command from a clap-style schema export.
#[derive(Debug, Clone, Deserialize)]
pub struct ClapCommandDraft {
    /// Subcommand path.
    pub name: String,
    /// Description/help.
    #[serde(default)]
    pub about: String,
    /// Fixed argv tokens.
    #[serde(default)]
    pub command: Vec<String>,
}

/// Inspect a clap-style schema JSON document into an inferred manifest.
pub fn inspect_clap_schema_json(json_text: &str) -> Result<Manifest, InspectError> {
    let draft: ClapSchemaDraft = serde_json::from_str(json_text)?;
    let binary = draft.binary.clone().unwrap_or_else(|| draft.name.clone());
    let mut manifest = inspect_help(&draft.name, &binary, "Usage: generated-from-clap-schema")?;
    manifest.package.description =
        format!("Inferred MCP adapter for {} from clap schema", draft.name);
    manifest.tools.clear();
    for command in draft.commands {
        manifest.tools.push(ToolSpec {
            name: format!("{}_{}", sanitize(&draft.name), sanitize(&command.name)),
            title: Some(command.name.clone()),
            description: if command.about.is_empty() {
                "Draft tool inferred from clap schema. Review before use.".into()
            } else {
                command.about
            },
            command: if command.command.is_empty() {
                command
                    .name
                    .split_whitespace()
                    .map(str::to_string)
                    .collect()
            } else {
                command.command
            },
            args: IndexMap::new(),
            output: OutputSpec::default(),
            policy: PolicySpec {
                authority: AuthorityClass::Observe,
                read_only: false,
                network: false,
                writes_files: false,
                approval: mcpact_core::ApprovalMode::Never,
                requires_approval: false,
                timeout_seconds: 60,
            },
            env: EnvSpec::default(),
        });
    }
    if manifest.tools.is_empty() {
        manifest.tools.push(ToolSpec {
            name: format!("{}_run_inferred", sanitize(&draft.name)),
            title: Some(format!("{} inferred command", draft.name)),
            description: "Draft tool inferred from clap schema. Review before use.".into(),
            command: Vec::new(),
            args: IndexMap::new(),
            output: OutputSpec::default(),
            policy: PolicySpec::default(),
            env: EnvSpec::default(),
        });
    }
    Ok(manifest)
}

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

    #[test]
    fn parses_help_options_as_inferred_manifest() {
        let help = r#"
Usage: taudit scan [OPTIONS] <PATH>
  --format <FORMAT>  Output format
  --quiet            Suppress output
"#;
        let manifest = inspect_help("taudit", "taudit", help).unwrap();
        assert_eq!(manifest.package.trust, TrustCeiling::Inferred);
        assert!(manifest.tools[0].args.contains_key("format"));
        assert!(manifest.tools[0].args.contains_key("quiet"));
    }

    #[test]
    fn parses_clap_schema_as_inferred_manifest() {
        let json = r#"{"name":"cargo","commands":[{"name":"test","about":"run tests"}]}"#;
        let manifest = inspect_clap_schema_json(json).unwrap();
        assert_eq!(manifest.package.trust, TrustCeiling::Inferred);
        assert_eq!(manifest.tools[0].name, "cargo_test");
    }

    #[test]
    fn parses_workspace_help_fixture() {
        let path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
            .join("../../fixtures/help/taudit-scan.txt");
        let help = std::fs::read_to_string(path).unwrap();
        let manifest = inspect_help("taudit", "taudit", &help).unwrap();
        assert!(manifest.tools[0].args.contains_key("format"));
        assert!(manifest.tools[0].args.contains_key("quiet"));
    }
}
