<?php
// workers/metadata.php - Hamtar "Nu spelas" fran SR API och ICY-metadata.
// Kor via cron med flock sa att flera metadatajobb inte startar samtidigt.

require __DIR__ . '/../config/config.php';

$db = new PDO('sqlite:' . DB_PATH);
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
ensureMetadataTables($db);

$streams = $db->query("
    SELECT st.id AS stream_id, st.station_id, st.url AS stream_url,
           st.priority, st.label, s.name AS station_name, s.sort_order
    FROM streams st
    JOIN stations s ON s.id = st.station_id
    WHERE s.active = 1 AND st.active = 1
    ORDER BY s.sort_order, s.name, st.priority, st.id
")->fetchAll(PDO::FETCH_ASSOC);

$stationMetaByStation = [];

foreach ($streams as $stream) {
    if (!$stream['stream_url']) {
        continue;
    }

    $meta = fetchStreamMeta($stream['stream_url'], $stream['station_name']);
    upsertStreamMeta($db, (int)$stream['stream_id'], (int)$stream['station_id'], $meta);

    $stationId = (int)$stream['station_id'];
    if (
        !isset($stationMetaByStation[$stationId]) ||
        (!$stationMetaByStation[$stationId]['raw'] && $meta['raw'])
    ) {
        $stationMetaByStation[$stationId] = $meta;
    }

    echo date('H:i:s') . " [{$stream['station_name']} / {$stream['label']}] "
       . ($meta['raw'] ?: '(ingen metadata)') . "\n";
}

foreach ($stationMetaByStation as $stationId => $meta) {
    upsertStationMeta($db, $stationId, $meta);
}

function ensureMetadataTables(PDO $db): void
{
    $db->exec("
        CREATE TABLE IF NOT EXISTS stream_metadata (
            stream_id INTEGER PRIMARY KEY,
            station_id INTEGER,
            title TEXT,
            artist TEXT,
            raw TEXT,
            fetched_at DATETIME DEFAULT CURRENT_TIMESTAMP
        )
    ");
    $db->exec("CREATE INDEX IF NOT EXISTS idx_stream_metadata_station ON stream_metadata(station_id)");
}

function upsertStreamMeta(PDO $db, int $streamId, int $stationId, array $meta): void
{
    $db->prepare("
        INSERT INTO stream_metadata (stream_id, station_id, title, artist, raw, fetched_at)
        VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
        ON CONFLICT(stream_id) DO UPDATE SET
            station_id=excluded.station_id,
            title=excluded.title,
            artist=excluded.artist,
            raw=excluded.raw,
            fetched_at=excluded.fetched_at
    ")->execute([$streamId, $stationId, $meta['title'], $meta['artist'], $meta['raw']]);
}

function upsertStationMeta(PDO $db, int $stationId, array $meta): void
{
    $db->prepare("
        INSERT INTO metadata (station_id, title, artist, raw, fetched_at)
        VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)
        ON CONFLICT(station_id) DO UPDATE SET
            title=excluded.title,
            artist=excluded.artist,
            raw=excluded.raw,
            fetched_at=excluded.fetched_at
    ")->execute([$stationId, $meta['title'], $meta['artist'], $meta['raw']]);
}

function fetchStreamMeta(string $url, string $stationName): array
{
    $srMeta = fetchSverigesRadioMeta($url, $stationName);
    if ($srMeta !== null) {
        return $srMeta;
    }

    return fetchIcyMeta($url, $stationName);
}

function fetchSverigesRadioMeta(string $url, string $stationName): ?array
{
    if (!preg_match('~srapi/(\d+)\.(?:mp3|aac|m4a)~i', $url, $m)) {
        return null;
    }

    $apiUrl = 'https://api.sr.se/api/v2/playlists/rightnow?channelid='
        . rawurlencode($m[1]) . '&format=json';

    $ch = curl_init($apiUrl);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_CONNECTTIMEOUT => 3,
        CURLOPT_TIMEOUT => 5,
        CURLOPT_USERAGENT => 'RadioMetadata/1.0',
        CURLOPT_FOLLOWLOCATION => true,
    ]);

    $body = curl_exec($ch);
    $code = (int)curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
    curl_close($ch);

    if ($body === false || $code < 200 || $code >= 300) {
        return emptyMeta();
    }

    $json = json_decode($body, true);
    $song = $json['playlist']['song'] ?? null;
    if (!is_array($song)) {
        return emptyMeta();
    }

    $title = cleanMetaText($song['title'] ?? '');
    $artist = cleanMetaText($song['artist'] ?? '');
    $description = cleanMetaText($song['description'] ?? '');

    if ($description !== '') {
        return normaliseRawMeta($description, $stationName);
    }

    if ($title === '' && $artist === '') {
        return emptyMeta();
    }

    return [
        'title' => $title ?: null,
        'artist' => $artist ?: null,
        'raw' => trim(($artist ? $artist . ' - ' : '') . $title) ?: null,
    ];
}

// ICY socket-lasare for Shoutcast/Icecast "StreamTitle".
function fetchIcyMeta(string $url, string $stationName, int $redirects = 0): array
{
    $parsed = parse_url($url);
    if (!$parsed) {
        return emptyMeta();
    }

    $scheme = strtolower($parsed['scheme'] ?? 'http');
    $host = $parsed['host'] ?? '';
    if ($host === '') {
        return emptyMeta();
    }

    $port = (int)($parsed['port'] ?? ($scheme === 'https' ? 443 : 80));
    $path = ($parsed['path'] ?? '/') . (isset($parsed['query']) ? '?' . $parsed['query'] : '');
    $socketHost = ($scheme === 'https' ? 'ssl://' : '') . $host;

    $fp = @fsockopen($socketHost, $port, $errno, $errstr, 4);
    if (!$fp) {
        return emptyMeta();
    }

    stream_set_timeout($fp, 5);

    $defaultPort = ($scheme === 'https') ? 443 : 80;
    $hostHeader = $host . ($port !== $defaultPort ? ':' . $port : '');
    $request = "GET $path HTTP/1.0\r\n"
        . "Host: $hostHeader\r\n"
        . "User-Agent: RadioMetadata/1.0\r\n"
        . "Accept: */*\r\n"
        . "Icy-MetaData: 1\r\n"
        . "Connection: close\r\n\r\n";

    fwrite($fp, $request);

    $statusLine = fgets($fp, 4096) ?: '';
    $metaInterval = 0;
    $location = null;

    while (!feof($fp)) {
        $line = fgets($fp, 4096);
        if ($line === false || trim($line) === '') {
            break;
        }

        if (preg_match('/^icy-metaint:\s*(\d+)/i', $line, $m)) {
            $metaInterval = (int)$m[1];
        } elseif (preg_match('/^location:\s*(.+)$/i', $line, $m)) {
            $location = trim($m[1]);
        }
    }

    if (preg_match('~\s(30[1278])\s~', $statusLine) && $location && $redirects < 2) {
        fclose($fp);
        return fetchIcyMeta(resolveUrl($url, $location), $stationName, $redirects + 1);
    }

    if ($metaInterval <= 0) {
        fclose($fp);
        return emptyMeta();
    }

    $bytesRead = 0;
    while (!feof($fp) && $bytesRead < $metaInterval) {
        $chunk = fread($fp, min(4096, $metaInterval - $bytesRead));
        if ($chunk === false || $chunk === '') {
            fclose($fp);
            return emptyMeta();
        }
        $bytesRead += strlen($chunk);
    }

    $lenByte = fread($fp, 1);
    if ($lenByte === false || $lenByte === '') {
        fclose($fp);
        return emptyMeta();
    }

    $metaLen = ord($lenByte) * 16;
    if ($metaLen <= 0 || $metaLen > 4080) {
        fclose($fp);
        return emptyMeta();
    }

    $metaRaw = rtrim((string)fread($fp, $metaLen), "\0");
    fclose($fp);

    if (
        !preg_match("/StreamTitle='(.*?)';\s*[A-Za-z][A-Za-z0-9_]*=/is", $metaRaw, $m) &&
        !preg_match("/StreamTitle='(.*)';\s*$/is", $metaRaw, $m) &&
        !preg_match("/StreamTitle='((?:\\\\'|[^'])*)'/i", $metaRaw, $m)
    ) {
        return emptyMeta();
    }

    return normaliseRawMeta(str_replace("\\'", "'", $m[1]), $stationName);
}

function normaliseRawMeta(?string $raw, string $stationName): array
{
    $raw = cleanMetaText($raw ?? '');
    $raw = trim($raw, " \t\n\r\0\x0B-");

    if (
        $raw === '' ||
        $raw === '-' ||
        sameText($raw, $stationName) ||
        looksLikeStationName($raw, $stationName) ||
        preg_match('/^(unknown|stop_ad_break|ad_break|live)$/i', $raw)
    ) {
        return emptyMeta();
    }

    $parts = preg_split('/\s+[-–—]\s+/', $raw, 2);
    if (count($parts) === 2) {
        return [
            'artist' => cleanMetaText($parts[0]) ?: null,
            'title' => cleanMetaText($parts[1]) ?: null,
            'raw' => $raw,
        ];
    }

    return ['artist' => null, 'title' => $raw, 'raw' => $raw];
}

function cleanMetaText(?string $text): string
{
    if (function_exists('mb_check_encoding') && !mb_check_encoding((string)$text, 'UTF-8')) {
        $text = mb_convert_encoding((string)$text, 'UTF-8', 'ISO-8859-1');
    }
    $text = html_entity_decode((string)$text, ENT_QUOTES | ENT_HTML5, 'UTF-8');
    $text = strip_tags($text);
    $text = preg_replace('/\s+/u', ' ', $text) ?? $text;
    return trim($text);
}

function emptyMeta(): array
{
    return ['title' => null, 'artist' => null, 'raw' => null];
}

function sameText(string $a, string $b): bool
{
    return comparableText($a) === comparableText($b);
}

function looksLikeStationName(string $raw, string $stationName): bool
{
    $rawKey = comparableText($raw);
    $stationKey = comparableText($stationName);

    if (strlen($rawKey) < 5 || strlen($stationKey) < 5) {
        return false;
    }

    return str_contains($stationKey, $rawKey) || str_contains($rawKey, $stationKey);
}

function comparableText(string $text): string
{
    $text = cleanMetaText($text);
    if (function_exists('mb_strtolower')) {
        $text = mb_strtolower($text, 'UTF-8');
    } else {
        $text = strtolower($text);
    }

    return preg_replace('/[^a-z0-9åäöéü]+/iu', '', $text) ?? $text;
}

function resolveUrl(string $base, string $location): string
{
    if (preg_match('~^https?://~i', $location)) {
        return $location;
    }

    $parsed = parse_url($base);
    if (!$parsed || empty($parsed['scheme']) || empty($parsed['host'])) {
        return $location;
    }

    $port = isset($parsed['port']) ? ':' . $parsed['port'] : '';
    if (str_starts_with($location, '/')) {
        return $parsed['scheme'] . '://' . $parsed['host'] . $port . $location;
    }

    $path = $parsed['path'] ?? '/';
    $dir = preg_replace('~/[^/]*$~', '/', $path) ?: '/';
    return $parsed['scheme'] . '://' . $parsed['host'] . $port . $dir . $location;
}
