<?php
ob_start();

// Подключение необходимых файлов конфигурации и аутентификации
require_once 'core/config/config.php';

/**
 * Класс для генерации плейлистов различных типов
 */
class PlaylistGenerator
{
    private $db;
    private $token;
    private $user;
    private $customEPGURLs; // Ожидается строка с EPG URL (может быть разделена запятыми)
    private $errorBannedURL;
    private $errorSubscriptionExpiredURL;
    private $cdnUrl;
    private $appName;
    private $iconHost; // Хост для подстановки в плейсхолдер {this} в иконках
    private $internalHosts = []; // Список «своих» хостов для перезаписи в URL иконок
    private $initResult; // Результат инициализации
    private $deviceHash; // 8 hex-символов, серверный device-hash для composite-токена в auth.php

    /**
     * Конструктор класса
     *
     * @param PDO    $db    Подключение к базе данных
     * @param string $token Токен пользователя
     */
    public function __construct(PDO $db, $token)
    {
        $this->db         = $db;
        $this->token      = $token;
        $this->deviceHash = self::makeDeviceHash();
        $this->initialize();
    }

    /**
     * Генерация device-hash: 8 hex-символов криптослучайных байт.
     * Новый хеш на каждый запрос плейлиста — два одинаковых устройства
     * за одним IP/UA будут различимы. Формат совместим с composite-парсером
     * в auth.php (TOKEN-XXXXXXXX, /^[a-f0-9]{8}$/).
     */
    private static function makeDeviceHash()
    {
        return bin2hex(random_bytes(4));
    }

    /**
     * Возвращает composite-токен `TOKEN-XXXXXXXX` для подстановки в URL потоков.
     */
    private function tokenWithDevice()
    {
        return $this->user->token . '-' . $this->deviceHash;
    }

    /**
     * Инициализация пользователя и загрузка настроек
     */
    private function initialize()
    {
        $this->user = $this->getUserByToken($this->token);
        if (!$this->user) {
            $this->initResult = ['type' => 'no_user'];
            return;
        }

        if ($this->isUserBanned($this->user->id)) {
            $this->initResult = ['type' => 'banned'];
            return;
        }

        if ($this->isSubscriptionExpired($this->user->id)) {
            $this->initResult = ['type' => 'subscription_expired'];
            return;
        }

        // Загружаем все настройки одним запросом
        $this->loadSettings();
        $this->initResult = null;
    }

    /**
     * Получение результата инициализации
     * 
     * @return array|null Массив с типом ошибки ('no_user', 'banned', 'subscription_expired') или null если всё ОК
     */
    public function getInitResult()
    {
        return $this->initResult;
    }

    /**
     * Получение пользователя по токену
     */
    private function getUserByToken($token)
    {
        $stmt = $this->db->prepare("SELECT * FROM users WHERE token = ?");
        $stmt->execute([$token]);
        return $stmt->fetch(PDO::FETCH_OBJ);
    }

    /**
     * Загрузка всех необходимых настроек одним запросом.
     * Если у пользователя задан provider_id — соответствующие поля провайдера
     * переопределяют глобальные настройки.
     */
    private function loadSettings()
    {
        $requiredKeys = ['customEPGURL', 'errorBannedURL', 'errorSubscriptionExpiredURL', 'cdn_url', 'app_name'];
        $placeholders = implode(',', array_fill(0, count($requiredKeys), '?'));
        $stmt = $this->db->prepare("SELECT `key`, `value` FROM `settings` WHERE `key` IN ($placeholders)");
        $stmt->execute($requiredKeys);
        $settings = [];
        while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
            $settings[$row['key']] = $row['value'];
        }

        $this->customEPGURLs               = $settings['customEPGURL'] ?? null;
        $this->errorBannedURL              = $settings['errorBannedURL'] ?? null;
        $this->errorSubscriptionExpiredURL = $settings['errorSubscriptionExpiredURL'] ?? null;
        $this->cdnUrl                      = $settings['cdn_url'] ?? null;
        $this->appName                     = $settings['app_name'] ?? null;
        $this->iconHost                    = $_SERVER["HTTP_HOST"] ?? '';

        // Собираем «свои» хосты — для перезаписи в URL иконок: HTTP_HOST + глобальный domain + все домены провайдеров
        $this->internalHosts = [];
        if (!empty($_SERVER['HTTP_HOST'])) $this->internalHosts[] = strtolower($_SERVER['HTTP_HOST']);
        try {
            $stmt = $this->db->prepare("SELECT `value` FROM `settings` WHERE `key` = 'domain'");
            $stmt->execute();
            $row = $stmt->fetch(PDO::FETCH_ASSOC);
            if ($row && !empty($row['value'])) {
                $h = parse_url($row['value'], PHP_URL_HOST);
                if ($h) $this->internalHosts[] = strtolower($h);
            }
        } catch (Exception $e) {}
        try {
            $rows = $this->db->query("SELECT domain FROM providers WHERE domain IS NOT NULL AND domain != ''")->fetchAll(PDO::FETCH_COLUMN);
            foreach ($rows as $d) {
                $h = parse_url($d, PHP_URL_HOST);
                if ($h) $this->internalHosts[] = strtolower($h);
            }
        } catch (Exception $e) {}
        $this->internalHosts = array_values(array_unique($this->internalHosts));

