<?php
/*******************************************************************************************************************
| Software Name        : EasyStream
| Software Description : High End YouTube Clone Script with Videos, Shorts, Streams, Images, Audio, Documents, Blogs
| Software Author      : (c) Sami Ahmed
|*******************************************************************************************************************
|
|*******************************************************************************************************************
| This source file is subject to the EasyStream Proprietary License Agreement.
| 
| By using this software, you acknowledge having read this Agreement and agree to be bound thereby.
|*******************************************************************************************************************
| Copyright (c) 2025 Sami Ahmed. All rights reserved.
|*******************************************************************************************************************/

defined('_ISVALID') or header('Location: /error');

/**
 * Video Streaming and Delivery System
 */
class VStreaming
{
    private $logger;
    private $db;
    private $baseUrl;
    private $streamingPath;
    
    public function __construct()
    {
        $this->logger = VLogger::getInstance();
        $this->db = VDatabase::getInstance();
        $this->baseUrl = $this->getBaseUrl();
        $this->streamingPath = _FPATH . 'f_data/processed/';
    }
    
    /**
     * Get video streaming URLs for a video
     * @param string $videoKey Video key
     * @param string $userId User ID (for access control)
     * @return array Streaming URLs and metadata
     */
    public function getVideoStreams($videoKey, $userId = null)
    {
        try {
            // Get video information
            $video = $this->getVideoInfo($videoKey);
            
            if (!$video) {
                throw new Exception('Video not found');
            }
            
            // Check access permissions
            if (!$this->checkVideoAccess($video, $userId)) {
                throw new Exception('Access denied');
            }
            
            // Get available formats
            $availableFormats = json_decode($video['available_formats'] ?? '[]', true);
            $streams = [];
            
            // Progressive download streams
            foreach ($availableFormats as $format) {
                if ($format === 'hls' || $format === 'thumbnails' || $format === 'preview') {
                    continue;
                }
                
                $streamUrl = $this->getProgressiveStreamUrl($videoKey, $format);
                if ($streamUrl) {
                    $streams['progressive'][$format] = [
                        'url' => $streamUrl,
                        'format' => $format,
                        'type' => 'video/mp4',
                        'quality' => $this->getQualityLabel($format)
                    ];
                }
            }
            
            // HLS adaptive streaming
            if ($video['hls_available']) {
                $hlsUrl = $this->getHLSStreamUrl($videoKey);
                if ($hlsUrl) {
                    $streams['hls'] = [
                        'url' => $hlsUrl,
                        'type' => 'application/x-mpegURL',
                        'adaptive' => true
                    ];
                }
            }
            
            // Thumbnails
            $thumbnails = $this->getThumbnailUrls($videoKey);
            
            // Track view
            $this->trackView($videoKey, $userId);
            
            return [
                'success' => true,
                'video_key' => $videoKey,
                'streams' => $streams,
                'thumbnails' => $thumbnails,
                'duration' => (float)$video['duration'],
                'title' => $video['file_title'],
                'description' => $video['file_description']
            ];
            
        } catch (Exception $e) {
            $this->logger->error('Failed to get video streams', [
                'video_key' => $videoKey,
                'user_id' => $userId,
                'error' => $e->getMessage()
            ]);
            
            return [
                'success' => false,
                'error' => $e->getMessage()
            ];
        }
    }
    
    /**
     * Stream video file with range support
     * @param string $videoKey Video key
     * @param string $format Video format
     * @param string $userId User ID (for access control)
     */
    public function streamVideo($videoKey, $format, $userId = null)
    {
        try {
            // Get video information
            $video = $this->getVideoInfo($videoKey);
            
            if (!$video) {
                http_response_code(404);
                exit('Video not found');
            }
            
            // Check access permissions
            if (!$this->checkVideoAccess($video, $userId)) {
                http_response_code(403);
                exit('Access denied');
            }
            
            // Get file path
            $filePath = $this->getVideoFilePath($videoKey, $format);
            
            if (!$filePath || !file_exists($filePath)) {
                http_response_code(404);
                exit('Video file not found');
            }
            
            // Track analytics
            $this->trackStreamRequest($videoKey, $format, $userId);
            
            // Stream the file with range support
            $this->streamFileWithRanges($filePath, $this->getMimeType($format));
            
        } catch (Exception $e) {
            $this->logger->error('Video streaming failed', [
                'video_key' => $videoKey,
                'format' => $format,
                'user_id' => $userId,
                'error' => $e->getMessage()
            ]);
            
            http_response_code(500);
            exit('Streaming error');
        }
    }
    
