<?php

namespace App\Agent\Tools\Campaign;

use App\Agent\Tools\Campaign\Concerns\ActsOnFocusedCampaign;
use App\Agent\Workspace;
use App\Campaigns\Platforms\Platforms;
use App\Models\Asset;
use App\Models\AssetPlatformRef;
use App\Models\Campaign;
use App\Services\LinkedIn\LinkedInException;
use App\Services\LinkedIn\LinkedInRestApi;
use App\Services\LinkedIn\LinkedInUrn;
use App\Services\Meta\Meta;
use App\Services\Meta\MetaException;
use Illuminate\Contracts\JsonSchema\JsonSchema;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Storage;
use Laravel\Ai\Contracts\Tool;
use Laravel\Ai\Tools\Request;
use Stringable;

/**
 * Creatives attached to this conversation, with their status on the platform.
 *
 * Lists and uploads in one call on purpose. Splitting them led to the agent
 * asking the user to upload something they had already uploaded, because
 * listing alone left it unsure whether the work was done.
 *
 * Uploading goes to each platform's own API rather than the MCP: every MCP
 * upload tool is still gated on our accounts, and its local-file variant is
 * interactive by design. Meta goes through Graph and LinkedIn through its REST
 * images endpoint, which is why this reads the platform before uploading
 * rather than holding one client.
 */
class ReviewAssets implements Tool
{
    use ActsOnFocusedCampaign;

    public function __construct(
        private readonly Workspace $workspace,
        private readonly Meta $meta,
        private readonly ?LinkedInRestApi $linkedInApi = null,
        private readonly ?Platforms $platforms = null,
    ) {}

    public function description(): Stringable|string
    {
        return 'List the creatives attached to this conversation and upload any that are not '
            .'yet on the ad account. Always call this before asking the user to upload anything.';
    }

    public function schema(JsonSchema $schema): array
    {
        return [];
    }

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

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

        if ($assets->isEmpty()) {
            return $this->json([
                'ok' => true,
                'attached' => 0,
                'note' => 'No creative has been attached yet. Ask the user to upload one.',
            ]);
        }

        if (blank($campaign->ad_account_id)) {
            return $this->json([
                'ok' => false,
                'error' => 'Verify the ad account before uploading; creatives are uploaded per account.',
                'attached' => $assets->count(),
            ]);
        }

        $ready = [];
        $justUploaded = [];
        $failed = [];

        foreach ($assets as $asset) {
            if ($existing = $asset->refFor($campaign->platform, $campaign->ad_account_id)?->reference()) {
                $ready[] = ['asset_id' => $asset->id, 'name' => $asset->original_name, 'reference' => $existing];

                continue;
            }

            try {
                // Uploads live on the default disk, generated creatives on the public
                // one. Reading the wrong disk failed only at the upload to Meta.
                $path = Storage::disk($asset->disk ?: 'local')->path($asset->path);

                if ($campaign->platform === 'linkedin') {
                    $api = $this->linkedInApi ?? app(LinkedInRestApi::class);
                    $pageId = $campaign->page_id ?: config('platforms.linkedin.organization_id');
                    $ownerUrn = $pageId ? LinkedInUrn::organization((string) $pageId) : null;
                    $reference = $api->uploadImage($campaign->ad_account_id, $path, (string) $asset->original_name, $ownerUrn);

                    AssetPlatformRef::updateOrCreate(
                        [
                            'asset_id' => $asset->id,
                            'platform' => $campaign->platform,
                            'ad_account_id' => $campaign->ad_account_id,
                        ],
                        ['external_id' => $reference],
                    );
                } else {
                    $meta = $this->meta->on($campaign->id, 'agent');
                    $reference = $asset->kind === 'video'
                        ? $meta->uploadVideo($campaign->ad_account_id, $path, (string) $asset->original_name)
                        : $meta->uploadImage($campaign->ad_account_id, $path, (string) $asset->original_name);

                    AssetPlatformRef::updateOrCreate(
                        [
                            'asset_id' => $asset->id,
                            'platform' => $campaign->platform,
                            'ad_account_id' => $campaign->ad_account_id,
                        ],
                        $asset->kind === 'video'
                            ? ['external_id' => $reference]
                            : ['external_hash' => $reference],
                    );
                }

                $justUploaded[] = ['asset_id' => $asset->id, 'name' => $asset->original_name, 'reference' => $reference];
            } catch (MetaException|LinkedInException|\Throwable $e) {
                $failed[] = ['asset_id' => $asset->id, 'name' => $asset->original_name, 'error' => $e->getMessage()];
            }
        }

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