        // Override от провайдера, если у пользователя есть provider_id
        $providerId = isset($this->user->provider_id) ? (int)$this->user->provider_id : 0;
        if ($providerId > 0) {
            try {
                $stmt = $this->db->prepare(
                    "SELECT domain, cdn_url, epg_url, error_banned_url, error_subscription_expired_url
                     FROM providers WHERE id = ?"
                );
                $stmt->execute([$providerId]);
                $provider = $stmt->fetch(PDO::FETCH_ASSOC);
                if ($provider) {
                    if (!empty($provider['cdn_url']))                        $this->cdnUrl                      = $provider['cdn_url'];
                    if (!empty($provider['epg_url']))                        $this->customEPGURLs               = $provider['epg_url'];
                    if (!empty($provider['error_banned_url']))               $this->errorBannedURL              = $provider['error_banned_url'];
                    if (!empty($provider['error_subscription_expired_url'])) $this->errorSubscriptionExpiredURL = $provider['error_subscription_expired_url'];
                    if (!empty($provider['domain'])) {
                        $providerHost = parse_url($provider['domain'], PHP_URL_HOST);
                        if (!empty($providerHost)) {
                            $this->iconHost = $providerHost;
                        }
                    }
                }
            } catch (Exception $e) {
                // Если таблица providers ещё не мигрирована — молча игнорируем, остаются глобальные
                error_log('PlaylistGenerator: provider override skipped — ' . $e->getMessage());
            }
        }
    }

    /**
     * Получение значения настройки по ключу (для обратной совместимости)
     */
    private function getSetting($key)
    {
        switch ($key) {
            case 'customEPGURL':
                return $this->customEPGURLs;
            case 'errorBannedURL':
                return $this->errorBannedURL;
            case 'errorSubscriptionExpiredURL':
                return $this->errorSubscriptionExpiredURL;
            case 'cdn_url':
                return $this->cdnUrl;
            case 'app_name':
                return $this->appName;
            default:
                $stmt = $this->db->prepare("SELECT `value` FROM `settings` WHERE `key` = ?");
                $stmt->execute([$key]);
                $result = $stmt->fetch(PDO::FETCH_ASSOC);
                return $result ? $result['value'] : null;
        }
    }

    /**
     * Проверка, забанен ли пользователь
     */
    private function isUserBanned($userId)
    {
        $stmt = $this->db->prepare("SELECT is_banned FROM users WHERE id = ?");
        $stmt->execute([$userId]);
        $result = $stmt->fetch(PDO::FETCH_ASSOC);
        return $result && $result['is_banned'] == 1;
    }

    /**
     * Проверка, истекла ли подписка пользователя
     */
    private function isSubscriptionExpired($userId)
    {
        $stmt = $this->db->prepare("SELECT * FROM users WHERE id = ? AND expiration_date < NOW()");
        $stmt->execute([$userId]);
        return $stmt->rowCount() > 0;
    }

    /**
     * Проверка валидности типа плейлиста
     * 
     * @param string $type Тип плейлиста
     * @return string|null Возвращает ошибку или null если тип валиден
     */
    private function validatePlaylistType($type)
    {
        $allowedTypes = ['m3u8', 'siptv', 'hls2', 'ott', 'e2', 'ssiptv'];
        if (!in_array($type, $allowedTypes)) {
            return "Неизвестный тип плейлиста: \"$type\"";
        }
        return null;
    }

    /**
     * Очистка строки от символов переноса строки
     */
    private function cleanString($string)
    {
        return str_replace(["\r\n", "\n", "\r"], '', $string);
    }

    /**
     * Перезапись URL иконки под домен провайдера:
     * 1) подставляем {this} → iconHost
     * 2) если URL абсолютный и его хост в списке «своих» (HTTP_HOST/глобальный domain/любой провайдерский) —
     *    меняем хост на iconHost провайдера
     */
    private function rewriteIcon($iconUrl)
    {
        $iconUrl = str_replace('{this}', $this->iconHost, $iconUrl);
        if (empty($this->iconHost)) return $iconUrl;
        $h = parse_url($iconUrl, PHP_URL_HOST);
        if ($h && strtolower($h) !== strtolower($this->iconHost) && in_array(strtolower($h), $this->internalHosts, true)) {
            $iconUrl = preg_replace('#^(https?://)' . preg_quote($h, '#') . '#i', '$1' . $this->iconHost, $iconUrl);
        }
        return $iconUrl;
    }

    /**
     * Генерация плейлиста с ошибкой (бан или истечение подписки)
     * 
     * @param string $type Тип ошибки ('banned' или 'subscription_expired')
     * @return array Массив с заголовками и содержимым плейлиста
     */
    private function generateErrorPlaylist($type)
    {
        $content = "#EXTM3U url-tvg=\"" . $this->cleanString($this->customEPGURLs) . "\"\n";

        if ($type === 'banned') {
            $content .= "#EXTINF:-1,доступ запрещён обратитесь в техподдержку\n";
            $content .= $this->cleanString($this->errorBannedURL) . "\n";
        } elseif ($type === 'subscription_expired') {
            $content .= "#EXTINF:-1,продлите вашу подписку\n";
            $content .= $this->cleanString($this->errorSubscriptionExpiredURL) . "\n";
        }
        
        return [
            'headers' => [
                'Content-type' => 'audio/x-mpegurl',
                'Content-disposition' => 'attachment; filename=playlist.m3u8'
            ],
            'content' => $content
        ];
    }

    /**
     * Генерация плейлиста заданного типа
     *
     * @param string $type ('m3u8', 'siptv', 'hls2', 'ott', 'e2', 'ssiptv')
     * @return array Массив с заголовками и содержимым плейлиста
     * @throws Exception Если тип плейлиста невалиден
     */
    public function generatePlaylist($type = 'm3u8')
    {
        $type = strtolower($type);
        $error = $this->validatePlaylistType($type);
        if ($error !== null) {
            throw new Exception($error);
        }

        // Метод для каждого типа плейлиста
        $methodName = "generatePlaylist_" . $type;
        return $this->$methodName();
    }

    /**
     * Генерация плейлиста M3U8 (дефолтный)
     * 
     * @return array Массив с заголовками и содержимым плейлиста
     */
    private function generatePlaylist_m3u8()
    {
        $content = "#EXTM3U url-tvg=\"" . $this->cleanString($this->customEPGURLs) . "\"\n";
        
        $streams = $this->getUserStreams();
        foreach ($streams as $stream) {
            if ($this->isChannelDisabled($stream)) {
                continue;
            }
            if ($stream->enabled === "true") {
                $tvg_id      = $this->cleanString($stream->name);
                $group_title = $this->cleanString($stream->group_name);
                $tvg_logo    = (trim($stream->icon) !== "")
                    ? " tvg-logo=\"" . $this->cleanString($this->rewriteIcon($stream->icon)) . "\""
                    : "";
                
                $catchup        = $stream->dvr_enabled === "true" ? "default" : "";
                $catchup_days   = $stream->dvr_enabled === "true" ? $this->cleanString($stream->dvr_days) : "0";
                $catchup_source = $stream->dvr_enabled === "true"
                    ? $this->buildUrl($this->cdnUrl ?: $stream->server, $stream->url . "/video-\${start}-\${duration}.m3u8") . "?token=" . $this->tokenWithDevice()
                    : "";
                $timeshift      = $stream->dvr_enabled === "true" ? $this->cleanString($stream->dvr_days) : "0";

                $streamName = $this->cleanString($stream->name);
                $stream_url = $this->buildUrl(
                    $this->cdnUrl ?: $stream->server,
                    $stream->url . "/video.m3u8"
                ) . "?token=" . $this->tokenWithDevice();

                $extinf  = "#EXTINF:-1 tvg-id=\"$tvg_id\" group-title=\"$group_title\"";
                if ($tvg_logo) {
                    $extinf .= " $tvg_logo";
                }
                if ($stream->dvr_enabled === "true") {
                    $extinf .= " catchup=\"$catchup\" catchup-days=\"$catchup_days\" catchup-source=\"$catchup_source\" timeshift=\"$timeshift\"";
                } else {
                    $extinf .= " catchup-days=\"0\"";
                }
                $extinf .= ", $streamName\n";

                $content .= $extinf;
                $content .= "$stream_url\n";
            }
        }
        
        return [
            'headers' => [
                'Content-type' => 'audio/x-mpegurl',
                'Content-disposition' => 'attachment; filename=playlist.m3u8'
            ],
            'content' => $content
        ];
    }

    /**
     * Генерация плейлиста (M3U) для "siptv"
     * 
     * @return array Массив с заголовками и содержимым плейлиста
     */
    private function generatePlaylist_siptv()
    {
        $content = "#EXTM3U url-tvg=\"" . $this->cleanString($this->customEPGURLs) . "\"\n";
        
        $streams = $this->getUserStreams();
        foreach ($streams as $stream) {
            if ($this->isChannelDisabled($stream)) {
                continue;
            }
            if ($stream->enabled === "true") {
                $tvg_id      = $this->cleanString($stream->name);
                $group_title = $this->cleanString($stream->group_name);
                $tvg_logo    = (trim($stream->icon) !== "")
                    ? " tvg-logo=\"" . $this->cleanString($this->rewriteIcon($stream->icon)) . "\""
                    : "";
                
                $catchup        = $stream->dvr_enabled === "true" ? "default" : "";
                $catchup_days   = $stream->dvr_enabled === "true" ? $this->cleanString($stream->dvr_days) : "0";
                $catchup_source = $stream->dvr_enabled === "true"
                    ? $this->buildUrl($this->cdnUrl ?: $stream->server, $stream->url . "/video.m3u8") . "?token=" . $this->tokenWithDevice()
                    : "";
                $timeshift      = $stream->dvr_enabled === "true" ? $this->cleanString($stream->dvr_days) : "0";

                $streamName = $this->cleanString($stream->name);
                $stream_url = $this->buildUrl(
                    $this->cdnUrl ?: $stream->server,
                    $stream->url . "/video.m3u8"
                ) . "?token=" . $this->tokenWithDevice();

                $extinf  = "#EXTINF:-1 tvg-id=\"$tvg_id\" group-title=\"$group_title\"";
                if ($tvg_logo) {
                    $extinf .= " $tvg_logo";
                }
                if ($stream->dvr_enabled === "true") {
                    $extinf .= " catchup=\"$catchup\" catchup-days=\"$catchup_days\" catchup-source=\"$catchup_source\" timeshift=\"$timeshift\"";
                } else {
                    $extinf .= " catchup-days=\"0\"";
                }
                $extinf .= ", $streamName\n";

                $content .= $extinf;
                $content .= "$stream_url\n";
            }
        }
        
        return [
            'headers' => [
                'Content-type' => 'audio/x-mpegurl',
                'Content-disposition' => 'attachment; filename=playlist_siptv.m3u8'
            ],
            'content' => $content
        ];
    }

    /**
     * Генерация плейлиста HLS2
     * 
     * @return array Массив с заголовками и содержимым плейлиста
     */
    private function generatePlaylist_hls2()
    {
        $content = "#EXTM3U url-tvg=\"" . $this->cleanString($this->customEPGURLs) . "\"\n";
        
        $streams = $this->getUserStreams();
        foreach ($streams as $stream) {
            if ($this->isChannelDisabled($stream)) {
                continue;
            }
            if ($stream->enabled === "true") {
                $tvg_id      = $this->cleanString($stream->name);
                $group_title = $this->cleanString($stream->group_name);
                $tvg_logo    = (trim($stream->icon) !== "")
                    ? " tvg-logo=\"" . $this->cleanString($this->rewriteIcon($stream->icon)) . "\""
                    : "";
                
                $catchup        = $stream->dvr_enabled === "true" ? "default" : "";
                $catchup_days   = $stream->dvr_enabled === "true" ? $this->cleanString($stream->dvr_days) : "0";
                $catchup_source = $stream->dvr_enabled === "true"
                    ? $this->buildUrl($this->cdnUrl ?: $stream->server, $stream->url . "/video-\${start}-\${duration}.m3u8") . "?token=" . $this->tokenWithDevice()
                    : "";
                $timeshift      = $stream->dvr_enabled === "true" ? $this->cleanString($stream->dvr_days) : "0";
                
                $streamName = $this->cleanString($stream->name);
                $stream_url = $this->buildUrl(
                    $this->cdnUrl ?: $stream->server,
                    $stream->url . "/video.m3u8"
                ) . "?token=" . $this->tokenWithDevice();

                $extinf  = "#EXTINF:-1 tvg-id=\"$tvg_id\" group-title=\"$group_title\"";
                if ($tvg_logo) {
                    $extinf .= " $tvg_logo";
                }
                if ($stream->dvr_enabled === "true") {
                    $extinf .= " catchup=\"$catchup\" catchup-days=\"$catchup_days\" catchup-source=\"$catchup_source\" timeshift=\"$timeshift\"";
                } else {
                    $extinf .= " catchup-days=\"0\"";
                }
                $extinf .= ", $streamName\n";

                $content .= $extinf;
                $content .= "$stream_url\n";
            }
        }
        
        return [
            'headers' => [
                'Content-type' => 'audio/x-mpegurl',
                'Content-disposition' => 'attachment; filename=playlist_hls2.m3u8'
            ],
            'content' => $content
        ];
    }

    /**
     * Генерация плейлиста OTT
     * 
     * @return array Массив с заголовками и содержимым плейлиста
     */
    private function generatePlaylist_ott()
    {
        $content = "#EXTM3U url-tvg=\"" . $this->cleanString($this->customEPGURLs) . "\"\n";

        $streams = $this->getUserStreams();
        foreach ($streams as $stream) {
            if ($this->isChannelDisabled($stream)) {
                continue;
            }
            if ($stream->enabled === "true") {
                $cuid     = isset($stream->cuid)   ? $this->cleanString($stream->cuid)   : "116";
                $number   = isset($stream->number) ? $this->cleanString($stream->number) : "2";
                $tvg_id   = $this->cleanString($stream->id);
                $tvg_name = isset($stream->tvg_name) ? $this->cleanString($stream->tvg_name) : $tvg_id;

                if ($stream->dvr_enabled === "true") {
                    $dvr_days      = (isset($stream->dvr_days) && $stream->dvr_days !== "") ? (int)$stream->dvr_days : 21;
                    $arc_hours     = $dvr_days * 24;
                    $arc_timeshift = $arc_hours;
                    $arc_time      = $arc_hours;
                    $catchup       = "shift";
                    $catchup_days  = $this->cleanString($stream->dvr_days);
                } else {
                    $arc_timeshift = "0";
                    $arc_time      = "0";
                    $catchup       = "";
                    $catchup_days  = "0";
                }

                $tvg_logo  = (trim($stream->icon) !== "")
                    ? " tvg-logo=\"" . $this->cleanString($this->rewriteIcon($stream->icon)) . "\""
                    : "";

                $group_title = $this->cleanString($stream->group_name);
                $streamName  = $this->cleanString($stream->name);
                $stream_url  = $this->buildUrl(
                    $this->cdnUrl ?: $stream->server,
                    $stream->url . "/video.m3u8"
                ) . "?token=" . $this->tokenWithDevice();

                $extinf  = "#EXTINF:-1";
                // OTT-специфичные атрибуты
                $extinf .= " CUID=\"" . $cuid . "\"";
                $extinf .= " number=\"" . $number . "\"";
                $extinf .= " tvg-id=\"" . $tvg_id . "\"";
                $extinf .= " tvg-name=\"" . $tvg_name . "\"";
                $extinf .= " arc-timeshift=\"" . $arc_timeshift . "\"";
                $extinf .= " arc-time=\"" . $arc_time . "\"";

                if ($stream->dvr_enabled === "true") {
                    $extinf .= " catchup=\"" . $catchup . "\" catchup-days=\"" . $catchup_days . "\"";
                } else {
                    $extinf .= " catchup-days=\"0\"";
                }
                $extinf .= $tvg_logo;
                $extinf .= " group-title=\"" . $group_title . "\"";
                $extinf .= ",$streamName\n";

                $content .= $extinf;
                $content .= "$stream_url\n";
            }
        }
        
        return [
            'headers' => [
                'Content-type' => 'audio/x-mpegurl',
                'Content-disposition' => 'attachment; filename=playlist_ott.m3u8'
            ],
            'content' => $content
        ];
    }

    /**
     * Генерация плейлиста Enigma2 (E2)
     * 
     * @return array Массив с заголовками и содержимым плейлиста
     */
    private function generatePlaylist_e2()
    {
        $content = '#NAME ' . $this->cleanString($this->appName) . "\n";

        $streams = $this->getUserStreams();
        foreach ($streams as $stream) {
            if ($this->isChannelDisabled($stream)) {
                continue;
            }
            if ($stream->enabled === "true") {
                $streamName  = $this->cleanString($stream->name);
                $location    = $this->cdnUrl ?: $this->cleanString($stream->server);
                $stream_url  = $this->buildUrl($location, $stream->url . "/video.m3u8") . "?token=" . $this->tokenWithDevice();
                $encoded_url = urlencode($stream_url);

                $content .= "#SERVICE 1:0:1:0:0:0:0:0:0:0:" . "$encoded_url:$streamName\n";
            }
        }
        
        return [
            'headers' => [
                'Content-type' => 'text/plain',
                'Content-disposition' => 'attachment; filename=playlist_e2.m3u'
            ],
            'content' => $content
        ];
    }

    /**
     * === ПЕРЕПИСАННЫЙ МЕТОД для SS IPTV (как "OTT") ===
     * 
     * @return array Массив с заголовками и содержимым плейлиста
     */
    private function generatePlaylist_ssiptv()
    {
        // Генерируем тот же плейлист, что и в "OTT",
        // только с другим именем файла (playlist_ssiptv.m3u8)
        $content = "#EXTM3U url-tvg=\"" . $this->cleanString($this->customEPGURLs) . "\"\n";

        $streams = $this->getUserStreams();
        foreach ($streams as $stream) {
            if ($this->isChannelDisabled($stream)) {
                continue;
            }
            if ($stream->enabled === "true") {
                $cuid     = isset($stream->cuid)   ? $this->cleanString($stream->cuid)   : "116";
                $number   = isset($stream->number) ? $this->cleanString($stream->number) : "2";
                $tvg_id   = $this->cleanString($stream->id);
                $tvg_name = isset($stream->tvg_name) ? $this->cleanString($stream->tvg_name) : $tvg_id;

                if ($stream->dvr_enabled === "true") {
                    $dvr_days      = (isset($stream->dvr_days) && $stream->dvr_days !== "") ? (int)$stream->dvr_days : 21;
                    $arc_hours     = $dvr_days * 24;
                    $arc_timeshift = $arc_hours;
                    $arc_time      = $arc_hours;
                    $catchup       = "shift";
                    $catchup_days  = $this->cleanString($stream->dvr_days);
                } else {
                    $arc_timeshift = "0";
                    $arc_time      = "0";
                    $catchup       = "";
                    $catchup_days  = "0";
                }

                $tvg_logo  = (trim($stream->icon) !== "")
                    ? " tvg-logo=\"" . $this->cleanString($this->rewriteIcon($stream->icon)) . "\""
                    : "";

                $group_title = $this->cleanString($stream->group_name);
                $streamName  = $this->cleanString($stream->name);
                $stream_url  = $this->buildUrl(
                    $this->cdnUrl ?: $stream->server,
                    $stream->url . "/video.m3u8"
                ) . "?token=" . $this->tokenWithDevice();

                // Формируем строку, аналогичную OTT
                $extinf  = "#EXTINF:-1";
                $extinf .= " CUID=\"" . $cuid . "\"";
                $extinf .= " number=\"" . $number . "\"";
                $extinf .= " tvg-id=\"" . $tvg_id . "\"";
                $extinf .= " tvg-name=\"" . $tvg_name . "\"";
                $extinf .= " arc-timeshift=\"" . $arc_timeshift . "\"";
                $extinf .= " arc-time=\"" . $arc_time . "\"";

                if ($stream->dvr_enabled === "true") {
                    $extinf .= " catchup=\"" . $catchup . "\" catchup-days=\"" . $catchup_days . "\"";
                } else {
                    $extinf .= " catchup-days=\"0\"";
                }
                $extinf .= $tvg_logo;
                $extinf .= " group-title=\"" . $group_title . "\"";
                $extinf .= ",$streamName\n";

                $content .= $extinf;
                $content .= "$stream_url\n";
            }
        }
        
        return [
            'headers' => [
                'Content-type' => 'audio/x-mpegurl',
                'Content-disposition' => 'attachment; filename=playlist_ssiptv.m3u8'
            ],
            'content' => $content
        ];
    }

    /**
     * Получение потоков пользователя на основе его пакетов с учетом персональных настроек
     * Оптимизировано с использованием JOIN для избежания N+1 проблемы
     */
    private function getUserStreams()
    {
        // Сортировка СТРОГО по streams.`order` — это базовый порядок отображения каналов
        // (идентично sta.php / js.php). pc.sort_order по-прежнему отдаётся, но в качестве
        // ключа сортировки больше НЕ используется.
        // Если у пользователя есть user_preferences (custom_channel_order / custom_category_order),
        // они перебьют этот порядок ниже в applyUserPreferences() через usort.
        // s.`order` IS NULL ASC — NULL-каналы уходят в конец.
        // CAST(s.id AS UNSIGNED) ASC — детерминированный fallback при равных `order`.
        // `order` — зарезервированное слово SQL, поэтому в backticks.
        $stmt = $this->db->prepare("
            SELECT DISTINCT s.*, pc.sort_order
            FROM streams s
            INNER JOIN packagechannels pc ON s.id = pc.channel_id
            INNER JOIN packages_users pu ON pc.package_id = pu.packages_id
            WHERE pu.users_id = ?
            ORDER BY
                s.`order` IS NULL ASC,
                s.`order` ASC,
                CAST(s.id AS UNSIGNED) ASC
        ");
        $stmt->execute([$this->user->id]);
        $streams = $stmt->fetchAll(PDO::FETCH_OBJ);

        // Применяем персональные настройки пользователя
        $streams = $this->applyUserPreferences($streams);
        
        return $streams;
    }

    /**
     * Применение персональных настроек пользователя к потокам.
     * Логика идентична sta.php / js.php, чтобы порядок каналов совпадал во всех трёх скриптах.
     */
    private function applyUserPreferences($streams)
    {
        // Получаем персональные настройки пользователя
        $stmt = $this->db->prepare("
            SELECT preference_type, preference_data
            FROM user_preferences
            WHERE user_id = ?
        ");
        $stmt->execute([$this->user->id]);
        $preferences = [];
        while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
            $preferences[$row['preference_type']] = json_decode($row['preference_data'], true);
        }

        // Фильтруем скрытые каналы
        if (isset($preferences['hidden_channels']) && !empty($preferences['hidden_channels'])) {
            $hiddenChannels = $preferences['hidden_channels'];
            $streams = array_filter($streams, function($stream) use ($hiddenChannels) {
                return !in_array($stream->id, $hiddenChannels);
            });
        }

        // Фильтруем скрытые категории
        if (isset($preferences['hidden_categories']) && !empty($preferences['hidden_categories'])) {
            $hiddenCategories = $preferences['hidden_categories'];
            $streams = array_filter($streams, function($stream) use ($hiddenCategories) {
                return !in_array($stream->group_name, $hiddenCategories);
            });
        }

        // Если у пользователя НЕТ своего custom_category_order — склеиваем категории,
        // начинающиеся с "Кинозалы", в один блок (чтобы они шли друг за другом).
        // Блок встаёт в позицию первой "Кинозалы"-категории; внутренний порядок
        // между Кинозалы-категориями сохраняется такой, в каком они появлялись.
        if (empty($preferences['custom_category_order'])) {
            $streams = $this->groupAdjacentKinozaly($streams);
        }

        // Применяем пользовательский порядок категорий
        if (isset($preferences['custom_category_order']) && !empty($preferences['custom_category_order'])) {
            $customOrder = $preferences['custom_category_order'];

            // Сортируем потоки по пользовательскому порядку категорий
            usort($streams, function($a, $b) use ($customOrder, $preferences) {
                $aCategory = $a->group_name ?: 'Без категории';
                $bCategory = $b->group_name ?: 'Без категории';

                $aIndex = array_search($aCategory, $customOrder);
                $bIndex = array_search($bCategory, $customOrder);

                // Если категория не найдена в пользовательском порядке, ставим в конец
                if ($aIndex === false) $aIndex = 9999;
                if ($bIndex === false) $bIndex = 9999;

                if ($aIndex === $bIndex) {
                    // Если категории одинаковые, применяем пользовательский порядок каналов
                    $customChannelOrder = $preferences['custom_channel_order'][$aCategory] ?? [];
                    if (!empty($customChannelOrder)) {
                        $aChannelIndex = array_search($a->id, array_column($customChannelOrder, 'id'));
                        $bChannelIndex = array_search($b->id, array_column($customChannelOrder, 'id'));

                        if ($aChannelIndex !== false && $bChannelIndex !== false) {
                            return $aChannelIndex - $bChannelIndex;
                        }
                    }
                    // Если нет пользовательского порядка каналов, сортируем по order
                    return $a->order - $b->order;
                }

                return $aIndex - $bIndex;
            });
        }

        // ИСПРАВЛЕНИЕ: Применяем пользовательский порядок каналов ГЛОБАЛЬНО
        if (isset($preferences['custom_channel_order']) && !empty($preferences['custom_channel_order'])) {
            // Создаем глобальный массив всех каналов в пользовательском порядке
            $globalChannelOrder = [];
            foreach ($preferences['custom_channel_order'] as $category => $channels) {
                foreach ($channels as $channel) {
                    $globalChannelOrder[] = (int)$channel['id'];
                }
            }

            usort($streams, function($a, $b) use ($globalChannelOrder) {
                $aIndex = array_search((int)$a->id, $globalChannelOrder);
                $bIndex = array_search((int)$b->id, $globalChannelOrder);

                if ($aIndex !== false && $bIndex !== false) {
                    return $aIndex - $bIndex;
                } elseif ($aIndex !== false) {
                    return -1;
                } elseif ($bIndex !== false) {
                    return 1;
                } else {
                    return $a->order - $b->order;
                }
            });
        }

        // НЕ перемещаем избранные каналы в начало - возвращаем каналы в обычном порядке
        // Избранные каналы обрабатываются только на стороне клиента в категории "Мои каналы"
        return $streams;
    }

    /**
     * Склеивает категории с префиксом "Кинозалы" в один смежный блок.
     * Используется как дефолтное поведение, когда у юзера нет custom_category_order.
     *
     * Алгоритм:
     *   1. Группируем потоки по group_name, запоминая порядок появления групп
     *      (порядок задаётся первой встреченной строкой в исходном массиве,
     *      т.е. фактически по streams.`order`).
     *   2. Все группы, чьё название НАЧИНАЕТСЯ с "Кинозалы", собираем в отдельный
     *      список с сохранением их относительного порядка появления.
     *   3. Финальный порядок групп: проходим по исходному, заменяя первое появление
     *      "Кинозалы*" на склеенный блок ВСЕХ Кинозалов, последующие "Кинозалы*" пропускаем.
     *   4. Внутри каждой группы сохраняем порядок потоков (по streams.`order`).
     *
     * Многобайтная проверка префикса — иврит/кириллица в group_name безопасны.
     */
    private function groupAdjacentKinozaly($streams)
    {
        // streams приходит и как array (после usort), и как результат array_filter.
        // Нормализуем в обычный список:
        $streams = array_values($streams);
        if (empty($streams)) return $streams;

        $byGroup = [];        // group_name => [stream, stream, ...]
        $groupOrder = [];     // порядок появления групп в исходном массиве

        foreach ($streams as $s) {
            $g = $s->group_name ?: 'Без категории';
            if (!isset($byGroup[$g])) {
                $byGroup[$g] = [];
                $groupOrder[] = $g;
            }
            $byGroup[$g][] = $s;
        }

        $isKinozaly = function ($name) {
            // mb_strpos для UTF-8; префикс — "Кинозалы" (нечувствительно к регистру).
            return mb_stripos($name, 'Кинозалы', 0, 'UTF-8') === 0;
        };

        // Собираем список Кинозалы-категорий в их естественном порядке появления
        $kinoGroups = [];
        foreach ($groupOrder as $g) {
            if ($isKinozaly($g)) {
                $kinoGroups[] = $g;
            }
        }
        if (count($kinoGroups) < 2) {
            // Меньше двух Кинозалы — нечего склеивать
            return $streams;
        }

        // Строим финальный порядок групп
        $finalGroupOrder = [];
        $kinoInserted = false;
        foreach ($groupOrder as $g) {
            if ($isKinozaly($g)) {
                if (!$kinoInserted) {
                    foreach ($kinoGroups as $kg) {
                        $finalGroupOrder[] = $kg;
                    }
                    $kinoInserted = true;
                }
                // последующие Кинозалы — пропускаем (уже в блоке)
            } else {
                $finalGroupOrder[] = $g;
            }
        }

        // Восстанавливаем массив потоков в новом порядке
        $result = [];
        foreach ($finalGroupOrder as $g) {
            foreach ($byGroup[$g] as $s) {
                $result[] = $s;
            }
        }
        return $result;
    }

    /**
     * Проверка, отключён ли канал или группа каналов у пользователя
     */
    private function isChannelDisabled($stream)
    {
        $disabledChannels = array_map('trim', explode(",", $this->user->disabled_channels));
        $disabledGroups   = array_map('trim', explode(",", $this->user->disabled_groups));
        return in_array($stream->name, $disabledChannels) || in_array($stream->group_name, $disabledGroups);
    }

    /**
     * Построение полного URL (с проверкой наличия http:// или https://)
     */
    private function buildUrl($base, $path)
    {
        $base = trim($base);
        $path = trim($path);
        if (!preg_match("#^https?://#", $base)) {
            $base = "http://" . $base;
        }
        $path = ltrim($path, '/');
        return rtrim($base, '/') . '/' . $path;
    }
}

