<?php

namespace App\Agent;

use Illuminate\Contracts\JsonSchema\JsonSchema;
use Laravel\Ai\Approvals\Approval;
use Laravel\Ai\Contracts\Approvable;
use Laravel\Ai\Contracts\Tool;
use Laravel\Ai\Tools\Request;
use Laravel\Ai\Tools\ToolNameResolver;
use Stringable;

/**
 * Presents a tool to the model under a namespaced name.
 *
 * Two teams can both ship a "list" tool; the model sees `creative__list` and
 * `campaign__list` and can tell from the name alone which part of the product
 * it is reaching into. The wrapped tool is unchanged and knows nothing about
 * this, so a module's tools stay plain and testable on their own.
 *
 * The separator is a double underscore rather than a dot because OpenAI
 * rejects anything outside [a-zA-Z0-9_-] in a tool name, with a 400 at request
 * time rather than a clear error. Gemini accepts dots, so this only shows up
 * once you switch provider.
 *
 * laravel/ai resolves a name via ToolNameResolver, which prefers a name()
 * method when one exists. That is the hook this relies on.
 */
class NamespacedTool implements Approvable, Tool
{
    public function __construct(
        private readonly string $namespace,
        private readonly Tool $tool,
    ) {}

    /** The separator every provider accepts. */
    public const SEPARATOR = '__';

    public function name(): string
    {
        return $this->namespace.self::SEPARATOR.$this->shortName();
    }

    public function description(): Stringable|string
    {
        return $this->tool->description();
    }

    public function schema(JsonSchema $schema): array
    {
        return $this->tool->schema($schema);
    }

    public function handle(Request $request): Stringable|string
    {
        return $this->tool->handle($request);
    }

    /**
     * Forward the wrapped tool's approval requirement.
     *
     * The agent only ever sees this wrapper, and the run loop decides whether
     * to pause by testing the tool it holds for Approvable. Without these three
     * methods a tool that declares it needs approval is executed straight
     * through: the wrapper is not Approvable, so the check is skipped and the
     * pause never happens. That silently disabled the approval gate on
     * publishing, which is the one thing it exists to protect.
     */
    public function shouldRequestApproval(Request $request): ?Approval
    {
        return $this->tool instanceof Approvable
            ? $this->tool->shouldRequestApproval($request)
            : null;
    }

    public function requireApproval(?string $reason = null): static
    {
        if ($this->tool instanceof Approvable) {
            $this->tool->requireApproval($reason);
        }

        return $this;
    }

    public function withoutApproval(): static
    {
        if ($this->tool instanceof Approvable) {
            $this->tool->withoutApproval();
        }

        return $this;
    }

    /** The tool this wraps, for tests and for tooling that needs the real class. */
    public function inner(): Tool
    {
        return $this->tool;
    }

    /**
     * Hyphens become underscores so every tool reads the same way.
     *
     * MCP names its tools in kebab case, so adapting the Google Ads tools into
     * this registry produced `google__list-accounts` beside `campaign__list_ad_accounts`.
     * Both are names a provider accepts, which is why nothing failed; they just
     * made the list look like it came from two products, which it did.
     */
    private function shortName(): string
    {
        return str(ToolNameResolver::resolve($this->tool))
            ->replace('-', '_')
            ->snake()
            ->toString();
    }
}