        $sharedTargets = $campaign->brief !== null
            ? $campaign->brief->campaigns()->get()->reject(fn ($c) => $c->isPublished() && $c->isNot($campaign))->values()
            : collect([$campaign]);

        $ratioAdvice = $this->ratioAdvice($sharedTargets, $assets);

        return $this->json(array_filter([
            'ok' => $failed === [],
            // A failure says so in a sentence, not only in a list.
            //
            // This returned ok:false carrying only the per-asset `failed`
            // entries, thirteen times across the stored conversations, and the
            // model had no single reason to relay. Kennon's report is what that
            // looks like from the outside: the copy was ready and both
            // creatives were silently blocked.
            'error' => $failed === [] ? null : sprintf(
                '%d of %d creatives could not be uploaded: %s',
                count($failed),
                $assets->count(),
                implode('; ', array_map(
                    fn (array $f): string => trim(($f['name'] ?? 'creative').': '.($f['error'] ?? 'unknown reason')),
                    $failed,
                )),
            ),
            'attached' => $assets->count(),
            'already_on_account' => $ready ?: null,
            'just_uploaded' => $justUploaded ?: null,
            'failed' => $failed ?: null,
            // Uploading puts a creative on the account; it does not decide
            // which ad uses it. Say so, or the model reports success while the
            // gate still refuses for a reason it has not been told about.
            'ads_without_a_creative' => $unassigned ?: null,
            'ratio_advice' => $ratioAdvice,
            'next_step' => $unassigned ? 'Call attach_creatives to decide which ad uses which creative.' : null,
        ], fn ($v) => $v !== null));
    }

    /**
     * Highlights creative aspect ratio mismatches across platforms sharing this brief.
     *
     * @param  Collection<int, Campaign>  $sharedTargets
     * @param  Collection<int, Asset>  $assets
     * @return array<string, mixed>|null
     */
    private function ratioAdvice(Collection $sharedTargets, Collection $assets): ?array
    {
        $platforms = $this->platforms ?? app(Platforms::class);
        $issues = [];

        foreach ($assets as $asset) {
            $ratio = $asset->aspectRatio();
            if ($ratio === null) {
                continue;
            }

            $unsupportedOn = [];
            $supportedOn = [];

            foreach ($sharedTargets as $target) {
                $platform = (string) $target->platform;
                if (! $platforms->supports($platform)) {
                    continue;
                }

                $rules = $platforms->for($platform);
                if (in_array($ratio, $rules->supportedAssetRatios(), true)) {
                    $supportedOn[] = $platform;
                } else {
                    $unsupportedOn[] = $platform;
                }
            }

            if ($unsupportedOn !== []) {
                $issues[] = [
                    'asset_id' => $asset->id,
                    'name' => $asset->original_name,
                    'ratio' => $ratio,
                    'unsupported_on' => array_values(array_unique($unsupportedOn)),
                    'supported_on' => array_values(array_unique($supportedOn)),
                ];
            }
        }

        if ($issues === []) {
            return null;
        }

        return [
            'incompatible' => $issues,
            'note' => 'One or more attached assets have aspect ratios not supported by all shared platforms. '
                .'Consider uploading platform-tailored creatives (e.g. 1:1 square or 1.91:1 landscape).',
        ];
    }
}
