<?php

namespace App\Agent;

use Illuminate\Contracts\Container\Container;
use InvalidArgumentException;
use Laravel\Ai\Contracts\Tool;
use Laravel\Ai\Tools\McpServerTool;
use Laravel\Mcp\Server\Tool as McpTool;

/**
 * Every tool the agent can call.
 *
 * The agent owns no domain logic. Each module registers its own tools from its
 * own service provider, so campaign, creative, landing page and analysis work
 * can proceed in parallel without anyone editing a shared file.
 *
 *     ToolRegistry::register('creative', GenerateImage::class, ListCreatives::class);
 *
 * Names are namespaced (`creative.generate_image`) so two teams can both have
 * a "list" tool without colliding, and so the model can tell from the name
 * alone which part of the product a tool belongs to.
 */
class ToolRegistry
{
    /** @var array<string, list<class-string<Tool>>> */
    private array $tools = [];

    public function __construct(private readonly Container $container) {}

    /**
     * Register one or more tool classes under a namespace.
     *
     * @param  class-string<Tool>  ...$tools
     */
    public function register(string $namespace, string ...$tools): void
    {
        // Providers disagree on what a tool name may contain. OpenAI rejects
        // anything outside [a-zA-Z0-9_-] with a 400 at request time, so the
        // strictest rule is enforced here, at registration, where the message
        // can name the offending namespace.
        if (! preg_match('/^[a-z][a-z0-9_]*$/', $namespace)) {
            throw new InvalidArgumentException(
                "Tool namespace [{$namespace}] must be lower snake case, for example 'landing_page'. "
                .'Dots and other punctuation are rejected by some providers.'
            );
        }

        foreach ($tools as $tool) {
            if (! is_subclass_of($tool, Tool::class) && ! is_subclass_of($tool, McpTool::class)) {
                throw new InvalidArgumentException(
                    "[{$tool}] must implement ".Tool::class.' or extend '.McpTool::class
                    .' to be registered as a tool.'
                );
            }
        }

        $this->tools[$namespace] = [...$this->tools[$namespace] ?? [], ...$tools];
    }

    /** @return list<string> */
    public function namespaces(): array
    {
        return array_keys($this->tools);
    }

    /**
     * Resolve every registered tool, each wrapped so the model sees a
     * namespaced name.
     *
     * @param  list<string>|null  $only  limit to these namespaces
     * @return list<Tool>
     */
    public function resolve(?array $only = null): array
    {
        $resolved = [];

        foreach ($this->tools as $namespace => $classes) {
            if ($only !== null && ! in_array($namespace, $only, true)) {
                continue;
            }

            foreach ($classes as $class) {
                $resolved[] = new NamespacedTool($namespace, $this->adapt($this->container->make($class)));
            }
        }

        return $resolved;
    }

    public function count(): int
    {
        return array_sum(array_map('count', $this->tools));
    }

    /**
     * Present an MCP server tool as an ordinary agent tool.
     *
     * The Google Ads work arrived as MCP tools, which are a different contract:
     * they extend Laravel\Mcp\Server\Tool and return an MCP Response rather than
     * a string. laravel/ai ships the adapter for exactly this, so the two kinds
     * can sit in one registry and the model sees a single flat list of
     * namespaced tools with no idea that half of them came from elsewhere.
     *
     * Anything already an agent tool passes through untouched.
     */
    private function adapt(object $tool): Tool
    {
        return McpServerTool::supports($tool) ? new McpServerTool($tool) : $tool;
    }
}
