<?php

namespace App\Services;

use App\Models\LotterySetting;
use Illuminate\Support\Facades\Cache;

/**
 * 【配置中心】统一读入口：按 key 取运行期配置，默认带缓存。
 *
 * - 写入：控制台 / Seeder / 后续管理端可调 {@see put()}；
 * - 【重要】库里不存在的 key **不写入缓存**，避免长期命中默认值导致后台新增配置不生效。
 */
final class LotterySettings
{
    /** 仅由 config/.env 控制，后台不可写入；旧 DB 行不再生效。 */
    public const ENV_LOCKED_KEYS = [
        'currency.default_code',
        'draw.timezone',
        'draw.interval_minutes',
        'draw.betting_window_seconds',
        'draw.close_before_draw_seconds',
        'draw.buffer_draws_ahead',
    ];

    private const CURRENCY_DISPLAY_DECIMALS = 2;
    private const CURRENCY_DECIMAL_SEPARATOR = '.';
    private const CURRENCY_THOUSANDS_SEPARATOR = ',';

    public static function isEnvLocked(string $key): bool
    {
        return in_array($key, self::ENV_LOCKED_KEYS, true);
    }

    public static function defaultCurrency(): string
    {
        $value = (string) config('lottery.default_currency', 'NPR');

        return strtoupper(substr(trim($value), 0, 16));
    }

    public static function drawTimezone(): string
    {
        return (string) config('lottery.draw.timezone', 'UTC');
    }

    public static function drawIntervalMinutes(): int
    {
        return max(1, min(1440, (int) config('lottery.draw.interval_minutes', 5)));
    }

    public static function drawBufferDrawsAhead(): int
    {
        return max(1, (int) config('lottery.draw.buffer_draws_ahead', 8));
    }

    public static function drawBettingWindowSeconds(): int
    {
        return max(10, (int) config('lottery.draw.betting_window_seconds', 270));
    }

    public static function drawCloseBeforeDrawSeconds(): int
    {
        return max(5, (int) config('lottery.draw.close_before_draw_seconds', 30));
    }

    /**
     * 后台「部署参数」只读快照：供运营查看当前生效值与对应 env 变量名。
     *
     * @return array{
     *   items: list<array{key: string, label_key: string, value: mixed, env_var: string}>
     * }
     */
    public static function deploymentSnapshot(): array
    {
        return [
            'items' => [
                [
                    'key' => 'currency.default_code',
                    'label_key' => 'default_currency',
                    'value' => self::defaultCurrency(),
                    'env_var' => 'LOTTERY_DEFAULT_CURRENCY',
                ],
                [
                    'key' => 'draw.timezone',
                    'label_key' => 'draw_timezone',
                    'value' => self::drawTimezone(),
                    'env_var' => 'LOTTERY_DRAW_TIMEZONE',
                ],
                [
                    'key' => 'draw.interval_minutes',
                    'label_key' => 'draw_interval_minutes',
                    'value' => self::drawIntervalMinutes(),
                    'env_var' => 'LOTTERY_DRAW_INTERVAL_MINUTES',
                ],
                [
                    'key' => 'draw.betting_window_seconds',
                    'label_key' => 'draw_betting_window_seconds',
                    'value' => self::drawBettingWindowSeconds(),
                    'env_var' => 'LOTTERY_DRAW_BETTING_WINDOW_SECONDS',
                ],
                [
                    'key' => 'draw.close_before_draw_seconds',
                    'label_key' => 'draw_close_before_draw_seconds',
                    'value' => self::drawCloseBeforeDrawSeconds(),
                    'env_var' => 'LOTTERY_DRAW_CLOSE_BEFORE_SECONDS',
                ],
                [
                    'key' => 'draw.buffer_draws_ahead',
                    'label_key' => 'draw_buffer_draws_ahead',
                    'value' => self::drawBufferDrawsAhead(),
                    'env_var' => 'LOTTERY_DRAW_BUFFER_AHEAD',
                ],
            ],
        ];
    }

