<?php

namespace App\Services\Meta;

use App\Models\ApiCall;
use App\Services\Platforms\Support\Guardrails;

/**
 * Wraps every call to Meta: guardrails before, audit row after, whatever the
 * outcome. Both transports go through this, so /activity is a complete record
 * regardless of which one ran.
 */
class CallLogger
{
    private ?int $campaignId = null;

    private ?string $actor = null;

    public function __construct(
        private readonly Guardrails $guardrails,
    ) {}

    public function attributeTo(?int $campaignId, ?string $actor): static
    {
        $this->campaignId = $campaignId;
        $this->actor = $actor;

        return $this;
    }

    /**
     * @param  \Closure(): array<string,mixed>  $call
     * @return array<string,mixed>
     *
     * @throws MetaException
     */
    public function record(CallContext $context, \Closure $call): array
    {
        if ($context->guarded) {
            $this->guardrails->assertWithinCallBudget($context->adAccountId);

            if ($context->adAccountId) {
                $this->guardrails->assertAccountAllowed($context->adAccountId);
            }
        }

        $startedAt = microtime(true);
        $response = null;
        $error = null;

        try {
            return $response = $call();
        } catch (\Throwable $e) {
            $error = $e->getMessage();

            throw $e;
        } finally {
            ApiCall::create([
                'campaign_id' => $this->campaignId,
                'actor' => $this->actor,
                'transport' => $context->transport,
                'method' => $context->method,
                'endpoint' => $context->endpoint,
                'ad_account_id' => $context->adAccountId,
                'mutating' => $context->mutating,
                'dry_run' => $context->dryRun,
                'request' => $this->guardrails->scrub($context->request),
                'response' => $this->truncate($response),
                'status' => $error === null ? 200 : null,
                'ok' => $error === null,
                'object_id' => $response ? ObjectId::find($response) : null,
                'error' => $error ? mb_substr($error, 0, 900) : null,
                'duration_ms' => (int) round((microtime(true) - $startedAt) * 1000),
            ]);
        }
    }

    /** Keep enough of the response to diagnose a shape change, not all of it. */
    private function truncate(?array $response): ?array
    {
        return $response === null
            ? null
            : json_decode(mb_substr(json_encode($response), 0, 4000), true);
    }
}
