<?php

namespace App\Agent\Tools\Campaign;

use App\Agent\Tools\Campaign\Concerns\ActsOnFocusedCampaign;
use App\Agent\Workspace;
use App\Models\Asset;
use Illuminate\Contracts\JsonSchema\JsonSchema;
use Laravel\Ai\Contracts\Tool;
use Laravel\Ai\Tools\Request;
use Stringable;

/**
 * Decide which creative each ad uses.
 *
 * Copy and creative arrive in either order: the ads are usually written before
 * the user has uploaded anything, so something has to pair them afterwards.
 * Without this the ads keep no asset, the publish gate refuses, and the agent
 * has no way to act on "use one image across two ads".
 *
 * The same asset may back several ads on purpose. That is how one image ships
 * three ads that differ only by copy.
 */
class AttachCreatives implements Tool
{
    use ActsOnFocusedCampaign;

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

    public function description(): Stringable|string
    {
        return 'Attach creatives to the ads of the campaign in focus. Pass mapping to control '
            .'which asset each ad uses, or call with no arguments to spread the attached '
            .'creatives across the ads. The same asset may be used by several ads.';
    }

    public function schema(JsonSchema $schema): array
    {
        return [
            'mapping' => $schema->array()->items(
                $schema->object([
                    'ad_id' => $schema->integer()->description('The ad id shown in the campaign state.')->required(),
                    'asset_id' => $schema->integer()->description('The asset id to use for that ad.')->required(),
                ])
            )->description('Explicit ad to asset pairs. Omit to spread the available creatives automatically.'),
        ];
    }

    public function handle(Request $request): Stringable|string
    {
        if (! $campaign = $this->focused()) {
            return $this->noCampaign();
        }

        $ads = $campaign->ads()->orderBy('id')->get();

        if ($ads->isEmpty()) {
            return $this->json(['ok' => false, 'error' => 'No ads written yet. Write the ad copy first.']);
        }

        $assets = $this->workspace->artifacts(Asset::class)
            ->map(fn ($link) => $link->artifactable)
            ->filter()
            ->values();

        if ($assets->isEmpty()) {
            return $this->json([
                'ok' => false,
                'error' => 'No creatives are attached to this conversation. Ask the user to upload one.',
            ]);
        }

        $mapping = (array) ($request->all()['mapping'] ?? []);

        return $mapping === []
            ? $this->spread($campaign, $ads, $assets)
            : $this->apply($campaign, $ads, $assets, $mapping);
    }

    /** Round robin, so fewer creatives than ads still fills every ad. */
    private function spread($campaign, $ads, $assets): string
    {
        foreach ($ads as $index => $ad) {
            $ad->update(['asset_id' => $assets[$index % $assets->count()]->id]);
        }

        return $this->json([
            'ok' => true,
            'campaign_id' => $campaign->id,
            'attached' => $ads->map(fn ($ad): array => [
                'ad_id' => $ad->id,
                'asset_id' => $ad->fresh()->asset_id,
            ])->all(),
        ]);
    }

    /** @param list<array{ad_id:int,asset_id:int}> $mapping */
    private function apply($campaign, $ads, $assets, array $mapping): string
    {
        $adIds = $ads->pluck('id')->all();
        $assetIds = $assets->pluck('id')->all();

        foreach ($mapping as $pair) {
            $adId = (int) ($pair['ad_id'] ?? 0);
            $assetId = (int) ($pair['asset_id'] ?? 0);

            if (! in_array($adId, $adIds, true)) {
                return $this->json([
                    'ok' => false,
                    'error' => "Ad {$adId} does not belong to this campaign.",
                    'ads' => $adIds,
                ]);
            }

            if (! in_array($assetId, $assetIds, true)) {
                return $this->json([
                    'ok' => false,
                    'error' => "Asset {$assetId} is not attached to this conversation.",
                    'assets' => $assetIds,
                ]);
            }
        }

        foreach ($mapping as $pair) {
            $ads->firstWhere('id', (int) $pair['ad_id'])->update(['asset_id' => (int) $pair['asset_id']]);
        }

        $unassigned = $campaign->ads()->whereNull('asset_id')->pluck('id')->all();

        return $this->json(array_filter([
            'ok' => true,
            'campaign_id' => $campaign->id,
            'attached' => $mapping,
            'still_without_a_creative' => $unassigned ?: null,
        ], fn ($v) => $v !== null));
    }
}
