<?php

namespace App\Agent;

use Laravel\Ai\Concerns\RemembersConversations;
use Laravel\Ai\Contracts\Agent;
use Laravel\Ai\Contracts\HasTools;
use Laravel\Ai\Contracts\RemembersConversations as RemembersConversationsContract;
use Laravel\Ai\Contracts\Tool;
use Laravel\Ai\Promptable;
use Stringable;

/**
 * The one agent the product talks to.
 *
 * It holds no domain knowledge. Tools come from the registry, which each module
 * fills from its own service provider, and state comes from the workspace. Add
 * a capability by registering a tool, never by editing this class.
 *
 * laravel/ai owns the transcript and the tool-calling loop. Two behaviours here
 * are load-bearing:
 *
 *   - instructions() is rebuilt per generation, so the model is re-shown current
 *     state every turn and cannot drift from what is stored.
 *   - binding the workspace to the conversation id is what lets a tool know
 *     which campaign "set the budget to $30" refers to.
 */
class MarketingAgent implements Agent, HasTools, RemembersConversationsContract
{
    use Promptable, RemembersConversations;

    public function __construct(
        private readonly ToolRegistry $registry,
        private readonly Workspace $workspace,
        private readonly Instructions $instructions,
    ) {
        $this->installResolver();
    }

    /**
     * forParticipant() and continue() return a clone, and a closure bound in the
     * constructor keeps pointing at the original, whose conversation is always
     * null. Re-binding on clone is what makes the resolver follow the instance
     * that actually has the conversation.
     */
    public function __clone(): void
    {
        $this->installResolver();
    }

    /**
     * The conversation does not exist when instructions are first built, but it
     * does by the time a tool runs. Handing the workspace a resolver rather than
     * an id closes that window: without it, anything created on the very first
     * turn is written against no conversation and silently orphaned.
     */
    private function installResolver(): void
    {
        $this->workspace->resolveIdUsing(fn (): ?string => $this->currentConversation());
    }

    public function instructions(): Stringable|string
    {
        $this->syncWorkspace();

        return $this->instructions->build();
    }

    /** @return list<Tool> */
    public function tools(): iterable
    {
        $this->syncWorkspace();

        return $this->registry->resolve();
    }

    public function workspace(): Workspace
    {
        return $this->workspace;
    }

    /**
     * Keep the workspace pointed at whichever conversation is running.
     *
     * Continuing an older conversation replaces the id outright; the resolver
     * only fills a blank one, so a rebind still has to be explicit.
     */
    private function syncWorkspace(): void
    {
        $conversationId = $this->currentConversation();

        if ($conversationId && $conversationId !== $this->workspace->id()) {
            $this->workspace->bindTo($conversationId);
        }
    }
}
