<?php

declare(strict_types=1);

namespace OCA\GlxRadio\Controller;

use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
use OCP\AppFramework\Http\JSONResponse;
use OCP\AppFramework\Http\StreamResponse;
use OCP\IRequest;
use Psr\Log\LoggerInterface;

class StreamProxyController extends Controller {
    public function __construct(
        string $appName,
        IRequest $request,
        private LoggerInterface $logger,
    ) {
        parent::__construct($appName, $request);
    }

    /**
     * Holt einen Radiostream (egal ob http/https) und reicht ihn ueber unsere
     * eigene https-Adresse weiter - umgeht damit Mixed-Content-Blockaden im
     * Browser. Loest ausserdem .asx/.pls-Wiedergabelisten automatisch auf und
     * reicht stattdessen den darin enthaltenen echten Stream weiter.
     *
     * Fragt den Sender zusaetzlich per Icy-MetaData-Header nach eingestreuten
     * "jetzt laeuft"-Infos, entfernt diese aus dem Audio-Datenstrom (der
     * Browser kann damit nichts anfangen) und zwischenspeichert den Titel,
     * damit ihn der Player per nowPlaying() abfragen kann.
     */
    #[NoAdminRequired]
    #[NoCSRFRequired]
    public function proxy(string $url) {
        $url = trim($url);
        if (!preg_match('#^https?://#i', $url)) {
            http_response_code(400);
            echo 'Ungueltige URL';
            exit;
        }

        // .asx/.pls-Playlisten sind klein und enthalten nur Text - koennen wir
        // direkt laden und den eigentlichen Stream-Link herausfischen.
        $metaCacheKey = $url;
        if (preg_match('/\.(asx|pls|m3u8?)(\?|$)/i', $url)) {
            $resolved = $this->resolvePlaylist($url);
            if ($resolved) {
                $url = $resolved;
            }
        }

        $metaCachePath = $this->metaCachePath($metaCacheKey);

        // Status fuer das Herausloesen der ICY-Metadaten aus dem Bytestrom.
        // Sender, die "Icy-MetaData: 1" unterstuetzen, schicken alle
        // $icyMetaInt Bytes einen Metadaten-Block eingestreut, den wir aus
        // dem Audio-Strom rausschneiden muessen, bevor er beim Browser
        // ankommt.
        $icyMetaInt = null;
        $state = 'audio';
        $audioBytesRemaining = null;
        $metaBytesRemaining = 0;
        $metaBuffer = '';

        $ch = curl_init($url);
        curl_setopt_array($ch, [
            CURLOPT_FOLLOWLOCATION => true,
            CURLOPT_MAXREDIRS => 5,
            CURLOPT_CONNECTTIMEOUT => 8,
            // WICHTIG: KEIN CURLOPT_TIMEOUT hier - das wuerde die gesamte
            // Stream-Dauer begrenzen (Live-Radio soll aber stundenlang
            // laufen koennen). Stattdessen erkennen wir tote/haengende
            // Verbindungen ueber die Mindestgeschwindigkeit: kommt ueber
            // 15 Sekunden praktisch nichts an, brechen wir ab.
            CURLOPT_LOW_SPEED_LIMIT => 1,
            CURLOPT_LOW_SPEED_TIME => 15,
            // Manche Icecast-/Shoutcast-Mounts blocken unbekannte oder nach
            // "Proxy" klingende User-Agents. Ein gaengiger Player-UA (wie
            // VLC) wird von so gut wie jedem Sender akzeptiert.
            CURLOPT_USERAGENT => 'VLC/3.0.20 LibVLC/3.0.20',
            CURLOPT_HTTPHEADER => ['Icy-MetaData: 1'],
            CURLOPT_HEADERFUNCTION => function ($curl, $header) use (&$icyMetaInt) {
                $len = strlen($header);
                $parts = explode(':', $header, 2);
                if (count($parts) === 2) {
                    $name = strtolower(trim($parts[0]));
                    if ($name === 'content-type' && !headers_sent()) {
                        header('Content-Type: ' . trim($parts[1]));
                    }
                    if ($name === 'icy-metaint') {
                        $icyMetaInt = (int) trim($parts[1]);
                    }
                }
                return $len;
            },
            CURLOPT_WRITEFUNCTION => function ($curl, $chunk) use (
                &$icyMetaInt, &$state, &$audioBytesRemaining, &$metaBytesRemaining, &$metaBuffer, $metaCachePath
            ) {
                // Kein icy-metaint -> Sender streamt ohne eingestreute
                // Metadaten, einfach unveraendert durchreichen (schneller Pfad).
                if (!$icyMetaInt) {
                    echo $chunk;
                    @ob_flush();
                    @flush();
                    return strlen($chunk);
                }

                if ($audioBytesRemaining === null) {
                    $audioBytesRemaining = $icyMetaInt;
                }

                $len = strlen($chunk);
                $pos = 0;
                $out = '';

                while ($pos < $len) {
                    if ($state === 'audio') {
                        $take = min($audioBytesRemaining, $len - $pos);
                        $out .= substr($chunk, $pos, $take);
                        $pos += $take;
                        $audioBytesRemaining -= $take;
                        if ($audioBytesRemaining <= 0) {
                            $state = 'meta_length';
                        }
                    } elseif ($state === 'meta_length') {
                        $lengthByte = ord($chunk[$pos]);
                        $pos += 1;
                        $metaBytesRemaining = $lengthByte * 16;
                        $metaBuffer = '';
                        if ($metaBytesRemaining === 0) {
                            $state = 'audio';
                            $audioBytesRemaining = $icyMetaInt;
                        } else {
                            $state = 'meta_data';
                        }
                    } else { // meta_data
                        $take = min($metaBytesRemaining, $len - $pos);
                        $metaBuffer .= substr($chunk, $pos, $take);
                        $pos += $take;
                        $metaBytesRemaining -= $take;
                        if ($metaBytesRemaining <= 0) {
                            $this->storeNowPlaying($metaBuffer, $metaCachePath);
                            $state = 'audio';
                            $audioBytesRemaining = $icyMetaInt;
                        }
                    }
                }

                echo $out;
                @ob_flush();
                @flush();
                return strlen($chunk);
            },
        ]);

        set_time_limit(0);
        while (ob_get_level() > 0) {
            ob_end_flush();
        }

        curl_exec($ch);
        if (curl_errno($ch)) {
            $this->logger->warning('GLX Radio: Proxy-Fehler fuer ' . $url . ': ' . curl_error($ch));
        }
        curl_close($ch);
        exit;
    }

