<?php

namespace App\Services\Traffic;

use App\Models\Instance;
use App\Models\InstanceTraffic;

/**
 * What a customer has used this month, and what that means.
 *
 * The quota comes from the plan plus whatever traffic add-ons were bought; the
 * usage is the outbound total the collector accumulates. Everything the portal,
 * the console and the throttling decision need is derived here, so those three
 * can never disagree about whether someone is over their limit.
 */
final readonly class TrafficMeter
{
    private function __construct(
        public Instance $instance,
        public int $usedBytes,
        public int $quotaBytes,
        public bool $throttled,
    ) {}

    public static function for(Instance $instance, ?InstanceTraffic $row = null): self
    {
        $row ??= InstanceTraffic::query()
            ->where('instance_id', $instance->id)
            ->where('period', InstanceTraffic::currentPeriod())
            ->first();

        return new self(
            instance: $instance,
            usedBytes: (int) ($row?->tx_bytes ?? 0),
            quotaBytes: self::quotaGb($instance) * 1000 ** 3,
            throttled: (bool) ($row?->throttled ?? false),
        );
    }

    /**
     * Plan allowance plus purchased add-on packs.
     *
     * The allowance is the contract's, not the catalogue's: cutting a plan's
     * traffic must not start throttling someone who bought the larger one. An
     * instance with no contract has nothing to meter against and is treated as
     * unmetered rather than as exhausted — a machine we cannot price should not
     * be throttled on a guess.
     */
    public static function quotaGb(Instance $instance): int
    {
        $plan = (int) ($instance->subscription?->traffic_gb ?? 0);
        $addonGb = (int) config('provisioning.traffic.addon.gb', 1000);

        return $plan + ($instance->traffic_addons ?? 0) * $addonGb;
    }

    public function percent(): float
    {
        if ($this->quotaBytes <= 0) {
            return 0.0;   // an unmetered plan is never "at" anything
        }

        return round($this->usedBytes / $this->quotaBytes * 100, 1);
    }

    public function remainingBytes(): int
    {
        return max(0, $this->quotaBytes - $this->usedBytes);
    }

    public function isExhausted(): bool
    {
        return $this->quotaBytes > 0 && $this->usedBytes >= $this->quotaBytes;
    }

    /**
     * ok → normal, warn → 80 %, critical → 95 %, exhausted → throttled.
     * Thresholds are config so the portal wording and the collector's decision
     * cannot drift apart.
     */
    public function state(): string
    {
        if ($this->isExhausted()) {
            return 'exhausted';
        }

        [$warn, $critical] = array_pad((array) config('provisioning.traffic.warn_percent', [80, 95]), 2, 95);
        $percent = $this->percent();

        return match (true) {
            $percent >= $critical => 'critical',
            $percent >= $warn => 'warn',
            default => 'ok',
        };
    }

    /** Days left in the billing month — "resets in 6 days" beats a raw date. */
    public function daysUntilReset(): int
    {
        return (int) ceil(now()->diffInDays(now()->endOfMonth(), absolute: true));
    }
}