    public static function drawRequireManualReview(): bool
    {
        $fallback = (bool) config('lottery.draw.require_manual_review', true);

        return (bool) self::get('draw.require_manual_review', $fallback);
    }

    public static function drawCooldownMinutes(): int
    {
        $fallback = (int) config('lottery.draw.cooldown_minutes', 15);

        return max(0, (int) self::get('draw.cooldown_minutes', $fallback));
    }

    public static function currencyDisplayDecimals(): int
    {
        return self::CURRENCY_DISPLAY_DECIMALS;
    }

    public static function currencyDecimalSeparator(): string
    {
        return self::CURRENCY_DECIMAL_SEPARATOR;
    }

    public static function currencyThousandsSeparator(): string
    {
        return self::CURRENCY_THOUSANDS_SEPARATOR;
    }

    public static function cacheTtlSeconds(): int
    {
        return max(5, (int) config('lottery.settings.cache_ttl_seconds', 60));
    }

    /** 取单个配置；若无行则返回 $default（不写缓存）。 */
    public static function get(string $key, mixed $default = null): mixed
    {
        $cacheKey = self::cacheKey($key);
        if (Cache::has($cacheKey)) {
            return Cache::get($cacheKey);
        }

        /** @var LotterySetting|null $row */
        $row = LotterySetting::query()->where('setting_key', $key)->first();
        if ($row === null) {
            return $default;
        }

        $value = self::normalizeValue($row->value_json);
        Cache::put($cacheKey, $value, self::cacheTtlSeconds());

        return $value;
    }

    /**
     * 批量读取（逐项走 get；已存在的键会受益于缓存）。
     *
     * @param  array<string, mixed>  $keysToDefault  key => default
     * @return array<string, mixed>
     */
    public static function many(array $keysToDefault): array
    {
        $out = [];
        foreach ($keysToDefault as $key => $def) {
            $out[$key] = self::get($key, $def);
        }

        return $out;
    }

    /** 删单 key 缓存；写入 lottery_settings 后务必调用（或运维 `php artisan cache:clear`） */
    public static function forgetKey(string $key): void
    {
        Cache::forget(self::cacheKey($key));
    }

    /**
     * 写入或更新一行配置并刷新该 key 的缓存。（Seeder / 后续管理端共用）
     */
    public static function put(
        string $key,
        mixed $value,
        string $groupName = 'general',
        ?string $descriptionZh = null,
    ): void {
        LotterySetting::query()->updateOrCreate(
            ['setting_key' => $key],
            [
                'value_json' => $value,
                'group_name' => $groupName,
                'description_zh' => $descriptionZh,
            ],
        );
        self::forgetKey($key);
        // 写入后立即预热缓存，下一次 get 可走 Cache::has
        Cache::put(self::cacheKey($key), self::normalizeValue($value), self::cacheTtlSeconds());
    }

    /**
     * 批量写入（管理端一次保存多块配置，避免 N 次 HTTP + 审计）。
     *
     * @param  list<array{key: string, value: mixed}>  $items
     */
    public static function putMany(array $items): void
    {
        if ($items === []) {
            return;
        }

        $keys = array_values(array_unique(array_map(
            static fn (array $item): string => (string) $item['key'],
            $items,
        )));

        /** @var array<string, LotterySetting> $existing */
        $existing = LotterySetting::query()
            ->whereIn('setting_key', $keys)
            ->get()
            ->keyBy('setting_key')
            ->all();

        foreach ($items as $item) {
            $key = (string) $item['key'];
            $value = $item['value'];
            $row = $existing[$key] ?? null;

            self::put(
                $key,
                $value,
                $row ? (string) $row->group_name : (explode('.', $key)[0] ?? 'general'),
                $row ? $row->description_zh : null,
            );
        }
    }

    public static function cacheKey(string $key): string
    {
        return 'lottery_settings:'.$key;
    }

    private static function normalizeValue(mixed $value): mixed
    {
        return $value;
    }
}
