<?php

namespace App\Agent\Tools\Campaign;

use App\Agent\Choices;
use App\Agent\Tools\Campaign\Concerns\ActsOnFocusedCampaign;
use App\Agent\Workspace;
use App\Campaigns\Objectives;
use App\Models\ArtifactFieldSource;
use App\Models\Campaign;
use App\Services\Meta\Meta;
use App\Services\Meta\MetaException;
use Illuminate\Contracts\JsonSchema\JsonSchema;
use Laravel\Ai\Contracts\Tool;
use Laravel\Ai\Tools\Request;
use Stringable;

/**
 * Which pixel event the ad set optimises toward.
 *
 * This was never asked. The event came from the objective alone: leads meant
 * LEAD, sales meant PURCHASE, and whether the pixel had ever fired such an event
 * was not consulted. Meta accepts it either way and the campaign then spends
 * against something it can never observe, so delivery reads as underdelivery and
 * every number on the report looks plausible. It is the hardest kind of mistake
 * to trace, which is why it is asked rather than inferred.
 *
 * Kaushal's campaign 120252047635070626 is the live example: pixel
 * 1017787001151608 fires PageView and Subscribe, 102 and 1 times, and has never
 * once fired Lead, which is what the ad set is asking Meta to optimise toward.
 *
 * The pixel's own history is the list, because it is the only evidence of what
 * the site actually sends. The objective's default is still offered and still
 * marked, so the ordinary case stays one click, but an event the pixel has never
 * fired says so rather than being presented as the obvious choice.
 */
class ChooseConversionEvent implements Tool
{
    use ActsOnFocusedCampaign;

    public function __construct(
        private readonly Workspace $workspace,
        private readonly Meta $meta,
        private readonly Choices $choices,
    ) {}

    public function description(): Stringable|string
    {
        return 'List the events the campaign\'s pixel has been firing and set which one the ad set '
            .'optimises toward, by passing conversion_event. Meta only, and only for objectives that '
            .'optimise toward a pixel event. Call this after choose_pixel. Never invent an event name.';
    }

    public function schema(JsonSchema $schema): array
    {
        return [
            'conversion_event' => $schema->string()
                ->description('The pixel event name as Meta reports it, such as Purchase, Lead or Subscribe.'),
        ];
    }

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

        if ($campaign->platform !== 'meta') {
            return $this->json([
                'ok' => false,
                'error' => sprintf(
                    'Optimising toward a pixel event is a Meta setting and this campaign is on %s.',
                    $campaign->platform,
                ),
            ]);
        }

        // No pixel event to optimise toward on a traffic or awareness campaign:
        // the ad set promotes the Page, and sending custom_event_type against it
        // is rejected by Meta rather than ignored.
        if (Objectives::conversionEvent($campaign->objective) === null) {
            return $this->json([
                'ok' => false,
                'error' => sprintf(
                    'The %s objective does not optimise toward a pixel event, so there is nothing to choose here.',
                    $campaign->objective ?? 'selected',
                ),
                'note' => 'Do not ask the user about this. Move on to the next step.',
            ]);
        }

        if (blank($campaign->pixel_id)) {
            return $this->json([
                'ok' => false,
                'error' => 'Choose the pixel first. The events on offer are the ones that pixel has been firing.',
            ]);
        }

        $default = Objectives::conversionEvent($campaign->objective);
        $defaultName = Objectives::eventName($default);

        try {
            $fired = $this->meta->pixelEvents($campaign->pixel_id);
        } catch (MetaException $e) {
            return $this->json(['ok' => false, 'error' => $e->getMessage()]);
        }

        $arguments = $request->all();

        if (! array_key_exists('conversion_event', $arguments)) {
            return $this->offer($campaign, $fired, $defaultName);
        }

        $chosen = trim((string) $arguments['conversion_event']);

        if ($chosen === '') {
            return $this->json([
                'ok' => false,
                'error' => 'Name the event, or leave it unset to keep the objective\'s default.',
            ]);
        }

        // Matched against what the pixel reported rather than against Meta's
        // enum, because a custom event is a legitimate answer and will not
        // appear in any list we hold.
        $matched = collect(array_keys($fired))
            ->first(fn (string $name): bool => strcasecmp($name, $chosen) === 0);

