<?php

declare(strict_types=1);

@set_time_limit(0);
@ini_set('max_execution_time', '0');

header_remove('X-Powered-By');

$dbFile = __DIR__ . '/../../../../../database/visionplay.db';

const MAX_TOKEN_LENGTH = 512;
const MAX_DEVICE_ID_LENGTH = 200;

const CONNECT_TIMEOUT_SECONDS = 10;
const STREAM_TIMEOUT_SECONDS = 0;

const CHUNK_SIZE = 1024 * 64;

const MAX_RECONNECTS = 500;
const RECONNECT_DELAY_MICROSECONDS = 0;

/*
 * Diagnóstico:
 * Registramos aproximadamente cada 1 MB recibido del proveedor.
 */
const DEBUG_LOG_BYTES_INTERVAL = 1024 * 1024;

function jsonError(int $status, string $message): never
{
    if (!headers_sent()) {
        http_response_code($status);
        header('Content-Type: application/json; charset=utf-8');
        header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
    }

    echo json_encode([
        'success' => false,
        'error' => $message,
    ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);

    exit;
}

function writePlaybackDebug(string $message, array $context = []): void
{
    $line = [
        'time' => date('Y-m-d H:i:s'),
        'message' => $message,
    ];

    foreach ($context as $key => $value) {
        if (is_scalar($value) || $value === null) {
            $line[$key] = $value;
        }
    }

    @file_put_contents(
        __DIR__ . '/playback_debug.log',
        json_encode(
            $line,
            JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES
        ) . PHP_EOL,
        FILE_APPEND | LOCK_EX
    );
}

function validatePlaybackSession(
    PDO $db,
    string $tokenHash,
    string $deviceId
): ?array {
    $sql = "
        SELECT
            ps.id AS playback_id,
            ps.activation_id,
            ps.device_id AS playback_device_id,
            ps.stream_id,
            ps.category_id,
            ps.provider_type,
            ps.status AS playback_status,
            ps.expires_at AS playback_expires_at,

            a.status AS activation_status,
            a.expires_at AS activation_expires_at,
            a.iptv_url,
            a.iptv_username,
            a.iptv_password,

            s.status AS session_status,
            s.expires_at AS session_expires_at,

            d.device_id AS registered_device_id,
            d.status AS device_status

        FROM playback_sessions ps

        INNER JOIN activations a
            ON a.id = ps.activation_id

        INNER JOIN sessions s
            ON s.activation_id = ps.activation_id
           AND s.device_id = ps.device_id

        INNER JOIN devices d
            ON d.activation_id = ps.activation_id
           AND d.device_id = ps.device_id

        WHERE ps.session_token_hash = :token_hash
          AND ps.device_id = :device_id

        ORDER BY s.last_seen DESC
        LIMIT 1
    ";

    $stmt = $db->prepare($sql);

    $stmt->execute([
        ':token_hash' => $tokenHash,
        ':device_id' => $deviceId,
    ]);

    $row = $stmt->fetch(PDO::FETCH_ASSOC);

    if (!$row) {
        return null;
    }

    $now = time();

    if (($row['registered_device_id'] ?? '') !== $deviceId) {
        return null;
    }

    if (($row['playback_status'] ?? '') !== 'active') {
        return null;
    }

    if (($row['activation_status'] ?? '') !== 'active') {
        return null;
    }

    if (($row['session_status'] ?? '') !== 'active') {
        return null;
    }

    if (($row['device_status'] ?? '') !== 'active') {
        return null;
    }

    $playbackExpires = strtotime(
        (string)($row['playback_expires_at'] ?? '')
    );

    $sessionExpires = strtotime(
        (string)($row['session_expires_at'] ?? '')
    );

    $activationExpires = strtotime(
        (string)($row['activation_expires_at'] ?? '')
    );

    if ($playbackExpires === false || $playbackExpires <= $now) {
        return null;
    }

    if ($sessionExpires === false || $sessionExpires <= $now) {
        return null;
    }

    if ($activationExpires === false || $activationExpires <= $now) {
        return null;
    }

    if (($row['provider_type'] ?? '') !== 'xtream') {
        return null;
    }

    if (
        trim((string)($row['iptv_url'] ?? '')) === '' ||
        trim((string)($row['iptv_username'] ?? '')) === '' ||
        trim((string)($row['iptv_password'] ?? '')) === ''
    ) {
        return null;
    }

    return $row;
}

function sendLiveHeaders(): void
{
    if (headers_sent()) {
        return;
    }

    http_response_code(200);

    /*
     * Flujo MPEG-TS continuo.
     */
    header('Content-Type: video/mp2t');

    /*
     * No queremos cache ni almacenamiento intermedio
     * del contenido del canal.
     */
    header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
    header('Pragma: no-cache');
    header('Expires: 0');

    /*
     * Evita MIME sniffing.
     */
    header('X-Content-Type-Options: nosniff');

    /*
     * Cabecera utilizada por algunos proxies/reverse proxies
     * para indicar que no deben almacenar la respuesta.
     *
     * IIS no utiliza esta cabecera directamente, pero no
     * perjudica el funcionamiento y será útil si en el futuro
     * se coloca un proxy compatible delante del servidor.
     */
    header('X-Accel-Buffering: no');

    /*
     * No establecemos Content-Length.
     *
     * El canal Live TV es indefinido y debe permanecer como
     * respuesta HTTP abierta mientras existan datos.
     */
}

function disableOutputBuffering(): void
{
    @ini_set('output_buffering', '0');
    @ini_set('zlib.output_compression', '0');

    /*
     * Intentamos desactivar cualquier buffer de salida
     * creado por PHP.
     */
    while (ob_get_level() > 0) {
        @ob_end_flush();
    }

    /*
     * Forzamos el envío de la salida tan pronto como sea posible.
     */
    @ob_implicit_flush(true);
}

if (($_SERVER['REQUEST_METHOD'] ?? '') !== 'GET') {
    jsonError(405, 'Método no permitido');
}

$token = trim((string)($_GET['token'] ?? ''));
$deviceId = trim((string)($_GET['device_id'] ?? ''));

if ($token === '') {
    jsonError(401, 'Token de reproducción requerido');
}

if ($deviceId === '') {
    jsonError(401, 'device_id requerido');
}

if (strlen($token) > MAX_TOKEN_LENGTH) {
    jsonError(400, 'Token no válido');
}

if (strlen($deviceId) > MAX_DEVICE_ID_LENGTH) {
    jsonError(400, 'device_id no válido');
}

if (!is_file($dbFile)) {
    jsonError(500, 'Base de datos no encontrada');
}

try {
    $db = new PDO('sqlite:' . $dbFile);

    $db->setAttribute(
        PDO::ATTR_ERRMODE,
        PDO::ERRMODE_EXCEPTION
    );

    $db->setAttribute(
        PDO::ATTR_DEFAULT_FETCH_MODE,
        PDO::FETCH_ASSOC
    );

} catch (Throwable $e) {

    writePlaybackDebug('database_error', [
        'error' => $e->getMessage(),
    ]);

    jsonError(500, 'No se pudo abrir la base de datos');
}

$tokenHash = hash('sha256', $token);

$row = validatePlaybackSession(
    $db,
    $tokenHash,
    $deviceId
);

if ($row === null) {

    writePlaybackDebug('authorization_failed', [
        'device_id' => $deviceId,
    ]);

    jsonError(
        401,
        'Token de reproducción no autorizado'
    );
}

$streamId = trim(
    (string)$row['stream_id']
);

$providerUrl = trim(
    (string)$row['iptv_url']
);

$providerUsername = (string)$row['iptv_username'];
$providerPassword = (string)$row['iptv_password'];

if ($streamId === '') {
    jsonError(400, 'Stream no válido');
}

$providerUrl = rtrim(
    $providerUrl,
    '/'
);

$remoteUrl =
    $providerUrl .
    '/live/' .
    rawurlencode($providerUsername) .
    '/' .
    rawurlencode($providerPassword) .
    '/' .
    rawurlencode($streamId) .
    '.ts';

try {

    $stmt = $db->prepare("
        UPDATE playback_sessions
        SET last_seen = :last_seen
        WHERE id = :id
    ");

    $stmt->execute([
        ':last_seen' => date('c'),
        ':id' => $row['playback_id'],
    ]);

    $stmt = $db->prepare("
        UPDATE sessions
        SET last_seen = :last_seen
        WHERE activation_id = :activation_id
          AND device_id = :device_id
          AND status = 'active'
    ");

    $stmt->execute([
        ':last_seen' => date('c'),
        ':activation_id' => $row['activation_id'],
        ':device_id' => $deviceId,
    ]);

    $stmt = $db->prepare("
        UPDATE devices
        SET last_seen = :last_seen
        WHERE activation_id = :activation_id
          AND device_id = :device_id
          AND status = 'active'
    ");

    $stmt->execute([
        ':last_seen' => date('c'),
        ':activation_id' => $row['activation_id'],
        ':device_id' => $deviceId,
    ]);

} catch (Throwable $e) {

    writePlaybackDebug(
        'last_seen_update_failed',
        [
            'error' => $e->getMessage(),
        ]
    );
}

disableOutputBuffering();
sendLiveHeaders();

writePlaybackDebug(
    'stream_started',
    [
        'device_id' => $deviceId,
        'stream_id' => $streamId,
    ]
);

$reconnectCount = 0;

while (!connection_aborted()) {

    /*
     * Antes de cada conexión con el proveedor
     * comprobamos nuevamente la autorización.
     */
    $current = validatePlaybackSession(
        $db,
        $tokenHash,
        $deviceId
    );

    if ($current === null) {

        writePlaybackDebug(
            'stream_authorization_lost',
            [
                'device_id' => $deviceId,
                'reconnects' => $reconnectCount,
            ]
        );

        break;
    }

    $ch = curl_init();

    if ($ch === false) {

        writePlaybackDebug(
            'curl_init_failed'
        );

        break;
    }

    /*
     * Variables de diagnóstico para esta conexión.
     */
    $connectionStartedAt = microtime(true);
    $bytesReceived = 0;
    $chunksReceived = 0;
    $nextDebugBytes = DEBUG_LOG_BYTES_INTERVAL;

    $providerHeaders = [];

    curl_setopt_array(
        $ch,
        [

            CURLOPT_URL => $remoteUrl,

            CURLOPT_RETURNTRANSFER => false,

            CURLOPT_FOLLOWLOCATION => true,

            CURLOPT_MAXREDIRS => 3,

            CURLOPT_CONNECTTIMEOUT =>
                CONNECT_TIMEOUT_SECONDS,

            CURLOPT_TIMEOUT =>
                STREAM_TIMEOUT_SECONDS,

            CURLOPT_HEADER => false,

            CURLOPT_HTTPHEADER => [
                'User-Agent: VisionPlay/1.0',
                'Accept: */*',
                'Connection: keep-alive',
            ],

            CURLOPT_BUFFERSIZE =>
                CHUNK_SIZE,

            /*
             * Capturamos las cabeceras del proveedor
             * solamente para diagnóstico.
             *
             * NO se envían al cliente.
             */
            CURLOPT_HEADERFUNCTION =>
                function ($curl, string $headerLine)
                use (&$providerHeaders): int {

                    $trimmed = trim($headerLine);

                    if (
                        $trimmed !== '' &&
                        strpos($trimmed, ':') !== false
                    ) {
                        [$name, $value] =
                            explode(
                                ':',
                                $trimmed,
                                2
                            );

                        $name = strtolower(
                            trim($name)
                        );

                        /*
                         * No registramos cookies ni
                         * información potencialmente sensible.
                         */
                        if (
                            in_array(
                                $name,
                                [
                                    'content-type',
                                    'content-length',
                                    'transfer-encoding',
                                    'connection',
                                    'cache-control',
                                ],
                                true
                            )
                        ) {
                            $providerHeaders[$name] =
                                trim($value);
                        }
                    }

                    return strlen($headerLine);
                },

            CURLOPT_WRITEFUNCTION =>
                function ($curl, string $data)
                use (
                    &$bytesReceived,
                    &$chunksReceived,
                    &$nextDebugBytes,
                    $connectionStartedAt
                ): int {

                    if (connection_aborted()) {
                        return 0;
                    }

                    $length = strlen($data);

                    if ($length <= 0) {
                        return 0;
                    }

                    $bytesReceived += $length;
                    $chunksReceived++;

                    /*
                     * Enviamos inmediatamente los datos
                     * recibidos al cliente.
                     */
                    echo $data;

                    if (function_exists('ob_flush')) {
                        @ob_flush();
                    }

                    flush();

                    /*
                     * Registrar aproximadamente cada MB.
                     */
                    if (
                        $bytesReceived >=
                        $nextDebugBytes
                    ) {

                        $elapsed =
                            microtime(true) -
                            $connectionStartedAt;

                        $speedMbps = 0.0;

                        if ($elapsed > 0) {
                            $speedMbps =
                                (
                                    $bytesReceived *
                                    8
                                ) /
                                $elapsed /
                                1000000;
                        }

                        writePlaybackDebug(
                            'provider_data_received',
                            [
                                'bytes_received' =>
                                    $bytesReceived,

                                'chunks_received' =>
                                    $chunksReceived,

                                'elapsed_seconds' =>
                                    round(
                                        $elapsed,
                                        2
                                    ),

                                'speed_mbps' =>
                                    round(
                                        $speedMbps,
                                        2
                                    ),
                            ]
                        );

                        $nextDebugBytes +=
                            DEBUG_LOG_BYTES_INTERVAL;
                    }

                    return $length;
                },
        ]
    );

    $result = curl_exec($ch);

    $httpCode =
        (int)curl_getinfo(
            $ch,
            CURLINFO_HTTP_CODE
        );

    $contentType =
        curl_getinfo(
            $ch,
            CURLINFO_CONTENT_TYPE
        );

    $downloadSize =
        (int)curl_getinfo(
            $ch,
            CURLINFO_SIZE_DOWNLOAD
        );

    $totalTime =
        (float)curl_getinfo(
            $ch,
            CURLINFO_TOTAL_TIME
        );

    $curlErrno =
        curl_errno($ch);

    $curlError =
        curl_error($ch);

    /*
     * Último registro de diagnóstico
     * antes de cerrar cURL.
     */
    writePlaybackDebug(
        'provider_connection_finished',
        [
            'http_code' => $httpCode,

            'curl_errno' => $curlErrno,

            'curl_error' => $curlError,

            'content_type' =>
                $contentType ?: null,

            'download_size' =>
                $downloadSize,

            'bytes_received' =>
                $bytesReceived,

            'chunks_received' =>
                $chunksReceived,

            'total_time_seconds' =>
                round(
                    $totalTime,
                    2
                ),

            'provider_headers' =>
                json_encode(
                    $providerHeaders,
                    JSON_UNESCAPED_SLASHES
                ),

            'reconnects' =>
                $reconnectCount,
        ]
    );

    curl_close($ch);

    if (connection_aborted()) {

        writePlaybackDebug(
            'client_disconnected',
            [
                'reconnects' =>
                    $reconnectCount,

                'bytes_received' =>
                    $bytesReceived,
            ]
        );

        break;
    }

    /*
     * Si el proveedor cerró una conexión 2xx,
     * intentamos abrir otra conexión al mismo
     * stream_id sin cerrar la respuesta hacia
     * ExoPlayer.
     */
    if (
        $httpCode >= 200 &&
        $httpCode < 300
    ) {

        $reconnectCount++;

        if (
            $reconnectCount >
            MAX_RECONNECTS
        ) {

            writePlaybackDebug(
                'max_reconnects_reached',
                [
                    'reconnects' =>
                        $reconnectCount,
                ]
            );

            break;
        }

        usleep(
            RECONNECT_DELAY_MICROSECONDS
        );

        continue;
    }

    /*
     * Error temporal de conexión.
     */
    if (
        $curlErrno !== 0 ||
        $httpCode === 0
    ) {

        $reconnectCount++;

        if (
            $reconnectCount >
            MAX_RECONNECTS
        ) {

            writePlaybackDebug(
                'max_reconnects_after_curl_error',
                [
                    'curl_errno' =>
                        $curlErrno,

                    'curl_error' =>
                        $curlError,

                    'reconnects' =>
                        $reconnectCount,
                ]
            );

            break;
        }

        usleep(
            RECONNECT_DELAY_MICROSECONDS
        );

        continue;
    }

    writePlaybackDebug(
        'provider_http_error',
        [
            'http_code' =>
                $httpCode,

            'reconnects' =>
                $reconnectCount,
        ]
    );

    break;
}

writePlaybackDebug(
    'stream_finished',
    [
        'device_id' =>
            $deviceId,

        'reconnects' =>
            $reconnectCount,
    ]
);