    /**
     * Stream HLS playlist or segment
     * @param string $videoKey Video key
     * @param string $path HLS path (playlist or segment)
     * @param string $userId User ID (for access control)
     */
    public function streamHLS($videoKey, $path, $userId = null)
    {
        try {
            // Get video information
            $video = $this->getVideoInfo($videoKey);
            
            if (!$video || !$video['hls_available']) {
                http_response_code(404);
                exit('HLS not available');
            }
            
            // Check access permissions
            if (!$this->checkVideoAccess($video, $userId)) {
                http_response_code(403);
                exit('Access denied');
            }
            
            $sanitizedPath = $this->sanitizeHLSPath($path);
            if (!$sanitizedPath) {
                http_response_code(400);
                exit('Invalid HLS path');
            }
            
            $hlsRoot = $this->streamingPath . $videoKey . '/hls/';
            $hlsPath = $hlsRoot . $sanitizedPath;
            
            if (!file_exists($hlsPath) || !is_file($hlsPath)) {
                http_response_code(404);
                exit('HLS file not found');
            }
            
            $realRoot = realpath($hlsRoot);
            $realFile = realpath($hlsPath);
            if ($realRoot === false || $realFile === false || strpos($realFile, $realRoot) !== 0) {
                http_response_code(403);
                exit('Invalid HLS path');
            }
            
            $extension = strtolower(pathinfo($sanitizedPath, PATHINFO_EXTENSION));
            
            if ($extension === 'm3u8') {
                $playlist = file_get_contents($hlsPath);
                if ($playlist === false) {
                    throw new Exception('Failed to read HLS playlist');
                }
                
                $playlist = $this->rewritePlaylistUrls($playlist, $videoKey, $sanitizedPath);
                
                header('Content-Type: application/vnd.apple.mpegurl');
                header('Cache-Control: no-cache, max-age=0');
                header('Access-Control-Allow-Origin: *');
                header('Content-Length: ' . strlen($playlist));
                
                if (basename($sanitizedPath) === 'master.m3u8') {
                    $this->trackStreamRequest($videoKey, 'hls_master', $userId);
                }
                
                echo $playlist;
                exit;
            }
            
            $contentType = 'application/octet-stream';
            if ($extension === 'ts') {
                $contentType = 'video/MP2T';
            } elseif ($extension === 'mp4') {
                $contentType = 'video/mp4';
            }
            
            header('Content-Type: ' . $contentType);
            header('Content-Length: ' . filesize($hlsPath));
            header('Cache-Control: max-age=30');
            header('Access-Control-Allow-Origin: *');
            
            readfile($hlsPath);
            
        } catch (Exception $e) {
            $this->logger->error('HLS streaming failed', [
                'video_key' => $videoKey,
                'path' => $path,
                'user_id' => $userId,
                'error' => $e->getMessage()
            ]);
            
            http_response_code(500);
            exit('HLS streaming error');
        }
    }
    
    /**
     * Get video analytics data
     * @param string $videoKey Video key
     * @return array Analytics data
     */
    public function getVideoAnalytics($videoKey)
    {
        try {
            $query = "SELECT 
                        COUNT(*) as total_views,
                        COUNT(DISTINCT user_id) as unique_viewers,
                        AVG(watch_duration) as avg_watch_duration,
                        SUM(watch_duration) as total_watch_time,
                        DATE(created_at) as view_date,
                        COUNT(*) as daily_views
                      FROM db_video_analytics 
                      WHERE video_key = ? 
                      GROUP BY DATE(created_at)
                      ORDER BY view_date DESC
                      LIMIT 30";
            
            $result = $this->db->doQuery($query, [$videoKey]);
            
            $analytics = [];
            while ($row = $this->db->doFetch($result)) {
                $analytics[] = $row;
            }
            
            // Get overall stats
            $overallQuery = "SELECT 
                               COUNT(*) as total_views,
                               COUNT(DISTINCT user_id) as unique_viewers,
                               AVG(watch_duration) as avg_watch_duration,
                               SUM(watch_duration) as total_watch_time
                             FROM db_video_analytics 
                             WHERE video_key = ?";
            
            $overallResult = $this->db->doQuery($overallQuery, [$videoKey]);
            $overallStats = $this->db->doFetch($overallResult);
            
            return [
                'success' => true,
                'video_key' => $videoKey,
                'overall_stats' => $overallStats,
                'daily_analytics' => $analytics
            ];
            
        } catch (Exception $e) {
            $this->logger->error('Failed to get video analytics', [
                'video_key' => $videoKey,
                'error' => $e->getMessage()
            ]);
            
            return [
                'success' => false,
                'error' => $e->getMessage()
            ];
        }
    }
    
