BareGit
//! Fixed Pandoc HTML-to-Markdown conversion for invocation-local artifacts.

use std::time::Duration;

use serde_json::Value;
use tempfile::tempdir;
use tokio::{io::AsyncWriteExt, process::Command, time::timeout};

use crate::config::WebConfig;

use super::{ToolResult, ToolScope};

#[derive(Clone)]
pub(super) struct HtmlToMarkdownTool
{
    config: WebConfig,
}

impl HtmlToMarkdownTool
{
    pub(super) fn new(config: WebConfig) -> Self
    {
        Self { config }
    }

    pub(super) async fn execute(&self, arguments: Value, scope: &mut ToolScope) -> ToolResult
    {
        let Some(object) = arguments.as_object() else
        {
            return ToolResult::failure("invalid_arguments", "Tool arguments must be an object.");
        };
        if object.len() != 1 || !object.contains_key("artifact_id")
        {
            return ToolResult::failure("invalid_arguments", "Only an artifact ID argument is accepted.");
        }
        let Some(identifier) = object.get("artifact_id").and_then(Value::as_str) else
        {
            return ToolResult::failure("invalid_arguments", "An artifact ID string is required.");
        };
        let Some(artifact) = scope.artifacts.get(identifier) else
        {
            return ToolResult::failure("artifact_not_found", "The artifact does not exist in this invocation.");
        };
        if artifact.content_type != "text/html"
        {
            return ToolResult::failure("invalid_arguments", "Only HTML artifacts can be converted to Markdown.");
        }
        let html = artifact.bytes.clone();
        let final_url = artifact.final_url.clone();
        let Ok(directory) = tempdir() else
        {
            return ToolResult::failure("conversion_failed", "The converter could not create its temporary directory.");
        };
        let mut command = Command::new("pandoc");
        command.args(["--from=html", "--to=gfm", "--wrap=none", "--sandbox"])
            .current_dir(directory.path()).env_clear()
            .stdin(std::process::Stdio::piped()).stdout(std::process::Stdio::piped())
            .stderr(std::process::Stdio::null()).kill_on_drop(true);
        let Ok(mut child) = command.spawn() else
        {
            return ToolResult::failure("conversion_failed", "Pandoc is unavailable.");
        };
        let Some(mut stdin) = child.stdin.take() else
        {
            return ToolResult::failure("conversion_failed", "Pandoc standard input was unavailable.");
        };
        if stdin.write_all(&html).await.is_err() || stdin.shutdown().await.is_err()
        {
            return ToolResult::failure("conversion_failed", "Pandoc could not read the HTML artifact.");
        }
        let output = match timeout(Duration::from_secs(self.config.pandoc_timeout_seconds), child.wait_with_output()).await
        {
            Ok(Ok(output)) if output.status.success() => output.stdout,
            Ok(Ok(_)) => return ToolResult::failure("conversion_failed", "Pandoc could not convert the page."),
            Ok(Err(_)) => return ToolResult::failure("conversion_failed", "Pandoc failed while converting the page."),
            Err(_) => return ToolResult::failure("timeout", "Pandoc exceeded the configured time limit."),
        };
        if output.len() > self.config.max_tool_output_characters as usize * 4
        {
            return ToolResult::failure("output_too_large", "Pandoc produced too much output.");
        }
        let markdown: String = String::from_utf8_lossy(&output).replace("\r\n", "\n")
            .chars().filter(|character| !character.is_control() || *character == '\n' || *character == '\t')
            .take(self.config.max_tool_output_characters as usize).collect();
        let mut metadata = serde_json::Map::new();
        metadata.insert("final_url".to_owned(), Value::String(final_url.to_string()));
        let markdown_artifact_id = match scope.artifacts.insert(
            "text/markdown".to_owned(), markdown.as_bytes().to_vec(), final_url,
        )
        {
            Ok(identifier) => identifier,
            Err(error) => return error,
        };
        metadata.insert("artifact_id".to_owned(), Value::String(markdown_artifact_id));
        ToolResult::success("conversion_ok", markdown, metadata)
    }
}