<?php

namespace App\Http\Controllers;

use App\Jobs\CompressVideoJob;
use App\Jobs\NasSyncVideoJob;
use App\Mail\NewVideoNotification;
use App\Mail\VideoUploaded;
use App\Notifications\NewVideoUploaded as NewVideoUploadedNotification;
use App\Models\AuditLog;
use App\Models\Playlist;
use App\Models\Setting;
use App\Models\Video;
use App\Models\VideoSlide;
use App\Services\GeoIpService;
use FFMpeg\FFMpeg;
use FFMpeg\FFProbe;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;

class VideoController extends Controller
{
    public function __construct()
    {
        $this->middleware('auth')->except(['index', 'show', 'search', 'stream', 'hls', 'trending', 'shorts', 'download', 'downloadMp3', 'recordShare', 'ogImage', 'accessShare', 'showByToken', 'recommendations', 'slideshowProgress']);
    }

    public function index()
    {
        $filter = request('filter', 'all');

        // ── Playlists-only browse ──────────────────────────────────
        if ($filter === 'playlists') {
            $playlists = Playlist::where('visibility', 'public')
                ->withCount('videos')
                ->with('user')
                ->latest()
                ->limit(60)
                ->get();
            return view('videos.index', compact('playlists', 'filter') + [
                'videos' => collect(), 'shorts' => collect(), 'matches' => collect(),
            ]);
        }

        // ── Shorts-only browse ────────────────────────────────────
        if ($filter === 'shorts') {
            $videos = Video::public()->shorts()->latest()->limit(60)->get();
            return view('videos.index', compact('videos', 'filter') + [
                'shorts' => collect(), 'matches' => collect(), 'playlists' => collect(),
            ]);
        }

        // ── Sports/Matches-only browse ────────────────────────────
        if ($filter === 'match') {
            $videos = Video::public()->where('type', 'match')->notShorts()->latest()->limit(60)->get();
            return view('videos.index', compact('videos', 'filter') + [
                'shorts' => collect(), 'matches' => collect(), 'playlists' => collect(),
            ]);
        }

        // ── Filtered single-type views ────────────────────────────
        if (in_array($filter, ['music', 'latest'])) {
            $query = Video::public()->notShorts();
            if ($filter === 'music') $query->where('type', 'music');
            else $query->latest();
            $videos = $query->limit(50)->get();
            return view('videos.index', compact('videos', 'filter') + [
                'shorts' => collect(), 'matches' => collect(), 'playlists' => collect(),
            ]);
        }

        // ── All (home) — mixed chronological feed ─────────────────
        $videos = Video::public()->latest()->limit(60)->get();

        $playlistQuery = Playlist::withCount('videos')->with('user')->latest()->limit(40);
        $playlistQuery->where('visibility', 'public');

        $playlists = $playlistQuery->where('is_default', false)->get()
            ->filter(fn($pl) => $pl->videos_count > 0)
            ->values();

        // Tag each item with its kind so the view can pick the right card
        $feedItems = $videos->map(fn($v) => ['kind' => 'video', 'item' => $v, 'date' => $v->created_at])
            ->concat($playlists->map(fn($p) => ['kind' => 'playlist', 'item' => $p, 'date' => $p->created_at]))
            ->sortByDesc('date')
            ->values();

        return view('videos.index', compact('feedItems', 'filter') + [
            'videos' => collect(), 'shorts' => collect(), 'matches' => collect(), 'playlists' => collect(),
        ]);
    }

    public function search(Request $request)
    {
        $query = $request->get('q', '');

        if (empty($query)) {
            return redirect()->route('videos.index');
        }

        $matchCondition = function ($q) use ($query) {
            $q->where('title', 'like', "%{$query}%")
              ->orWhere('description', 'like', "%{$query}%");
        };

        $allVideos = Video::public()->where($matchCondition)->latest()->get();
        $videos = $allVideos->where('is_shorts', false)->values();
        $shorts = $allVideos->where('is_shorts', true)->values();

        $playlistQuery = Playlist::where('name', 'like', "%{$query}%")
            ->withCount('videos')
            ->with('user')
            ->latest()
            ->limit(12);

        $playlistQuery->where('visibility', 'public');

        $playlists = $playlistQuery->get();

        return view('videos.index', compact('videos', 'shorts', 'query', 'playlists'));
    }

    public function create()
    {
        return view('videos.create');
    }

    public function store(Request $request)
    {
        try {
        $audioExtensions = ['mp3', 'm4a', 'aac', 'flac', 'wav'];
        $uploadedExt = strtolower($request->file('video')?->getClientOriginalExtension() ?? '');
        $isAudioUpload = in_array($uploadedExt, $audioExtensions);

        $request->validate([
            'title'       => 'required|string|max:255',
            'description' => 'nullable|string',
            'video'       => $isAudioUpload
                ? 'required|file|max:512000'
                : 'required|file|mimes:mp4,webm,ogg,mov,avi,wmv,flv,mkv|max:512000',
            'thumbnail'   => $isAudioUpload
                ? 'nullable|image|mimes:jpg,jpeg,png,webp|max:20480'
                : 'nullable|image|mimes:jpg,jpeg,png,webp|max:20480',
            'slides'      => $isAudioUpload ? 'required|array|min:1' : 'nullable',
            'slides.*'    => 'image|mimes:jpg,jpeg,png,webp|max:20480',
            'visibility'      => 'nullable|in:public,unlisted,private',
            'type'            => 'nullable|in:generic,music,match',
            'download_access' => 'nullable|in:disabled,everyone,registered,subscribers',
        ]);

        $videoFile = $request->file('video');
        $filename = self::generateFilename($videoFile->getClientOriginalExtension());
        $path = $videoFile->storeAs('public/videos', $filename);

        // Get file info
        $fileSize = $videoFile->getSize();
        $mimeType = $videoFile->getMimeType();

        $thumbnailPath = null;
        $slideFiles    = [];

        if ($isAudioUpload && $request->hasFile('slides')) {
            // Audio upload: save all slides; first slide becomes the thumbnail
            foreach ($request->file('slides') as $slideFile) {
                $fname = self::generateFilename($slideFile->getClientOriginalExtension());
                $slideFile->storeAs('public/thumbnails', $fname);
                $slideFiles[] = $fname;
            }
            $thumbnailPath = 'public/thumbnails/' . $slideFiles[0];
        } elseif ($request->hasFile('thumbnail')) {
            $thumbFilename = self::generateFilename($request->file('thumbnail')->getClientOriginalExtension());
            $thumbnailPath = $request->file('thumbnail')->storeAs('public/thumbnails', $thumbFilename);
        } elseif (! $isAudioUpload) {
            try {
                $ffmpeg = FFMpeg::create();
                $videoPath = storage_path('app/'.$path);

                if (file_exists($videoPath)) {
                    $video = $ffmpeg->open($videoPath);
                    $frame = $video->frame(\FFMpeg\Coordinate\TimeCode::fromSeconds(1));

                    $thumbFilename = Str::uuid().'.jpg';
                    $thumbFullPath = storage_path('app/public/thumbnails/'.$thumbFilename);

                    if (! file_exists(storage_path('app/public/thumbnails'))) {
                        mkdir(storage_path('app/public/thumbnails'), 0755, true);
                    }

                    $frame->save($thumbFullPath);
                    $thumbnailPath = 'public/thumbnails/'.$thumbFilename;
                }
            } catch (\Exception $e) {
                \Log::error('FFmpeg thumbnail error: '.$e->getMessage());
            }
        }

        $width = null;
        $height = null;
        $orientation = 'landscape';

        if (! $isAudioUpload) {
            try {
                $ffprobe = FFProbe::create();
                $videoPath = storage_path('app/'.$path);

                if (file_exists($videoPath)) {
                    $streams = $ffprobe->streams($videoPath);
                    $videoStream = $streams->videos()->first();

                    if ($videoStream) {
                        $width = $videoStream->get('width');
                        $height = $videoStream->get('height');

                        if ($width && $height) {
                            if ($height > $width) $orientation = 'portrait';
                            elseif ($width > $height) $orientation = 'landscape';
                            else $orientation = 'square';
                        }
                    }
                }
            } catch (\Exception $e) {
                \Log::error('FFprobe error: '.$e->getMessage());
            }
        }

        $video = Video::create([
            'user_id' => Auth::id(),
            'title' => $request->title,
            'description' => $request->description,
            'filename' => $filename,
            'path' => $path,
            'thumbnail' => $thumbnailPath ? basename($thumbnailPath) : null,
            'size' => $fileSize,
            'mime_type' => $mimeType,
            'orientation' => $orientation,
            'width' => $width,
            'height' => $height,
            'status' => $isAudioUpload ? 'ready' : 'processing',
            'visibility'      => $request->visibility ?? 'public',
            'type'            => $isAudioUpload ? 'music' : ($request->type ?? 'generic'),
            'download_access' => $request->input('download_access', 'disabled'),
            'share_token'     => Str::random(32),
        ]);

        // Save individual slide records for audio uploads with multiple images
        foreach ($slideFiles as $position => $fname) {
            VideoSlide::create([
                'video_id' => $video->id,
                'filename' => $fname,
                'position' => $position,
            ]);
        }

        $nas = app(\App\Services\NasSyncService::class);

        if ($nas->isEnabled()) {
            // ── NAS-primary: push directly to NAS, delete local temp files ──
            try {
                $video->load('slides');

                // Build slide abs-paths map for audio uploads
                $slideAbsPaths = [];
                foreach ($slideFiles as $pos => $fname) {
                    $slideAbsPaths[$pos] = storage_path('app/public/thumbnails/' . $fname);
                }

                $tempThumbAbs = $thumbnailPath ? storage_path('app/' . $thumbnailPath) : null;
                // For audio, thumbnail IS slide 0 — don't pass separately (handled via slides)
                if ($isAudioUpload) $tempThumbAbs = null;

                $nas->uploadDirectToNas(
                    $video,
                    storage_path('app/' . $path),
                    $tempThumbAbs,
                    $slideAbsPaths
                );
                $video->refresh();
            } catch (\Throwable $e) {
                \Log::error('uploadDirectToNas failed: ' . $e->getMessage());
            }

            // For non-audio: HLS generation still runs (downloads from NAS, keeps HLS local)
            if (! $isAudioUpload) {
                \App\Jobs\GenerateHlsJob::dispatch($video->fresh())
                    ->onQueue('video-processing')
                    ->onConnection('database');
            }
        } else {
            // ── Local storage: move into NAS-mirrored local directory schema ──
            try {
                $video->load('slides');
                $nas->organizeLocalFiles($video);
                $video->refresh();
            } catch (\Throwable $e) {
                \Log::error('organizeLocalFiles failed: ' . $e->getMessage());
            }

            // Compress + HLS pipeline for local storage
            if (! $isAudioUpload) {
                CompressVideoJob::dispatch($video)
                    ->onQueue('video-processing')
                    ->onConnection('database');
            }
        }

        $video->load('user');

        $userEmail = Auth::user()->email;
        $userName  = Auth::user()->name;

        $uploader         = Auth::user();
        $subscribers      = $uploader->subscribers()->get();
        $subscriberEmails = $subscribers->pluck('email')->toArray();

        // In-app DB notifications (fast — just inserts, no network)
        if ($video->visibility === 'public') {
            foreach ($subscribers as $subscriber) {
                try {
                    $subscriber->notify(new NewVideoUploadedNotification($video, $uploader));
                } catch (\Throwable $e) {
                    \Log::error('In-app notification error: '.$e->getMessage());
                }
            }
        }

        app()->terminating(function () use ($video, $userEmail, $userName, $uploader, $subscriberEmails) {
            // Confirm upload to the uploader
            try {
                Mail::to($userEmail)->send(new VideoUploaded($video, $userName));
            } catch (\Throwable $e) {
                \Log::error('Email error: '.$e->getMessage());
            }

            // Email subscribers (only for public videos)
            if ($video->visibility === 'public' && count($subscriberEmails) > 0) {
                foreach ($subscriberEmails as $email) {
                    try {
                        Mail::to($email)->send(new NewVideoNotification($video, $uploader));
                    } catch (\Throwable $e) {
                        \Log::error('Subscriber notification error: '.$e->getMessage());
                    }
                }
            }
        });

        AuditLog::record('video.uploaded', [
            'subject_type'  => 'Video',
            'subject_id'    => (string) $video->id,
            'subject_label' => $video->title,
            'details'       => ['type' => $video->type, 'visibility' => $video->visibility],
        ]);

        return response()->json([
            'success' => true,
            'redirect' => route('videos.show', $video),
        ]);
        } catch (\Throwable $e) {
            \Log::error('Video store error: ' . $e->getMessage() . "\n" . $e->getTraceAsString());
            return response()->json([
                'success' => false,
                'message' => $e->getMessage(),
            ], 500);
        }
    }