    /**
     * Update watch progress for a user
     * @param string $videoKey Video key
     * @param string $userId User ID
     * @param float $currentTime Current playback time
     * @param float $duration Video duration
     */
    public function updateWatchProgress($videoKey, $userId, $currentTime, $duration)
    {
        try {
            if (!$userId) {
                return; // Skip for anonymous users
            }
            
            $watchData = [
                'user_id' => $userId,
                'video_key' => $videoKey,
                'current_time' => $currentTime,
                'duration' => $duration,
                'watch_percentage' => ($duration > 0) ? ($currentTime / $duration) * 100 : 0,
                'updated_at' => date('Y-m-d H:i:s')
            ];
            
            // Check if record exists
            $existingQuery = "SELECT id FROM db_watch_progress WHERE user_id = ? AND video_key = ?";
            $existingResult = $this->db->doQuery($existingQuery, [$userId, $videoKey]);
            $existing = $this->db->doFetch($existingResult);
            
            if ($existing) {
                // Update existing record
                $this->db->doUpdate('db_watch_progress', 'id', $watchData, $existing['id']);
            } else {
                // Create new record
                $watchData['created_at'] = date('Y-m-d H:i:s');
                $this->db->doInsert('db_watch_progress', $watchData);
            }
            
        } catch (Exception $e) {
            $this->logger->error('Failed to update watch progress', [
                'video_key' => $videoKey,
                'user_id' => $userId,
                'error' => $e->getMessage()
            ]);
        }
    }
    
    /**
     * Get user's watch progress for a video
     * @param string $videoKey Video key
     * @param string $userId User ID
     * @return array|null Watch progress data
     */
    public function getWatchProgress($videoKey, $userId)
    {
        if (!$userId) {
            return null;
        }
        
        try {
            $query = "SELECT * FROM db_watch_progress WHERE user_id = ? AND video_key = ?";
            $result = $this->db->doQuery($query, [$userId, $videoKey]);
            
            return $this->db->doFetch($result);
            
        } catch (Exception $e) {
            $this->logger->error('Failed to get watch progress', [
                'video_key' => $videoKey,
                'user_id' => $userId,
                'error' => $e->getMessage()
            ]);
            
            return null;
        }
    }
    
    /**
     * Private helper methods
     */
    
    private function getVideoInfo($videoKey)
    {
        $query = "SELECT * FROM db_videofiles WHERE file_key = ? AND file_active = 1";
        $result = $this->db->doQuery($query, [$videoKey]);
        
        return $this->db->doFetch($result);
    }
    
    private function checkVideoAccess($video, $userId)
    {
        // Check if video is public
        if ($video['privacy'] === 'public') {
            return true;
        }
        
        // Check if user owns the video
        if ($userId && $video['usr_id'] == $userId) {
            return true;
        }
        
        // Check if video is unlisted and user has direct link
        if ($video['privacy'] === 'unlisted') {
            return true;
        }
        
        // Private videos require ownership or special permissions
        if ($video['privacy'] === 'private') {
            return $userId && $video['usr_id'] == $userId;
        }
        
        return false;
    }
    
    private function getProgressiveStreamUrl($videoKey, $format)
    {
        $filePath = $this->getVideoFilePath($videoKey, $format);
        
        if ($filePath && file_exists($filePath)) {
            return $this->baseUrl . '/stream.php?v=' . urlencode($videoKey) . '&f=' . urlencode($format);
        }
        
        return null;
    }
    
