<?php

namespace App\Campaigns;

use App\Agent\Workspace;
use App\DTOs\LandingPageAnalysis;
use App\Models\Brief;
use App\Models\Campaign;

/**
 * The brief belonging to the conversation in hand.
 *
 * One per conversation, made the first time something has a shared answer to
 * record. Nothing asks the user to start one: it appears when the landing page
 * is read, which is the first question anyway.
 *
 * Kept out of the tools so none of them has to know how a brief comes into
 * being, and so the rule "one per conversation" lives in one place rather than
 * being re-implemented wherever a tool needs one.
 */
class Briefs
{
    /**
     * An analysis recorded before there was a brief to put it on.
     *
     * The first message of a new chat runs its whole turn before the
     * conversation exists, which the Workspace documents and handles the same
     * way for artifacts. Without this the landing page read on that turn is
     * dropped, and it is the one turn that always carries it.
     */
    private ?LandingPageAnalysis $pending = null;

    /**
     * Campaigns created before there was a brief to put them on.
     *
     * Same problem as $pending and the same turn: start_campaign runs while the
     * conversation still does not exist, so current() has nothing to key on and
     * correctly declines to invent a brief. The campaigns are created anyway,
     * because refusing to create them would be worse, and they are remembered
     * here so settle() can finish the job.
     *
     * @var list<int>
     */
    private array $orphans = [];

    /**
     * A total named before there was a brief to put it on.
     *
     * @var array{budget: float, budget_mode: ?string}|null
     */
    private ?array $pendingTotal = null;

    public function __construct(private readonly Workspace $workspace) {}

    /** Remember a campaign that was created with no brief to join. */
    public function orphaned(Campaign $campaign): void
    {
        $this->orphans[] = $campaign->id;
    }

    /**
     * How many distinct platforms this brief will cover once it has settled.
     *
     * The campaigns already attached, plus the ones created this turn that are
     * waiting for settle() to attach them. Asking the brief alone is right from
     * the second message on and wrong during the first, when nothing is
     * attached yet: that is how "advertise this on Meta and Google, 20 a day
     * total" came to write the whole 20 to whichever campaign was in focus.
     *
     * Deliberately the brief's campaigns rather than the conversation's. Two
     * campaigns that do not share a brief do not share a budget either, and
     * counting everything in the conversation would turn "make that one 12 a
     * day" into a total the moment a second campaign existed.
     */
    public function sharedPlatforms(): int
    {
        $attached = $this->current()?->campaigns()->pluck('platform') ?? collect();

        return $attached
            ->merge(Campaign::query()->whereIn('id', $this->orphans)->pluck('platform'))
            ->filter()
            ->unique()
            ->count();
    }

    /**
     * Finish what the first message could not.
     *
     * Called once the conversation exists, which on a new chat is only after
     * the turn has run. Everything before this point had nowhere to write: the
     * page analysis was held, and any campaign created in the same turn was
     * left without a brief.
     *
     * Found on the dev server on 23 Sep. Saying "advertise this on Meta and
     * Google" in one message produced two campaigns and no brief at all, so the
     * landing page, the money and the dates were shared by nothing and every
     * question would have been asked twice.
     *
     * Only the campaigns this instance created are adopted, by id. Sweeping up
     * everything brief-less in the conversation would pull in campaigns from
     * earlier turns and adopted ones from the platform, none of which were made
     * from this brief.
     */
    public function settle(): void
    {
        if ($this->pending === null && $this->pendingTotal === null && $this->orphans === []) {
            return;
        }

        $brief = $this->current();

        if ($brief === null) {
            return;
        }

        // current() already flushed $pending. What is left is the adoption.
        if ($this->orphans !== []) {
            Campaign::query()
                ->whereIn('id', $this->orphans)
                ->whereNull('brief_id')
                ->update(['brief_id' => $brief->id]);

            $this->orphans = [];
        }
    }

    /**
     * Keep what the landing page said, now or as soon as there is somewhere.
     *
     * Returns whether it was written. False means it is held and will be
     * flushed by the next current() once the conversation exists.
     */
    public function remember(LandingPageAnalysis $analysis): bool
    {
        if ($brief = $this->current()) {
            $brief->rememberPageAnalysis($analysis);

            return true;
        }

        $this->pending = $analysis;

        return false;
    }

    /**
     * Keep the total to be divided, now or as soon as there is somewhere.
     *
     * The same holding pattern as remember() above, for the same reason. A
     * budget named in the first message has no brief to land on: "advertise
     * this on Meta and Google, 20 a day total" arrives while the conversation
     * still does not exist, and on dev the 20 went to whichever campaign was in
     * focus while the other platform got nothing and split_budget had no total
     * to divide.
     *
     * Returns whether it was written. False means it is held for settle().
     */
    public function rememberTotal(float $budget, ?string $mode = null): bool
    {
        if ($brief = $this->current()) {
            $brief->update(array_filter([
                'budget' => $budget,
                'budget_mode' => $mode,
            ], fn (mixed $value): bool => $value !== null));

            return true;
        }

        $this->pendingTotal = ['budget' => $budget, 'budget_mode' => $mode];

        return false;
    }

    /**
     * The conversation's brief, or null when there is no conversation to hang
     * one on - a tool called outside a chat, which happens in tests and in the
     * controller's own step-in calls.
     */
    public function current(): ?Brief
    {
        $conversation = $this->workspace->id();

        // The workspace knows the owner in a chat; auth does outside one, which
        // is how a tool called directly still gets the right brief. With
        // neither there is nobody to own it, and a brief nobody owns is not
        // something to invent - the caller carries on without one.
        $owner = $this->workspace->owner()?->getKey() ?? auth()->id();

        if (blank($conversation) || blank($owner)) {
            return null;
        }

        // Keyed by owner as well as conversation, deliberately. Keying on the
        // conversation alone means anything that binds a workspace without
        // checking who owns the conversation hands back somebody else's brief,
        // with their landing page, budget and page analysis in it. Every path
        // does check today; this makes it impossible rather than conventional.
        $brief = Brief::query()->firstOrCreate(
            ['conversation_id' => $conversation, 'user_id' => $owner],
        );

        // Anything recorded before the conversation existed belongs to it.
        if ($this->pending !== null) {
            $brief->rememberPageAnalysis($this->pending);
            $this->pending = null;
        }

        if ($this->pendingTotal !== null) {
            $brief->update(array_filter(
                $this->pendingTotal,
                fn (mixed $value): bool => $value !== null,
            ));
            $this->pendingTotal = null;
        }

        return $brief;
    }

    /**
     * Attach a campaign to the conversation's brief, if it has none.
     *
     * Never reassigns: a campaign that already belongs to a brief keeps it, and
     * an adopted campaign pulled in from the platform is left alone entirely
     * unless something deliberately gives it one.
     */
    public function attach(Campaign $campaign): ?Brief
    {
        $brief = $this->current();

        if ($brief === null) {
            return null;
        }

        if ($campaign->brief_id === null) {
            $campaign->update(['brief_id' => $brief->id]);
        }

        return $brief;
    }
}