    public function showByToken(Request $request, string $token)
    {
        $video = Video::where('share_token', $token)->firstOrFail();
        if (! $video->canView(Auth::user())) {
            abort(404);
        }
        return $this->show($request, $video);
    }

    public function show(Request $request, Video $video)
    {
        if (! $video->canView(Auth::user())) {
            $message = $video->isPrivate()
                ? 'This video is private.'
                : 'This video is no longer available.';
            return redirect('/')->with('toast_error', $message);
        }

        $ip  = $request->header('CF-Connecting-IP')
            ?? $request->header('X-Real-IP')
            ?? $request->ip();
        $geo = GeoIpService::lookup($ip);

        if (Auth::check()) {
            $exists = \DB::table('video_views')
                ->where('user_id', Auth::id())
                ->where('video_id', $video->id)
                ->where('watched_at', '>', now()->subHour())
                ->exists();

            if (! $exists) {
                \DB::table('video_views')->insert([
                    'user_id'      => Auth::id(),
                    'video_id'     => $video->id,
                    'ip_address'   => $ip,
                    'country'      => $geo['country'],
                    'country_name' => $geo['country_name'],
                    'watched_at'   => now(),
                ]);
            }
        } else {
            // Guest: deduplicate by IP within the last hour
            $exists = \DB::table('video_views')
                ->whereNull('user_id')
                ->where('video_id', $video->id)
                ->where('ip_address', $ip)
                ->where('watched_at', '>', now()->subHour())
                ->exists();

            if (! $exists) {
                \DB::table('video_views')->insert([
                    'user_id'      => null,
                    'video_id'     => $video->id,
                    'ip_address'   => $ip,
                    'country'      => $geo['country'],
                    'country_name' => $geo['country_name'],
                    'watched_at'   => now(),
                ]);
            }
        }

        $video->load(['comments.user', 'comments.replies.user', 'matchRounds.points', 'coachReviews', 'slides']);

        $playlist = null;
        $nextVideo = null;
        $previousVideo = null;
        $playlistVideos = null;

        $playlistParam = $request->query('playlist');
        if ($playlistParam) {
            $playlist = Playlist::where('share_token', $playlistParam)->first();
            if ($playlist && $playlist->canViewViaToken(Auth::user())) {
                $nextVideo = $playlist->getNextVideo($video);
                $previousVideo = $playlist->getPreviousVideo($video);
                $playlistVideos = $playlist->videos;
            }
        }

        $recommendedVideos = Video::public()
            ->where('id', '!=', $video->id)
            ->latest()
            ->limit(20)
            ->get();

        $view = match ($video->type) {
            'match' => 'videos.types.match',
            'music' => 'videos.types.music',
            default => 'videos.types.generic',
        };

        // Set persistent device-ID cookie used for share-link dedup
        $did = $request->cookie('_did') ?: (string) Str::uuid();

        return response()
            ->view($view, compact('video', 'playlist', 'nextVideo', 'previousVideo', 'recommendedVideos', 'playlistVideos'))
            ->header('Cache-Control', 'no-store, no-cache, must-revalidate')
            ->withCookie(cookie('_did', $did, 60 * 24 * 365 * 5));
    }

    public function playerData(Video $video, Request $request)
    {
        if (! $video->canView(Auth::user())) {
            abort(403);
        }

        $coverUrl = $video->thumbnail
            ? route('media.thumbnail', $video->thumbnail)
            : asset('storage/images/logo.png');

        $slides = $video->slides->count() > 1
            ? $video->slides->map(fn ($s) => route('media.thumbnail', $s->filename))->values()->all()
            : [];

        return response()->json([
            'id'         => $video->id,
            'key'        => $video->getRouteKey(),
            'type'       => $video->type,
            'has_hls'    => (bool) $video->has_hls,
            'hls_url'    => $video->has_hls ? route('videos.hls', ['video' => $video, 'file' => 'master.m3u8']) : null,
            'stream_url' => route('videos.stream', $video),
            'cover_url'  => $coverUrl,
            'slides'     => $slides,
            'title'      => $video->title,
            'author'     => $video->user->name ?? '',
            'duration'   => $video->duration,
        ]);
    }

    public function matchData(Video $video)
    {
        if (! $video->canView(Auth::user())) {
            abort(403);
        }

        return response()->json([
            'success' => true,
            'rounds' => $video->matchRounds()->with('points')->orderBy('round_number')->get(),
            'reviews' => $video->coachReviews()->orderBy('start_time_seconds')->get(),
        ]);
    }

    public function edit(Video $video, Request $request)
    {
        if (Auth::id() !== $video->user_id) {
            abort(403);
        }

        if (! $request->expectsJson() && ! $request->ajax()) {
            return view('videos.edit', compact('video'));
        }

        $slides = $video->slides()->orderBy('position')->get()->map(fn($s) => [
            'id'  => $s->id,
            'url' => $s->url,
        ])->values();

        return response()->json([
            'success' => true,
            'video' => [
                'id'             => $video->id,
                'title'          => $video->title,
                'description'    => $video->description,
                'thumbnail'      => $video->thumbnail,
                'thumbnail_url'  => $video->thumbnail_url,
                'visibility'     => $video->visibility ?? 'public',
                'type'           => $video->type ?? 'generic',
                'download_access' => $video->download_access,
                'is_audio'       => $this->isAudioOnlyFile($video),
                'slides'         => $slides,
            ],
        ]);
    }