    private function getHLSStreamUrl($videoKey)
    {
        $playlistPath = $this->streamingPath . $videoKey . '/hls/master.m3u8';
        
        if (file_exists($playlistPath)) {
            return $this->baseUrl . '/stream.php?v=' . urlencode($videoKey) . '&hls=master.m3u8';
        }
        
        return null;
    }
    
    private function getThumbnailUrls($videoKey)
    {
        $thumbnailDir = $this->streamingPath . $videoKey . '/thumbnails/';
        $thumbnails = [];
        
        // Main thumbnail
        $mainThumb = $thumbnailDir . $videoKey . '_thumb.jpg';
        if (file_exists($mainThumb)) {
            $thumbnails['main'] = $this->baseUrl . '/f_data/processed/' . $videoKey . '/thumbnails/' . $videoKey . '_thumb.jpg';
        }
        
        // Preview thumbnails
        $previewThumbs = [];
        for ($i = 1; $i <= 5; $i++) {
            $previewThumb = $thumbnailDir . $videoKey . "_preview_{$i}.jpg";
            if (file_exists($previewThumb)) {
                $previewThumbs[] = $this->baseUrl . '/f_data/processed/' . $videoKey . '/thumbnails/' . $videoKey . "_preview_{$i}.jpg";
            }
        }
        $thumbnails['preview'] = $previewThumbs;
        
        return $thumbnails;
    }
    
    private function getVideoFilePath($videoKey, $format)
    {
        $safeFormat = $this->sanitizeFormat($format);
        if (!$safeFormat) {
            return null;
        }
        
        return $this->streamingPath . $videoKey . '/' . $videoKey . '_' . $safeFormat . '.mp4';
    }
    
    private function trackView($videoKey, $userId)
    {
        try {
            $viewData = [
                'video_key' => $videoKey,
                'user_id' => $userId,
                'ip_address' => $this->getRealIpAddress(),
                'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? '',
                'created_at' => date('Y-m-d H:i:s')
            ];
            
            $this->db->doInsert('db_video_views', $viewData);
            
            // Update video view count
            $this->db->doQuery("UPDATE db_videofiles SET file_views = file_views + 1 WHERE file_key = ?", [$videoKey]);
            
        } catch (Exception $e) {
            $this->logger->error('Failed to track view', [
                'video_key' => $videoKey,
                'user_id' => $userId,
                'error' => $e->getMessage()
            ]);
        }
    }
    
    private function trackStreamRequest($videoKey, $format, $userId)
    {
        try {
            $analyticsData = [
                'video_key' => $videoKey,
                'user_id' => $userId,
                'format' => $format,
                'ip_address' => $this->getRealIpAddress(),
                'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? '',
                'created_at' => date('Y-m-d H:i:s')
            ];
            
            $this->db->doInsert('db_video_analytics', $analyticsData);
            
        } catch (Exception $e) {
            $this->logger->error('Failed to track stream request', [
                'video_key' => $videoKey,
                'format' => $format,
                'error' => $e->getMessage()
            ]);
        }
    }
    
    private function streamFileWithRanges($filePath, $contentType)
    {
        $fileSize = filesize($filePath);
        $start = 0;
        $end = $fileSize - 1;
        
        // Handle range requests
        if (isset($_SERVER['HTTP_RANGE'])) {
            $range = $_SERVER['HTTP_RANGE'];
            if (preg_match('/bytes=(\d+)-(\d*)/', $range, $matches)) {
                $start = intval($matches[1]);
                if (!empty($matches[2])) {
                    $end = intval($matches[2]);
                }
            }
            
            http_response_code(206); // Partial Content
            header('Accept-Ranges: bytes');
            header("Content-Range: bytes $start-$end/$fileSize");
        } else {
            http_response_code(200);
        }
        
        // Set headers
        header('Content-Type: ' . $contentType);
        header('Content-Length: ' . ($end - $start + 1));
        header('Cache-Control: max-age=3600');
        header('Access-Control-Allow-Origin: *');
        
        // Stream the file
        $handle = fopen($filePath, 'rb');
        fseek($handle, $start);
        
        $chunkSize = 8192;
        $bytesRemaining = $end - $start + 1;
        
        while ($bytesRemaining > 0 && !feof($handle)) {
            $bytesToRead = min($chunkSize, $bytesRemaining);
            echo fread($handle, $bytesToRead);
            $bytesRemaining -= $bytesToRead;
            
            if (ob_get_level()) {
                ob_flush();
            }
            flush();
        }
        
        fclose($handle);
    }
    
