BareGit
#pragma once

#include <chrono>
#include <concepts>
#include <functional>
#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include <unordered_map>
#include <utility>
#include <vector>

#include <nlohmann/json.hpp>
#include <spdlog/spdlog.h>

#include "cancellation.hpp"
#include "runtime.hpp"

namespace agent_smith
{

namespace internal
{
struct ToolAccess;
}

/// OpenAI-compatible schema and presentation metadata for one tool.
struct ToolSchema
{
    /// Unique function name sent to the LLM provider.
    std::string name;

    /// Human-readable explanation of the tool's purpose.
    std::string description;

    /// JSON-Schema object accepted as the tool's argument shape.
    nlohmann::json parameters;
};

/// Per-invocation resources and constraints supplied to a tool.
struct ToolContext
{
    /// Cooperative cancellation signal for this invocation.
    CancellationToken cancel;

    /// Remaining execution-time budget for this invocation.
    std::chrono::milliseconds budget{0};

    /// Identifier that matches the LLM-generated tool call.
    std::string call_id;

    /// Optional logger scoped to tool execution.
    spdlog::logger* logger = nullptr;

    /// Runtime used by tools that need framework services.
    Runtime* runtime = nullptr;
};

/// A normalized result returned by one tool invocation.
struct ToolResult
{
    /// Text content supplied to the LLM as the tool result.
    std::string content;

    /// Whether the tool result represents an expected tool-level failure.
    bool is_error = false;

    /// Optional structured representation of the result.
    nlohmann::json structured = nullptr;
};

/// Per-tool invocation options stored by the registry.
struct ToolOptions
{
    /// Optional maximum execution duration for the tool.
    std::optional<std::chrono::milliseconds> timeout;
};

/// Interface implemented by all callable tools.
class Tool
{
public:
    /// Destroy a tool instance through the interface.
    virtual ~Tool() = default;

    /// Return the immutable schema advertised for this tool.
    virtual const ToolSchema& schema() const = 0;