    public function update(Request $request, Video $video)
    {
        if (Auth::id() !== $video->user_id) {
            abort(403);
        }

        $request->validate([
            'title'           => 'required|string|max:255',
            'description'     => 'nullable|string',
            'thumbnail'       => 'nullable|image|mimes:jpg,jpeg,png,webp|max:20480',
            'visibility'      => 'nullable|in:public,unlisted,private',
            'type'            => 'nullable|in:generic,music,match',
            'download_access' => 'nullable|in:disabled,everyone,registered,subscribers',
            'slides_add'      => 'nullable|array',
            'slides_add.*'    => 'image|mimes:jpg,jpeg,png,webp|max:20480',
        ]);

        $oldTitle = $video->title;

        $data = $request->only(['title', 'description', 'visibility', 'type']);
        $data['download_access'] = $request->input('download_access', 'disabled');

        if ($request->hasFile('thumbnail')) {
            if ($video->thumbnail) {
                Storage::delete($video->thumbnailStorageKey());
            }
            if (str_starts_with($video->path, 'users/')) {
                // New-format video: store thumbnail in the video's local dir
                $nas      = app(\App\Services\NasSyncService::class);
                $localDir = $nas->localVideoDir($video);
                @mkdir($localDir, 0755, true);
                $ext      = $request->file('thumbnail')->getClientOriginalExtension();
                $request->file('thumbnail')->move($localDir, "thumb.{$ext}");
                $userSlug = $nas->userSlug($video->user);
                $data['thumbnail'] = 'users/' . $userSlug . '/videos/' . basename($localDir) . "/thumb.{$ext}";
            } else {
                // Legacy video: push thumbnail directly to NAS
                $nas          = app(\App\Services\NasSyncService::class);
                $thumbFilename = self::generateFilename($request->file('thumbnail')->getClientOriginalExtension());
                $request->file('thumbnail')->storeAs('public/thumbnails', $thumbFilename);
                $tempAbs      = storage_path('app/public/thumbnails/' . $thumbFilename);
                $nasDir       = $nas->resolveVideoDir($video);
                $ext          = pathinfo($thumbFilename, PATHINFO_EXTENSION);
                $nas->mkdirp($nasDir);
                $nas->putFile($tempAbs, "{$nasDir}/thumb.{$ext}");
                @unlink($tempAbs);
                $data['thumbnail'] = "{$nasDir}/thumb.{$ext}";
            }
        }

        if (! isset($data['visibility'])) {
            unset($data['visibility']);
        }

        // Handle slide reorder / removal / additions for audio tracks
        $slidesChanged = false;
        if ($this->isAudioOnlyFile($video)) {
            // slides_order is a JSON array of kept slide IDs in their new order
            $keptOrder = json_decode($request->input('slides_order', '[]'), true) ?: [];

            // Delete slides not in the kept list
            $video->slides()->whereNotIn('id', $keptOrder)->each(function ($slide) use (&$slidesChanged) {
                Storage::delete($slide->storageKey());
                $slide->delete();
                $slidesChanged = true;
            });

            // Reorder kept slides
            foreach ($keptOrder as $pos => $slideId) {
                VideoSlide::where('id', $slideId)->where('video_id', $video->id)
                    ->update(['position' => $pos]);
            }
            if (! empty($keptOrder)) $slidesChanged = true;

            // Add new slides
            if ($request->hasFile('slides_add')) {
                $nextPos = count($keptOrder);
                $isNewFormat = str_starts_with($video->path, 'users/');
                $nasForSlides = app(\App\Services\NasSyncService::class);
                if ($isNewFormat) {
                    $localDir = $nasForSlides->localVideoDir($video);
                    @mkdir("{$localDir}/slides", 0755, true);
                    $userSlug = $nasForSlides->userSlug($video->user);
                    $relDir   = 'users/' . $userSlug . '/videos/' . basename($localDir);
                }
                foreach ($request->file('slides_add') as $file) {
                    if ($isNewFormat) {
                        // Create placeholder record to get the auto-increment ID
                        $slide = VideoSlide::create(['video_id' => $video->id, 'filename' => '__pending__', 'position' => $nextPos]);
                        $ext   = $file->getClientOriginalExtension();
                        $file->move("{$localDir}/slides", "{$slide->id}.{$ext}");
                        $slide->update(['filename' => "{$relDir}/slides/{$slide->id}.{$ext}"]);
                    } else {
                        // Legacy video: push slide directly to NAS
                        $fname   = self::generateFilename($file->getClientOriginalExtension());
                        $file->storeAs('public/thumbnails', $fname);
                        $tempAbs = storage_path('app/public/thumbnails/' . $fname);
                        $nasDir  = $nasForSlides->resolveVideoDir($video);
                        $nasForSlides->mkdirp("{$nasDir}/slides");
                        $ext     = pathinfo($fname, PATHINFO_EXTENSION);
                        $slide   = VideoSlide::create(['video_id' => $video->id, 'filename' => '__pending__', 'position' => $nextPos]);
                        $nasSlide = "{$nasDir}/slides/{$slide->id}.{$ext}";
                        $nasForSlides->putFile($tempAbs, $nasSlide);
                        @unlink($tempAbs);
                        $slide->update(['filename' => $nasSlide]);
                    }
                    $nextPos++;
                    $slidesChanged = true;
                }
            }

            // Keep thumbnail in sync with the first slide
            $firstSlide = $video->slides()->orderBy('position')->first();
            if ($firstSlide) {
                $data['thumbnail'] = $firstSlide->filename;
            }

            // Invalidate cached slideshow video whenever slides change
            if ($slidesChanged) {
                @unlink(storage_path('app/public/slideshow/' . $video->id . '_slideshow.mp4'));
                $data['slideshow_video_path'] = null;
            }
        }

        $video->update($data);

        // If the title changed, rename the NAS/local folder before syncing so
        // the sync job writes to the correctly-named directory.
        if (($data['title'] ?? $oldTitle) !== $oldTitle) {
            try {
                $nas = app(\App\Services\NasSyncService::class);
                if ($nas->isEnabled()) {
                    $nas->renameVideoDir($video->fresh());
                }
            } catch (\Throwable $e) {
                \Illuminate\Support\Facades\Log::warning('NAS video dir rename failed: ' . $e->getMessage());
            }
        }

        try {
            NasSyncVideoJob::dispatch($video->fresh());
        } catch (\Throwable $e) {
            \Illuminate\Support\Facades\Log::warning('NasSyncVideoJob dispatch failed: ' . $e->getMessage());
        }

        AuditLog::record('video.updated', [
            'subject_type'  => 'Video',
            'subject_id'    => (string) $video->id,
            'subject_label' => $video->title,
        ]);

        if ($request->expectsJson() || $request->ajax()) {
            return response()->json([
                'success' => true,
                'message' => 'Video updated successfully!',
                'video' => [
                    'id'             => $video->id,
                    'title'          => $video->title,
                    'description'    => $video->description,
                    'visibility'     => $video->visibility,
                    'download_access' => $video->download_access,
                ],
            ]);
        }

        return redirect()->route('videos.show', $video)->with('success', 'Video updated!');
    }

    public function destroy(Request $request, Video $video)
    {
        if (Auth::id() !== $video->user_id) {
            abort(403);
        }

        // If the owner has 2FA enabled, require a valid OTP
        $user = Auth::user();
        if ($user->two_factor_enabled && $user->two_factor_secret) {
            $code = $request->header('X-2FA-Code') ?? $request->input('otp_code', '');
            $google2fa = app('pragmarx.google2fa');
            if (! $google2fa->verifyKey(decrypt($user->two_factor_secret), (string) $code)) {
                return response()->json(['message' => 'Invalid 2FA code. Please try again.'], 422);
            }
        }

        AuditLog::record('video.deleted', [
            'subject_type'  => 'Video',
            'subject_id'    => (string) $video->id,
            'subject_label' => $video->title,
            'details'       => ['owner_id' => $video->user_id, 'type' => $video->type],
        ]);

        Storage::delete($video->path);
        if ($video->thumbnail) {
            Storage::delete($video->thumbnailStorageKey());
        }

        $nasSync = app(\App\Services\NasSyncService::class);
        if ($nasSync->isEnabled()) {
            $nasSync->deleteVideo($video);
        }

        $video->delete();

        if ($request->expectsJson() || $request->ajax()) {
            return response()->json([
                'success' => true,
                'message' => 'Video deleted successfully!',
            ]);
        }

        return redirect()->route('videos.index')->with('success', 'Video deleted!');
    }

    // ─────────────────────────────────────────────────────────────────────────
    //  Replace media file (keeps all metadata, views, likes, comments intact)
    // ─────────────────────────────────────────────────────────────────────────
    //  Replace media file (keeps all metadata, views, likes, comments intact)
    // ─────────────────────────────────────────────────────────────────────────
    public function replaceFile(Request $request, Video $video)
    {
        $user = Auth::user();

        if ($user->id !== $video->user_id && ! $user->isSuperAdmin()) {
            abort(403);
        }

        $request->validate([
            'replacement_file' => [
                'required',
                'file',
                'mimes:mp4,webm,ogg,mov,avi,wmv,flv,mkv,mp3,m4a,aac,wav,flac,opus',
                'max:512000',
            ],
        ]);

        $newFile  = $request->file('replacement_file');
        $mimeType = $newFile->getMimeType();
        $isAudio  = str_starts_with($mimeType, 'audio/');
        $newSize  = $newFile->getSize();
        $newExt   = strtolower($newFile->getClientOriginalExtension() ?: ($isAudio ? 'mp3' : 'mp4'));
        $nas      = app(\App\Services\NasSyncService::class);

        // ── 1. Clear old HLS ─────────────────────────────────────────────────
        if ($video->has_hls && $video->hls_path) {
            \Storage::deleteDirectory($video->hls_path);
        }

        // ── 2. Delete old media file ─────────────────────────────────────────
        //      NAS: only delete the video file; thumbnail/slides/meta.json stay
        //      Local: unlink the local copy
        if ($nas->isEnabled() && str_starts_with($video->path, 'users/')) {
            try { $nas->deleteFile($video->path); } catch (\Throwable) {}
        } else {
            $oldLocal = storage_path('app/' . $video->path);
            if (file_exists($oldLocal)) @unlink($oldLocal);
        }

        // ── 3. Store new file to a temporary local path ──────────────────────
        $tempFilename = \Str::uuid() . '.' . $newExt;
        $tempRelPath  = 'public/videos/' . $tempFilename;
        $newFile->storeAs('public/videos', $tempFilename);
        $tempAbsPath  = storage_path('app/' . $tempRelPath);

        if (! file_exists($tempAbsPath)) {
            return response()->json(['success' => false, 'message' => 'Failed to store the uploaded file.'], 500);
        }

        // ── 4. Extract metadata via FFprobe ──────────────────────────────────
        $width = $height = null;
        $orientation = 'landscape';
        $duration    = 0;

        try {
            $ffprobeBin = config('ffmpeg.ffprobe', '/usr/bin/ffprobe');
            $out = [];
            exec("{$ffprobeBin} -v error -show_entries format=duration -of csv=p=0 " . escapeshellarg($tempAbsPath), $out);
            $duration = (int) round((float) ($out[0] ?? 0));

            if (! $isAudio) {
                $ffprobe = \FFMpeg\FFProbe::create();
                $stream  = $ffprobe->streams($tempAbsPath)->videos()->first();
                if ($stream) {
                    $width  = $stream->get('width');
                    $height = $stream->get('height');
                    if ($width && $height) {
                        if      ($height > $width) $orientation = 'portrait';
                        elseif  ($width > $height) $orientation = 'landscape';
                        else                       $orientation = 'square';
                    }
                }
            }
        } catch (\Throwable $e) {
            \Log::warning('replaceFile: FFprobe failed: ' . $e->getMessage());
        }

        // ── 5. Persist the file to its final location ─────────────────────────
        //
        //   NAS path:   push directly to NAS using uploadDirectToNas()
        //               This handles legacy paths correctly by computing the
        //               proper users/.../videos/... directory.
        //               uploadDirectToNas() updates path/filename in DB.
        //
        //   Local path: update path/filename to temp location, then call
        //               organizeLocalFiles() which moves it to users/... layout.
        //               CompressVideoJob() sets status=ready and chains HLS.

        if ($nas->isEnabled()) {

            // Point filename at the new file (uploadDirectToNas uses this for ext)
            $video->update(['filename' => $tempFilename, 'mime_type' => $mimeType, 'size' => $newSize]);

            try {
                // Pass null for thumb — we don't want to overwrite the existing thumbnail
                $nas->uploadDirectToNas($video, $tempAbsPath, null);
                $video->refresh();
            } catch (\Throwable $e) {
                \Log::error('replaceFile: NAS upload failed: ' . $e->getMessage());
                @unlink($tempAbsPath);
                return response()->json(['success' => false, 'message' => 'NAS upload failed. Please try again.'], 500);
            }

            $metaUpdates = [
                'size'     => $newSize,
                'mime_type'=> $mimeType,
                'has_hls'  => false,
                'hls_path' => null,
                // For NAS the upload is the "done" state — set ready so GenerateHlsJob runs
                'status'   => 'ready',
            ];
            if (! $isAudio) {
                $metaUpdates['duration']    = $duration ?: $video->duration;
                $metaUpdates['width']       = $width    ?: $video->width;
                $metaUpdates['height']      = $height   ?: $video->height;
                $metaUpdates['orientation'] = $orientation;
                $metaUpdates['is_shorts']   = ($duration ?: $video->duration) <= 60 && $orientation === 'portrait';
            }
            $video->update($metaUpdates);

            if (! $isAudio) {
                \App\Jobs\GenerateHlsJob::dispatch($video->fresh())
                    ->onQueue('video-processing')
                    ->onConnection('database');
            }

        } else {

            // Point the record at the temp file so organizeLocalFiles can move it
            $video->update(['path' => $tempRelPath, 'filename' => $tempFilename]);

            try {
                $nas->organizeLocalFiles($video);
                $video->refresh();
            } catch (\Throwable $e) {
                \Log::warning('replaceFile: organizeLocalFiles failed: ' . $e->getMessage());
            }

            $metaUpdates = [
                'size'     => $newSize,
                'mime_type'=> $mimeType,
                'has_hls'  => false,
                'hls_path' => null,
                'status'   => $isAudio ? 'ready' : 'processing',
            ];
            if (! $isAudio) {
                $metaUpdates['duration']    = $duration ?: $video->duration;
                $metaUpdates['width']       = $width    ?: $video->width;
                $metaUpdates['height']      = $height   ?: $video->height;
                $metaUpdates['orientation'] = $orientation;
                $metaUpdates['is_shorts']   = ($duration ?: $video->duration) <= 60 && $orientation === 'portrait';
            }
            $video->update($metaUpdates);

            if (! $isAudio) {
                \App\Jobs\CompressVideoJob::dispatch($video->fresh())
                    ->onQueue('video-processing')
                    ->onConnection('database');
            }
        }

        return response()->json([
            'success' => true,
            'message' => $isAudio
                ? 'Audio file replaced successfully.'
                : 'File replaced — re-encoding has started. The video will be ready shortly.',
            'status'   => $video->fresh()->status,
            'is_audio' => $isAudio,
        ]);
    }

