<?php

namespace App\Services\Platforms\Support;

use App\Models\ApiCall;
use App\Models\CampaignChange;
use App\Services\Meta\MetaException;

/**
 * Limits that hold regardless of what the model decides or the user types.
 *
 * Checked inside the API client, below the agent and below the controller, so
 * there is no path around them.
 */
class Guardrails
{
    /**
     * Refuse any ad account that is not explicitly allowed, before a single
     * call is made. Reads included: an account we may not spend on is an
     * account we have no business reading either.
     *
     * @throws MetaException
     */
    public function assertAccountAllowed(string $adAccountId): void
    {
        $allowed = (array) config('platforms.guardrails.allowed_ad_accounts');
        $account = $this->normalise($adAccountId);

        if ($allowed === []) {
            throw new MetaException(
                'No ad accounts are allowed. Set ARB_ALLOWED_AD_ACCOUNTS before ARB can reach Meta.'
            );
        }

        if (! in_array($account, $allowed, true)) {
            throw new MetaException(sprintf(
                '%s is not on the allowed list. Permitted: %s',
                $account,
                implode(', ', $allowed),
            ));
        }
    }

    /**
     * Stay inside a share of Meta's hourly limit, so anything else using the
     * same token keeps its own headroom.
     *
     * @throws MetaException
     */
    public function assertWithinCallBudget(?string $adAccountId): void
    {
        if (! $adAccountId) {
            return;
        }

        $limit = (int) config('platforms.guardrails.max_calls_per_hour');

        $used = ApiCall::query()
            ->where('ad_account_id', $adAccountId)
            ->where('created_at', '>=', now()->subHour())
            ->count();

        if ($used >= $limit) {
            throw new MetaException(sprintf(
                'ARB has used its hourly call budget for %s (%d of %d). Pausing so other systems keep their share.',
                $adAccountId,
                $used,
                $limit,
            ));
        }
    }

    /**
     * Refuse everything that writes to a platform.
     *
     * A switch rather than a deploy, because the moment you need it you cannot
     * wait for one. Reads keep working, so the product can still explain what
     * it did before it was stopped.
     *
     * @throws MetaException
     */
    public function assertWritesAllowed(): void
    {
        if (config('platforms.guardrails.paused')) {
            throw new MetaException(
                'All changes to advertising platforms are currently paused. Nothing was sent.'
            );
        }
    }

    /**
     * Hold a live campaign to a change rate.
     *
     * Two separate risks. The cooldown stops a retry loop, or a user and the
     * agent disagreeing, from rewriting the same campaign repeatedly within
     * seconds. The daily limit bounds the damage of anything that gets past it.
     *
     * @throws MetaException
     */
    public function assertChangeAllowed(int $campaignId): void
    {
        $cooldown = (int) config('platforms.guardrails.change_cooldown_seconds');
        $dailyLimit = (int) config('platforms.guardrails.max_changes_per_day');

        $recent = CampaignChange::query()
            ->where('campaign_id', $campaignId)
            ->where('state', 'applied')
            ->latest('applied_at');

        if ($cooldown > 0 && ($last = (clone $recent)->first())
            && $last->applied_at?->gt(now()->subSeconds($cooldown))) {
            throw new MetaException(sprintf(
                'This campaign was changed %d seconds ago. Changes are limited to one every %d seconds.',
                now()->diffInSeconds($last->applied_at, absolute: true),
                $cooldown,
            ));
        }

        $today = CampaignChange::query()
            ->where('campaign_id', $campaignId)
            ->where('state', 'applied')
            ->where('applied_at', '>=', now()->startOfDay())
            ->count();

        if ($today >= $dailyLimit) {
            throw new MetaException(sprintf(
                'This campaign has already been changed %d times today, which is the limit.',
                $today,
            ));
        }
    }

    /** Remove credentials and truncate anything oversized before logging. */
    public function scrub(array $payload): array
    {
        unset($payload['access_token']);

        return array_map(
            fn (mixed $value): mixed => is_string($value) && strlen($value) > 500
                ? substr($value, 0, 500).sprintf('… [%d chars]', strlen($value))
                : $value,
            $payload,
        );
    }

    private function normalise(string $adAccountId): string
    {
        return str_starts_with($adAccountId, 'act_') ? $adAccountId : 'act_'.$adAccountId;
    }
}
