<?php
/**
 * Sonora — api.php v6
 * Sessão PHP nativa + proxy de áudio + upload de avatar + playlists
 */
session_name('sonora_sess');
session_start();

// Rota especial: streaming de áudio (não usa JSON header)
$action = $_POST['action'] ?? $_GET['action'] ?? (json_decode(file_get_contents('php://input'), true)['action'] ?? '');

if ($action === 'stream_audio') {
    streamAudio();
    exit;
}

header('Content-Type: application/json; charset=utf-8');

$BODY  = json_decode(file_get_contents('php://input'), true) ?: [];
$action = trim($BODY['action'] ?? $_GET['action'] ?? '');

// Caminhos
$DATA      = __DIR__ . '/data';
$UFILE     = $DATA . '/users.json';
$AFILE     = $DATA . '/admins.json';
$SFILE     = $DATA . '/settings.json';
$FFILE     = $DATA . '/favorites.json';
$PLFILE    = $DATA . '/playlists.json';
$AVDIR     = $DATA . '/avatars';
$CACHEDIR  = $DATA . '/stream_cache';

foreach ([$DATA, $AVDIR, $CACHEDIR] as $d) {
    if (!is_dir($d)) @mkdir($d, 0755, true);
}
if (!file_exists($DATA . '/.htaccess')) {
    @file_put_contents($DATA . '/.htaccess', "Deny from all\n");
}

// ── Helpers ───────────────────────────────────────────────────────────────────
function rj($f)    { return file_exists($f) ? (json_decode(@file_get_contents($f), true) ?: []) : []; }
function wj($f,$d) { return @file_put_contents($f, json_encode($d, JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE)) !== false; }
function ok($d=[]) { echo json_encode(['ok'=>true]+$d, JSON_UNESCAPED_UNICODE); exit; }
function fail($m,$c=400) { http_response_code($c); echo json_encode(['ok'=>false,'error'=>$m], JSON_UNESCAPED_UNICODE); exit; }
function nopass($u){ return array_diff_key($u, ['password'=>'']); }
function reqAuth()  { if(empty($_SESSION['role'])) fail('Faça login', 401); }
function reqAdmin() { if(empty($_SESSION['role']) || $_SESSION['role'] !== 'admin') fail('Não autorizado', 401); }

// ── Defaults ──────────────────────────────────────────────────────────────────
function initDefaults() {
    global $AFILE, $UFILE, $SFILE;
    if (!file_exists($AFILE)) wj($AFILE, [[
        'id'=>'adm_001','name'=>'Administrador','email'=>'admin@sonora.com',
        'password'=>password_hash('admin123', PASSWORD_DEFAULT),'created_at'=>date('c')
    ]]);
    if (!file_exists($UFILE)) wj($UFILE, [[
        'id'=>'usr_demo','name'=>'Cliente Demo','email'=>'cliente@sonora.com',
        'password'=>password_hash('sonora123', PASSWORD_DEFAULT),
        'plan'=>'Premium','active'=>true,'avatar'=>'🎵','created_at'=>date('c')
    ]]);
    if (!file_exists($SFILE)) wj($SFILE, [
        'site_name'=>'Sonora','site_logo'=>'🎵','site_color'=>'#00e5a0',
        'plans'=>[
            ['id'=>'basico', 'name'=>'Básico', 'price'=>'Grátis','price_value'=>0,'duration'=>0,'duration_unit'=>'','color'=>'#6b6b85','features'=>'Reprodução limitada','disabled'=>'Downloads\nOffline'],
            ['id'=>'premium','name'=>'Premium','price'=>'R$19,90','price_value'=>19.90,'duration'=>1,'duration_unit'=>'month','color'=>'#00e5a0','features'=>'Reprodução ilimitada\nSem anúncios\nDownloads','disabled'=>''],
            ['id'=>'familiar','name'=>'Familiar','price'=>'R$34,90','price_value'=>34.90,'duration'=>1,'duration_unit'=>'month','color'=>'#7c6ff7','features'=>'Até 6 contas\nTudo do Premium','disabled'=>''],
        ]
    ]);
}
initDefaults();

// ─────────────────────────────────────────────────────────────────────────────
// STREAM DE ÁUDIO
// Tenta extrair URL direta via yt-dlp → Invidious → retorna URL para o cliente
// ─────────────────────────────────────────────────────────────────────────────

