<?php

namespace App\Models;

use Carbon\Carbon;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Stancl\Tenancy\Database\Concerns\CentralConnection;

/**
 * Tracks, per tenant + AutoTrader advertiser (location), whether we currently
 * have access to fetch stock. Failures are recorded during the stock sync and
 * surfaced by the CheckAutoTraderLocationAccess command on a Fibonacci cadence.
 */
class AutoTraderLocationStatus extends Model
{
    use CentralConnection;

    /**
     * Gap (in days) to wait between successive alerts for a still-failing
     * location, indexed by how many alerts have already been sent. The gap
     * grows so a long-standing issue is not re-reported every day; it caps
     * at the final value once exhausted.
     */
    public const FIBONACCI_GAP_DAYS = [1, 1, 2, 3, 5, 8, 13];

    protected $fillable = [
        'tenant_id',
        'advertiser_id',
        'last_success_at',
        'last_failure_at',
        'consecutive_failures',
        'last_alerted_at',
        'alert_count',
        'last_error',
    ];

    protected $casts = [
        'last_success_at' => 'datetime',
        'last_failure_at' => 'datetime',
        'last_alerted_at' => 'datetime',
        'consecutive_failures' => 'integer',
        'alert_count' => 'integer',
    ];

    public function scopeForTenant(Builder $query, string $tenantId): Builder
    {
        return $query->where('tenant_id', $tenantId);
    }

    public function scopeFailing(Builder $query): Builder
    {
        return $query->where('consecutive_failures', '>', 0);
    }

    /**
     * Record the outcome of an access check for a location. Only definitive
     * signals move the state: a 2xx is a success (and a recovery), a 401/403
     * is an access failure. Transient errors (5xx, connection issues) are
     * ignored so they don't trigger or reset access alerts.
     */
    public static function recordCheck(string $tenantId, string $advertiserId, int $status): void
    {
        if ($status >= 200 && $status < 300) {
            self::recordSuccess($tenantId, $advertiserId);
        } elseif (in_array($status, [401, 403], true)) {
            self::recordAccessFailure($tenantId, $advertiserId, $status);
        }
    }

    public static function recordSuccess(string $tenantId, string $advertiserId): void
    {
        $record = self::lookup($tenantId, $advertiserId);
        $record->fill([
            'last_success_at' => Carbon::now(),
            'consecutive_failures' => 0,
            'last_error' => null,
            'last_alerted_at' => null,
            'alert_count' => 0,
        ])->save();
    }

    public static function recordAccessFailure(string $tenantId, string $advertiserId, int $status): void
    {
        $record = self::lookup($tenantId, $advertiserId);
        $record->fill([
            'last_failure_at' => Carbon::now(),
            'consecutive_failures' => $record->consecutive_failures + 1,
            'last_error' => "HTTP {$status} - no access to advertiser {$advertiserId}",
        ])->save();
    }

    /**
     * Whether a failing location is due for an alert under the Fibonacci gate.
     */
    public function isDueForAlert(): bool
    {
        if ($this->consecutive_failures < 1) {
            return false;
        }

        if ($this->last_alerted_at === null) {
            return true;
        }

        $gap = self::FIBONACCI_GAP_DAYS[min($this->alert_count, count(self::FIBONACCI_GAP_DAYS) - 1)];

        return Carbon::now()->greaterThanOrEqualTo($this->last_alerted_at->copy()->addDays($gap));
    }

    public function markAlerted(): void
    {
        $this->fill([
            'last_alerted_at' => Carbon::now(),
            'alert_count' => $this->alert_count + 1,
        ])->save();
    }

    private static function lookup(string $tenantId, string $advertiserId): self
    {
        return self::query()->firstOrNew([
            'tenant_id' => $tenantId,
            'advertiser_id' => $advertiserId,
        ]);
    }
}
