<?php

namespace App\Agent;

use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Str;
use Laravel\Ai\Contracts\ConversationStore;
use Laravel\Ai\Messages\Message;
use Laravel\Ai\Messages\UserMessage;
use Laravel\Ai\Prompts\AgentPrompt;
use Laravel\Ai\Responses\AgentResponse;
use Laravel\Ai\Responses\Data\ToolResult;
use Throwable;

/**
 * The package's conversation store, with the overflow summarised instead of dropped.
 *
 * DatabaseConversationStore takes the newest N messages and discards the rest
 * silently. Campaign values survive that because they live in the database and
 * are re-injected each turn, so what is actually lost is the reasoning around
 * them: what the user asked for and, more importantly, what they already
 * rejected. A long chat starts suggesting the thing it was told no to.
 *
 * This decorates the real store rather than replacing it. Everything is
 * delegated except the read, which prepends a short summary of the turns that
 * fell out of the window.
 */
class SummarisingConversationStore implements ConversationStore
{
    public function __construct(
        private readonly ConversationStore $store,
        private readonly int $sentences = 4,
    ) {}

    /**
     * Recent turns verbatim, with anything older folded into one message.
     *
     * @return Collection<int, Message>
     */
    public function getLatestConversationMessages(string $conversationId, int $limit): Collection
    {
        $recent = $this->store->getLatestConversationMessages($conversationId, $limit);

        // Nothing has been dropped until the window is actually full.
        if ($recent->count() < $limit) {
            return $recent;
        }

        $summary = $this->summarise($conversationId, $limit);

        return $summary === null
            ? $recent
            : $recent->prepend(new UserMessage("Earlier in this conversation:\n".$summary));
    }

    /**
     * Summarise the turns that fell out of the window.
     *
     * Cached per conversation and window position: the same overflow is summarised
     * once rather than on every turn, which keeps a long chat from paying for an
     * extra model call each time the user speaks.
     */
    private function summarise(string $conversationId, int $limit): ?string
    {
        $everything = $this->store->getLatestConversationMessages($conversationId, $limit * 10);
        $older = $everything->slice(0, max(0, $everything->count() - $limit));

        if ($older->isEmpty()) {
            return null;
        }

        $text = $older
            ->map(fn (Message $message): string => trim((string) $message->content))
            ->filter()
            ->implode("\n");

        if ($text === '') {
            return null;
        }

        return Cache::remember(
            'conversation-summary:'.$conversationId.':'.$older->count(),
            now()->addDay(),
            function () use ($text): ?string {
                try {
                    return Str::summarize(
                        'Summarise this advertising conversation. Keep decisions the user made, '
                        ."constraints they stated, and anything they rejected.\n\n".$text,
                        $this->sentences,
                    );
                } catch (Throwable) {
                    // Losing the summary is worse than losing nothing, but it is
                    // not worth failing the user's turn over.
                    return null;
                }
            },
        );
    }

    // ------------------------------------------------- delegated, unchanged

    public function latestConversationId(string $participantType, string|int $participantId): ?string
    {
        return $this->store->latestConversationId($participantType, $participantId);
    }

    public function storeConversation(?string $participantType, string|int|null $participantId, string $title): string
    {
        return $this->store->storeConversation($participantType, $participantId, $title);
    }

    public function storeUserMessage(string $conversationId, ?string $participantType, string|int|null $participantId, AgentPrompt $prompt): string
    {
        return $this->store->storeUserMessage($conversationId, $participantType, $participantId, $prompt);
    }

    public function storeAssistantMessage(string $conversationId, ?string $participantType, string|int|null $participantId, AgentPrompt $prompt, AgentResponse $response): ?string
    {
        return $this->store->storeAssistantMessage($conversationId, $participantType, $participantId, $prompt, $response);
    }

    /** @param  list<ToolResult>  $toolResults */
    public function storeApprovalResults(string $conversationId, ?string $participantType, string|int|null $participantId, array $toolResults): void
    {
        $this->store->storeApprovalResults($conversationId, $participantType, $participantId, $toolResults);
    }
}