// ── FONTE 0: Cobalt.tools API (mais estável — infraestrutura própria) ─────────
function getAudioUrlCobalt($videoId) {
    $vid = preg_replace('/[^a-zA-Z0-9_-]/', '', $videoId);
    $ytUrl = "https://www.youtube.com/watch?v={$vid}";

    // Instâncias públicas do Cobalt (ordem de preferência)
    $endpoints = [
        'https://api.cobalt.tools/',
        'https://cobalt.api.vrsal.xyz/',
        'https://cobalt-api.ayo.tf/',
    ];

    foreach ($endpoints as $endpoint) {
        $payload = json_encode([
            'url'          => $ytUrl,
            'downloadMode' => 'audio',
            'audioFormat'  => 'mp3',
            'audioBitrate' => '128',
        ]);
        $ch = curl_init($endpoint);
        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_POST           => true,
            CURLOPT_POSTFIELDS     => $payload,
            CURLOPT_TIMEOUT        => 12,
            CURLOPT_CONNECTTIMEOUT => 5,
            CURLOPT_SSL_VERIFYPEER => false,
            CURLOPT_HTTPHEADER     => [
                'Content-Type: application/json',
                'Accept: application/json',
                'User-Agent: Mozilla/5.0 (compatible; Sonora/1.0)',
            ],
        ]);
        $body = curl_exec($ch);
        $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);

        if ($code !== 200 || !$body) continue;
        $data = json_decode($body, true);
        if (!$data) continue;

        // Cobalt retorna status: redirect (URL direta) ou tunnel (proxy deles)
        $status = $data['status'] ?? '';
        if (in_array($status, ['redirect', 'tunnel', 'stream'])) {
            $url = $data['url'] ?? '';
            if ($url && strpos($url, 'http') === 0) return $url;
        }
    }
    return null;
}

// ── AUTO-INSTALL yt-dlp (baixa binário estático se não encontrar) ─────────────
function autoInstallYtDlp() {
    $dest = __DIR__ . '/yt-dlp';
    if (is_executable($dest)) return $dest;
    if (!is_writable(__DIR__)) return null;

    // Binário estático Linux x86_64 — funciona em cPanel/VPS sem dependências
    $binUrl = 'https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp';
    $ch = curl_init($binUrl);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_FOLLOWLOCATION => true,
        CURLOPT_TIMEOUT        => 60,
        CURLOPT_SSL_VERIFYPEER => false,
        CURLOPT_USERAGENT      => 'Mozilla/5.0',
    ]);
    $bin = curl_exec($ch);
    $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    if ($code !== 200 || !$bin || strlen($bin) < 1000000) return null;
    file_put_contents($dest, $bin);
    chmod($dest, 0755);
    return is_executable($dest) ? $dest : null;
}

function getYtDlpPath() {
    // Locais comuns em cPanel/Linux
    $paths = [
        '/usr/local/bin/yt-dlp',
        '/usr/bin/yt-dlp',
        trim(shell_exec('which yt-dlp 2>/dev/null') ?: ''),
        __DIR__ . '/yt-dlp',       // na mesma pasta do projeto
        __DIR__ . '/bin/yt-dlp',
    ];
    foreach ($paths as $p) {
        if ($p && is_executable($p)) return $p;
    }
    return null;
}

function getAudioUrlYtDlp($videoId) {
    $ytdlp = getYtDlpPath();
    if (!$ytdlp) return null;

    $vid   = preg_replace('/[^a-zA-Z0-9_-]/', '', $videoId);
    $ytUrl = "https://www.youtube.com/watch?v={$vid}";

    // Tenta múltiplos user-agents — servidores são bloqueados pelo YouTube como bot
    $agents = [
        'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
        'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 Version/17.0 Mobile/15E148 Safari/604.1',
        'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/119.0.0.0 Safari/537.36',
    ];
    foreach ($agents as $agent) {
        $cmd = escapeshellcmd($ytdlp)
             . ' --no-check-certificate'
             . ' --no-playlist'
             . ' -f "bestaudio[ext=m4a]/bestaudio[ext=webm]/bestaudio/best"'
             . ' --user-agent ' . escapeshellarg($agent)
             . ' --get-url'
             . ' ' . escapeshellarg($ytUrl)
             . ' 2>/dev/null';
        $url = trim(shell_exec($cmd) ?: '');
        if (strlen($url) > 50 && strpos($url, 'http') === 0) return $url;
    }
    return null;
}