// ===================== Основной код выполнения скрипта =====================

try {
    $db = new PDO("mysql:host=$db_host;dbname=$db_name;charset=utf8", $db_user, $db_pass);
    $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch (PDOException $e) {
    header("HTTP/1.1 500 Internal Server Error");
    echo "Ошибка подключения к базе данных: " . htmlspecialchars($e->getMessage());
    die();
}

if (isset($_GET["token"])) {
    $token        = $_GET["token"];
    // Если в GET нет "type", по умолчанию будет "m3u8"
    $playlistType = isset($_GET["type"]) ? $_GET["type"] : 'm3u8';

    // Создаём генератор
    $playlistGenerator = new PlaylistGenerator($db, $token);
    
    // Проверяем результат инициализации
    $initResult = $playlistGenerator->getInitResult();
    if ($initResult !== null) {
        // Обрабатываем ошибки инициализации
        if ($initResult['type'] === 'no_user') {
            header('Content-type: audio/x-mpegurl');
            header('Content-disposition: attachment; filename=playlist.m3u8');
            echo "#EXTM3U\n";
            echo "#EXTINF:-1,нет регистрации\n";
            exit;
        } else {
            $errorPlaylist = $playlistGenerator->generateErrorPlaylist($initResult['type']);
            foreach ($errorPlaylist['headers'] as $headerName => $headerValue) {
                header("$headerName: $headerValue");
            }
            echo $errorPlaylist['content'];
            exit;
        }
    }
    
    // Генерируем плейлист
    try {
        $playlist = $playlistGenerator->generatePlaylist($playlistType);
        foreach ($playlist['headers'] as $headerName => $headerValue) {
            header("$headerName: $headerValue");
        }
        echo $playlist['content'];
    } catch (Exception $e) {
        header("HTTP/1.1 403 Forbidden");
        die($e->getMessage());
    }

} else {
    // Если нет токена — доступ запрещён
    header("HTTP/1.1 403 Forbidden");
    header("Location: https://www.google.com");
    exit;
}

// Выводим всё, что накопилось в буфере, и завершаем
$content = ob_get_clean();
echo trim($content);
?>