    /**
     * Liefert den zuletzt fuer diesen Sender erkannten "Jetzt laeuft"-Titel
     * zurueck, sofern er nicht aelter als 2 Minuten ist (Sender inzwischen
     * gewechselt/gestoppt).
     */
    #[NoAdminRequired]
    #[NoCSRFRequired]
    public function nowPlaying(string $url): JSONResponse {
        $path = $this->metaCachePath(trim($url));
        if (!is_file($path)) {
            return new JSONResponse(['title' => null]);
        }
        $data = json_decode((string) file_get_contents($path), true);
        if (!is_array($data) || !isset($data['title'], $data['ts']) || time() - (int) $data['ts'] > 120) {
            return new JSONResponse(['title' => null]);
        }
        return new JSONResponse(['title' => $data['title']]);
    }

    /**
     * Extrahiert StreamTitle='...' aus einem rohen ICY-Metadaten-Block und
     * schreibt ihn mit Zeitstempel in eine kleine Cache-Datei.
     */
    private function storeNowPlaying(string $rawMeta, string $cachePath): void {
        if (preg_match("/StreamTitle='([^']*)'/", $rawMeta, $m)) {
            $title = trim($m[1]);
            if ($title !== '') {
                @file_put_contents($cachePath, json_encode(['title' => $title, 'ts' => time()]));
            }
        }
    }

    private function metaCachePath(string $url): string {
        return rtrim(sys_get_temp_dir(), '/') . '/glxradio-meta-' . md5($url) . '.json';
    }

    /**
     * Laedt eine .asx/.pls/.m3u-Datei und extrahiert die erste enthaltene
     * Stream-URL. Gibt null zurueck, wenn nichts gefunden wird.
     */
    private function resolvePlaylist(string $url): ?string {
        try {
            $ch = curl_init($url);
            curl_setopt_array($ch, [
                CURLOPT_RETURNTRANSFER => true,
                CURLOPT_CONNECTTIMEOUT => 6,
                CURLOPT_TIMEOUT => 6,
                CURLOPT_FOLLOWLOCATION => true,
                CURLOPT_USERAGENT => 'VLC/3.0.20 LibVLC/3.0.20',
            ]);
            $body = curl_exec($ch);
            curl_close($ch);
            if (!$body) {
                return null;
            }

            // ASX (XML): <ref href="..."/>
            if (preg_match('/<ref\s+href=["\']([^"\']+)["\']/i', $body, $m)) {
                return $m[1];
            }
            // PLS: File1=...
            if (preg_match('/File1\s*=\s*(\S+)/i', $body, $m)) {
                return trim($m[1]);
            }
            // M3U: erste Zeile, die mit http beginnt
            foreach (preg_split('/\r?\n/', $body) as $line) {
                $line = trim($line);
                if (preg_match('#^https?://#i', $line)) {
                    return $line;
                }
            }
        } catch (\Exception $e) {
            $this->logger->warning('GLX Radio: Playlist-Aufloesung fehlgeschlagen: ' . $e->getMessage());
        }
        return null;
    }
}