    private function getMimeType($format)
    {
        switch (strtolower($format)) {
            case 'mp4':
            case '1080p':
            case '720p':
            case '480p':
            case '360p':
            case '240p':
                return 'video/mp4';
            case 'webm':
                return 'video/webm';
            case 'ogv':
                return 'video/ogg';
            default:
                return 'video/mp4';
        }
    }
    
    private function getQualityLabel($format)
    {
        $labels = [
            '1080p' => '1080p HD',
            '720p' => '720p HD',
            '480p' => '480p',
            '360p' => '360p',
            '240p' => '240p'
        ];
        
        return $labels[$format] ?? $format;
    }
    
    private function sanitizeFormat($format)
    {
        $format = trim((string)$format);
        if ($format === '') {
            return null;
        }
        
        if (!preg_match('/^[a-zA-Z0-9_\-]+$/', $format)) {
            return null;
        }
        
        return $format;
    }
    
    private function sanitizeHLSPath($path)
    {
        $path = str_replace('\\', '/', (string)$path);
        $path = trim($path, '/');
        
        if ($path === '') {
            return null;
        }
        
        $segments = explode('/', $path);
        $cleanSegments = [];
        
        foreach ($segments as $segment) {
            if ($segment === '' || $segment === '.' || $segment === '..') {
                return null;
            }
            
            if (!preg_match('/^[a-zA-Z0-9_\-\.]+$/', $segment)) {
                return null;
            }
            
            $cleanSegments[] = $segment;
        }
        
        return implode('/', $cleanSegments);
    }
    
    private function rewritePlaylistUrls($playlist, $videoKey, $currentPath)
    {
        $lines = preg_split('/\r\n|\r|\n/', $playlist);
        $baseDir = trim(str_replace('\\', '/', dirname($currentPath)), '/');
        
        foreach ($lines as $index => $line) {
            $trimmed = trim($line);
            
            if ($trimmed === '' || strpos($trimmed, '#') === 0) {
                continue;
            }
            
            if (preg_match('/^https?:\/\//i', $trimmed)) {
                continue;
            }
            
            if (!preg_match('/\.(m3u8|ts)$/i', $trimmed)) {
                continue;
            }
            
            $resolved = $this->normalizeHLSRelativePath($baseDir, $trimmed);
            $safePath = $this->sanitizeHLSPath($resolved);
            
            if (!$safePath) {
                continue;
            }
            
            $lines[$index] = '/stream.php?v=' . rawurlencode($videoKey) . '&hls=' . $this->encodeHLSPath($safePath);
        }
        
        return implode("\n", $lines);
    }
    
    private function normalizeHLSRelativePath($baseDir, $target)
    {
        $baseDir = trim($baseDir, '/');
        $target = str_replace('\\', '/', (string)$target);
        
        if (strpos($target, '/') === 0) {
            $target = ltrim($target, '/');
        } elseif ($baseDir !== '') {
            $target = $baseDir . '/' . $target;
        }
        
        $parts = [];
        foreach (explode('/', $target) as $part) {
            if ($part === '' || $part === '.') {
                continue;
            }
            if ($part === '..') {
                array_pop($parts);
                continue;
            }
            $parts[] = $part;
        }
        
        return implode('/', $parts);
    }
    
    private function encodeHLSPath($path)
    {
        $segments = explode('/', $path);
        $encodedSegments = array_map('rawurlencode', $segments);
        
        return implode('/', $encodedSegments);
    }
    
    private function getBaseUrl()
    {
        $protocol = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http';
        $host = $_SERVER['HTTP_HOST'] ?? 'localhost';
        
        return $protocol . '://' . $host;
    }
    
    private function getRealIpAddress()
    {
        if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
            return $_SERVER['HTTP_CLIENT_IP'];
        } elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
            return $_SERVER['HTTP_X_FORWARDED_FOR'];
        } else {
            return $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
        }
    }
}