function getAudioUrlInvidious($videoId) {
    $vid = preg_replace('/[^a-zA-Z0-9_-]/', '', $videoId);

    // Lista ampliada de instâncias — embaralha para distribuir carga
    $instances = [
        'https://inv.nadeko.net',
        'https://invidious.nerdvpn.de',
        'https://yewtu.be',
        'https://invidious.privacyredirect.com',
        'https://invidious.lunar.icu',
        'https://iv.ggtyler.dev',
        'https://invidious.privacydev.net',
        'https://vid.puffyan.us',
        'https://invidious.protokolla.fi',
    ];
    shuffle($instances);

    foreach ($instances as $invBase) {
        // Tenta primeiro com fields (mais rápido), depois sem (mais compatível)
        $endpoints = [
            "$invBase/api/v1/videos/$vid?fields=adaptiveFormats,formatStreams",
            "$invBase/api/v1/videos/$vid",
        ];
        foreach ($endpoints as $endpoint) {
            $ch = curl_init();
            curl_setopt_array($ch, [
                CURLOPT_URL            => $endpoint,
                CURLOPT_RETURNTRANSFER => true,
                CURLOPT_TIMEOUT        => 8,
                CURLOPT_CONNECTTIMEOUT => 4,
                CURLOPT_SSL_VERIFYPEER => false,
                CURLOPT_USERAGENT      => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0 Safari/537.36',
                CURLOPT_HTTPHEADER     => ['Accept: application/json','Accept-Language: pt-BR,pt;q=0.9'],
            ]);
            $body = curl_exec($ch);
            $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
            curl_close($ch);

            if ($code !== 200 || !$body) break; // tenta próxima instância

            $data = json_decode($body, true);
            if (!$data || isset($data['error'])) break;

            $formats = array_merge(
                $data['adaptiveFormats'] ?? [],
                $data['formatStreams']   ?? []
            );
            if (empty($formats)) break;

            // Seleciona melhor formato de áudio
            $best = null; $bestPrio = 99;
            foreach ($formats as $f) {
                $type = strtolower($f['type'] ?? '');
                $furl = $f['url']  ?? '';
                if (!$furl || strpos($furl,'http') !== 0) continue;
                // Só áudio (não vídeo)
                if (strpos($type,'audio') === false &&
                    strpos($type,'webm')  === false &&
                    strpos($type,'m4a')   === false) continue;
                // Ignora formatos de vídeo com áudio (codec de vídeo presente)
                if (strpos($type,'avc')  !== false) continue;
                if (strpos($type,'vp9')  !== false && strpos($type,'audio') === false) continue;

                $prio = 99;
                if (strpos($type,'m4a')  !== false) $prio = 1;
                elseif(strpos($type,'opus')!== false) $prio = 2;
                elseif(strpos($type,'webm')!==false)  $prio = 3;
                elseif(strpos($type,'audio')!==false) $prio = 4;
                if ($prio < $bestPrio) { $best = $furl; $bestPrio = $prio; }
            }
            if ($best) return $best;
            break;
        }
    }
    return null;
}

// Tenta Piped API como segunda fonte (diferente infraestrutura do Invidious)
function getAudioUrlPiped($videoId) {
    $vid = preg_replace('/[^a-zA-Z0-9_-]/', '', $videoId);
    $instances = [
        'https://pipedapi.kavin.rocks',
        'https://pipedapi.adminforge.de',
        'https://piped-api.garudalinux.org',
    ];
    shuffle($instances);
    foreach ($instances as $base) {
        $ch = curl_init("$base/streams/$vid");
        curl_setopt_array($ch,[
            CURLOPT_RETURNTRANSFER=>true, CURLOPT_TIMEOUT=>8,
            CURLOPT_CONNECTTIMEOUT=>4,    CURLOPT_SSL_VERIFYPEER=>false,
            CURLOPT_USERAGENT=>'Mozilla/5.0',
            CURLOPT_HTTPHEADER=>['Accept: application/json'],
        ]);
        $body = curl_exec($ch);
        $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);
        if ($code !== 200 || !$body) continue;
        $data = json_decode($body, true);
        if (empty($data['audioStreams'])) continue;
        // Pega stream de melhor qualidade
        $streams = $data['audioStreams'];
        usort($streams, fn($a,$b) => ($b['bitrate']??0) - ($a['bitrate']??0));
        foreach ($streams as $s) {
            $url = $s['url'] ?? '';
            if ($url && strpos($url,'http')===0) return $url;
        }
    }
    return null;
}

