<?php

namespace App\Agent\Tools\Campaign;

use App\Agent\Choices;
use App\Agent\Tools\Campaign\Concerns\ActsOnFocusedCampaign;
use App\Agent\Workspace;
use App\Campaigns\Briefs;
use App\Campaigns\Platforms\PlatformRules;
use App\Campaigns\Platforms\Platforms;
use App\Models\ArtifactFieldSource;
use Illuminate\Contracts\JsonSchema\JsonSchema;
use Laravel\Ai\Contracts\Tool;
use Laravel\Ai\Tools\Request;
use Stringable;

/**
 * Offer the objectives this platform supports, or set the one chosen.
 *
 * Called with no objective it offers; called with one it sets. The same shape
 * as import_creatives, and it is what lets the first step of a campaign be a
 * click rather than the user having to know that Meta calls it OUTCOME_LEADS.
 *
 * The options come from the platform's own rules, never from the model, so a
 * button can never offer an objective the platform would reject.
 */
class ChooseObjective implements Tool
{
    use ActsOnFocusedCampaign;

    public function __construct(
        private readonly Workspace $workspace,
        private readonly Platforms $platforms,
        private readonly Choices $choices,
        private readonly Briefs $briefs,
    ) {}

    public function description(): Stringable|string
    {
        return 'Set the campaign objective, or call with no objective to show the user the '
            .'objectives this platform supports so they can pick one. Prefer offering over '
            .'asking in prose.';
    }

    public function schema(JsonSchema $schema): array
    {
        return [
            'objective' => $schema->string()
                ->description('The chosen objective. Omit to offer the supported objectives instead.'),
        ];
    }

    /**
     * The objective the landing page points at, if it points at one.
     *
     * The brief holds the outcome in plain words - "sales", "leads" - because
     * that is what survives across platforms. Turning it into this platform's
     * own name happens here, and only ever suggests something the platform
     * actually supports.
     *
     * @param  list<string>  $supported
     * @return array{objective: string, because: string}|null
     */
    private function suggestedFrom(array $supported, PlatformRules $rules): ?array
    {
        $suggestion = $this->briefs->current()?->pageAnalysis()?->suggestedOutcome();

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

        // Asked of the platform. This was 'OUTCOME_'.strtoupper($outcome),
        // which is Meta's spelling written into shared code: on Google it
        // produced OUTCOME_SALES, matched nothing in the supported list, and
        // silently dropped the suggestion the whole landing page analysis
        // exists to produce.
        $objective = $rules->objectiveFor($suggestion['outcome']);

        return $objective !== null && in_array($objective, $supported, true)
            ? ['objective' => $objective, 'because' => $suggestion['because']]
            : null;
    }

    /**
     * The outcomes a brief can hold, in the words objectiveFor() accepts.
     *
     * Deliberately the plain-language list rather than any platform's enum.
     * Meta's OUTCOME_ prefix was written into shared code once already and
     * produced OUTCOME_SALES on Google, which matched nothing and silently
     * dropped the suggestion the landing page analysis exists to make.
     *
     * @var list<string>
     */
    private const OUTCOMES = ['SALES', 'LEADS', 'TRAFFIC', 'AWARENESS', 'ENGAGEMENT', 'VIEWS'];

    /**
     * This platform's word for the outcome the brief already holds.
     *
     * Null when the brief has no outcome yet, or when this platform cannot
     * serve it. A platform that does not sell the thing the buyer asked for
     * has to ask, rather than be handed the nearest match: that is how a lead
     * generation brief becomes a traffic campaign without anyone saying so.
     */
    private function sharedObjective(array $supported, PlatformRules $rules): ?string
    {
        $outcome = $this->briefs->current()?->outcome;

        if (blank($outcome)) {
            return null;
        }

        $objective = $rules->objectiveFor((string) $outcome);

        return $objective !== null && in_array($objective, $supported, true) ? $objective : null;
    }

