<?php

namespace App\Support;

use Illuminate\Support\Facades\Cache;
use Illuminate\Database\QueryException;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;

/**
 * Runtime settings an operator can change from the console.
 *
 * Cached because the public-site gate reads one of these on every single
 * request; the cache is dropped on write, so a flip takes effect immediately
 * rather than "within a minute".
 */
final class Settings
{
    private const CACHE_PREFIX = 'app_setting:';

    public static function get(string $key, mixed $default = null): mixed
    {
        $cacheKey = self::CACHE_PREFIX.$key;

        if (Cache::has($cacheKey)) {
            $raw = Cache::get($cacheKey);
        } else {
            try {
                $row = DB::table('app_settings')->where('key', $key)->value('value');
            } catch (QueryException $e) {
                // The gate reads a setting on every request, so an unreachable
                // or not-yet-migrated table would take the whole site down —
                // including during a deploy where new code runs before migrate.
                // Fall back to the caller's default, but do NOT cache that:
                // caching a failure would keep a hidden site publicly visible
                // long after the database came back.
                Log::warning('app_settings unavailable, using default', [
                    'key' => $key, 'error' => $e->getMessage(),
                ]);

                return $default;
            }

            // Sentinel: distinguishes "stored null" from "never stored", so a
            // missing row does not get re-queried on every request.
            $raw = $row ?? '__missing__';
            Cache::forever($cacheKey, $raw);
        }

        return $raw === '__missing__' ? $default : json_decode($raw, true);
    }

    public static function set(string $key, mixed $value): void
    {
        DB::table('app_settings')->updateOrInsert(
            ['key' => $key],
            ['value' => json_encode($value), 'updated_at' => now(), 'created_at' => now()],
        );

        Cache::forget(self::CACHE_PREFIX.$key);
    }

    public static function bool(string $key, bool $default = false): bool
    {
        return (bool) self::get($key, $default);
    }

    /**
     * Remove a setting entirely, falling back to the caller's own default
     * from then on.
     *
     * Not `set($key, null)`: that would still leave a row (and a cache
     * entry) behind — indistinguishable to `get()`, but a migration's
     * `down()` undoing its own `up()` should leave no row, not a null one.
     * Deletes the row before dropping the cache, the same order `set()`
     * writes-then-invalidates in — a reader between the two statements sees
     * the OLD value from a cache that is about to be dropped, never a stale
     * one that outlives the row it described.
     */
    public static function forget(string $key): void
    {
        DB::table('app_settings')->where('key', $key)->delete();
        Cache::forget(self::CACHE_PREFIX.$key);
    }
}
