BareGit
//! Fixed, host-controlled tools exposed to the LLM during one invocation.

mod fetch_web_page;
mod html_to_markdown;
mod web_search;

use std::collections::HashMap;

use rand::random;
use serde::Serialize;
use serde_json::{Map, Value};
use url::Url;

use crate::config::{SearchConfig, WebConfig};

use self::{
    fetch_web_page::FetchWebPageTool,
    html_to_markdown::HtmlToMarkdownTool,
    web_search::WebSearchTool,
};

/// Returns bounded host-produced output from a fixed tool call.
#[derive(Debug, Serialize)]
pub struct ToolResult
{
    /// Indicates whether the tool completed successfully.
    pub ok: bool,
    /// Stable non-sensitive result or failure code.
    pub code: &'static str,
    /// Bounded text provided to the LLM as untrusted data.
    pub content: String,
    /// Non-sensitive structured metadata for a successful tool result.
    pub metadata: Map<String, Value>,
}

impl ToolResult
{
    /// Creates a successful bounded tool result.
    #[must_use]
    pub fn success(code: &'static str, content: String, metadata: Map<String, Value>) -> Self
    {
        Self { ok: true, code, content, metadata }
    }

    /// Creates a non-sensitive tool failure visible to the LLM.
    #[must_use]
    pub fn failure(code: &'static str, content: impl Into<String>) -> Self
    {
        Self { ok: false, code, content: content.into(), metadata: Map::new() }
    }
}

/// Holds downloaded content that is reachable only during one invocation.
pub struct ArtifactStore
{
    artifacts: HashMap<String, Artifact>,
    total_bytes: usize,
    maximum_total_bytes: usize,
}

struct Artifact
{
    content_type: String,
    bytes: Vec<u8>,
    final_url: Url,
}

/// Holds validated research sources in first-use order for one invocation.
#[derive(Default)]
pub struct SourceRegistry
{
    sources: Vec<Source>,
}

/// Describes one host-validated public source URL.
#[derive(Clone, Debug)]
pub struct Source
{
    /// Normalized absolute HTTP or HTTPS source URL.
    pub url: Url,
    /// Bounded source title supplied by search or page metadata.
    pub title: String,
}

/// Owns all mutable tool state for exactly one LLM invocation.
pub struct ToolScope
{
    /// Invocation-local downloaded artifacts.
    pub artifacts: ArtifactStore,
    /// Invocation-local validated research sources.
    pub sources: SourceRegistry,
}

/// Dispatches the three fixed model-visible tools.
#[derive(Clone)]
pub struct ToolRegistry
{
    search: WebSearchTool,
    fetch: FetchWebPageTool,
    convert: HtmlToMarkdownTool,
}

impl ArtifactStore
{
    /// Creates an empty invocation-local artifact store.
    #[must_use]
    pub fn new(maximum_total_bytes: usize) -> Self
    {
        Self { artifacts: HashMap::new(), total_bytes: 0, maximum_total_bytes }
    }

    fn insert(
        &mut self,
        content_type: String,
        bytes: Vec<u8>,
        final_url: Url,
    ) -> Result<String, ToolResult>
    {
        if bytes.len() > self.maximum_total_bytes.saturating_sub(self.total_bytes)
        {
            return Err(ToolResult::failure(
                "artifact_limit",
                "The invocation's artifact storage limit was reached.",
            ));
        }
        let identifier = format!("art_{:016x}", random::<u64>());
        self.total_bytes += bytes.len();
        self.artifacts.insert(identifier.clone(), Artifact { content_type, bytes, final_url });
        Ok(identifier)
    }

    fn get(&self, identifier: &str) -> Option<&Artifact>
    {
        self.artifacts.get(identifier)
    }
}

impl SourceRegistry
{
    /// Returns whether successful tools registered any research source.
    #[must_use]
    pub fn is_empty(&self) -> bool
    {
        self.sources.is_empty()
    }

    /// Adds one normalized source URL, preserving first-use ordering.
    pub fn register(&mut self, url: Url, title: impl Into<String>)
    {
        if self.sources.iter().any(|source| source.url == url)
        {
            return;
        }
        let title: String = title.into().chars().take(200).collect();
        self.sources.push(Source { url, title });
    }

    /// Returns only requested URLs that came from successful host tool results.
    #[must_use]
    pub fn select(&self, urls: &[Url]) -> Option<Vec<Source>>
    {
        let mut selected = Vec::new();
        for url in urls
        {
            let source = self.sources.iter().find(|source| source.url == *url)?;
            if !selected.iter().any(|existing: &Source| existing.url == source.url)
            {
                selected.push(source.clone());
            }
        }
        Some(selected)
    }
}

impl ToolScope
{
    /// Creates clean tool state that cannot be shared with another invocation.
    #[must_use]
    pub fn new(web: &WebConfig) -> Self
    {
        Self {
            artifacts: ArtifactStore::new(web.max_total_artifact_bytes),
            sources: SourceRegistry::default(),
        }
    }
}

impl ToolRegistry
{
    /// Builds isolated HTTP and conversion implementations from validated configuration.
    pub fn new(search: SearchConfig, web: WebConfig) -> Result<Self, reqwest::Error>
    {
        Ok(Self {
            search: WebSearchTool::new(search)?,
            fetch: FetchWebPageTool::new(web.clone())?,
            convert: HtmlToMarkdownTool::new(web),
        })
    }

    /// Executes one exact tool name with host-validated JSON arguments.
    pub async fn execute(
        &self,
        name: &str,
        arguments: Value,
        scope: &mut ToolScope,
    ) -> ToolResult
    {
        match name
        {
            "web_search" => self.search.execute(arguments, &mut scope.sources).await,
            "fetch_web_page" => self.fetch.execute(arguments, scope).await,
            "html_to_markdown" => self.convert.execute(arguments, scope).await,
            _ => ToolResult::failure("invalid_tool", "This tool name is not available."),
        }
    }

    /// Returns the strict OpenAI function schemas exposed to the LLM.
    #[must_use]
    pub fn schemas() -> Vec<Value>
    {
        vec![
            serde_json::json!({"type":"function","function":{"name":"web_search","parameters":{"type":"object","additionalProperties":false,"properties":{"query":{"type":"string","minLength":1,"maxLength":500},"result_count":{"type":"integer","minimum":1}},"required":["query"]}}}),
            serde_json::json!({"type":"function","function":{"name":"fetch_web_page","parameters":{"type":"object","additionalProperties":false,"properties":{"url":{"type":"string","minLength":1,"maxLength":4096}},"required":["url"]}}}),
            serde_json::json!({"type":"function","function":{"name":"html_to_markdown","parameters":{"type":"object","additionalProperties":false,"properties":{"artifact_id":{"type":"string","minLength":1,"maxLength":128}},"required":["artifact_id"]}}}),
        ]
    }
}

#[cfg(test)]
mod tests
{
    use url::Url;

    use super::SourceRegistry;

    #[test]
    fn rejects_unregistered_model_sources()
    {
        let mut registry = SourceRegistry::default();
        let source = Url::parse("https://example.test/article").unwrap();
        registry.register(source.clone(), "Article");
        assert!(registry.select(&[source]).is_some());
        assert!(registry.select(&[Url::parse("https://example.test/invented").unwrap()]).is_none());
    }
}