    public function trending(Request $request)
    {
        $hours = $request->get('hours', 48);
        $limit = $request->get('limit', 50);

        $hours = min(max($hours, 24), 168);
        $limit = min(max($limit, 10), 100);

        $videos = Video::public()
            ->where('status', 'ready')
            ->where('created_at', '>=', now()->subDays(10))
            ->with('user')
            ->get();

        $videos = $videos->map(function ($video) use ($hours) {
            $recentViews = \DB::table('video_views')
                ->where('video_id', $video->id)
                ->where('watched_at', '>=', now()->subHours($hours))
                ->count();

            $likeCount = \DB::table('video_likes')
                ->where('video_id', $video->id)
                ->count();

            $ageHours = $video->created_at->diffInHours(now());
            $velocity = $recentViews / $hours;
            $recencyBonus = max(0, 1 - ($ageHours / 240));

            $score = ($recentViews * 0.70) +
                     ($velocity * 100 * 0.15) +
                     ($recencyBonus * 50 * 0.10) +
                     ($likeCount * 0.1 * 0.05);

            $video->trending_score = round($score, 2);
            $video->view_count = $recentViews;
            $video->like_count = $likeCount;

            return $video;
        });

        $trendingVideos = $videos
            ->filter(fn ($v) => $v->trending_score > 0)
            ->sortByDesc('trending_score')
            ->take($limit)
            ->values();

        return view('videos.trending', [
            'videos' => $trendingVideos,
            'hours' => $hours,
            'limit' => $limit,
        ]);
    }

    public function shorts(Request $request)
    {
        $videos = Video::public()
            ->where('is_shorts', true)
            ->where('status', 'ready')
            ->with('user')
            ->latest()
            ->get();

        return view('videos.shorts', compact('videos'));
    }

    public function stream(Video $video)
    {
        if (! $video->canView(Auth::user())) {
            abort(404, 'Video not found');
        }

        $path = $video->localVideoPath();

        // If not on local disk, try to pull from NAS (primary storage when NAS is enabled)
        if (! file_exists($path)) {
            $nas  = app(\App\Services\NasSyncService::class);
            $path = $nas->ensureLocalCopy($video);
            if (! $path) {
                abort(404, 'Video file not found');
            }
        }

        $fileSize = filesize($path);
        $mimeType = $video->mime_type ?: 'video/mp4';

        $handle = fopen($path, 'rb');
        if (! $handle) {
            abort(500, 'Cannot open video file');
        }

        $range = request()->header('Range');

        if ($range) {
            preg_match('/bytes=(\d+)-(\d*)/', $range, $matches);
            $start = intval($matches[1] ?? 0);
            $end = $matches[2] ? intval($matches[2]) : $fileSize - 1;

            $length = $end - $start + 1;

            header('HTTP/1.1 206 Partial Content');
            header('Content-Type: '.$mimeType);
            header('Content-Length: '.$length);
            header('Content-Range: bytes '.$start.'-'.$end.'/'.$fileSize);
            header('Accept-Ranges: bytes');
            header('Cache-Control: public, max-age=3600');

            fseek($handle, $start);
            $chunkSize = 8192;
            $bytesToRead = $length;

            while (! feof($handle) && $bytesToRead > 0) {
                $buffer = fread($handle, min($chunkSize, $bytesToRead));
                echo $buffer;
                flush();
                $bytesToRead -= strlen($buffer);
            }

            fclose($handle);
            exit;
        } else {
            header('Content-Type: '.$mimeType);
            header('Content-Length: '.$fileSize);
            header('Accept-Ranges: bytes');
            header('Cache-Control: public, max-age=3600');

            fpassthru($handle);
            fclose($handle);
            exit;
        }
    }

    public function hls(Video $video, $file = 'playlist.m3u8')
    {
        if (! $video->canView(Auth::user())) {
            abort(404);
        }

        if (! $video->has_hls) {
            abort(404, 'HLS unavailable');
        }

        $hlsPath = storage_path('app/' . $video->hls_path . '/' . $file);

        if (! file_exists($hlsPath)) {
            abort(404);
        }

        $mimeType = pathinfo($hlsPath, PATHINFO_EXTENSION) === 'm3u8'
            ? 'application/vnd.apple.mpegurl'
            : 'video/mp2t';

        header('Content-Type: '.$mimeType);
        header('Accept-Ranges: bytes');
        header('Cache-Control: public, max-age=3600');
        header('Access-Control-Allow-Origin: *');
        header('Access-Control-Allow-Headers: Range');

        if (request()->header('Range')) {
            $size = filesize($hlsPath);
            preg_match('/bytes=(\d+)-(\d*)/', request()->header('Range'), $matches);
            $start = intval($matches[1] ?? 0);
            $end = $matches[2] ? intval($matches[2]) : $size - 1;
            $length = $end - $start + 1;

            header('HTTP/1.1 206 Partial Content');
            header('Content-Length: '.$length);
            header('Content-Range: bytes '.$start.'-'.$end.'/'.$size);

            $handle = fopen($hlsPath, 'rb');
            fseek($handle, $start);
            echo fread($handle, $length);
            fclose($handle);
        } else {
            header('Content-Length: '.filesize($hlsPath));
            readfile($hlsPath);
        }
        exit;
    }

    private function checkDownloadAccess(Video $video): void
    {
        $access = $video->download_access ?? 'disabled';
        $user   = Auth::user();

        match ($access) {
            'disabled'    => abort(403, 'Downloads are not enabled for this video.'),
            'registered'  => $user ? null : abort(403, 'You must be logged in to download this video.'),
            'subscribers' => ($user && ($user->id === $video->user_id || $user->isSubscribedTo($video->user)))
                                 ? null
                                 : abort(403, 'You must be subscribed to this channel to download this video.'),
            default       => null, // 'everyone' — allow
        };
    }

    public function download(Video $video)
    {
        $this->checkDownloadAccess($video);
        $this->recordDownload($video, 'video');

        $path = $video->localVideoPath();

        // If not on local disk, try to pull from NAS
        if (! file_exists($path)) {
            $nas  = app(\App\Services\NasSyncService::class);
            $path = $nas->ensureLocalCopy($video);
            if (! $path) {
                abort(404, 'Video file not found.');
            }
        }

        // Already a video — serve directly, no conversion needed
        if (! $this->isAudioOnlyFile($video)) {
            $ext = pathinfo($video->filename, PATHINFO_EXTENSION) ?: 'mp4';
            return response()->download($path, $this->safeFilename($video->title, 'video') . '.' . $ext, [
                'Content-Type'        => $video->mime_type ?: 'video/mp4',
                'Content-Disposition' => $this->contentDisposition($video->title, $ext),
            ]);
        }

        // Audio-only file → generate video

        // Serve pre-generated slideshow — DB column must confirm it's the current version
        if ($video->slideshow_video_path) {
            $slideshowCache = storage_path('app/public/slideshow/' . $video->id . '_slideshow.mp4');
            if (file_exists($slideshowCache) && filesize($slideshowCache) > 0) {
                return response()->download($slideshowCache, $this->safeFilename($video->title, 'video') . '.mp4', [
                    'Content-Type'        => 'video/mp4',
                    'Content-Disposition' => $this->contentDisposition($video->title, 'mp4'),
                ]);
            }
            // DB says cached but file is gone — clear the stale column
            $video->update(['slideshow_video_path' => null]);
        }

        // Not cached — redirect to the video page which will auto-trigger the
        // background generation progress bar (startSlideshowDownload). This avoids
        // blocking the HTTP worker with a synchronous FFmpeg encode.
        return redirect()->route('videos.show', $video)->with('_auto_dl_video', true);
    }

    // ── Background slideshow generation + progress polling ────────