function streamAudio() {
    // Requer sessão válida
    if (empty($_SESSION['role'])) {
        http_response_code(401); echo 'Unauthorized'; exit;
    }
    $vid = preg_replace('/[^a-zA-Z0-9_-]/', '', $_GET['id'] ?? '');
    if (!$vid) { http_response_code(400); echo 'Invalid ID'; exit; }

    // 0. Cobalt.tools API (mais confiável)
    $url = getAudioUrlCobalt($vid);

    // 1. Invidious
    if (!$url) $url = getAudioUrlInvidious($vid);

    // 2. Piped API
    if (!$url) $url = getAudioUrlPiped($vid);

    // 3. yt-dlp local (com auto-install se necessário)
    if (!$url) {
        autoInstallYtDlp(); // tenta instalar se não existir
        $url = getAudioUrlYtDlp($vid);
    }

    if ($url) {
        proxyAudioStream($url);
        exit;
    }

    // 4. Sem fonte disponível
    http_response_code(503);
    header('Content-Type: application/json');
    echo json_encode(['ok'=>false,'error'=>'Nenhuma fonte de áudio disponível']);
}

/**
 * Proxy de áudio — passagem ÚNICA de curl.
 * URLs do Invidious/Piped são assinadas e expiram em segundos;
 * qualquer segunda requisição à mesma URL falha.
 * Aqui abrimos UMA conexão e enviamos os dados diretamente ao cliente.
 */
function proxyAudioStream($remoteUrl) {
    // Detecta tipo de áudio pela URL
    $contentType = 'audio/mpeg';
    $urlLower = strtolower(parse_url($remoteUrl, PHP_URL_PATH) ?? '');
    if (strpos($urlLower, 'm4a') !== false)  $contentType = 'audio/mp4';
    elseif (strpos($urlLower, '.ogg') !== false) $contentType = 'audio/ogg';
    elseif (strpos($urlLower, 'opus') !== false) $contentType = 'audio/ogg; codecs=opus';
    elseif (strpos($urlLower, 'webm') !== false) $contentType = 'audio/webm';

    // Repassa Range do cliente (seek na barra de progresso)
    $reqHeaders = [
        'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36',
        'Accept: audio/*,*/*;q=0.9',
    ];
    if (!empty($_SERVER['HTTP_RANGE'])) {
        $reqHeaders[] = 'Range: ' . $_SERVER['HTTP_RANGE'];
    }

    // Envia headers ao browser ANTES de abrir o curl
    // (URLs do Invidious/Piped expiram em segundos — não podemos fazer 2 requisicoes)
    http_response_code(!empty($_SERVER['HTTP_RANGE']) ? 206 : 200);
    header('Content-Type: ' . $contentType);
    header('Accept-Ranges: bytes');
    header('Cache-Control: no-cache, no-store');
    header('X-Content-Type-Options: nosniff');
    header_remove('X-Powered-By');

    // Limpa qualquer buffer de saida aberto
    while (ob_get_level()) ob_end_clean();

    // Passagem unica: curl -> stdout
    $ch = curl_init($remoteUrl);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => false,
        CURLOPT_FOLLOWLOCATION => true,
        CURLOPT_MAXREDIRS      => 5,
        CURLOPT_SSL_VERIFYPEER => false,
        CURLOPT_TIMEOUT        => 0,
        CURLOPT_CONNECTTIMEOUT => 10,
        CURLOPT_HTTPHEADER     => $reqHeaders,
        CURLOPT_HEADERFUNCTION => function($ch, $h) {
            $hl = strtolower($h);
            if (strpos($hl, 'content-length:') === 0)
                header('Content-Length: ' . trim(substr($h, 15)));
            if (strpos($hl, 'content-range:') === 0)
                header('Content-Range: ' . trim(substr($h, 14)));
            return strlen($h);
        },
        CURLOPT_WRITEFUNCTION => function($ch, $data) {
            echo $data;
            flush();
            return strlen($data);
        },
    ]);
    curl_exec($ch);
    curl_close($ch);
}

