<?php

namespace App\Agent\Tools\Campaign;

use App\Agent\Choices;
use App\Agent\Tools\Campaign\Concerns\ActsOnFocusedCampaign;
use App\Agent\Workspace;
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;

/**
 * Record what the advertiser says the campaign is about, legally.
 *
 * Meta requires housing, employment, credit and social issues advertising to be
 * declared, and restricts targeting accordingly. Every campaign built here so
 * far declared none of them, because the write paths sent an empty list without
 * anyone being asked. For a housing ad that is a false declaration, not a
 * missing field.
 *
 * Deliberately its own tool rather than part of the campaign basics, so it is
 * an answer somebody gave rather than a default that rode along with a name and
 * an objective.
 */
class DeclareAdCategory implements Tool
{
    use ActsOnFocusedCampaign;

    /** What a person calls them, since the API names are not readable. */
    private const LABELS = [
        'NONE' => 'None of these',
        'HOUSING' => 'Housing',
        'EMPLOYMENT' => 'Employment',
        'CREDIT' => 'Credit',
        'ISSUES_ELECTIONS_POLITICS' => 'Social issues, elections or politics',
    ];

    private const HINTS = [
        'NONE' => 'Standard advertising with no restricted category.',
        'HOUSING' => 'Property for sale or rent, mortgages, insurance.',
        'EMPLOYMENT' => 'Jobs, internships, certifications.',
        'CREDIT' => 'Loans, credit cards, financing offers.',
        'ISSUES_ELECTIONS_POLITICS' => 'Political or social issue advocacy.',
    ];

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

    public function description(): Stringable|string
    {
        return 'Record whether this campaign advertises housing, employment, credit or social '
            .'issues, or none of them. Required before publishing. Call with no categories to '
            .'put the choice on screen for the user. Never assume NONE, and never infer it from '
            .'the landing page or the copy.';
    }

    public function schema(JsonSchema $schema): array
    {
        return [
            'categories' => $schema->array()->items($schema->string())
                ->description('One or more of HOUSING, EMPLOYMENT, CREDIT, ISSUES_ELECTIONS_POLITICS, or the single value NONE. Omit to offer the choice instead.'),
        ];
    }

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

        $allowed = $this->platforms->for($campaign)->specialAdCategories();

        if ($allowed === []) {
            return $this->json([
                'ok' => false,
                'error' => 'This platform does not use special ad categories.',
            ]);
        }

        $given = collect((array) ($request->all()['categories'] ?? []))
            ->map(fn ($c): string => strtoupper(trim((string) $c)))
            ->filter()
            ->unique()
            ->values();

        // Offered rather than asked in prose. This is a declaration with legal
        // weight, and "no" typed in passing is exactly the answer that should
        // not be easy to give without seeing what is being declined.
        if ($given->isEmpty()) {
            $this->choices->offer(
                field: 'special_ad_categories',
                question: 'Does this campaign advertise any of these? Meta requires it to be declared, and it restricts targeting.',
                options: array_map(fn (string $c): array => [
                    'value' => $c,
                    'label' => self::LABELS[$c] ?? $c,
                    'hint' => self::HINTS[$c] ?? null,
                    'sends' => $c === 'NONE'
                        ? 'None of those categories apply'
                        : 'This campaign advertises '.(self::LABELS[$c] ?? $c),
                ], $allowed),
            );

            return $this->json([
                'ok' => true,
                'offered' => $allowed,
                'note' => 'The choice is on screen. Say one short line and wait; do not restate the options.',
            ]);
        }

        if ($unknown = $given->diff($allowed)->all()) {
            return $this->json([
                'ok' => false,
                'error' => 'Unknown category: '.implode(', ', $unknown),
                'allowed' => $allowed,
            ]);
        }

        // NONE is an answer, not a category, so it cannot be combined with one.
        if ($given->contains('NONE') && $given->count() > 1) {
            return $this->json([
                'ok' => false,
                'error' => 'NONE means none of the categories apply, so it cannot be combined with another.',
            ]);
        }

        $campaign->update(['special_ad_categories' => $given->all()]);

        // Recorded as the user's own statement: this must never show up as
        // something the model worked out for itself.
        $this->workspace->recordSource($campaign, 'special_ad_categories', ArtifactFieldSource::USER_STATED);

        return $this->json([
            'ok' => true,
            'special_ad_categories' => $given->all(),
            'note' => $given->contains('NONE')
                ? 'Declared as no special category.'
                : 'Declared. Meta restricts targeting for these categories, so some audience options may be refused.',
        ]);
    }
}