    public function slideshowGenerate(Video $video)
    {
        $this->checkDownloadAccess($video);

        if (! $this->isAudioOnlyFile($video)) {
            return response()->json(['error' => 'Not an audio file'], 422);
        }

        $ffmpeg  = \App\Models\Setting::ffmpegBinary();
        $ffprobe = config('ffmpeg.ffprobe', '/usr/bin/ffprobe');
        $audioPath = $video->localVideoPath();

        if (! file_exists($audioPath)) {
            return response()->json(['error' => 'Audio file not found'], 404);
        }

        $cacheDir     = storage_path('app/public/slideshow');
        if (! is_dir($cacheDir)) mkdir($cacheDir, 0755, true);

        $outPath      = $cacheDir . '/' . $video->id . '_slideshow.mp4';
        $progressFile = sys_get_temp_dir() . '/sl_progress_' . $video->id . '.txt';
        $pidFile      = sys_get_temp_dir() . '/sl_pid_'      . $video->id . '.txt';
        $logFile      = sys_get_temp_dir() . '/sl_log_'      . $video->id . '.txt';

        // Already cached — only trust it when the DB column confirms it's the right version
        if ($video->slideshow_video_path && file_exists($outPath) && filesize($outPath) > 0) {
            return response()->json(['status' => 'ready']);
        }

        // File on disk but DB says it's stale (e.g. slides were edited) — delete it
        if (file_exists($outPath)) {
            @unlink($outPath);
        }

        // Already running — return so the frontend keeps polling
        if (file_exists($pidFile)) {
            $pid = (int) trim(file_get_contents($pidFile));
            if ($pid > 0 && file_exists("/proc/{$pid}")) {
                exec("{$ffprobe} -v error -show_entries format=duration -of csv=p=0 "
                    . escapeshellarg($audioPath) . ' 2>/dev/null', $dO);
                return response()->json(['status' => 'running', 'duration' => (float) trim($dO[0] ?? '0')]);
            }
        }

        // Probe duration
        exec("{$ffprobe} -v error -show_entries format=duration -of csv=p=0 "
            . escapeshellarg($audioPath) . ' 2>/dev/null', $durOut);
        $dur = isset($durOut[0]) ? (float) trim($durOut[0]) : null;
        if (! $dur || $dur < 1) {
            return response()->json(['error' => 'Cannot probe audio duration'], 500);
        }

        $slides      = $video->slides()->orderBy('position')->get();
        $validSlides = $slides->filter(fn($s) => file_exists($s->localPath()))->values();

        $scale = 'scale=1280:720:force_original_aspect_ratio=decrease,'
               . 'pad=1280:720:(ow-iw)/2:(oh-ih)/2:color=black,format=yuv420p';

        @unlink($progressFile);
        @unlink($pidFile);

        // Pre-compute codec flags; CPU variant used as automatic fallback if GPU fails
        $isStillImage = ($validSlides->count() === 1);
        $vFlags   = $this->ffmpegVideoFlags($isStillImage);
        $cpuFlags = $this->ffmpegVideoFlagsCpu($isStillImage);

        if ($validSlides->count() >= 2) {
            $n    = $validSlides->count();
            $fade = max(0.5, min(1.0, ($dur / $n) * 0.15));
            $T    = ($dur + ($n - 1) * $fade) / $n;

            $inputs = '';
            $scaleFc = [];
            foreach ($validSlides as $i => $slide) {
                $imgPath  = $slide->localPath();
                $inputs  .= ' -loop 1 -t ' . number_format($T + 1, 3) . ' -i ' . escapeshellarg($imgPath);
                $scaleFc[] = "[{$i}:v]{$scale}[s{$i}]";
            }
            $inputs .= ' -i ' . escapeshellarg($audioPath);

            $xfadeFc = [];
            $prev = '[s0]';
            for ($i = 1; $i < $n; $i++) {
                $offset    = number_format($i * ($T - $fade), 3);
                $outLabel  = $i === $n - 1 ? '[vout]' : "[v{$i}]";
                $xfadeFc[] = "{$prev}[s{$i}]xfade=transition=fade:duration="
                           . number_format($fade, 3) . ":offset={$offset}{$outLabel}";
                $prev = $outLabel;
            }
            if ($n === 1) $xfadeFc[] = '[s0]copy[vout]';

            $fc  = implode(';', array_merge($scaleFc, $xfadeFc));
            $cmd = "{$ffmpeg} -y{$inputs}"
                 . ' -filter_complex ' . escapeshellarg($fc)
                 . ' -map [vout] -map ' . $n . ':a'
                 . ' ' . $vFlags
                 . ' -c:a aac -b:a 192k -movflags +faststart -shortest';

        } elseif ($validSlides->count() === 1) {
            $imgPath = $validSlides->first()->localPath();
            $cmd = "{$ffmpeg} -y"
                 . ' -loop 1 -r 1 -i ' . escapeshellarg($imgPath)
                 . ' -i ' . escapeshellarg($audioPath)
                 . ' -map 0:v:0 -map 1:a:0 -shortest'
                 . ' -vf ' . escapeshellarg($scale)
                 . ' ' . $vFlags
                 . ' -c:a aac -b:a 192k -movflags +faststart';
        } else {
            $cmd = "{$ffmpeg} -y"
                 . ' -f lavfi -i color=c=black:s=1280x720:r=1'
                 . ' -i ' . escapeshellarg($audioPath)
                 . ' -map 0:v:0 -map 1:a:0 -shortest'
                 . ' ' . $vFlags
                 . ' -c:a aac -b:a 192k -movflags +faststart';
        }

        $cmd .= ' -progress ' . escapeshellarg($progressFile)
              . ' '           . escapeshellarg($outPath);

        // When GPU is active, wrap in a bash fallback: if GPU command fails, clear the
        // progress file and retry immediately with CPU (libx264) so the download still works.
        if (Setting::gpuEnabled() && $vFlags !== $cpuFlags) {
            $cpuCmd  = str_replace($vFlags, $cpuFlags, $cmd);
            $inner   = $cmd
                     . ' || { truncate -s 0 ' . escapeshellarg($progressFile) . '; ' . $cpuCmd . '; }';
            $bgCmd   = 'nohup bash -c ' . escapeshellarg($inner)
                     . ' 2>' . escapeshellarg($logFile)
                     . ' & echo $! > ' . escapeshellarg($pidFile);
        } else {
            $bgCmd = 'nohup ' . $cmd
                   . ' 2>'    . escapeshellarg($logFile)
                   . ' & echo $! > ' . escapeshellarg($pidFile);
        }

        exec($bgCmd);

        return response()->json(['status' => 'started', 'duration' => $dur]);
    }

    public function slideshowProgress(Video $video)
    {
        $outPath      = storage_path('app/public/slideshow/' . $video->id . '_slideshow.mp4');
        $progressFile = sys_get_temp_dir() . '/sl_progress_' . $video->id . '.txt';
        $pidFile      = sys_get_temp_dir() . '/sl_pid_'      . $video->id . '.txt';

        // Read progress content once
        $content = file_exists($progressFile) ? file_get_contents($progressFile) : '';
        $isDone  = strpos($content, 'progress=end') !== false;

        // If progress says done OR the output file exists and process is gone
        if (! $isDone && file_exists($outPath) && filesize($outPath) > 0) {
            $pid = file_exists($pidFile) ? (int) trim(file_get_contents($pidFile)) : 0;
            if (! ($pid > 0 && file_exists("/proc/{$pid}"))) {
                $isDone = true;
            }
        }

        if ($isDone && file_exists($outPath) && filesize($outPath) > 0) {
            if (! $video->slideshow_video_path) {
                $video->update(['slideshow_video_path' => 'public/slideshow/' . $video->id . '_slideshow.mp4']);
            }
            return response()->json(['percent' => 100, 'status' => 'ready']);
        }

        if (! $content) {
            // If a PID was recorded but the process is already gone, generation failed
            if (file_exists($pidFile)) {
                $pid = (int) trim(file_get_contents($pidFile));
                if ($pid > 0 && ! file_exists("/proc/{$pid}")) {
                    return response()->json(['percent' => 0, 'status' => 'error']);
                }
            }
            return response()->json(['percent' => 0, 'status' => 'waiting']);
        }

        // Parse the most-recent out_time_ms value from the progress file
        preg_match_all('/out_time_ms=(\d+)/', $content, $m);
        $outTimeMs = ! empty($m[1]) ? (int) end($m[1]) : 0;

        // FFmpeg's out_time_ms field is actually in microseconds despite its name
        $totalUs = (float) request()->query('duration', 0) * 1_000_000;
        $percent = $totalUs > 0 ? min(99, (int) round(($outTimeMs / $totalUs) * 100)) : 2;

        return response()->json(['percent' => $percent, 'status' => 'processing']);
    }

    public function downloadMp3(Video $video)
    {
        $this->checkDownloadAccess($video);

        $path = $video->localVideoPath();

        if (! file_exists($path)) {
            $nas  = app(\App\Services\NasSyncService::class);
            $path = $nas->ensureLocalCopy($video);
            if (! $path) {
                abort(404, 'Video file not found.');
            }
        }

        $slug   = $this->safeFilename($video->title, 'audio');
        $ffmpeg = \App\Models\Setting::ffmpegBinary();

        // Already MP3 — serve directly
        if (strtolower(pathinfo($video->filename, PATHINFO_EXTENSION)) === 'mp3') {
            $this->recordDownload($video, 'mp3');
            return response()->download($path, $this->safeFilename($video->title, 'audio') . '.mp3', [
                'Content-Type'        => 'audio/mpeg',
                'Content-Disposition' => $this->contentDisposition($video->title, 'mp3'),
            ]);
        }

        $isAudio = $this->isAudioOnlyFile($video);
        $hwaccel = $this->ffmpegHwaccelFlags(! $isAudio);
        $escaped = escapeshellarg($path);
        $tmp     = sys_get_temp_dir() . '/' . \Str::uuid() . '.mp3';

        $cmd = "{$ffmpeg} -y {$hwaccel}-i {$escaped}"
             . " -vn -acodec libmp3lame -q:a 2"
             . ' ' . escapeshellarg($tmp) . ' 2>/dev/null';

        set_time_limit(0);
        exec($cmd, $out, $exitCode);

        if ($exitCode !== 0 || ! file_exists($tmp) || filesize($tmp) === 0) {
            @unlink($tmp);
            abort(500, 'Failed to generate MP3 file.');
        }

        $this->recordDownload($video, 'mp3');

        return response()->download($tmp, $this->safeFilename($video->title, 'audio') . '.mp3', [
            'Content-Type'        => 'audio/mpeg',
            'Content-Disposition' => $this->contentDisposition($video->title, 'mp3'),
        ])->deleteFileAfterSend(true);
    }