    /**
     * Write the chosen objective back to the brief as a shared outcome.
     *
     * Found by asking the platform which outcome produces this objective,
     * rather than by adding a second mapping that could disagree with the
     * first. Synonyms collapse harmlessly: SALES and CONVERSIONS reach the
     * same objective on every platform, so either records the same intent.
     *
     * Never overwrites. The brief holds what the buyer first decided, and a
     * later platform whose vocabulary happens to differ must not silently
     * redefine the campaign that came before it.
     */
    private function rememberOutcome(string $objective, PlatformRules $rules): void
    {
        $brief = $this->briefs->current();

        if ($brief === null || filled($brief->outcome)) {
            return;
        }

        foreach (self::OUTCOMES as $outcome) {
            if ($rules->objectiveFor($outcome) === $objective) {
                $brief->update(['outcome' => $outcome]);

                return;
            }
        }
    }

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

        $rules = $this->platforms->for($campaign);
        $supported = $rules->objectives();
        $chosen = trim((string) ($request->all()['objective'] ?? ''));

        if ($chosen === '') {
            // Already answered, on another platform, in another vocabulary.
            //
            // The same intent is spelled OUTCOME_TRAFFIC on Meta,
            // WEBSITE_TRAFFIC on Google and WEBSITE_VISITS on LinkedIn, and
            // the buyer was made to say it once per platform and translate it
            // themselves each time. Seen on arb-dev on 25 Sep: three
            // objectives set across three turns, all of them traffic.
            //
            // The brief is where a shared answer belongs, and objectiveFor()
            // already existed on every platform to turn an outcome into that
            // platform's word for it. Nothing was writing the outcome down.
            if ($fromBrief = $this->sharedObjective($supported, $rules)) {
                $campaign->update(['objective' => $fromBrief]);
                $this->workspace->recordSource($campaign, 'objective', ArtifactFieldSource::USER_STATED);

                return $this->json([
                    'ok' => true,
                    'objective' => $fromBrief,
                    'optimization_goal' => $rules->optimizationGoal($fromBrief),
                    'requires_pixel' => $rules->requiresPixel($fromBrief),
                    'from_brief' => true,
                    'note' => sprintf(
                        'Already answered for this brief, so %s is set to %s without asking again. '
                        .'Say so in one short line, mention they can change it, and move on.',
                        $rules->name(),
                        $fromBrief,
                    ),
                ]);
            }

            // What the landing page implies, so this reads as a recommendation
            // rather than a blank question. A guess, and shown as one: the
            // reason travels with it, and nothing is recorded until the user
            // actually picks something.
            $suggested = $this->suggestedFrom($supported, $rules);

            $this->choices->offer(
                field: 'objective',
                question: 'What should this campaign optimise for?',
                options: array_map(function (string $o) use ($rules, $suggested): array {
                    // The words come from the platform too. Objectives::label()
                    // knows Meta's five and falls back to the raw enum for
                    // everything else, so Google's goals arrived on screen as
                    // "BRAND_AWARENESS_AND_REACH".
                    ['label' => $label, 'hint' => $hint] = $rules->describeObjective($o);

                    return [
                        'value' => $o,
                        'label' => $label,
                        'hint' => $o === ($suggested['objective'] ?? null)
                            ? trim($hint.' Suggested because '.$suggested['because'].'.')
                            : $hint,
                        'badge' => $o === ($suggested['objective'] ?? null) ? 'Suggested' : null,
                        'sends' => 'Set the objective to '.$o,
                    ];
                }, $supported),
            );

            return $this->json(array_filter([
                'ok' => true,
                'offered' => $supported,
                'suggested' => $suggested['objective'] ?? null,
                'note' => 'The options are on screen for the user to pick. Say one short line and wait; do not list them again in prose.',
            ], fn (mixed $value): bool => $value !== null));
        }

        $chosen = strtoupper($chosen);

        if (! in_array($chosen, $supported, true)) {
            return $this->json([
                'ok' => false,
                'error' => sprintf('%s does not support %s.', $rules->name(), $chosen),
                'supported' => $supported,
            ]);
        }

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

        // Recorded as the outcome rather than as this platform's enum, so the
        // next platform can be given its own word for the same thing.
        $this->rememberOutcome($chosen, $rules);

        return $this->json([
            'ok' => true,
            'objective' => $chosen,
            'optimization_goal' => $rules->optimizationGoal($chosen),
            'requires_pixel' => $rules->requiresPixel($chosen),
        ]);
    }
}
