<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;

class Setting extends Model
{
    protected $fillable = ['key', 'value'];

    private static array $cache = [];

    public static function get(string $key, mixed $default = null): mixed
    {
        if (! array_key_exists($key, self::$cache)) {
            $row = static::where('key', $key)->first();
            self::$cache[$key] = $row ? $row->value : $default;
        }
        return self::$cache[$key];
    }

    public static function set(string $key, mixed $value): void
    {
        static::updateOrCreate(['key' => $key], ['value' => (string) $value]);
        self::$cache[$key] = (string) $value;
    }

    /** Returns the configured FFmpeg binary path, falling back to the config file default. */
    public static function ffmpegBinary(): string
    {
        return static::get('ffmpeg_binary', config('ffmpeg.ffmpeg', '/usr/bin/ffmpeg'));
    }

    /** Raw setting: is GPU encoding switched on in the admin panel? */
    public static function gpuEnabled(): bool
    {
        return static::get('gpu_enabled', 'true') === 'true';
    }

    public static function gpuDevice(): int
    {
        return (int) static::get('gpu_device', '0');
    }

    /**
     * Runtime gate used by every encode path: the GPU is switched on AND it is
     * actually reachable and able to encode right now. Unlike gpuEnabled() this
     * runs a live smoke-test (cached briefly) so a misconfigured / unplugged /
     * driver-mismatched GPU automatically routes work to the CPU instead of
     * producing a hung or failed encode.
     */
    public static function gpuUsable(): bool
    {
        if (! static::gpuEnabled()) {
            return false;
        }

        // Cache the smoke-test result briefly, but never let a cache backend problem
        // (e.g. an unwritable cache dir) break an encode — fall back to a direct probe.
        try {
            return (bool) Cache::remember('gpu_usable_probe', 60, fn () => static::probeGpu());
        } catch (\Throwable $e) {
            return static::probeGpu();
        }
    }

    /** Forget the cached probe result — call this whenever GPU settings change. */
    public static function flushGpuProbe(): void
    {
        try {
            Cache::forget('gpu_usable_probe');
        } catch (\Throwable $e) {
            // ignore — a missing/unwritable cache just means the next call re-probes
        }
    }

    /**
     * Smoke-test the GPU encode path. Two checks:
     *   1. the device is visible to the NVIDIA driver (nvidia-smi), and
     *   2. the encoder actually produces a frame.
     * Step 2 is the decisive one — it catches CUDA / driver-version mismatches
     * that nvidia-smi cannot see. Returns true only if the GPU can really encode.
     *
     * @param string|null $encoder Encoder to test; defaults to the configured one.
     */
    public static function probeGpu(?string $encoder = null): bool
    {
        $encoder = $encoder ?: static::get('gpu_encoder', 'h264_nvenc');
        $device  = static::gpuDevice();
        $isNvenc = str_contains($encoder, 'nvenc');

        // 1) Device visible to the driver? (catches unplugged card / unloaded module)
        if ($isNvenc) {
            @exec('nvidia-smi -i ' . (int) $device
                . ' --query-gpu=name --format=csv,noheader,nounits 2>/dev/null', $smi, $smiExit);
            if ($smiExit !== 0 || trim(implode('', $smi)) === '') {
                Log::warning('GPU probe: device not visible via nvidia-smi — using CPU', [
                    'device' => $device,
                ]);
                return false;
            }
        }

        // 2) Encoder actually works? (encode a single throwaway frame)
        $ffmpeg = static::ffmpegBinary();
        $tmp    = sys_get_temp_dir() . '/gpu_probe_' . getmypid() . '_' . uniqid() . '.mp4';
        $gpuArg = $isNvenc ? ' -gpu ' . (int) $device : '';

        // 256x144 is the smallest 16:9 size NVENC will accept (it rejects tiny frames
        // such as 128x72 with "Frame Dimension less than the minimum supported value").
        @exec(
            escapeshellcmd($ffmpeg)
            . ' -hide_banner -loglevel error'
            . ' -f lavfi -i color=c=black:s=256x144:r=1 -frames:v 1'
            . ' -c:v ' . escapeshellarg($encoder) . $gpuArg
            . ' -y ' . escapeshellarg($tmp) . ' 2>/dev/null',
            $o, $exit
        );

        $ok = ($exit === 0 && is_file($tmp) && filesize($tmp) > 0);
        @unlink($tmp);

        if (! $ok) {
            Log::warning('GPU probe: encode smoke-test failed — using CPU', [
                'encoder' => $encoder,
                'device'  => $device,
                'binary'  => $ffmpeg,
            ]);
        }

        return $ok;
    }

    public static function gpuEncoder(): string
    {
        return static::gpuUsable()
            ? static::get('gpu_encoder', 'h264_nvenc')
            : 'libx264';
    }

    public static function gpuPreset(): string
    {
        return static::gpuUsable()
            ? static::get('gpu_preset', 'p4')
            : 'fast';
    }

    public static function gpuHwaccel(): string
    {
        return static::get('gpu_hwaccel', 'cuda');
    }

    /** Returns the full video codec flags for FFmpeg shell commands. */
    public static function ffmpegVideoFlags(bool $stillImage = false): string
    {
        if (static::gpuUsable()) {
            $enc    = static::get('gpu_encoder', 'h264_nvenc');
            $preset = static::get('gpu_preset',  'p4');
            $device = static::gpuDevice();
            $gpuFlag = str_contains($enc, 'nvenc') ? " -gpu {$device}" : '';
            return "-c:v {$enc} -preset {$preset} -rc vbr -cq 23{$gpuFlag} -pix_fmt yuv420p";
        }
        $tune = $stillImage ? ' -tune stillimage' : '';
        return "-c:v libx264 -preset fast -crf 23{$tune} -pix_fmt yuv420p";
    }

    /** CPU-only video codec flags — used as automatic fallback when GPU encoding fails. */
    public static function ffmpegVideoFlagsCpu(bool $stillImage = false): string
    {
        $tune = $stillImage ? ' -tune stillimage' : '';
        return "-c:v libx264 -preset fast -crf 23{$tune} -pix_fmt yuv420p";
    }

    /** Returns hwaccel decode flags when the input source is a video file. */
    public static function ffmpegHwaccelFlags(bool $inputIsVideo): string
    {
        if (! $inputIsVideo || ! static::gpuUsable()) return '';
        $hwaccel = static::get('gpu_hwaccel', 'cuda');
        $device  = static::gpuDevice();
        return "-hwaccel {$hwaccel} -hwaccel_device {$device} ";
    }
}