    private function safeFilename(string $title, string $fallback): string
    {
        // Strip characters illegal on Windows/Linux/macOS filesystems, then trim
        $name = preg_replace('/[\\\\\/:\*\?"<>\|]/', '', $title);
        $name = trim($name);
        return $name !== '' ? $name : $fallback;
    }

    private function contentDisposition(string $title, string $ext): string
    {
        $ascii = \Str::slug($title) ?: 'download';
        $safe  = $this->safeFilename($title, $ascii);
        $utf8  = rawurlencode($safe . '.' . $ext);
        return 'attachment; filename="' . $ascii . '.' . $ext . '"; filename*=UTF-8\'\'' . $utf8;
    }

    private function downloadSlideshowVideo(Video $video, $slides, string $audioPath, string $ffmpeg)
    {
        if ($video->slideshow_video_path) {
            $cached = storage_path('app/' . $video->slideshow_video_path);
            if (file_exists($cached) && filesize($cached) > 0) {
                return response()->download($cached, $this->safeFilename($video->title, 'video') . '.mp4', [
                    'Content-Type'        => 'video/mp4',
                    'Content-Disposition' => $this->contentDisposition($video->title, 'mp4'),
                ]);
            }
        }

        return redirect()->route('videos.show', $video)->with('_auto_dl_video', true);
    }

    /** Full video codec flags for raw FFmpeg shell commands (GPU if enabled, else CPU). */
    private function ffmpegVideoFlags(bool $stillImage = false): string
    {
        return Setting::ffmpegVideoFlags($stillImage);
    }

    /** CPU-only codec flags — used as fallback when GPU encoding fails. */
    private function ffmpegVideoFlagsCpu(bool $stillImage = false): string
    {
        return Setting::ffmpegVideoFlagsCpu($stillImage);
    }

    /** hwaccel decode flags — only used when input is a real video file and GPU is on. */
    private function ffmpegHwaccelFlags(bool $inputIsVideo): string
    {
        return Setting::ffmpegHwaccelFlags($inputIsVideo);
    }

    private function isAudioOnlyFile(Video $video): bool
    {
        if ($video->mime_type) {
            return str_starts_with($video->mime_type, 'audio/');
        }
        $audioExts = ['mp3', 'aac', 'm4a', 'ogg', 'wav', 'flac', 'opus', 'wma'];
        return in_array(strtolower(pathinfo($video->filename, PATHINFO_EXTENSION)), $audioExts);
    }

    private function recordDownload(Video $video, string $type): void
    {
        $request = request();
        $ip      = $request->header('CF-Connecting-IP')
                ?? $request->header('X-Real-IP')
                ?? $request->ip();
        $userId  = Auth::id();

        // Deduplicate: browsers/download managers sometimes retry the connection.
        // Skip recording if the same user (or guest IP) already downloaded this
        // video+type within the last 10 minutes.
        $alreadyRecorded = \DB::table('video_downloads')
            ->where('video_id', $video->id)
            ->where('type', $type)
            ->where('downloaded_at', '>=', now()->subMinutes(10))
            ->when(
                $userId !== null,
                fn ($q) => $q->where('user_id', $userId),
                fn ($q) => $q->whereNull('user_id')->where('ip_address', $ip)
            )
            ->exists();

        if ($alreadyRecorded) {
            return;
        }

        $geo = GeoIpService::lookup($ip);

        \DB::table('video_downloads')->insert([
            'video_id'      => $video->id,
            'user_id'       => $userId,
            'ip_address'    => $ip,
            'country'       => $geo['country'],
            'country_name'  => $geo['country_name'],
            'type'          => $type,
            'downloaded_at' => now(),
        ]);

        \DB::table('videos')->where('id', $video->id)->increment('download_count');
    }

    public function recordShare(Video $video)
    {
        $userId = Auth::id();

        // Authenticated users reuse their existing share token for this video
        if ($userId) {
            $existing = \DB::table('video_shares')
                ->where('video_id', $video->id)
                ->where('user_id', $userId)
                ->first();
            if ($existing) {
                return response()->json(['url' => route('share.access', $existing->token)]);
            }
        }

        // Generate a unique 10-char token
        do {
            $token = Str::random(10);
        } while (\DB::table('video_shares')->where('token', $token)->exists());

        \DB::table('video_shares')->insert([
            'video_id'   => $video->id,
            'user_id'    => $userId,
            'token'      => $token,
            'created_at' => now(),
        ]);

        return response()->json(['url' => route('share.access', $token)]);
    }

    public function accessShare(Request $request, string $token)
    {
        $share = \DB::table('video_shares')->where('token', $token)->first();
        if (! $share) {
            return redirect('/');
        }

        $video = Video::find($share->video_id);
        if (! $video || ! $video->canView(Auth::user())) {
            return redirect('/');
        }

        // Identify device via persistent cookie; generate one for first-time visitors
        $did   = $request->cookie('_did') ?: (string) Str::uuid();

        // Record only the first access from this device for this share link
        $seen = \DB::table('share_accesses')
            ->where('share_id', $share->id)
            ->where('device_id', $did)
            ->exists();

        if (! $seen) {
            $ip  = $request->header('CF-Connecting-IP')
                ?? $request->header('X-Real-IP')
                ?? $request->ip();
            $geo = GeoIpService::lookup($ip);

            \DB::table('share_accesses')->insert([
                'share_id'    => $share->id,
                'device_id'   => $did,
                'ip_address'  => $ip,
                'country'     => $geo['country']      ?? null,
                'country_name'=> $geo['country_name'] ?? null,
                'accessed_at' => now(),
            ]);
        }

        return redirect(route('videos.showByToken', $video->share_token))
            ->withCookie(cookie('_did', $did, 60 * 24 * 365 * 5));
    }

    public function ogImage(Video $video)
    {
        // If video has a thumbnail, convert + resize to a small JPEG for WhatsApp/social previews
        if ($video->thumbnail) {
            $path = $video->localThumbnailPath();
            if ($path && file_exists($path)) {
                $ext = strtolower(pathinfo($path, PATHINFO_EXTENSION));
                // Load source image via GD
                $src = match($ext) {
                    'png'  => @imagecreatefrompng($path),
                    'webp' => @imagecreatefromwebp($path),
                    'gif'  => @imagecreatefromgif($path),
                    default => @imagecreatefromjpeg($path),
                };
                if ($src) {
                    $ow = imagesx($src); $oh = imagesy($src);
                    // Fit inside 1200×630, preserving aspect ratio
                    $maxW = 1200; $maxH = 630;
                    $ratio = min($maxW / $ow, $maxH / $oh, 1.0);
                    $nw = (int)round($ow * $ratio); $nh = (int)round($oh * $ratio);
                    $dst = imagecreatetruecolor($nw, $nh);
                    // Preserve transparency before converting to JPEG (fill white)
                    $white = imagecolorallocate($dst, 255, 255, 255);
                    imagefill($dst, 0, 0, $white);
                    imagecopyresampled($dst, $src, 0, 0, 0, 0, $nw, $nh, $ow, $oh);
                    imagedestroy($src);
                    ob_start();
                    imagejpeg($dst, null, 82);
                    imagedestroy($dst);
                    $jpeg = ob_get_clean();
                    return response($jpeg, 200, [
                        'Content-Type'  => 'image/jpeg',
                        'Cache-Control' => 'public, max-age=86400',
                    ]);
                }
            }
        }

        // Generate a branded 1200×630 fallback card
        $w = 1200; $h = 630;
        $img = imagecreatetruecolor($w, $h);

        $cBg     = imagecolorallocate($img, 10, 10, 10);
        $cRed    = imagecolorallocate($img, 230, 30, 30);
        $cWhite  = imagecolorallocate($img, 240, 240, 240);
        $cGray   = imagecolorallocate($img, 120, 120, 120);
        $cDark   = imagecolorallocate($img, 28, 28, 28);

        imagefill($img, 0, 0, $cBg);

        // Subtle vignette border
        imagefilledrectangle($img, 0, 0, $w - 1, $h - 1, $cDark);
        imagefilledrectangle($img, 4, 4, $w - 5, $h - 5, $cBg);

        // Red accent top bar
        imagefilledrectangle($img, 0, 0, $w, 8, $cRed);

        // Red accent bottom bar
        imagefilledrectangle($img, 0, $h - 8, $w, $h, $cRed);

        // Play button circle (center)
        $cx = (int)($w / 2); $cy = (int)($h / 2) - 30;
        $r  = 72;
        imagefilledellipse($img, $cx, $cy, $r * 2, $r * 2, $cRed);
        // Play triangle
        $tri = [
            $cx - 22, $cy - 30,
            $cx - 22, $cy + 30,
            $cx + 34, $cy,
        ];
        imagefilledpolygon($img, $tri, $cWhite);

        $fontBold   = '/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf';
        $fontNormal = '/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf';

        // App name (top-left)
        imagettftext($img, 22, 0, 40, 56, $cRed, $fontBold, strtoupper(config('app.name')));

        // Video title (wrap long titles)
        $title = $video->title ?: 'Video';
        $maxChars = 42;
        if (mb_strlen($title) > $maxChars) {
            $lines = [];
            $words = explode(' ', $title);
            $line  = '';
            foreach ($words as $word) {
                if (mb_strlen($line . ' ' . $word) > $maxChars) {
                    $lines[] = trim($line);
                    $line = $word;
                } else {
                    $line .= ($line ? ' ' : '') . $word;
                }
            }
            if ($line) $lines[] = trim($line);
        } else {
            $lines = [$title];
        }
        $lines = array_slice($lines, 0, 2);
        $titleY = $cy + $r + 60;
        foreach ($lines as $i => $line) {
            $bbox = imagettfbbox(28, 0, $fontBold, $line);
            $tw   = $bbox[2] - $bbox[0];
            $tx   = (int)(($w - $tw) / 2);
            imagettftext($img, 28, 0, $tx, $titleY + $i * 44, $cWhite, $fontBold, $line);
        }

        // Channel name (below title)
        $channel = $video->user?->name ?? config('app.name');
        $bbox  = imagettfbbox(16, 0, $fontNormal, $channel);
        $tw    = $bbox[2] - $bbox[0];
        $tx    = (int)(($w - $tw) / 2);
        imagettftext($img, 16, 0, $tx, $titleY + count($lines) * 44 + 20, $cGray, $fontNormal, $channel);

        // Domain (bottom-right)
        $domain = parse_url(config('app.url'), PHP_URL_HOST) ?? config('app.url');
        imagettftext($img, 14, 0, $w - 300, $h - 24, $cGray, $fontNormal, $domain);

        ob_start();
        imagejpeg($img, null, 85);
        imagedestroy($img);
        $jpeg = ob_get_clean();

        return response($jpeg, 200, [
            'Content-Type'  => 'image/jpeg',
            'Cache-Control' => 'public, max-age=86400',
        ]);
    }