// ─────────────────────────────────────────────────────────────────────────────
// ROTEAMENTO
// ─────────────────────────────────────────────────────────────────────────────
switch ($action) {

case 'ping':
    $ytdlp = getYtDlpPath();
    ok([
        'php'        => PHP_VERSION,
        'data'       => is_writable($DATA) ? 'OK' : 'ERRO',
        'yt_dlp'     => $ytdlp ?: 'não encontrado',
        'curl'       => function_exists('curl_init') ? 'OK' : 'ERRO',
        'shell_exec' => function_exists('shell_exec') ? 'OK' : 'ERRO',
        'session'    => $_SESSION['role'] ?? 'nenhuma',
    ]);

case 'debug_stream':
    reqAuth();
    $vid = preg_replace('/[^a-zA-Z0-9_-]/', '', $BODY['id'] ?? $_GET['id'] ?? '');
    if (!$vid) fail('id obrigatório');
    $t0 = microtime(true);
    $cobalt = getAudioUrlCobalt($vid);
    $t1 = microtime(true);
    $inv  = getAudioUrlInvidious($vid);
    $t2 = microtime(true);
    $piped = getAudioUrlPiped($vid);
    $t3 = microtime(true);
    $ytdlp_path = getYtDlpPath();
    $yt   = $ytdlp_path ? getAudioUrlYtDlp($vid) : null;
    $t4 = microtime(true);
    ok([
        'cobalt'    => $cobalt ? substr($cobalt, 0, 80).'…' : 'FALHOU',
        'cobalt_ms'     => round(($t1-$t0)*1000),
        'invidious' => $inv    ? substr($inv,    0, 80).'…' : 'FALHOU',
        'invidious_ms'  => round(($t2-$t1)*1000),
        'piped'     => $piped  ? substr($piped,  0, 80).'…' : 'FALHOU',
        'piped_ms'      => round(($t3-$t2)*1000),
        'ytdlp'     => $yt     ? substr($yt,     0, 80).'…' : ($ytdlp_path ? 'FALHOU' : 'não instalado'),
        'ytdlp_ms'      => round(($t4-$t3)*1000),
        'resultado' => ($cobalt ?: $inv ?: $piped ?: $yt) ? 'OK' : 'NENHUMA FONTE FUNCIONOU',
    ]);

// ── AUTH ──────────────────────────────────────────────────────────────────────
case 'login':
    $email = strtolower(trim($BODY['email'] ?? ''));
    $pw    = $BODY['password'] ?? '';
    if (!$email || !$pw) fail('Preencha email e senha');
    foreach (rj($AFILE) as $a) {
        if (strtolower(trim($a['email'])) === $email && password_verify($pw, $a['password'])) {
            session_regenerate_id(true);
            $_SESSION = ['role'=>'admin','id'=>$a['id'],'name'=>$a['name'],'email'=>$a['email']];
            ok(['role'=>'admin','name'=>$a['name'],'email'=>$a['email']]);
        }
    }
    foreach (rj($UFILE) as $u) {
        if (strtolower(trim($u['email'])) === $email && password_verify($pw, $u['password'])) {
            if (!(bool)$u['active']) fail('Conta desativada. Fale com o administrador.');
            session_regenerate_id(true);
            $_SESSION = [
                'role'=>'client','id'=>$u['id'],'name'=>$u['name'],
                'email'=>$u['email'],'plan'=>$u['plan'],'avatar'=>$u['avatar'] ?? '🎵',
                'avatar_path'=>$u['avatar_path'] ?? ''
            ];
            ok(['role'=>'client','name'=>$u['name'],'plan'=>$u['plan'],
                'avatar'=>$u['avatar'] ?? '🎵','avatar_path'=>$u['avatar_path'] ?? '']);
        }
    }
    fail('Email ou senha incorretos');

case 'logout':
    session_destroy(); ok();

case 'me':
    if (empty($_SESSION['role'])) fail('Sem sessão', 401);
    $s = rj($SFILE);
    ok(array_merge($_SESSION, [
        'site_name'  => $s['site_name']  ?? 'Sonora',
        'site_logo'  => $s['site_logo']  ?? '🎵',
        'site_color' => $s['site_color'] ?? '#00e5a0',
    ]));

// ── CONFIGURAÇÕES ─────────────────────────────────────────────────────────────
case 'settings_get':
    ok(['settings' => rj($SFILE)]);

case 'settings_save':
    reqAdmin();
    $s = rj($SFILE);
    if (isset($BODY['site_name']))  $s['site_name']  = trim($BODY['site_name']);
    if (isset($BODY['site_logo']))  $s['site_logo']  = trim($BODY['site_logo']);
    if (isset($BODY['site_color'])) $s['site_color'] = trim($BODY['site_color']);
    if (isset($BODY['plans']))      $s['plans']       = $BODY['plans'];
    if (!wj($SFILE, $s)) fail('Erro ao salvar');
    ok(['settings' => $s]);

// ── STATS ─────────────────────────────────────────────────────────────────────
case 'stats':
    reqAdmin();
    $u = rj($UFILE);
    ok(['total'=>count($u),
        'active'  =>count(array_filter($u, fn($x)=>(bool)$x['active'])),
        'inactive'=>count(array_filter($u, fn($x)=>!(bool)$x['active'])),
        'premium' =>count(array_filter($u, fn($x)=>($x['plan']??'')==='Premium'))]);

// ── CLIENTES ──────────────────────────────────────────────────────────────────
case 'clients_list':
    reqAdmin();
    ok(['clients'=>array_values(array_map('nopass', rj($UFILE)))]);

case 'client_create':
    reqAdmin();
    $name  = trim($BODY['name']  ?? '');
    $email = strtolower(trim($BODY['email'] ?? ''));
    $pw    = $BODY['password'] ?? '';
    $plan  = $BODY['plan']   ?? 'Premium';
    $act   = isset($BODY['active']) ? (bool)$BODY['active'] : true;
    $av    = $BODY['avatar'] ?? '🎵';
    if (!$name) fail('Nome obrigatório');
    if (!filter_var($email, FILTER_VALIDATE_EMAIL)) fail('Email inválido');
    if (!$pw)   fail('Senha obrigatória');
    $users = rj($UFILE);
    foreach ($users as $u) { if (strtolower(trim($u['email'])) === $email) fail('Email já cadastrado'); }
    $new = ['id'=>'usr_'.uniqid(),'name'=>$name,'email'=>$email,
            'password'=>password_hash($pw, PASSWORD_DEFAULT),
            'plan'=>$plan,'active'=>$act,'avatar'=>$av,'avatar_path'=>'','created_at'=>date('c')];
    $users[] = $new;
    if (!wj($UFILE, $users)) fail('Erro ao salvar');
    ok(['client'=>nopass($new)]);

case 'client_update':
    reqAdmin();
    $id = $BODY['id'] ?? ''; $users = rj($UFILE); $idx = -1;
    foreach ($users as $i => $u) { if ($u['id'] === $id) { $idx = $i; break; } }
    if ($idx < 0) fail('Não encontrado');
    if (!empty($BODY['name']))     $users[$idx]['name']    = trim($BODY['name']);
    if (!empty($BODY['email']))    $users[$idx]['email']   = strtolower(trim($BODY['email']));
    if (!empty($BODY['password'])) $users[$idx]['password']= password_hash($BODY['password'], PASSWORD_DEFAULT);
    if (isset($BODY['plan']))      $users[$idx]['plan']    = $BODY['plan'];
    if (isset($BODY['active']))    $users[$idx]['active']  = (bool)$BODY['active'];
    if (isset($BODY['avatar']))    $users[$idx]['avatar']  = $BODY['avatar'];
    $users[$idx]['updated_at'] = date('c');
    if (!wj($UFILE, $users)) fail('Erro ao salvar');
    ok(['client'=>nopass($users[$idx])]);

case 'client_delete':
    reqAdmin();
    $id = $BODY['id'] ?? ''; $users = rj($UFILE);
    $new = array_values(array_filter($users, fn($u)=>$u['id']!==$id));
    if (count($new) === count($users)) fail('Não encontrado');
    wj($UFILE, $new); ok();

case 'client_toggle':
    reqAdmin();
    $id = $BODY['id'] ?? ''; $users = rj($UFILE); $ok = false;
    foreach ($users as &$u) { if ($u['id']===$id) { $u['active']=!(bool)$u['active']; $ok=true; break; } }
    unset($u); if (!$ok) fail('Não encontrado');
    wj($UFILE, $users); ok();

// ── ADMINS ────────────────────────────────────────────────────────────────────
case 'admins_list':
    reqAdmin();
    ok(['admins'=>array_values(array_map('nopass', rj($AFILE)))]);

case 'admin_create':
    reqAdmin();
    $name=trim($BODY['name']??''); $email=strtolower(trim($BODY['email']??'')); $pw=$BODY['password']??'';
    if (!$name||!$email||!$pw) fail('Preencha todos os campos');
    $admins = rj($AFILE);
    foreach ($admins as $a) { if (strtolower(trim($a['email']))===$email) fail('Email já existe'); }
    $admins[] = ['id'=>'adm_'.uniqid(),'name'=>$name,'email'=>$email,
                 'password'=>password_hash($pw, PASSWORD_DEFAULT),'created_at'=>date('c')];
    wj($AFILE, $admins); ok();

case 'admin_delete':
    reqAdmin();
    $id = $BODY['id'] ?? ''; $admins = rj($AFILE);
    if (!empty($admins[0]['id']) && $admins[0]['id']===$id) fail('Não pode remover o admin principal');
    wj($AFILE, array_values(array_filter($admins, fn($a)=>$a['id']!==$id))); ok();

// ── AVATAR UPLOAD ─────────────────────────────────────────────────────────────
case 'avatar_upload':
    reqAuth();
    $uid = $_SESSION['id'] ?? '';
    if (empty($_FILES['avatar'])) fail('Nenhum arquivo enviado');
    $file = $_FILES['avatar'];
    if ($file['error'] !== UPLOAD_ERR_OK) fail('Erro no upload: '.$file['error']);
    $allowed = ['image/jpeg','image/png','image/webp','image/gif'];
    $finfo   = finfo_open(FILEINFO_MIME_TYPE);
    $mime    = finfo_file($finfo, $file['tmp_name']);
    finfo_close($finfo);
    if (!in_array($mime, $allowed)) fail('Formato inválido. Use JPG, PNG ou WEBP');
    if ($file['size'] > 3 * 1024 * 1024) fail('Imagem muito grande. Máximo 3MB');

    // Remove avatar antigo
    $users = rj($UFILE);
    foreach ($users as $u) {
        if ($u['id'] === $uid && !empty($u['avatar_path'])) {
            $old = __DIR__ . '/' . $u['avatar_path'];
            if (file_exists($old)) @unlink($old);
        }
    }

    $ext  = ['image/jpeg'=>'jpg','image/png'=>'png','image/webp'=>'webp','image/gif'=>'gif'][$mime];
    $fname = $uid . '_' . time() . '.' . $ext;
    $dest  = $AVDIR . '/' . $fname;
    if (!move_uploaded_file($file['tmp_name'], $dest)) fail('Erro ao salvar imagem');

    // Redimensiona para 200×200 (quadrado) se GD disponível
    if (function_exists('imagecreatefromstring')) {
        try {
            $raw = file_get_contents($dest);
            $img = imagecreatefromstring($raw);
            if ($img) {
                $w = imagesx($img); $h = imagesy($img);
                $size = min($w, $h);
                $ox = (int)(($w - $size) / 2); $oy = (int)(($h - $size) / 2);
                $out = imagecreatetruecolor(200, 200);
                imagecopyresampled($out, $img, 0, 0, $ox, $oy, 200, 200, $size, $size);
                imagejpeg($out, $dest, 88);
                imagedestroy($img); imagedestroy($out);
            }
        } catch (Exception $e) { /* ignora se falhar */ }
    }

    $relPath = 'data/avatars/' . $fname;
    foreach ($users as &$u) {
        if ($u['id'] === $uid) { $u['avatar_path'] = $relPath; break; }
    } unset($u);
    wj($UFILE, $users);
    $_SESSION['avatar_path'] = $relPath;
    ok(['avatar_url' => $relPath . '?t=' . time()]);

// ── FAVORITOS ─────────────────────────────────────────────────────────────────
case 'favs_get':
    reqAuth();
    $uid = $_SESSION['id'] ?? ''; $favs = rj($FFILE);
    ok(['favs' => $favs[$uid] ?? []]);

case 'favs_toggle':
    reqAuth();
    $uid = $_SESSION['id'] ?? ''; $track = $BODY['track'] ?? null;
    if (!$track || empty($track['id'])) fail('Track inválida');
    $favs = rj($FFILE);
    if (!isset($favs[$uid])) $favs[$uid] = [];
    $idx = array_search($track['id'], array_column($favs[$uid], 'id'));
    if ($idx !== false) { array_splice($favs[$uid], $idx, 1); $act2='removed'; }
    else { array_unshift($favs[$uid], $track); $act2='added'; }
    wj($FFILE, $favs);
    ok(['action'=>$act2, 'favs'=>$favs[$uid]]);

// ── PLAYLISTS ─────────────────────────────────────────────────────────────────
case 'pl_list':
    reqAuth();
    $uid = $_SESSION['id'] ?? '';
    ok(['playlists' => rj($PLFILE)[$uid] ?? []]);

case 'pl_create':
    reqAuth();
    $uid   = $_SESSION['id'] ?? '';
    $name  = trim($BODY['name'] ?? '');
    $emoji = $BODY['emoji'] ?? '🎵';
    if (!$name) fail('Nome obrigatório');
    $pls = rj($PLFILE);
    if (!isset($pls[$uid])) $pls[$uid] = [];
    if (count($pls[$uid]) >= 30) fail('Limite de 30 playlists atingido');
    $pl = ['id'=>'pl_'.uniqid(),'name'=>$name,'emoji'=>$emoji,'tracks'=>[],'created_at'=>date('c')];
    array_unshift($pls[$uid], $pl);
    wj($PLFILE, $pls);
    ok(['playlist'=>$pl, 'playlists'=>$pls[$uid]]);

case 'pl_rename':
    reqAuth();
    $uid = $_SESSION['id'] ?? ''; $pid = $BODY['id'] ?? '';
    $name = trim($BODY['name'] ?? ''); $emoji = $BODY['emoji'] ?? null;
    if (!$name) fail('Nome obrigatório');
    $pls = rj($PLFILE);
    foreach (($pls[$uid] ?? []) as &$pl) {
        if ($pl['id'] === $pid) { $pl['name'] = $name; if ($emoji) $pl['emoji'] = $emoji; break; }
    } unset($pl);
    wj($PLFILE, $pls);
    ok(['playlists' => $pls[$uid]]);

case 'pl_delete':
    reqAuth();
    $uid = $_SESSION['id'] ?? ''; $pid = $BODY['id'] ?? '';
    $pls = rj($PLFILE);
    $pls[$uid] = array_values(array_filter($pls[$uid] ?? [], fn($p)=>$p['id']!==$pid));
    wj($PLFILE, $pls);
    ok(['playlists' => $pls[$uid]]);

case 'pl_track_add':
    reqAuth();
    $uid = $_SESSION['id'] ?? ''; $pid = $BODY['id'] ?? ''; $track = $BODY['track'] ?? null;
    if (!$track || empty($track['id'])) fail('Track inválida');
    $pls = rj($PLFILE);
    foreach (($pls[$uid] ?? []) as &$pl) {
        if ($pl['id'] === $pid) {
            $ids = array_column($pl['tracks'], 'id');
            if (!in_array($track['id'], $ids)) $pl['tracks'][] = $track;
            wj($PLFILE, $pls);
            ok(['playlist'=>$pl, 'playlists'=>$pls[$uid]]);
        }
    } unset($pl);
    fail('Playlist não encontrada');

case 'pl_track_remove':
    reqAuth();
    $uid = $_SESSION['id'] ?? ''; $pid = $BODY['id'] ?? ''; $tid = $BODY['track_id'] ?? '';
    $pls = rj($PLFILE);
    foreach (($pls[$uid] ?? []) as &$pl) {
        if ($pl['id'] === $pid) {
            $pl['tracks'] = array_values(array_filter($pl['tracks'], fn($t)=>$t['id']!==$tid));
            wj($PLFILE, $pls);
            ok(['playlist'=>$pl, 'playlists'=>$pls[$uid]]);
        }
    } unset($pl);
    fail('Playlist não encontrada');

default:
    fail("Ação '$action' inválida", 404);
}
