use runx_contracts::tools::{ToolBuildStatus, ToolCatalogSearchReport, ToolCatalogSearchResult};

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ToolSearchOptions {
    pub query: String,
    pub source: Option<String>,
    pub limit: usize,
    pub fixture_catalog_enabled: bool,
}

pub fn search_tools(options: &ToolSearchOptions) -> ToolCatalogSearchReport {
    let source = options.source.as_deref().unwrap_or("all").to_owned();
    let normalized_source = options
        .source
        .as_deref()
        .map(|source| source.trim().to_ascii_lowercase());
    let mut results = Vec::new();

    if fixture_catalog_allowed(
        options.fixture_catalog_enabled,
        normalized_source.as_deref(),
    ) {
        let query = options.query.trim().to_ascii_lowercase();
        for fixture in fixture_tools() {
            let result = fixture.search_result();
            if query.is_empty() || searchable_text(&result).contains(&query) {
                results.push(result);
            }
            if results.len() >= options.limit {
                break;
            }
        }
    }

    ToolCatalogSearchReport {
        status: ToolBuildStatus::Success,
        query: options.query.clone(),
        source,
        results,
    }
}

pub(crate) fn fixture_catalog_allowed(enabled: bool, source: Option<&str>) -> bool {
    enabled
        && matches!(
            source,
            None | Some("") | Some("catalog") | Some("fixture-mcp")
        )
}

pub(crate) fn fixture_tool(ref_or_name: &str) -> Option<FixtureTool> {
    let normalized = ref_or_name.trim().to_ascii_lowercase();
    fixture_tools().into_iter().find(|tool| {
        [
            tool.qualified_name(),
            tool.tool_id(),
            tool.catalog_ref(),
            format!("{}:{}", tool.source, tool.qualified_name()),
            tool.external_name.to_owned(),
        ]
        .into_iter()
        .any(|candidate| candidate.to_ascii_lowercase() == normalized)
    })
}

pub(crate) fn fixture_tools() -> Vec<FixtureTool> {
    [
        echo_fixture_tool(),
        fail_fixture_tool(),
        sleep_fixture_tool(),
        env_fixture_tool(),
    ]
    .to_vec()
}

fn echo_fixture_tool() -> FixtureTool {
    fixture_tool_with_inputs(
        "echo",
        Some("Echo a message through the fixture MCP server."),
        vec![FixtureInput {
            name: "message",
            input_type: "string",
            required: true,
            description: Some("Message to echo."),
        }],
    )
}

fn fail_fixture_tool() -> FixtureTool {
    fixture_tool_with_inputs(
        "fail",
        Some("Return a fixture MCP error for testing."),
        vec![FixtureInput {
            name: "message",
            input_type: "string",
            required: false,
            description: None,
        }],
    )
}

fn sleep_fixture_tool() -> FixtureTool {
    fixture_tool_with_inputs(
        "sleep",
        Some("Never respond, for timeout testing."),
        Vec::new(),
    )
}

fn env_fixture_tool() -> FixtureTool {
    fixture_tool_with_inputs(
        "env",
        Some("Return a single fixture server environment variable."),
        vec![FixtureInput {
            name: "name",
            input_type: "string",
            required: true,
            description: None,
        }],
    )
}

fn fixture_tool_with_inputs(
    name: &'static str,
    description: Option<&'static str>,
    inputs: Vec<FixtureInput>,
) -> FixtureTool {
    FixtureTool {
        name,
        description,
        source: "fixture-mcp",
        source_label: "Fixture MCP Catalog",
        source_type: "mcp",
        namespace: "fixture",
        external_name: name,
        tags: vec!["fixture", "mcp"],
        inputs,
    }
}

fn searchable_text(result: &ToolCatalogSearchResult) -> String {
    [
        result.tool_id.as_str(),
        result.name.as_str(),
        result.summary.as_deref().unwrap_or(""),
        result.source.as_str(),
        result.source_label.as_str(),
        result.source_type.as_str(),
        result.namespace.as_str(),
        result.external_name.as_str(),
        result.catalog_ref.as_str(),
        &result.tags.join(" "),
    ]
    .join(" ")
    .to_ascii_lowercase()
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct FixtureTool {
    pub name: &'static str,
    pub description: Option<&'static str>,
    pub source: &'static str,
    pub source_label: &'static str,
    pub source_type: &'static str,
    pub namespace: &'static str,
    pub external_name: &'static str,
    pub tags: Vec<&'static str>,
    pub inputs: Vec<FixtureInput>,
}

impl FixtureTool {
    pub(crate) fn qualified_name(&self) -> String {
        format!("{}.{}", self.namespace, self.name)
    }

    pub(crate) fn tool_id(&self) -> String {
        format!("{}/{}", self.source, self.qualified_name())
    }

    pub(crate) fn catalog_ref(&self) -> String {
        format!("{}:{}", self.source, self.qualified_name())
    }

    fn search_result(&self) -> ToolCatalogSearchResult {
        ToolCatalogSearchResult {
            tool_id: self.tool_id(),
            name: self.qualified_name(),
            summary: self.description.map(str::to_owned),
            source: self.source.to_owned(),
            source_label: self.source_label.to_owned(),
            source_type: self.source_type.to_owned(),
            namespace: self.namespace.to_owned(),
            external_name: self.external_name.to_owned(),
            required_scopes: vec![self.qualified_name()],
            tags: self.tags.iter().map(|tag| (*tag).to_owned()).collect(),
            catalog_ref: self.catalog_ref(),
        }
    }
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct FixtureInput {
    pub name: &'static str,
    pub input_type: &'static str,
    pub required: bool,
    pub description: Option<&'static str>,
}