    public function insights(Video $video)
    {
        if (Auth::id() !== $video->user_id) {
            abort(403);
        }

        $id  = $video->id;
        $now = now();

        $totalViews = \DB::table('video_views')->where('video_id', $id)->count();

        $uniqueViewers = \DB::table('video_views')
            ->where('video_id', $id)
            ->whereNotNull('user_id')
            ->distinct('user_id')
            ->count('user_id');

        $viewsToday = \DB::table('video_views')
            ->where('video_id', $id)
            ->where('watched_at', '>=', $now->copy()->startOfDay())
            ->count();

        $viewsThisWeek = \DB::table('video_views')
            ->where('video_id', $id)
            ->where('watched_at', '>=', $now->copy()->subDays(7))
            ->count();

        $viewsLastWeek = \DB::table('video_views')
            ->where('video_id', $id)
            ->whereBetween('watched_at', [
                $now->copy()->subDays(14),
                $now->copy()->subDays(7),
            ])
            ->count();

        $weekChange = $viewsLastWeek > 0
            ? round(($viewsThisWeek - $viewsLastWeek) / $viewsLastWeek * 100)
            : ($viewsThisWeek > 0 ? 100 : 0);

        // 14-day daily breakdown
        $rawDaily = \DB::table('video_views')
            ->selectRaw("date(watched_at) as day, count(*) as cnt")
            ->where('video_id', $id)
            ->where('watched_at', '>=', $now->copy()->subDays(13)->startOfDay())
            ->groupByRaw("date(watched_at)")
            ->orderBy('day')
            ->pluck('cnt', 'day');

        $daily = [];
        for ($i = 13; $i >= 0; $i--) {
            $d = $now->copy()->subDays($i);
            $daily[] = [
                'date'  => $d->format('Y-m-d'),
                'label' => $d->format('M d'),
                'short' => $d->format('D'),
                'count' => (int) ($rawDaily->get($d->format('Y-m-d')) ?? 0),
            ];
        }

        // Country breakdown (top 10)
        $rawCountries = \DB::table('video_views')
            ->selectRaw("country, country_name, count(*) as cnt")
            ->where('video_id', $id)
            ->whereNotNull('country')
            ->groupBy('country', 'country_name')
            ->orderByDesc('cnt')
            ->limit(10)
            ->get();

        $totalGeo = $rawCountries->sum('cnt');
        $countries = $rawCountries->map(fn ($c) => [
            'code'  => $c->country,
            'name'  => $c->country_name,
            'count' => (int) $c->cnt,
            'pct'   => $totalGeo > 0 ? round($c->cnt / $totalGeo * 100) : 0,
        ])->values();

        // Peak hour (0-23) across all views
        $driver  = \DB::getDriverName();
        $hourExpr = $driver === 'sqlite' ? "strftime('%H', watched_at)" : 'HOUR(watched_at)';
        $peakRow  = \DB::table('video_views')
            ->selectRaw("{$hourExpr} as hr, count(*) as cnt")
            ->where('video_id', $id)
            ->groupByRaw("{$hourExpr}")
            ->orderByDesc('cnt')
            ->first();
        $peakHour = $peakRow ? (int) $peakRow->hr : null;

        // Top registered viewers (by view count)
        $topViewers = \DB::table('video_views')
            ->join('users', 'users.id', '=', 'video_views.user_id')
            ->selectRaw('users.id, users.name, users.avatar, users.username, count(*) as cnt, max(video_views.watched_at) as last_at')
            ->where('video_views.video_id', $id)
            ->whereNotNull('video_views.user_id')
            ->groupBy('users.id', 'users.name', 'users.avatar', 'users.username')
            ->orderByDesc('cnt')
            ->limit(20)
            ->get()
            ->map(fn ($u) => [
                'id'      => $u->id,
                'channel' => $u->username,
                'name'    => $u->name,
                'avatar'  => $u->avatar
                    ? route('media.avatar', $u->avatar)
                    : 'https://i.pravatar.cc/150?u=' . $u->id,
                'count'   => (int) $u->cnt,
                'last_at' => $u->last_at,
            ]);

        $guestViews = \DB::table('video_views')
            ->where('video_id', $id)
            ->whereNull('user_id')
            ->count();

        // 10 most recent views
        $recentViewers = \DB::table('video_views')
            ->leftJoin('users', 'users.id', '=', 'video_views.user_id')
            ->select(
                'video_views.watched_at',
                'video_views.country',
                'users.id as user_id',
                'users.name as user_name',
                'users.avatar as user_avatar',
                'users.username as user_channel'
            )
            ->where('video_views.video_id', $id)
            ->orderByDesc('video_views.watched_at')
            ->limit(10)
            ->get()
            ->map(fn ($r) => [
                'at'           => $r->watched_at,
                'country'      => $r->country,
                'user_id'      => $r->user_id,
                'user_channel' => $r->user_channel,
                'user_name'    => $r->user_name ?? 'Guest',
                'user_avatar'  => $r->user_id
                    ? ($r->user_avatar
                        ? route('media.avatar', $r->user_avatar)
                        : 'https://i.pravatar.cc/150?u=' . $r->user_id)
                    : null,
            ]);

        // Download details
        $totalDownloads = \DB::table('video_downloads')->where('video_id', $id)->count();

        // Per-type breakdown
        $dlByType = \DB::table('video_downloads')
            ->selectRaw('type, count(*) as cnt')
            ->where('video_id', $id)
            ->groupBy('type')
            ->pluck('cnt', 'type');

        // Per-user download counts (top 20 logged-in users)
        $dlUsers = \DB::table('video_downloads')
            ->join('users', 'users.id', '=', 'video_downloads.user_id')
            ->selectRaw('users.id, users.name, users.avatar, count(*) as cnt, max(video_downloads.downloaded_at) as last_at')
            ->where('video_downloads.video_id', $id)
            ->whereNotNull('video_downloads.user_id')
            ->groupBy('users.id', 'users.name', 'users.avatar')
            ->orderByDesc('cnt')
            ->limit(20)
            ->get()
            ->map(fn ($u) => [
                'id'      => $u->id,
                'name'    => $u->name,
                'avatar'  => $u->avatar
                    ? route('media.avatar', $u->avatar)
                    : 'https://i.pravatar.cc/150?u=' . $u->id,
                'count'   => (int) $u->cnt,
                'last_at' => $u->last_at,
            ]);

        $dlGuests = \DB::table('video_downloads')
            ->where('video_id', $id)
            ->whereNull('user_id')
            ->count();

        // 10 most recent downloads
        $dlRecent = \DB::table('video_downloads')
            ->leftJoin('users', 'users.id', '=', 'video_downloads.user_id')
            ->select(
                'video_downloads.id',
                'video_downloads.type',
                'video_downloads.downloaded_at',
                'video_downloads.country',
                'video_downloads.country_name',
                'users.id as user_id',
                'users.name as user_name',
                'users.avatar as user_avatar'
            )
            ->where('video_downloads.video_id', $id)
            ->orderByDesc('video_downloads.downloaded_at')
            ->limit(10)
            ->get()
            ->map(fn ($r) => [
                'type'       => $r->type,
                'at'         => $r->downloaded_at,
                'country'    => $r->country,
                'country_name'=> $r->country_name,
                'user_id'    => $r->user_id,
                'user_name'  => $r->user_name ?? 'Guest',
                'user_avatar'=> $r->user_id
                    ? ($r->user_avatar
                        ? route('media.avatar', $r->user_avatar)
                        : 'https://i.pravatar.cc/150?u=' . $r->user_id)
                    : null,
            ]);

        // Gender breakdown (authenticated viewers only)
        $genderRows  = \DB::table('video_views')
            ->join('users', 'users.id', '=', 'video_views.user_id')
            ->selectRaw('users.gender, count(*) as cnt')
            ->where('video_views.video_id', $id)
            ->whereNotNull('video_views.user_id')
            ->whereNotNull('users.gender')
            ->groupBy('users.gender')
            ->get();

        $totalGender = $genderRows->sum('cnt');
        $genders = $genderRows->map(fn ($g) => [
            'gender' => $g->gender,
            'count'  => (int) $g->cnt,
            'pct'    => $totalGender > 0 ? round($g->cnt / $totalGender * 100) : 0,
        ])->values();

        // Age group breakdown (authenticated viewers with birthday set)
        $ageExpr = $driver === 'sqlite'
            ? "CAST((julianday(video_views.watched_at) - julianday(users.birthday)) / 365.25 AS INTEGER)"
            : "TIMESTAMPDIFF(YEAR, users.birthday, video_views.watched_at)";

        $rawAges = \DB::table('video_views')
            ->join('users', 'users.id', '=', 'video_views.user_id')
            ->selectRaw("{$ageExpr} as age")
            ->where('video_views.video_id', $id)
            ->whereNotNull('video_views.user_id')
            ->whereNotNull('users.birthday')
            ->get();

        $ageBuckets = ['Under 13' => 0, '13–17' => 0, '18–24' => 0, '25–34' => 0, '35–44' => 0, '45–54' => 0, '55–64' => 0, '65+' => 0];
        foreach ($rawAges as $row) {
            $age = (int) $row->age;
            $bucket = match (true) {
                $age < 13  => 'Under 13',
                $age <= 17 => '13–17',
                $age <= 24 => '18–24',
                $age <= 34 => '25–34',
                $age <= 44 => '35–44',
                $age <= 54 => '45–54',
                $age <= 64 => '55–64',
                default    => '65+',
            };
            $ageBuckets[$bucket]++;
        }

        $totalAge   = array_sum($ageBuckets);
        $ageGroups  = collect($ageBuckets)
            ->filter(fn ($cnt) => $cnt > 0)
            ->map(fn ($cnt, $label) => [
                'label' => $label,
                'count' => $cnt,
                'pct'   => $totalAge > 0 ? round($cnt / $totalAge * 100) : 0,
            ])->values();

        // Who liked this video
        $likers = \DB::table('video_likes')
            ->join('users', 'users.id', '=', 'video_likes.user_id')
            ->select('users.id', 'users.name', 'users.avatar', 'users.username', 'video_likes.created_at as liked_at')
            ->where('video_likes.video_id', $id)
            ->orderByDesc('video_likes.created_at')
            ->limit(50)
            ->get()
            ->map(fn ($u) => [
                'id'      => $u->id,
                'channel' => $u->username,
                'name'    => $u->name,
                'avatar'  => $u->avatar
                    ? route('media.avatar', $u->avatar)
                    : 'https://i.pravatar.cc/150?u=' . $u->id,
                'liked_at' => $u->liked_at,
            ]);

        // ── Share analytics ────────────────────────────────────────
        $shareLinks = \DB::table('video_shares')->where('video_id', $id)->get();
        $shareIds   = $shareLinks->pluck('id');

        $shareReach = $shareIds->isEmpty() ? 0
            : \DB::table('share_accesses')->whereIn('share_id', $shareIds)->count();

        $shareLinksCount = $shareLinks->count();

        // Per-link breakdown for insights panel
        $shareBreakdown = $shareLinks->map(function ($s) {
            $accesses = \DB::table('share_accesses')->where('share_id', $s->id)->count();
            $sharer   = $s->user_id
                ? \DB::table('users')->where('id', $s->user_id)->value('name')
                : 'Guest';
            return [
                'token'     => $s->token,
                'sharer'    => $sharer,
                'reach'     => $accesses,
                'created_at'=> $s->created_at,
            ];
        })->sortByDesc('reach')->values()->take(10);

        return response()->json([
            'total_views'      => $totalViews,
            'unique_viewers'   => $uniqueViewers,
            'top_viewers'      => $topViewers,
            'guest_views'      => $guestViews,
            'recent_viewers'   => $recentViewers,
            'views_today'      => $viewsToday,
            'views_this_week'  => $viewsThisWeek,
            'views_last_week'  => $viewsLastWeek,
            'week_change'      => $weekChange,
            'downloads'        => $totalDownloads,
            'dl_video'         => (int) ($dlByType->get('video') ?? 0),
            'dl_mp3'           => (int) ($dlByType->get('mp3') ?? 0),
            'dl_users'         => $dlUsers,
            'dl_guests'        => $dlGuests,
            'dl_recent'        => $dlRecent,
            'shares'           => $shareReach,
            'share_links'      => $shareLinksCount,
            'share_breakdown'  => $shareBreakdown,
            'countries'        => $countries,
            'daily'            => $daily,
            'peak_hour'        => $peakHour,
            'likes'            => $video->like_count,
            'likers'           => $likers,
            'genders'          => $genders,
            'age_groups'       => $ageGroups,
        ]);
    }

