<?php

namespace App\Models\Concerns;

/**
 * Shared answers fall back to the brief when the campaign has none of its own.
 *
 * A brief holds what does not change by platform, and a campaign holds what
 * does. But the same field can legitimately live in both places: the brief says
 * fifty dollars a day, and each platform's campaign holds its own share of it.
 * The campaign wins when it has an answer, because a platform-specific value is
 * always more specific than the shared one.
 *
 * Done here, on the model, rather than at each place that reads a campaign.
 * These fields are read by the publish gate, the launch flow, the panel, the
 * publisher, the tools and the pending-changes comparison, and a fallback
 * applied in some of those and not others would mean the gate approving a value
 * the publisher then fails to send.
 *
 * Hooked into getAttribute() rather than declared as accessors, deliberately.
 * An `Attribute::get()` accessor is handed the *raw* database value and its
 * return replaces the cast, so `locations` came back as a JSON string and
 * `starts_at` as a plain string. Going through the parent first means both
 * sides keep their casts: the campaign's and the brief's.
 *
 * Writes are untouched. Setting a value always writes it to the campaign, and
 * `getRawOriginal()` still reports what the campaign itself holds.
 *
 * A campaign with no brief - an adopted one, pulled in from the platform - sees
 * none of this, because there is nothing to fall back to.
 */
trait ResolvesFromBrief
{
    /**
     * The fields a brief can answer on a campaign's behalf.
     *
     * Everything else is platform-specific and has no shared equivalent: an ad
     * account, a Facebook Page and a pixel mean nothing to Google.
     *
     * @var list<string>
     */
    public const SHARED_WITH_BRIEF = [
        'landing_url', 'locations', 'budget', 'budget_mode',
    ];

    /**
     * Three fields that were listed here and should not have been.
     *
     * currency belongs to the ad account, not the brief. A Meta account billing
     * in USD and a LinkedIn account billing in EUR are both correct, and
     * BudgetCeiling::for() reads this field to decide which ceiling applies, so
     * inheriting another platform's currency would pick the wrong limit on
     * somebody's money. It was only ever safe because nothing wrote it.
     *
     * starts_at and ends_at cannot use this mechanism at all, because a null
     * here has two meanings: never asked, and asked and answered "no end date".
     * The fallback fires on blank() and cannot tell them apart, so a campaign
     * deliberately set to run continuously would silently inherit the brief's
     * end date and stop on a day nobody chose. LaunchFlow tells them apart by
     * the recorded source, which lives in the workspace and not on the model.
     *
     * They are shared instead by SetSchedule, which applies the brief's answer
     * and records it, so the campaign ends up holding a real value and the
     * ambiguity never arises. Removing them here changes nothing today: all
     * three were declared shared and never written.
     *
     * @var list<string>
     */
    public const NOT_SHARED_ON_PURPOSE = ['currency', 'starts_at', 'ends_at'];

    /**
     * @param  string  $key
     */
    public function getAttribute($key): mixed
    {
        $value = parent::getAttribute($key);

        // Anything not shared, and anything the campaign answers for itself,
        // leaves here exactly as it always did. blank() rather than null:
        // an empty country list is not an answer to "where should this run".
        if (! in_array($key, self::SHARED_WITH_BRIEF, true) || ! blank($value)) {
            return $value;
        }

        // 'brief' is not a shared field, so this returns above rather than
        // recursing. Loaded only when a fallback is actually needed, so a
        // campaign that answers for itself never touches the briefs table.
        return $this->brief?->getAttribute($key) ?? $value;
    }
}