    /// Invoke the tool with parsed JSON arguments and invocation context.
    virtual mw::Task<mw::E<ToolResult>> invoke(nlohmann::json arguments,
                                               ToolContext context) = 0;
};

/// Normalize a text result from a tool callable.
inline mw::E<ToolResult> normalizeToolResult(std::string value)
{
    return ToolResult{
        .content = std::move(value),
    };
}

/// Normalize a C-string result from a tool callable.
inline mw::E<ToolResult> normalizeToolResult(const char* value)
{
    return normalizeToolResult(value == nullptr ? std::string()
                                                : std::string(value));
}

/// Normalize structured JSON into both text and structured tool output.
inline mw::E<ToolResult> normalizeToolResult(nlohmann::json value)
{
    const std::string content = value.dump();
    return ToolResult{
        .content = content,
        .structured = std::move(value),
    };
}

/// Forward a fully normalized tool result unchanged.
inline mw::E<ToolResult> normalizeToolResult(ToolResult value)
{
    return value;
}

/// Forward a fallible normalized tool result unchanged.
inline mw::E<ToolResult> normalizeToolResult(mw::E<ToolResult> value)
{
    return value;
}

/// A callable that finishes synchronously with a normalizable result.
template <typename F>
concept SyncToolCallable = requires(F& function, nlohmann::json arguments,
                                    ToolContext context) {
    {
        normalizeToolResult(function(std::move(arguments), std::move(context)))
    } -> std::same_as<mw::E<ToolResult>>;
};

/// A callable that already exposes Agent Smith's asynchronous tool result.
template <typename F>
concept AsyncToolCallable =
    requires(F& function, nlohmann::json arguments, ToolContext context) {
        {
            function(std::move(arguments), std::move(context))
        } -> std::same_as<mw::Task<mw::E<ToolResult>>>;
    };

namespace internal
{

inline void validateToolSchema(const ToolSchema& schema)
{
    if(!schema.parameters.is_object())
    {
        spdlog::warn("tool '{}' has non-object JSON Schema parameters",
                     schema.name.c_str());
        return;
    }

    const auto type = schema.parameters.find("type");
    if(type == schema.parameters.end() || !type->is_string() ||
       *type != "object")
    {
        spdlog::warn("tool '{}' JSON Schema parameters should have type object",
                     schema.name.c_str());
    }

    const auto properties = schema.parameters.find("properties");
    if(properties == schema.parameters.end() || !properties->is_object())
    {
        spdlog::warn(
            "tool '{}' JSON Schema parameters should have object properties",
            schema.name.c_str());
    }
}

template <SyncToolCallable F> class SyncToolAdapter final : public Tool
{
public:
    SyncToolAdapter(ToolSchema schema, F function)
        : schema_(std::move(schema)), function_(std::move(function))
    {
        validateToolSchema(schema_);
    }

    const ToolSchema& schema() const override
    {
        return schema_;
    }

    mw::Task<mw::E<ToolResult>> invoke(nlohmann::json arguments,
                                       ToolContext context) override
    {
        if(context.runtime == nullptr)
        {
            co_return std::unexpected(
                mw::runtimeError("synchronous tool requires a runtime"));
        }

        std::function<mw::E<ToolResult>()> work =
            [function = function_, arguments = std::move(arguments),
             context = std::move(context)]() mutable
        {
            if(context.cancel.cancelled())
            {
                return mw::E<ToolResult>(std::unexpected(
                    mw::runtimeError("tool cancelled before execution")));
            }
            return normalizeToolResult(
                function(std::move(arguments), std::move(context)));
        };
        co_return co_await context.runtime->runOnWorker(std::move(work));
    }

private:
    ToolSchema schema_;
    F function_;
};

template <AsyncToolCallable F> class AsyncToolAdapter final : public Tool
{
public:
    AsyncToolAdapter(ToolSchema schema, F function)
        : schema_(std::move(schema)), function_(std::move(function))
    {
        validateToolSchema(schema_);
    }

    const ToolSchema& schema() const override
    {
        return schema_;
    }

    mw::Task<mw::E<ToolResult>> invoke(nlohmann::json arguments,
                                       ToolContext context) override
    {
        co_return co_await function_(std::move(arguments), std::move(context));
    }

private:
    ToolSchema schema_;
    F function_;
};

} // namespace internal

/// Adapt a synchronous callable so it runs on the runtime's worker pool.
template <SyncToolCallable F>
std::unique_ptr<Tool> makeTool(ToolSchema schema, F function)
{
    return std::make_unique<internal::SyncToolAdapter<F>>(std::move(schema),
                                                         std::move(function));
}

/// Adapt an asynchronous callable without moving it to a worker thread.
template <AsyncToolCallable F>
std::unique_ptr<Tool> makeTool(ToolSchema schema, F function)
{
    return std::make_unique<internal::AsyncToolAdapter<F>>(std::move(schema),
                                                           std::move(function));
}

/// Owns named tools and their options in deterministic insertion order.
/// Registry access is not thread-safe. Running calls keep replaced tools alive.
class ToolRegistry
{
public:
    /// Add a tool unless its name is already registered.
    bool add(std::unique_ptr<Tool> tool, ToolOptions options = {});

    /// Add a tool or replace the entry already registered under its name.
    void replace(std::unique_ptr<Tool> tool, ToolOptions options = {});

    /// Find a registered tool by name, or return null when it is absent.
    Tool* find(std::string_view name);

    /// Return registered options, or default options when the tool is absent.
    ToolOptions options(std::string_view name) const;

    /// Return schemas in deterministic insertion order.
    std::vector<ToolSchema> schemas() const;

    /// Return the number of registered tools.
    std::size_t size() const;

private:
    friend struct internal::ToolAccess;
    struct Entry
    {
        // In-flight invocations retain ownership after timeout or replacement.
        std::shared_ptr<Tool> tool;
        ToolOptions options;
    };

    std::unordered_map<std::string, Entry> by_name_;
    std::vector<std::string> insertion_order_;
};

} // namespace agent_smith