    // ── Drill-down: viewers from one country ──────────────────────────────
    public function insightsCountry(Video $video, string $country)
    {
        if (Auth::id() !== $video->user_id) abort(403);

        $id     = $video->id;
        $country = strtoupper(substr(preg_replace('/[^A-Za-z]/', '', $country), 0, 2));

        $totalViews  = \DB::table('video_views')->where('video_id', $id)->where('country', $country)->count();
        $countryName = \DB::table('video_views')->where('video_id', $id)->where('country', $country)->whereNotNull('country_name')->value('country_name');
        $guestCount  = \DB::table('video_views')->where('video_id', $id)->where('country', $country)->whereNull('user_id')->count();

        $registeredUsers = \DB::table('video_views')
            ->join('users', 'users.id', '=', 'video_views.user_id')
            ->selectRaw('users.id, users.name, users.avatar, users.username, count(*) as cnt, max(video_views.watched_at) as last_at')
            ->where('video_views.video_id', $id)
            ->where('video_views.country', $country)
            ->whereNotNull('video_views.user_id')
            ->groupBy('users.id', 'users.name', 'users.avatar', 'users.username')
            ->orderByDesc('cnt')
            ->limit(20)
            ->get()
            ->map(fn ($u) => [
                'id'      => $u->id,
                'channel' => $u->username,
                'name'    => $u->name,
                'avatar'  => $u->avatar ? route('media.avatar', $u->avatar) : 'https://i.pravatar.cc/150?u=' . $u->id,
                'count'   => (int) $u->cnt,
                'last_at' => $u->last_at,
            ]);

        // 14-day trend for this country
        $now      = now();
        $rawDaily = \DB::table('video_views')
            ->selectRaw("date(watched_at) as day, count(*) as cnt")
            ->where('video_id', $id)
            ->where('country', $country)
            ->where('watched_at', '>=', $now->copy()->subDays(13)->startOfDay())
            ->groupByRaw("date(watched_at)")
            ->orderBy('day')
            ->pluck('cnt', 'day');

        $daily = [];
        for ($i = 13; $i >= 0; $i--) {
            $d       = $now->copy()->subDays($i);
            $daily[] = ['label' => $d->format('M d'), 'short' => $d->format('D'), 'count' => (int) ($rawDaily->get($d->format('Y-m-d')) ?? 0)];
        }

        return response()->json([
            'country'          => $country,
            'country_name'     => $countryName ?? $country,
            'total_views'      => $totalViews,
            'registered_users' => $registeredUsers,
            'guest_count'      => $guestCount,
            'daily'            => $daily,
        ]);
    }

    // ── Drill-down: viewers on a specific day ─────────────────────────────
    public function insightsDay(Video $video, string $date)
    {
        if (Auth::id() !== $video->user_id) abort(403);

        try {
            $day = \Carbon\Carbon::createFromFormat('Y-m-d', $date, config('app.timezone'));
        } catch (\Throwable) {
            abort(400, 'Invalid date');
        }

        $id    = $video->id;
        $start = $day->copy()->startOfDay();
        $end   = $day->copy()->endOfDay();

        $totalViews = \DB::table('video_views')->where('video_id', $id)->whereBetween('watched_at', [$start, $end])->count();
        $guestCount = \DB::table('video_views')->where('video_id', $id)->whereBetween('watched_at', [$start, $end])->whereNull('user_id')->count();

        $registeredUsers = \DB::table('video_views')
            ->join('users', 'users.id', '=', 'video_views.user_id')
            ->selectRaw('users.id, users.name, users.avatar, users.username, count(*) as cnt, max(video_views.watched_at) as last_at')
            ->where('video_views.video_id', $id)
            ->whereBetween('video_views.watched_at', [$start, $end])
            ->whereNotNull('video_views.user_id')
            ->groupBy('users.id', 'users.name', 'users.avatar', 'users.username')
            ->orderByDesc('cnt')
            ->limit(20)
            ->get()
            ->map(fn ($u) => [
                'id'      => $u->id,
                'channel' => $u->username,
                'name'    => $u->name,
                'avatar'  => $u->avatar ? route('media.avatar', $u->avatar) : 'https://i.pravatar.cc/150?u=' . $u->id,
                'count'   => (int) $u->cnt,
                'last_at' => $u->last_at,
            ]);

        // Hourly breakdown (0-23)
        $driver   = \DB::getDriverName();
        $hourExpr = $driver === 'sqlite' ? "CAST(strftime('%H', watched_at) AS INTEGER)" : 'HOUR(watched_at)';
        $rawHourly = \DB::table('video_views')
            ->selectRaw("{$hourExpr} as hr, count(*) as cnt")
            ->where('video_id', $id)
            ->whereBetween('watched_at', [$start, $end])
            ->groupByRaw($hourExpr)
            ->orderBy('hr')
            ->pluck('cnt', 'hr');

        $hourly = [];
        for ($h = 0; $h < 24; $h++) {
            $hourly[] = [
                'hour'  => $h,
                'label' => ($h === 0 ? '12am' : ($h < 12 ? $h . 'am' : ($h === 12 ? '12pm' : ($h - 12) . 'pm'))),
                'count' => (int) ($rawHourly->get($h) ?? 0),
            ];
        }

        // Top countries that day
        $countries = \DB::table('video_views')
            ->selectRaw("country, country_name, count(*) as cnt")
            ->where('video_id', $id)
            ->whereBetween('watched_at', [$start, $end])
            ->whereNotNull('country')
            ->groupBy('country', 'country_name')
            ->orderByDesc('cnt')
            ->limit(5)
            ->get()
            ->map(fn ($c) => ['code' => $c->country, 'name' => $c->country_name, 'count' => (int) $c->cnt]);

        return response()->json([
            'date'             => $day->format('M d, Y'),
            'day_of_week'      => $day->format('l'),
            'total_views'      => $totalViews,
            'registered_users' => $registeredUsers,
            'guest_count'      => $guestCount,
            'hourly'           => $hourly,
            'countries'        => $countries,
        ]);
    }

    // ── Drill-down: one user's full download history on this video ────────
    public function insightsDownloaderHistory(Video $video, int $userId)
    {
        if (Auth::id() !== $video->user_id) abort(403);

        $user = \App\Models\User::findOrFail($userId);

        $records = \DB::table('video_downloads')
            ->where('video_id', $video->id)
            ->where('user_id', $userId)
            ->orderByDesc('downloaded_at')
            ->get(['id', 'type', 'country', 'country_name', 'downloaded_at'])
            ->map(fn ($r) => [
                'type'         => $r->type,
                'country'      => $r->country,
                'country_name' => $r->country_name,
                'at'           => $r->downloaded_at,
            ]);

        return response()->json([
            'user' => [
                'id'     => $user->id,
                'name'   => $user->name,
                'avatar' => $user->avatar ? route('media.avatar', $user->avatar) : 'https://i.pravatar.cc/150?u=' . $user->id,
            ],
            'total'   => $records->count(),
            'records' => $records,
        ]);
    }
}