        if ($matched === null && $fired !== []) {
            return $this->json([
                'ok' => false,
                'error' => sprintf(
                    'This pixel has not fired %s. It has fired: %s.',
                    $chosen,
                    collect($fired)->map(fn (int $count, string $name): string => "{$name} ({$count})")->implode(', '),
                ),
                'events' => $fired,
            ]);
        }

        $name = $matched ?? $chosen;
        $type = Objectives::eventType($name);

        $campaign->update(['conversion_event' => $type]);
        $this->workspace->recordSource($campaign, 'conversion_event', ArtifactFieldSource::USER_STATED);

        return $this->json(array_filter([
            'ok' => true,
            'conversion_event' => $type,
            'event_name' => $name,
            'times_fired' => $fired[$name] ?? 0,
            // OTHER is not a fallback here, it is the enum for anything outside
            // Meta's own list, and a buyer should know the event is being
            // published that way.
            'note' => $type === 'OTHER'
                ? sprintf('%s is a custom event, so it is published as OTHER. Say that in one line.', $name)
                : null,
        ], fn ($v) => $v !== null));
    }

    /**
     * Put the pixel's own events on screen.
     *
     * @param  array<string,int>  $fired
     */
    private function offer(Campaign $campaign, array $fired, ?string $defaultName): Stringable|string
    {
        // A pixel with no history at all is not a refusal. One installed an hour
        // ago has fired nothing, and the objective's default is then the only
        // thing anyone can go on.
        if ($fired === []) {
            // Written, not merely reported.
            //
            // This returned the default in the reply and left the column null,
            // so LaunchFlow::done() stayed false and the flow asked the same
            // question every turn with nothing on screen to click: a pixel that
            // had not fired yet made the campaign impossible to finish.
            //
            // Recorded as FALLBACK because that is what it is - the objective's
            // default, chosen with no evidence from the pixel - so the publish
            // gate asks the buyer to confirm it before it can spend.
            $campaign->update(['conversion_event' => Objectives::conversionEvent($campaign->objective)]);
            $this->workspace->recordSource($campaign, 'conversion_event', ArtifactFieldSource::FALLBACK);

            return $this->json([
                'ok' => true,
                'events' => [],
                'conversion_event' => Objectives::conversionEvent($campaign->objective),
                'note' => sprintf(
                    'This pixel has no recorded events yet, so the %s objective\'s default of %s stands. Tell the '
                    .'user in one line that the pixel has fired nothing so far, which usually means it is not '
                    .'installed on the site yet, and move on.',
                    $campaign->objective,
                    $defaultName ?? 'the default',
                ),
            ]);
        }

        $this->choices->offer(
            field: 'conversion_event',
            question: 'Which event should this campaign optimise toward?',
            options: collect($fired)
                ->map(fn (int $count, string $name): array => [
                    'value' => $name,
                    'label' => $name,
                    'hint' => sprintf('fired %s %s', number_format($count), $count === 1 ? 'time' : 'times'),
                    // Marked, not chosen. It is what the objective implies and
                    // usually right, and the count beside it is what says
                    // whether it is right here.
                    'badge' => $name === $defaultName ? 'Suggested' : null,
                    'sends' => 'Optimise toward '.$name,
                ])
                ->values()
                ->all(),
        );

        // The warning the whole tool exists for.
        $defaultNeverFired = $defaultName !== null && ! array_key_exists($defaultName, $fired);

        return $this->json(array_filter([
            'ok' => true,
            'events' => $fired,
            'default' => $defaultName,
            'default_never_fired' => $defaultNeverFired ?: null,
            'note' => $defaultNeverFired
                ? sprintf(
                    'Say this before anything else: the %s objective would optimise toward %s, and this pixel has '
                    .'never fired that event, so the campaign would be buying something it cannot measure. The '
                    .'events it has actually fired are on screen. Then wait.',
                    $campaign->objective,
                    $defaultName,
                )
                : 'The events are on screen for the user to pick. Say one short line and wait.',
        ], fn ($v) => $v !== null));
    }
}
