import os
import sys
import json
import subprocess
import time
import uuid
import warnings
import re
import shutil
import requests
from pathlib import Path

warnings.filterwarnings("ignore")
os.environ["QT_AUTO_SCREEN_SCALE_FACTOR"] = "1"

from PyQt5 import QtCore, QtWidgets, QtGui
from PyQt5.QtCore import QUrl, Qt, QTimer, QThread, pyqtSignal, QPropertyAnimation, QEasingCurve, QRect, QPoint, QSize
from PyQt5.QtWidgets import (
    QApplication, QWidget, QVBoxLayout, QHBoxLayout, QPushButton,
    QFileDialog, QMessageBox, QLabel, QDialog, QFrame, QSlider,
    QStyle, QGraphicsDropShadowEffect, QProgressBar, QSizePolicy,
    QStackedWidget, QStyleOptionSlider, QScrollArea, QLineEdit,
    QGraphicsOpacityEffect, QButtonGroup, QRadioButton, QSpacerItem,
    QCheckBox, QInputDialog, QComboBox, QShortcut, QGridLayout,
    QPlainTextEdit
)
from PyQt5.QtMultimedia import QMediaPlayer, QMediaContent
from PyQt5.QtMultimediaWidgets import QVideoWidget
from PyQt5.QtGui import QPixmap, QFont, QFontDatabase, QPainter, QColor, QPen, QBrush, QLinearGradient, QPalette, QIcon, QPolygon, QKeySequence, QDesktopServices, QPainterPath

try:
    from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEngineSettings
    HAS_WEBENGINE = True
except ImportError:
    HAS_WEBENGINE = False

# ─────────────────────────────────────────────
#  Caminhos
# ─────────────────────────────────────────────
BASE_DIR = Path(__file__).resolve().parent
PLAYLIST_FILE = BASE_DIR / 'playlist.json'
PLAYLISTS_FILE = BASE_DIR / 'playlists.json'
MUSIC_DIR = BASE_DIR / 'music'
THUMB_DIR = BASE_DIR / 'thumbs'
SETTINGS_FILE = BASE_DIR / 'neonwave_settings.json'
APP_ICON_FILE = BASE_DIR / 'favicon.ico'
DEFAULT_ART_FILE = BASE_DIR / 'logo.png'
DOWNLOAD_ERROR_FILE = BASE_DIR / 'download_error.log'
PREMIUM_VALIDATE_URL = 'https://neonwaveplayer.com.br/api/validate_player.php'
FREE_DOWNLOAD_LIMIT = 10
APP_VERSION = '1.1.0'
UPDATE_MANIFEST_URL = (
    'https://neonwaveplayer.com.br/api/private/neonwave_update.json'
)
UPDATE_PROGRAM_URL = (
    'https://neonwaveplayer.com.br/api/private/player_1790050397186.py'
)
# Este é o endereço público do painel administrativo no servidor.
# O parâmetro public_ad entrega somente a configuração pública da propaganda.
AD_CONFIG_URL = 'https://neonwaveplayer.com.br/api/admin.php?public_ad=1'
PLAYER_HTTP_HEADERS = {
    'User-Agent': 'NeonWave-Player/1.0',
    'Accept': 'application/json'
}
MUSIC_DIR.mkdir(exist_ok=True)
THUMB_DIR.mkdir(exist_ok=True)

# ─────────────────────────────────────────────
#  Tema
# ─────────────────────────────────────────────
BG       = "#090D14"
BG2      = "#111824"
BG3      = "#192333"
BG4      = "#253247"
ACCENT   = "#FF4F9A"
ACCENT2  = "#A98BFF"
ACCENT3  = "#D3C7FF"
TEXT     = "#F1F5FF"
TEXT2    = "#AAB7CC"
TEXT3    = "#6E7D95"
BORDER   = "#2C3A50"
PANEL_BORDER = "#3A4F70"
RED      = "#FF6B86"
GREEN    = "#55E6A5"
LIME     = "#B7FF3C"
DEFAULT_ART_PIXMAP = None

THEMES = {
    'neonwave': {
        'label': 'Neon Wave',
        'description': 'Rosa neon e violeta — tema padrão',
        'premium': False,
        'BG': '#090D14', 'BG2': '#111824', 'BG3': '#192333',
        'BG4': '#253247', 'ACCENT': '#FF4F9A', 'ACCENT2': '#A98BFF',
        'ACCENT3': '#D3C7FF', 'TEXT': '#F1F5FF', 'TEXT2': '#AAB7CC',
        'TEXT3': '#6E7D95', 'BORDER': '#2C3A50', 'PANEL_BORDER': '#3A4F70',
        'RED': '#FF6B86', 'GREEN': '#55E6A5', 'LIME': '#B7FF3C'
    },
    'aurora': {
        'label': 'Aurora Pulse',
        'description': 'Verde aurora e azul cristal — gratuito',
        'premium': False,
        'BG': '#071914', 'BG2': '#0D261F', 'BG3': '#12382D',
        'BG4': '#1C5140', 'ACCENT': '#39E6A8', 'ACCENT2': '#69DFFF',
        'ACCENT3': '#C7FFE9', 'TEXT': '#F0FFF8', 'TEXT2': '#A4CFC0',
        'TEXT3': '#6B9B8A', 'BORDER': '#285A4B', 'PANEL_BORDER': '#3B806A',
        'RED': '#FF7898', 'GREEN': '#7DFFB5', 'LIME': '#D5FF62'
    },
    'nebula': {
        'label': 'Nebula Royal',
        'description': 'Violeta cósmico e azul elétrico — Premium',
        'premium': True,
        'BG': '#0E091C', 'BG2': '#18102D', 'BG3': '#241846',
        'BG4': '#362766', 'ACCENT': '#C06CFF', 'ACCENT2': '#6F8CFF',
        'ACCENT3': '#E9D4FF', 'TEXT': '#FAF5FF', 'TEXT2': '#C9B9E8',
        'TEXT3': '#8E7DAF', 'BORDER': '#493478', 'PANEL_BORDER': '#704FA6',
        'RED': '#FF709F', 'GREEN': '#73F5C1', 'LIME': '#E4FF6B'
    },
    'ocean': {
        'label': 'Deep Ocean',
        'description': 'Ciano profundo e azul oceano — Premium',
        'premium': True,
        'BG': '#06131F', 'BG2': '#0A2031', 'BG3': '#10334B',
        'BG4': '#194A68', 'ACCENT': '#21D4FD', 'ACCENT2': '#4B7BFF',
        'ACCENT3': '#C8F6FF', 'TEXT': '#F1FCFF', 'TEXT2': '#A7CAD8',
        'TEXT3': '#6F9BAD', 'BORDER': '#23536E', 'PANEL_BORDER': '#327FA1',
        'RED': '#FF718E', 'GREEN': '#66F5C0', 'LIME': '#D5FF67'
    },
    'solar': {
        'label': 'Solar Flare',
        'description': 'Laranja solar e dourado quente — Premium',
        'premium': True,
        'BG': '#1A0D08', 'BG2': '#2A150C', 'BG3': '#432212',
        'BG4': '#61351A', 'ACCENT': '#FF7A45', 'ACCENT2': '#FFD166',
        'ACCENT3': '#FFE7B0', 'TEXT': '#FFF9EE', 'TEXT2': '#E4C9A5',
        'TEXT3': '#AD8B69', 'BORDER': '#70401F', 'PANEL_BORDER': '#A96835',
        'RED': '#FF5F6D', 'GREEN': '#A8F06E', 'LIME': '#F5FF6A'
    },
    'ruby': {
        'label': 'Ruby Afterdark',
        'description': 'Vermelho rubi e rosa intenso — Premium',
        'premium': True,
        'BG': '#1A080F', 'BG2': '#2A0D1A', 'BG3': '#43132A',
        'BG4': '#641D3D', 'ACCENT': '#FF4D79', 'ACCENT2': '#FF9A9E',
        'ACCENT3': '#FFD5E1', 'TEXT': '#FFF5F8', 'TEXT2': '#E2B7C5',
        'TEXT3': '#AE7186', 'BORDER': '#722744', 'PANEL_BORDER': '#A83D63',
        'RED': '#FF6B86', 'GREEN': '#77E8AC', 'LIME': '#E7FF69'
    }
}


def _theme_for_account(theme_key, premium=False):
    theme = THEMES.get(str(theme_key or '').strip().lower())
    if theme is None or (theme.get('premium') and not premium):
        return 'neonwave'
    return str(theme_key).strip().lower()


def _apply_theme_colors(theme_key, premium=False):
    theme_key = _theme_for_account(theme_key, premium)
    theme = THEMES[theme_key]
    color_names = (
        'BG', 'BG2', 'BG3', 'BG4', 'ACCENT', 'ACCENT2', 'ACCENT3',
        'TEXT', 'TEXT2', 'TEXT3', 'BORDER', 'PANEL_BORDER', 'RED',
        'GREEN', 'LIME'
    )
    globals().update({name: theme[name] for name in color_names})
    return theme_key


def _get_default_art_pixmap():
    global DEFAULT_ART_PIXMAP
    if DEFAULT_ART_PIXMAP is None:
        DEFAULT_ART_PIXMAP = QPixmap(str(DEFAULT_ART_FILE))
    return DEFAULT_ART_PIXMAP


def _make_skip_icon(forward=False):
    pix = QPixmap(24, 24)
    pix.fill(Qt.transparent)
    painter = QPainter(pix)
    painter.setRenderHint(QPainter.Antialiasing)
    painter.setPen(Qt.NoPen)
    painter.setBrush(QColor(TEXT))

    if forward:
        painter.drawRect(5, 6, 2, 12)
        painter.drawPolygon(QPolygon([
            QPoint(9, 6), QPoint(9, 18), QPoint(18, 12)
        ]))
    else:
        painter.drawRect(17, 6, 2, 12)
        painter.drawPolygon(QPolygon([
            QPoint(15, 6), QPoint(15, 18), QPoint(6, 12)
        ]))

    painter.end()
    return QIcon(pix)


def _make_play_icon(playing=False):
    pix = QPixmap(28, 28)
    pix.fill(Qt.transparent)
    painter = QPainter(pix)
    painter.setRenderHint(QPainter.Antialiasing)
    painter.setPen(Qt.NoPen)
    painter.setBrush(QColor(Qt.white))

    if playing:
        painter.drawRoundedRect(7, 6, 5, 16, 2, 2)
        painter.drawRoundedRect(16, 6, 5, 16, 2, 2)
    else:
        painter.drawPolygon(QPolygon([
            QPoint(8, 5), QPoint(8, 23), QPoint(22, 14)
        ]))

    painter.end()
    return QIcon(pix)


def _make_close_icon():
    pix = QPixmap(16, 16)
    pix.fill(Qt.transparent)
    painter = QPainter(pix)
    painter.setRenderHint(QPainter.Antialiasing)
    pen = QPen(QColor(TEXT))
    pen.setWidth(2)
    pen.setCapStyle(Qt.RoundCap)
    painter.setPen(pen)
    painter.drawLine(3, 3, 13, 13)
    painter.drawLine(13, 3, 3, 13)
    painter.end()
    return QIcon(pix)


def _make_lock_icon(color=None):
    color = color or ACCENT2
    pix = QPixmap(18, 18)
    pix.fill(Qt.transparent)
    painter = QPainter(pix)
    painter.setRenderHint(QPainter.Antialiasing)

    pen = QPen(QColor(color))
    pen.setWidth(2)
    pen.setCapStyle(Qt.RoundCap)
    pen.setJoinStyle(Qt.RoundJoin)
    painter.setPen(pen)
    painter.setBrush(Qt.NoBrush)
    painter.drawArc(5, 2, 8, 9, 0, 180 * 16)
    painter.drawRoundedRect(3, 8, 12, 8, 2, 2)

    painter.setPen(Qt.NoPen)
    painter.setBrush(QColor(color))
    painter.drawEllipse(8, 10, 2, 2)
    painter.drawRect(9, 12, 1, 2)

    painter.end()
    return QIcon(pix)


def _make_folder_icon(color=None):
    color = color or ACCENT2
    pix = QPixmap(20, 20)
    pix.fill(Qt.transparent)
    painter = QPainter(pix)
    painter.setRenderHint(QPainter.Antialiasing)
    pen = QPen(QColor(color))
    pen.setWidth(2)
    pen.setJoinStyle(Qt.RoundJoin)
    painter.setPen(pen)
    painter.setBrush(Qt.NoBrush)
    painter.drawRoundedRect(3, 6, 14, 10, 2, 2)
    painter.drawLine(4, 6, 8, 6)
    painter.drawLine(8, 6, 10, 4)
    painter.drawLine(10, 4, 14, 4)
    painter.end()
    return QIcon(pix)


def _make_playlist_icon(color=None):
    color = color or ACCENT2
    pix = QPixmap(20, 20)
    pix.fill(Qt.transparent)
    painter = QPainter(pix)
    painter.setRenderHint(QPainter.Antialiasing)
    pen = QPen(QColor(color))
    pen.setWidth(2)
    pen.setCapStyle(Qt.RoundCap)
    painter.setPen(pen)
    painter.drawLine(4, 5, 16, 5)
    painter.drawLine(4, 10, 16, 10)
    painter.drawLine(4, 15, 12, 15)
    painter.drawLine(15, 13, 15, 18)
    painter.drawLine(13, 16, 17, 16)
    painter.end()
    return QIcon(pix)


def _make_star_icon(filled=False, color=None):
    color = color or ACCENT
    pix = QPixmap(20, 20)
    pix.fill(Qt.transparent)
    painter = QPainter(pix)
    painter.setRenderHint(QPainter.Antialiasing)
    points = []
    center = QPoint(10, 10)
    import math
    for index in range(10):
        angle = -90 + index * 36
        radius = 8 if index % 2 == 0 else 3.6
        points.append(QPoint(
            int(center.x() + radius * math.cos(math.radians(angle))),
            int(center.y() + radius * math.sin(math.radians(angle)))
        ))
    pen = QPen(QColor(color))
    pen.setWidth(2)
    painter.setPen(pen)
    painter.setBrush(QBrush(QColor(color)) if filled else Qt.NoBrush)
    painter.drawPolygon(QPolygon(points))
    painter.end()
    return QIcon(pix)


def _make_menu_icon(kind, color=None):
    color = color or TEXT2
    pix = QPixmap(22, 22)
    pix.fill(Qt.transparent)
    painter = QPainter(pix)
    painter.setRenderHint(QPainter.Antialiasing)
    pen = QPen(QColor(color))
    pen.setWidth(2)
    pen.setCapStyle(Qt.RoundCap)
    pen.setJoinStyle(Qt.RoundJoin)
    painter.setPen(pen)
    painter.setBrush(Qt.NoBrush)

    if kind == 'file':
        painter.drawRoundedRect(5, 3, 12, 16, 2, 2)
        painter.drawLine(13, 3, 17, 7)
        painter.drawLine(13, 3, 13, 7)
        painter.drawLine(13, 7, 17, 7)
        painter.drawLine(8, 11, 14, 11)
        painter.drawLine(11, 8, 11, 14)
    elif kind == 'download':
        painter.drawLine(11, 3, 11, 14)
        painter.drawLine(7, 11, 11, 15)
        painter.drawLine(11, 15, 15, 11)
        painter.drawLine(5, 18, 17, 18)
    elif kind == 'queue':
        for y in (5, 10, 15):
            painter.drawLine(7, y, 18, y)
            painter.drawEllipse(3, y - 1, 2, 2)
    elif kind == 'browser':
        painter.drawEllipse(3, 3, 16, 16)
        painter.setBrush(QBrush(QColor(color)))
        painter.setPen(Qt.NoPen)
        painter.drawPolygon(QPolygon([
            QPoint(9, 7), QPoint(9, 15), QPoint(15, 11)
        ]))
    elif kind == 'settings':
        painter.drawEllipse(7, 7, 8, 8)
        for angle in range(0, 360, 45):
            import math
            radians = math.radians(angle)
            x1 = 11 + int(math.cos(radians) * 6)
            y1 = 11 + int(math.sin(radians) * 6)
            x2 = 11 + int(math.cos(radians) * 9)
            y2 = 11 + int(math.sin(radians) * 9)
            painter.drawLine(x1, y1, x2, y2)
    elif kind == 'account':
        painter.drawEllipse(8, 4, 6, 6)
        painter.drawArc(5, 10, 12, 10, 0, 180 * 16)
    elif kind == 'premium':
        painter.drawPolygon(QPolygon([
            QPoint(11, 3), QPoint(14, 8), QPoint(19, 9),
            QPoint(15, 13), QPoint(16, 18), QPoint(11, 15),
            QPoint(6, 18), QPoint(7, 13), QPoint(3, 9),
            QPoint(8, 8)
        ]))
        painter.drawEllipse(10, 10, 2, 2)

    painter.end()
    return QIcon(pix)


# O YouTube pode deixar um cliente sem formatos baixáveis quando exige PO
# Token. Tentamos clientes diferentes automaticamente, sem mudar a interface.
YTDLP_COMMON_ARGS = ['--remote-components', 'ejs:github']
YTDLP_CLIENT_PROFILES = [
    [],
    ['--extractor-args', 'youtube:player_client=android'],
    ['--extractor-args', 'youtube:player_client=tv'],
    ['--extractor-args', 'youtube:player_client=web_safari'],
]
# O YouTube pode exigir uma sessão autenticada para liberar determinados
# vídeos. O yt-dlp lê uma cópia local dos cookies, sem expor credenciais.
YTDLP_COOKIE_PROFILES = [
    [],
    ['--cookies-from-browser', 'chrome'],
    ['--cookies-from-browser', 'edge'],
    ['--cookies-from-browser', 'firefox'],
    ['--cookies-from-browser', 'brave'],
]
AUTO_COOKIES_FILE = Path(__file__).resolve().parent / 'cookies.txt'
YTDLP_UPDATE_CHECKED = False
DOWNLOAD_STATUS_MESSAGES = [
    "Baixando na velocidade da luz",
    "Capturando o melhor sinal",
    "Encontrando o melhor caminho",
    "Alinhando as batidas",
    "Deixando tudo no ritmo",
    "Preparando o refrão",
    "Dando aquele acabamento",
    "Espera, quase lá",
    "Finalizando com estilo",
    "Quase no ponto",
    "Conferindo a melhor qualidade",
    "Unindo áudio e vídeo",
    "Preservando a qualidade original",
    "Acelerando a finalização",
    "Organizando o arquivo",
]


def _normalize_download_url(value):
    value = (value or "").strip()
    urls = re.findall(r"https?://[^\s]+", value)
    if urls:
        return urls[-1].rstrip(".,;:)]}>\"'")
    return value


def _get_js_runtime_args():
    for runtime in ("deno", "node", "bun", "qjs", "quickjs"):
        if shutil.which(runtime):
            return ["--js-runtimes", runtime]
    return []


YTDLP_JS_RUNTIME_ARGS = _get_js_runtime_args()
FFMPEG_CHECKED = False
FFMPEG_BIN = None


def _get_ffmpeg_bin():
    global FFMPEG_CHECKED, FFMPEG_BIN
    if FFMPEG_CHECKED:
        return FFMPEG_BIN

    FFMPEG_BIN = shutil.which("ffmpeg")
    if not FFMPEG_BIN:
        try:
            import imageio_ffmpeg
            FFMPEG_BIN = imageio_ffmpeg.get_ffmpeg_exe()
        except Exception:
            try:
                subprocess.run(
                    [sys.executable, '-m', 'pip', 'install', 'imageio-ffmpeg'],
                    capture_output=True, text=True, encoding='utf-8',
                    errors='replace', timeout=120
                ).check_returncode()
                import imageio_ffmpeg
                FFMPEG_BIN = imageio_ffmpeg.get_ffmpeg_exe()
            except Exception:
                FFMPEG_BIN = None

    FFMPEG_CHECKED = True
    return FFMPEG_BIN

def _build_stylesheet():
    return f"""
QWidget {{
    background-color: {BG};
    color: {TEXT};
    font-family: 'Segoe UI', 'Inter', 'Ubuntu', sans-serif;
    font-size: 13px;
    border: none;
    outline: none;
}}

QLabel {{
    background: transparent;
    border: none;
}}

/* ─── Sidebar ─── */
QFrame#Sidebar {{
    background-color: {BG2};
    border-right: 1px solid {BORDER};
}}

QLabel#AppTitle {{
    color: {ACCENT2};
    font-size: 18px;
    font-weight: 900;
    letter-spacing: 3px;
    padding: 0 24px;
}}

QLabel#AppSub {{
    color: {TEXT3};
    font-size: 10px;
    letter-spacing: 2px;
    padding: 0 24px 4px;
}}

QPushButton#SideBtn {{
    background: transparent;
    color: {TEXT2};
    text-align: left;
    padding: 9px 18px;
    font-size: 13px;
    border-radius: 10px;
    border: 1px solid transparent;
    margin: 2px 12px;
}}
QPushButton#SideBtn:hover {{
    background: {BG3};
    color: {TEXT};
    border: 1px solid {BORDER};
}}
QPushButton#SideBtn:pressed {{
    background: {ACCENT}22;
    color: {ACCENT3};
    border: 1px solid {ACCENT}66;
}}

QLabel#SectionLabel {{
    color: {TEXT3};
    font-size: 10px;
    font-weight: 700;
    letter-spacing: 2px;
    padding: 8px 24px 4px;
}}

/* ─── Scroll ─── */
QScrollArea {{ background: transparent; border: none; }}
QScrollBar:vertical {{
    background: transparent;
    width: 5px;
    margin: 0;
}}
QScrollBar::handle:vertical {{
    background: {BG4};
    border-radius: 3px;
    min-height: 30px;
}}
QScrollBar::handle:vertical:hover {{ background: {ACCENT}; }}
QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {{ height: 0; }}

/* ─── Now Playing Panel ─── */
QFrame#NowPlayingPanel {{
    background: transparent;
    border-top: none;
}}

/* ─── Sliders ─── */
QSlider::groove:horizontal {{
    height: 5px;
    background: {BG4};
    border-radius: 2px;
}}
QSlider::sub-page:horizontal {{
    background: qlineargradient(x1:0,y1:0,x2:1,y2:0, stop:0 {ACCENT}, stop:1 {ACCENT2});
    border-radius: 2px;
}}
QSlider::add-page:horizontal {{
    background: {BG4};
    border-radius: 2px;
}}
QSlider::handle:horizontal {{
    background: white;
    width: 14px;
    height: 14px;
    margin: -5px 0;
    border-radius: 7px;
    border: 2px solid {ACCENT3};
}}
QSlider::handle:horizontal:hover {{
    background: {ACCENT3};
    border: 2px solid white;
}}

/* ─── Volume slider (thin) ─── */
QSlider#VolumeSlider::groove:horizontal {{ height: 3px; background: {BG4}; border-radius: 2px; }}
QSlider#VolumeSlider::sub-page:horizontal {{ background: {ACCENT}; border-radius: 2px; }}
QSlider#VolumeSlider::add-page:horizontal {{ background: {BG4}; border-radius: 2px; }}
QSlider#VolumeSlider::handle:horizontal {{
    background: white; width: 10px; height: 10px;
    margin: -4px 0; border-radius: 5px;
}}

/* ─── Buttons ─── */
QPushButton#CtrlBtn {{
    background: #182438;
    color: {TEXT};
    font-size: 18px;
    border: 1px solid {PANEL_BORDER};
    border-radius: 19px;
    padding: 0px;
    min-width: 38px; min-height: 38px;
    max-width: 44px; max-height: 44px;
}}
QPushButton#CtrlBtn:hover {{
    background: #22314A;
    color: {TEXT};
    border: 1px solid {ACCENT}99;
}}
QPushButton#CtrlBtn:pressed {{
    background: {ACCENT}33;
    color: {ACCENT3};
    border: 1px solid {ACCENT};
}}
QPushButton#CtrlBtn:checked {{
    background: {BG};
    color: {ACCENT};
    border: 1px solid {ACCENT};
    padding: 0px;
}}

QToolTip {{
    background: {BG2};
    color: {TEXT};
    border: 1px solid {ACCENT};
    padding: 4px 7px;
}}

QPushButton#PlayBtn {{
    background: {ACCENT};
    color: white;
    font-family: "DejaVu Sans";
    font-size: 17px;
    font-weight: 700;
    border: 2px solid {ACCENT};
    border-radius: 25px;
    padding: 0px;
    min-width: 50px; min-height: 50px;
    max-width: 50px; max-height: 50px;
}}
QPushButton#PlayBtn:hover {{
    background: #FF6DAE;
    border: 2px solid #FF9CC5;
}}
QPushButton#PlayBtn:pressed {{
    background: #E93D86;
    border: 2px solid {ACCENT};
}}

/* ─── Dialog ─── */
QDialog {{
    background: {BG2};
    border: 1px solid {BORDER};
    border-radius: 12px;
}}
QLineEdit {{
    background: {BG3};
    border: 1.5px solid {BORDER};
    border-radius: 8px;
    padding: 10px 14px;
    color: {TEXT};
    font-size: 13px;
    selection-background-color: {ACCENT};
}}
QLineEdit:focus {{ border: 1.5px solid {ACCENT}; }}
QComboBox {{
    background: {BG3};
    border: 1.5px solid {BORDER};
    border-radius: 10px;
    padding: 9px 38px 9px 14px;
    color: {TEXT};
    font-size: 13px;
    min-height: 18px;
}}
QComboBox:hover {{
    border: 1.5px solid {ACCENT}99;
    background: {BG4};
}}
QComboBox:focus {{
    border: 1.5px solid {ACCENT};
}}
QComboBox::drop-down {{
    subcontrol-origin: padding;
    subcontrol-position: top right;
    width: 32px;
    border: none;
    background: transparent;
}}
QComboBox::down-arrow {{
    width: 0px;
    height: 0px;
    border-left: 5px solid transparent;
    border-right: 5px solid transparent;
    border-top: 6px solid {ACCENT};
}}
QComboBox QAbstractItemView {{
    background: {BG};
    color: {TEXT};
    border: none;
    border-radius: 10px;
    padding: 5px;
    outline: none;
    selection-background-color: {ACCENT}33;
    selection-color: {TEXT};
}}
QComboBox QAbstractItemView::item {{
    background: {BG};
    padding: 7px 10px;
    min-height: 20px;
    border-radius: 6px;
}}
QComboBox#LibraryFilter {{
    background: {BG3};
    border: 1px solid {BORDER};
    border-radius: 10px;
    padding: 8px 34px 8px 12px;
    color: {TEXT};
    font-size: 12px;
    min-height: 18px;
}}
QComboBox#LibraryFilter:hover {{
    background: {BG4};
    border: 1px solid {ACCENT}88;
}}
QComboBox#LibraryFilter QAbstractItemView {{
    background: {BG2};
    border: 1px solid {BORDER};
    padding: 5px;
    selection-background-color: {ACCENT}33;
}}
QPushButton#PlaylistCreateBtn {{
    background: {BG3};
    color: {ACCENT3};
    border: 1px solid {BORDER};
    border-radius: 9px;
    padding: 0 10px;
    min-height: 34px;
    max-height: 34px;
    font-size: 11px;
    font-weight: 700;
}}
QPushButton#PlaylistCreateBtn:hover {{
    background: {BG4};
    border: 1px solid {ACCENT2};
    color: {TEXT};
}}
QPushButton#PlaylistDeleteBtn {{
    background: {BG3};
    color: {RED};
    border: 1px solid {BORDER};
    border-radius: 9px;
    padding: 0px;
    min-width: 34px;
    max-width: 34px;
    min-height: 34px;
    max-height: 34px;
    font-size: 17px;
    font-weight: 700;
}}
QPushButton#PlaylistDeleteBtn:hover {{
    background: {RED}22;
    border: 1px solid {RED};
    color: {TEXT};
}}
QPushButton#TrackActionBtn, QPushButton#TrackDeleteBtn {{
    background: {BG3};
    color: {TEXT2};
    border: 1px solid {BORDER};
    border-radius: 15px;
    padding: 0px;
    min-width: 30px;
    min-height: 30px;
    max-width: 30px;
    max-height: 30px;
}}
QPushButton#TrackActionBtn:hover {{
    background: {BG4};
    border: 1px solid {ACCENT2}99;
}}
QPushButton#TrackActionBtn:pressed {{
    background: {ACCENT}22;
    border: 1px solid {ACCENT};
}}
QPushButton#TrackDeleteBtn:hover {{
    background: {RED}22;
    border: 1px solid {RED}99;
}}
QPushButton#TrackDeleteBtn:pressed {{
    background: {RED}33;
    border: 1px solid {RED};
}}
QPushButton#TrackFavoriteBtn {{
    background: {BG3};
    color: {TEXT2};
    border: 1px solid {BORDER};
    border-radius: 15px;
    padding: 0px;
    min-width: 30px;
    min-height: 30px;
    max-width: 30px;
    max-height: 30px;
}}
QPushButton#TrackFavoriteBtn:hover {{
    background: {ACCENT}18;
    border: 1px solid {ACCENT}99;
}}
QPushButton#TrackFavoriteBtn:pressed {{
    background: {ACCENT}33;
    border: 1px solid {ACCENT};
}}
QPlainTextEdit {{
    background: {BG3};
    color: {TEXT};
    border: 1.5px solid {BORDER};
    border-radius: 8px;
    padding: 9px 11px;
    selection-background-color: {ACCENT};
}}
QMessageBox {{
    background: {BG2};
    border: 1px solid {PANEL_BORDER};
    border-radius: 12px;
}}
QMessageBox QLabel {{
    color: {TEXT};
    background: transparent;
    min-width: 280px;
    padding: 4px 2px;
}}
QMessageBox QPushButton {{
    background: {BG4};
    color: {TEXT};
    border: 1px solid {BORDER};
    border-radius: 8px;
    padding: 8px 20px;
    min-width: 58px;
}}
QMessageBox QPushButton:hover {{
    background: {ACCENT}22;
    border: 1px solid {ACCENT}99;
    color: {TEXT};
}}
QPushButton#DialogBtn {{
    background: qlineargradient(x1:0,y1:0,x2:1,y2:0, stop:0 {ACCENT}, stop:1 #FF7A59);
    color: white;
    border-radius: 8px;
    border: 1px solid #FFB5CF;
    padding: 10px 24px;
    font-weight: 700;
    font-size: 13px;
}}
QPushButton#DialogBtn:hover {{
    background: qlineargradient(x1:0,y1:0,x2:1,y2:0, stop:0 #FF79B2, stop:1 #FF9B69);
    border: 1px solid white;
}}
QPushButton#CancelBtn {{
    background: {BG4};
    color: {TEXT2};
    border-radius: 8px;
    border: 1px solid {BORDER};
    padding: 10px 24px;
    font-size: 13px;
}}
QPushButton#CancelBtn:hover {{
    background: {BG3};
    color: {TEXT};
    border: 1px solid {TEXT3};
}}
QRadioButton {{
    background: transparent;
    color: {TEXT2};
    font-size: 13px;
    spacing: 8px;
}}
QRadioButton:checked {{
    color: {TEXT};
}}
QCheckBox {{
    background: transparent;
}}
QRadioButton::indicator {{
    width: 16px; height: 16px;
    border-radius: 8px;
    border: 2px solid {BORDER};
    background: {BG3};
}}
QRadioButton::indicator:checked {{
    background: {ACCENT};
    border: 2px solid {ACCENT};
}}
QProgressBar {{
    background: {BG4};
    border: 1px solid {BORDER};
    border-radius: 4px;
    height: 6px;
    text-align: center;
}}
QProgressBar::chunk {{
    background: qlineargradient(x1:0,y1:0,x2:1,y2:0, stop:0 {ACCENT}, stop:1 #FF7A59);
    border-radius: 4px;
}}
    """


STYLESHEET = _build_stylesheet()


class PlaylistDeleteDialog(QDialog):
    def __init__(self, playlist_name, parent=None):
        super().__init__(parent)
        self.setObjectName("PlaylistDeleteDialog")
        self.setWindowTitle("Remover playlist")
        self.setFixedWidth(440)
        self.setWindowFlag(Qt.WindowContextHelpButtonHint, False)

        self.setStyleSheet(f"""
        QDialog#PlaylistDeleteDialog {{
            background: {BG2};
            border: 1px solid {PANEL_BORDER};
            border-radius: 14px;
        }}
        QFrame#PlaylistDeleteCard {{
            background: {BG2};
            border: none;
        }}
        QLabel#PlaylistDeleteIcon {{
            background: {ACCENT2}18;
            color: {RED};
            border: 1px solid {RED}66;
            border-radius: 22px;
            font-size: 22px;
            font-weight: 800;
        }}
        QLabel#PlaylistDeleteTitle {{
            background: transparent;
            color: {TEXT};
            font-size: 16px;
            font-weight: 800;
        }}
        QLabel#PlaylistDeleteText {{
            background: transparent;
            color: {TEXT2};
            font-size: 13px;
        }}
        QLabel#PlaylistDeleteHint {{
            background: transparent;
            color: {TEXT3};
            font-size: 11px;
        }}
        QPushButton#PlaylistDeleteCancel {{
            background: {BG4};
            color: {TEXT2};
            border: 1px solid {BORDER};
            border-radius: 8px;
            padding: 9px 20px;
            font-size: 12px;
        }}
        QPushButton#PlaylistDeleteCancel:hover {{
            background: {BG3};
            color: {TEXT};
            border: 1px solid {TEXT3};
        }}
        QPushButton#PlaylistDeleteConfirm {{
            background: {RED};
            color: white;
            border: 1px solid {RED};
            border-radius: 8px;
            padding: 9px 20px;
            font-size: 12px;
            font-weight: 700;
        }}
        QPushButton#PlaylistDeleteConfirm:hover {{
            background: #FF8A9E;
            border: 1px solid white;
        }}
        """)

        outer = QVBoxLayout(self)
        outer.setContentsMargins(14, 14, 14, 14)
        outer.setSpacing(0)

        card = QFrame()
        card.setObjectName("PlaylistDeleteCard")
        row = QHBoxLayout(card)
        row.setContentsMargins(6, 6, 6, 6)
        row.setSpacing(14)

        icon = QLabel("!")
        icon.setObjectName("PlaylistDeleteIcon")
        icon.setFixedSize(44, 44)
        icon.setAlignment(Qt.AlignCenter)
        row.addWidget(icon, 0, Qt.AlignTop)

        content = QVBoxLayout()
        content.setSpacing(5)

        title = QLabel("Remover playlist?")
        title.setObjectName("PlaylistDeleteTitle")
        content.addWidget(title)

        text = QLabel(f'Você deseja remover a playlist "{playlist_name}"?')
        text.setObjectName("PlaylistDeleteText")
        text.setWordWrap(True)
        content.addWidget(text)

        hint = QLabel("As músicas não serão excluídas da biblioteca.")
        hint.setObjectName("PlaylistDeleteHint")
        hint.setWordWrap(True)
        content.addWidget(hint)

        content.addSpacing(8)
        buttons = QHBoxLayout()
        buttons.setSpacing(8)
        buttons.addStretch()

        cancel = QPushButton("Cancelar")
        cancel.setObjectName("PlaylistDeleteCancel")
        cancel.clicked.connect(self.reject)
        buttons.addWidget(cancel)

        confirm = QPushButton("Remover")
        confirm.setObjectName("PlaylistDeleteConfirm")
        confirm.clicked.connect(self.accept)
        buttons.addWidget(confirm)

        content.addLayout(buttons)
        row.addLayout(content, 1)
        outer.addWidget(card)

        cancel.setFocus()


class PlaylistCreatePromptDialog(QDialog):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setObjectName("PlaylistCreatePromptDialog")
        self.setWindowTitle("Nenhuma playlist criada")
        self.setFixedWidth(440)
        self.setWindowFlag(Qt.WindowContextHelpButtonHint, False)

        outer = QVBoxLayout(self)
        outer.setContentsMargins(14, 14, 14, 14)
        outer.setSpacing(0)

        card = QFrame()
        card.setObjectName("PlaylistCreatePromptCard")
        card.setStyleSheet(
            f"QFrame#PlaylistCreatePromptCard {{ background: {BG2}; border: none; }}"
        )
        row = QHBoxLayout(card)
        row.setContentsMargins(6, 6, 6, 6)
        row.setSpacing(14)

        icon = QLabel()
        icon.setObjectName("PlaylistCreatePromptIcon")
        icon.setPixmap(_make_playlist_icon(ACCENT2).pixmap(QSize(24, 24)))
        icon.setFixedSize(30, 30)
        icon.setAlignment(Qt.AlignCenter)
        icon.setStyleSheet(
            "background: transparent; border: none;"
        )
        row.addWidget(icon, 0, Qt.AlignTop)

        content = QVBoxLayout()
        content.setSpacing(5)

        title = QLabel("Nenhuma playlist criada")
        title.setStyleSheet(
            f"color: {TEXT}; background: transparent; "
            "font-size: 16px; font-weight: 800;"
        )
        content.addWidget(title)

        text = QLabel("Você ainda não criou uma playlist.")
        text.setStyleSheet(
            f"color: {TEXT2}; background: transparent; font-size: 13px;"
        )
        content.addWidget(text)

        hint = QLabel("Deseja criar uma agora?")
        hint.setStyleSheet(
            f"color: {TEXT3}; background: transparent; font-size: 11px;"
        )
        content.addWidget(hint)

        content.addSpacing(8)
        buttons = QHBoxLayout()
        buttons.setSpacing(8)
        buttons.addStretch()

        cancel = QPushButton("Não")
        cancel.setObjectName("CancelBtn")
        cancel.clicked.connect(self.reject)
        buttons.addWidget(cancel)

        create = QPushButton("Criar playlist")
        create.setObjectName("DialogBtn")
        create.clicked.connect(self.accept)
        create.setDefault(True)
        buttons.addWidget(create)

        content.addLayout(buttons)
        row.addLayout(content, 1)
        outer.addWidget(card)

        create.setFocus()


class PremiumPromptDialog(QDialog):
    def __init__(self, detail, parent=None):
        super().__init__(parent)
        self.setObjectName("PremiumPromptDialog")
        self.setWindowTitle("Função Premium")
        self.setFixedWidth(460)
        self.setWindowFlag(Qt.WindowContextHelpButtonHint, False)

        outer = QVBoxLayout(self)
        outer.setContentsMargins(14, 14, 14, 14)
        outer.setSpacing(0)

        card = QFrame()
        card.setObjectName("PremiumPromptCard")
        card.setStyleSheet(
            f"QFrame#PremiumPromptCard {{ background: {BG2}; border: none; }}"
        )
        row = QHBoxLayout(card)
        row.setContentsMargins(6, 6, 6, 6)
        row.setSpacing(14)

        icon = QLabel()
        icon.setPixmap(_make_menu_icon('premium', ACCENT2).pixmap(QSize(26, 26)))
        icon.setFixedSize(30, 30)
        icon.setAlignment(Qt.AlignCenter)
        icon.setStyleSheet("background: transparent; border: none;")
        row.addWidget(icon, 0, Qt.AlignTop)

        content = QVBoxLayout()
        content.setSpacing(5)

        title = QLabel("Recurso exclusivo do Premium")
        title.setStyleSheet(
            f"color: {TEXT}; background: transparent; "
            "font-size: 16px; font-weight: 800;"
        )
        content.addWidget(title)

        text = QLabel(detail)
        text.setWordWrap(True)
        text.setStyleSheet(
            f"color: {TEXT2}; background: transparent; font-size: 13px;"
        )
        content.addWidget(text)

        hint = QLabel("Deseja ativar o Premium agora?")
        hint.setStyleSheet(
            f"color: {TEXT3}; background: transparent; font-size: 11px;"
        )
        content.addWidget(hint)

        content.addSpacing(8)
        buttons = QHBoxLayout()
        buttons.setSpacing(8)
        buttons.addStretch()

        cancel = QPushButton("Agora não")
        cancel.setObjectName("CancelBtn")
        cancel.clicked.connect(self.reject)
        buttons.addWidget(cancel)

        activate = QPushButton("Ativar Premium")
        activate.setObjectName("DialogBtn")
        activate.clicked.connect(self.accept)
        activate.setDefault(True)
        buttons.addWidget(activate)

        content.addLayout(buttons)
        row.addLayout(content, 1)
        outer.addWidget(card)

        activate.setFocus()


# ─────────────────────────────────────────────
#  Dados
# ─────────────────────────────────────────────
class Song:
    def __init__(
        self, title, path, source='local', thumb=None, is_video=False,
        favorite=False, folder='Geral', premium_only=False
    ):
        self.title = title
        self.path = path
        self.source = source
        self.thumb = thumb
        self.is_video = is_video
        self.favorite = bool(favorite)
        self.folder = str(folder or 'Geral').strip() or 'Geral'
        self.premium_only = premium_only

    def to_dict(self):
        return {
            'title': self.title, 'path': str(self.path),
            'source': self.source,
            'thumb': str(self.thumb) if self.thumb else None,
            'is_video': self.is_video,
            'favorite': self.favorite,
            'folder': self.folder,
            'premium_only': self.premium_only
        }

    @staticmethod
    def from_dict(d):
        return Song(
            d.get('title', ''),
            d.get('path', ''),
            d.get('source', 'local'),
            d.get('thumb'),
            d.get('is_video', False),
            d.get('favorite', False),
            d.get('folder', 'Geral'),
            d.get('premium_only')
        )


def load_settings():
    defaults = {
        'parent_pin': '',
        'child_mode': False,
        'allowed_youtube': [],
        'premium': False,
        'premium_serial': '',
        'premium_plan': '',
        'premium_expires_at': '',
        'premium_server_offset': 0,
        'youtube_download_count': 0,
        'theme': 'neonwave',
        'ad_enabled': True,
        'ad_text': 'Desfrute do NeonWave sem anúncios com o Premium.',
        'ad_image_url': '',
        'ad_link_url': ''
    }
    try:
        data = json.loads(Path(SETTINGS_FILE).read_text(encoding='utf-8'))
        defaults.update(data)
    except (OSError, ValueError, TypeError):
        pass
    if isinstance(defaults.get('premium'), str):
        defaults['premium'] = defaults['premium'].strip().lower() in {
            '1', 'true', 'sim', 'yes', 'on'
        }
    return defaults


def save_settings(settings):
    Path(SETTINGS_FILE).write_text(
        json.dumps(settings, ensure_ascii=False, indent=2), encoding='utf-8'
    )


def load_playlists():
    try:
        data = json.loads(Path(PLAYLISTS_FILE).read_text(encoding='utf-8'))
    except (OSError, ValueError, TypeError):
        return {}

    if not isinstance(data, dict):
        return {}

    playlists = {}
    for name, items in data.items():
        name = str(name or '').strip()[:80]
        if not name or not isinstance(items, list):
            continue
        playlists[name] = [
            str(item) for item in items
            if str(item or '').strip()
        ]
    return playlists


def save_playlists(playlists):
    Path(PLAYLISTS_FILE).write_text(
        json.dumps(playlists, ensure_ascii=False, indent=2), encoding='utf-8'
    )


class AdConfigWorker(QThread):
    loaded = pyqtSignal(dict, object)

    def run(self):
        try:
            response = requests.get(
                AD_CONFIG_URL,
                headers={
                    **PLAYER_HTTP_HEADERS,
                    'Cache-Control': 'no-cache',
                    'Pragma': 'no-cache'
                },
                timeout=8
            )
            response.raise_for_status()
            config = response.json()
            if not isinstance(config, dict):
                return

            image_data = b''
            image_url = str(config.get('image_url') or '').strip()
            if image_url.startswith(('http://', 'https://')):
                try:
                    if image_url.startswith('http://'):
                        image_url = 'https://' + image_url[7:]
                    image_response = requests.get(
                        image_url,
                        headers={
                            **PLAYER_HTTP_HEADERS,
                            'Accept': 'image/*'
                        },
                        timeout=15
                    )
                    image_response.raise_for_status()
                    image_data = image_response.content
                except requests.RequestException:
                    image_data = b''
            self.loaded.emit(config, image_data)
        except (requests.RequestException, ValueError, TypeError):
            pass


def _version_tuple(value):
    numbers = re.findall(r'\d+', str(value or '0'))
    return tuple(int(item) for item in numbers[:4]) or (0,)


class UpdateWorker(QThread):
    checked = pyqtSignal(bool, str, object)
    updated = pyqtSignal(str)
    failed = pyqtSignal(str)

    def __init__(self, install=False, manifest=None, parent=None):
        super().__init__(parent)
        self.install = bool(install)
        self.manifest = manifest if isinstance(manifest, dict) else None

    def run(self):
        try:
            manifest = self.manifest
            if manifest is None:
                response = requests.get(
                    UPDATE_MANIFEST_URL,
                    headers={
                        **PLAYER_HTTP_HEADERS,
                        'Cache-Control': 'no-cache',
                        'Pragma': 'no-cache'
                    },
                    timeout=10
                )
                response.raise_for_status()
                manifest = response.json()
            if not isinstance(manifest, dict):
                raise ValueError("O arquivo de atualização não possui um formato válido.")

            latest = str(manifest.get('version') or '').strip()
            if not latest:
                raise ValueError("O arquivo de atualização não informou a versão.")

            if not self.install:
                if _version_tuple(latest) <= _version_tuple(APP_VERSION):
                    self.checked.emit(
                        False,
                        f"O programa já está atualizado na versão {APP_VERSION}.",
                        manifest
                    )
                else:
                    self.checked.emit(
                        True,
                        f"Nova versão disponível: {latest}.",
                        manifest
                    )
                return

            if _version_tuple(latest) <= _version_tuple(APP_VERSION):
                self.updated.emit(
                    f"O programa já está na versão {APP_VERSION}."
                )
                return

            download_url = str(
                manifest.get('download_url') or UPDATE_PROGRAM_URL
            ).strip()
            if not download_url.startswith(('http://', 'https://')):
                raise ValueError("O endereço da atualização é inválido.")

            response = requests.get(
                download_url,
                headers={
                    **PLAYER_HTTP_HEADERS,
                    'Accept': 'text/x-python, text/plain, */*'
                },
                timeout=30
            )
            response.raise_for_status()
            content = response.content
            if not content:
                raise ValueError("O arquivo de atualização está vazio.")

            decoded = content.decode('utf-8')
            compile(decoded, str(Path(__file__).name), 'exec')
            program_path = Path(__file__).resolve()
            temporary_path = program_path.with_suffix(
                program_path.suffix + '.update'
            )
            backup_path = program_path.with_suffix(
                program_path.suffix + '.backup'
            )
            temporary_path.write_text(decoded, encoding='utf-8')
            shutil.copy2(program_path, backup_path)
            os.replace(temporary_path, program_path)
            self.updated.emit(
                f"Versão {latest} instalada. Feche e abra o programa novamente."
            )
        except (
            OSError, UnicodeError, ValueError, TypeError,
            requests.RequestException
        ) as exc:
            self.failed.emit(str(exc))


class ParentDialog(QDialog):
    def __init__(self, settings, parent=None):
        super().__init__(parent)
        self.setWindowFlag(Qt.WindowContextHelpButtonHint, False)
        self.settings = settings
        self.setWindowTitle("Controle dos pais")
        self.setFixedWidth(460)
        self._build()

    def _build(self):
        outer = QVBoxLayout(self)
        outer.setContentsMargins(12, 12, 12, 12)
        card = QFrame()
        card.setObjectName("ParentCard")
        card.setStyleSheet(
            f"QFrame#ParentCard {{ background: {BG2}; "
            "border: none; border-radius: 16px; }}"
        )
        lay = QVBoxLayout(card)
        lay.setContentsMargins(28, 26, 28, 26)
        lay.setSpacing(14)

        title = QLabel("Controle dos pais")
        title.setStyleSheet(f"color: {TEXT}; font-size: 18px; font-weight: 700;")
        lay.addWidget(title)
        hint = QLabel("Defina um PIN para gerenciar o modo infantil e os conteúdos permitidos.")
        hint.setWordWrap(True)
        hint.setStyleSheet(f"color: {TEXT2}; font-size: 12px;")
        lay.addWidget(hint)

        pin_row = QHBoxLayout()
        pin_row.addWidget(QLabel("PIN dos pais"))
        self.pin_input = QLineEdit()
        self.pin_input.setPlaceholderText("4 ou mais números")
        self.pin_input.setEchoMode(QLineEdit.Password)
        self.pin_input.setText(self.settings.get('parent_pin', ''))
        pin_row.addWidget(self.pin_input)
        lay.addLayout(pin_row)

        self.child_check = QCheckBox("Ativar modo infantil")
        self.child_check.setChecked(bool(self.settings.get('child_mode')))
        lay.addWidget(self.child_check)

        allow_row = QVBoxLayout()
        allow_row.addWidget(QLabel("Vídeos permitidos no modo infantil (um título ou URL por linha)"))
        self.allowed_input = QLineEdit(", ".join(self.settings.get('allowed_youtube', [])))
        self.allowed_input.setPlaceholderText("Separe títulos ou URLs por vírgula")
        allow_row.addWidget(self.allowed_input)
        lay.addLayout(allow_row)

        buttons = QHBoxLayout()
        buttons.addStretch()
        cancel = QPushButton("Cancelar")
        cancel.setObjectName("CancelBtn")
        cancel.clicked.connect(self.reject)
        save = QPushButton("Salvar")
        save.setObjectName("DialogBtn")
        save.clicked.connect(self._save)
        buttons.addWidget(cancel)
        buttons.addWidget(save)
        lay.addLayout(buttons)
        outer.addWidget(card)

    def _save(self):
        pin = self.pin_input.text().strip()
        if pin and (len(pin) < 4 or not pin.isdigit()):
            QMessageBox.warning(self, "PIN inválido", "Use pelo menos 4 números.")
            return
        if not pin and self.child_check.isChecked():
            QMessageBox.warning(self, "PIN necessário", "Defina um PIN antes de ativar o modo infantil.")
            return
        self.settings.update({
            'parent_pin': pin,
            'child_mode': self.child_check.isChecked(),
            'allowed_youtube': [item.strip() for item in self.allowed_input.text().split(',') if item.strip()]
        })
        save_settings(self.settings)
        self.accept()


class SettingsDialog(QDialog):
    controls_requested = pyqtSignal()
    check_update_requested = pyqtSignal()
    install_update_requested = pyqtSignal()

    def __init__(self, parent=None):
        super().__init__(parent)
        self.setWindowFlag(Qt.WindowContextHelpButtonHint, False)
        self.setWindowTitle("Configuração")
        self.setFixedWidth(460)
        self.setMinimumHeight(0)
        self._owner = parent
        self._settings = getattr(parent, 'settings', {})
        self._is_premium = bool(self._settings.get('premium'))
        self._build()
        self.adjustSize()

    def _build(self):
        outer = QVBoxLayout(self)
        outer.setContentsMargins(10, 10, 10, 10)

        card = QFrame()
        card.setObjectName("SettingsCard")
        card.setStyleSheet(
            f"QFrame#SettingsCard {{ background: {BG2}; "
            "border: none; border-radius: 16px; }}"
        )
        lay = QVBoxLayout(card)
        lay.setContentsMargins(22, 18, 22, 18)
        lay.setSpacing(10)

        title = QLabel("Configuração")
        title.setStyleSheet(f"color: {TEXT}; font-size: 18px; font-weight: 700;")
        lay.addWidget(title)

        hint = QLabel("Ajuste os recursos de segurança e as preferências do programa.")
        hint.setWordWrap(True)
        hint.setStyleSheet(f"color: {TEXT2}; font-size: 12px;")
        lay.addWidget(hint)

        appearance_section = QLabel("APARÊNCIA")
        appearance_section.setStyleSheet(
            f"color: {TEXT3}; font-size: 10px; font-weight: 700; "
            "letter-spacing: 2px;"
        )
        lay.addWidget(appearance_section)

        theme_option = QFrame()
        theme_option.setObjectName("SettingsOption")
        theme_option.setStyleSheet(
            f"QFrame#SettingsOption {{ background: {BG3}; "
            "border: none; border-radius: 12px; }}"
        )
        theme_lay = QVBoxLayout(theme_option)
        theme_lay.setContentsMargins(12, 9, 12, 9)
        theme_lay.setSpacing(5)

        theme_top = QHBoxLayout()
        theme_icon = QLabel("✦")
        theme_icon.setAlignment(Qt.AlignCenter)
        theme_icon.setFixedWidth(28)
        theme_icon.setStyleSheet(f"color: {ACCENT}; font-size: 20px;")
        theme_top.addWidget(theme_icon)

        theme_title = QLabel("Tema da interface")
        theme_title.setStyleSheet(
            f"color: {TEXT}; font-size: 13px; font-weight: 700;"
        )
        theme_top.addWidget(theme_title, 1)

        self.theme_combo = QComboBox()
        self.theme_combo.setMinimumWidth(200)
        selected_theme = _theme_for_account(
            self._settings.get('theme', 'neonwave'),
            self._is_premium
        )
        for key, theme in THEMES.items():
            suffix = "  •  Premium" if theme['premium'] else "  •  Gratuito"
            self.theme_combo.addItem(theme['label'] + suffix, key)
            index = self.theme_combo.count() - 1
            if theme['premium'] and not self._is_premium:
                item = self.theme_combo.model().item(index)
                if item is not None:
                    item.setEnabled(False)
                    item.setToolTip("Disponível somente na conta Premium.")
            if key == selected_theme:
                self.theme_combo.setCurrentIndex(index)
        theme_top.addWidget(self.theme_combo)
        theme_lay.addLayout(theme_top)

        self.theme_hint = QLabel(
            "Escolha uma identidade visual. Os temas Premium ficam disponíveis "
            "quando a assinatura estiver ativa."
        )
        self.theme_hint.setWordWrap(True)
        self.theme_hint.setStyleSheet(f"color: {TEXT2}; font-size: 11px;")
        theme_lay.addWidget(self.theme_hint)
        self.theme_combo.currentIndexChanged.connect(self._save_theme)
        lay.addWidget(theme_option)

        section = QLabel("SEGURANÇA")
        section.setStyleSheet(
            f"color: {TEXT3}; font-size: 10px; font-weight: 700; "
            "letter-spacing: 2px;"
        )
        lay.addWidget(section)

        option = QFrame()
        option.setObjectName("SettingsOption")
        option.setStyleSheet(
            f"QFrame#SettingsOption {{ background: {BG3}; "
            "border: none; border-radius: 12px; }}"
        )
        option_lay = QHBoxLayout(option)
        option_lay.setContentsMargins(12, 9, 10, 9)
        option_lay.setSpacing(10)

        icon = QLabel("⚙")
        icon.setAlignment(Qt.AlignCenter)
        icon.setFixedWidth(28)
        icon.setStyleSheet(f"color: {ACCENT2}; font-size: 20px;")
        option_lay.addWidget(icon)

        option_text = QVBoxLayout()
        option_text.setSpacing(2)
        option_title = QLabel("Controle dos pais")
        option_title.setStyleSheet(
            f"color: {TEXT}; font-size: 13px; font-weight: 700;"
        )
        option_description = QLabel(
            "PIN, modo infantil e conteúdos permitidos."
        )
        option_description.setWordWrap(True)
        option_description.setStyleSheet(f"color: {TEXT2}; font-size: 11px;")
        option_text.addWidget(option_title)
        option_text.addWidget(option_description)
        option_lay.addLayout(option_text, 1)

        parent_btn = QPushButton("Abrir")
        parent_btn.setObjectName("DialogBtn")
        parent_btn.setMinimumHeight(34)
        parent_btn.setMinimumWidth(82)
        parent_btn.clicked.connect(self._request_parent_controls)
        option_lay.addWidget(parent_btn)
        lay.addWidget(option)

        update_section = QLabel("PROGRAMA")
        update_section.setStyleSheet(
            f"color: {TEXT3}; font-size: 10px; font-weight: 700; "
            "letter-spacing: 2px;"
        )
        lay.addWidget(update_section)

        update_option = QFrame()
        update_option.setObjectName("SettingsOption")
        update_option.setStyleSheet(
            f"QFrame#SettingsOption {{ background: {BG3}; "
            "border: none; border-radius: 12px; }}"
        )
        update_lay = QHBoxLayout(update_option)
        update_lay.setContentsMargins(12, 9, 10, 9)
        update_lay.setSpacing(8)

        update_text = QVBoxLayout()
        update_text.setSpacing(2)
        update_title = QLabel("Atualizações")
        update_title.setStyleSheet(
            f"color: {TEXT}; font-size: 13px; font-weight: 700;"
        )
        update_description = QLabel(
            f"Versão instalada: {APP_VERSION}. Consulte o servidor oficial."
        )
        update_description.setWordWrap(True)
        update_description.setStyleSheet(f"color: {TEXT2}; font-size: 11px;")
        update_text.addWidget(update_title)
        update_text.addWidget(update_description)
        update_lay.addLayout(update_text, 1)

        check_update_btn = QPushButton("Verificar")
        check_update_btn.setObjectName("DialogBtn")
        check_update_btn.setMinimumHeight(34)
        check_update_btn.clicked.connect(self.check_update_requested.emit)
        update_lay.addWidget(check_update_btn)

        install_update_btn = QPushButton("Atualizar")
        install_update_btn.setObjectName("DialogBtn")
        install_update_btn.setMinimumHeight(34)
        install_update_btn.clicked.connect(self.install_update_requested.emit)
        update_lay.addWidget(install_update_btn)
        lay.addWidget(update_option)

        buttons = QHBoxLayout()
        buttons.addStretch()
        about_btn = QPushButton("Sobre")
        about_btn.setObjectName("CancelBtn")
        about_btn.setMinimumHeight(34)
        about_btn.setMinimumWidth(70)
        about_btn.clicked.connect(self._show_about)
        buttons.addWidget(about_btn)
        close_btn = QPushButton("Fechar")
        close_btn.setObjectName("CancelBtn")
        close_btn.clicked.connect(self.reject)
        buttons.addWidget(close_btn)
        lay.addLayout(buttons)

        outer.addWidget(card)

    def _show_about(self):
        self._about_dialog = AboutDialog(self)
        self._about_dialog.exec_()
        self._about_dialog = None

    def _save_theme(self, index):
        theme_key = self.theme_combo.itemData(index)
        if not theme_key:
            return
        theme = THEMES.get(str(theme_key))
        if theme is None or (theme['premium'] and not self._is_premium):
            return
        self._settings['theme'] = str(theme_key)
        save_settings(self._settings)
        if self._owner is not None and hasattr(self._owner, '_apply_theme_live'):
            self._owner._apply_theme_live(str(theme_key))
            self.theme_hint.setText(
                f"{theme['description']}. Tema aplicado agora."
            )
        else:
            self.theme_hint.setText(
                f"{theme['description']}. O tema será aplicado na próxima abertura."
            )

    def _request_parent_controls(self):
        self.accept()
        self.controls_requested.emit()


class AboutBackdrop(QWidget):
    def paintEvent(self, event):
        painter = QPainter(self)
        painter.setRenderHint(QPainter.Antialiasing)

        area = self.rect().adjusted(1, 1, -1, -1)
        gradient = QLinearGradient(area.topLeft(), area.bottomRight())
        gradient.setColorAt(0.0, QColor(BG2))
        gradient.setColorAt(0.52, QColor(BG3))
        gradient.setColorAt(1.0, QColor(BG))
        painter.setBrush(gradient)
        painter.setPen(Qt.NoPen)
        painter.drawRoundedRect(area, 24, 24)

        clip_path = QPainterPath()
        clip_path.addRoundedRect(
            float(area.x()), float(area.y()),
            float(area.width()), float(area.height()),
            24.0, 24.0
        )
        painter.save()
        painter.setClipPath(clip_path)

        glow = QColor(ACCENT)
        glow.setAlpha(48)
        painter.setPen(Qt.NoPen)
        painter.setBrush(glow)
        painter.drawEllipse(-95, -85, 270, 270)

        glow = QColor(ACCENT2)
        glow.setAlpha(42)
        painter.setBrush(glow)
        painter.drawEllipse(self.width() - 175, self.height() - 175, 260, 260)

        ring_pen = QPen(QColor(ACCENT3))
        ring_pen.setWidth(1)
        ring_pen.setColor(QColor(ACCENT3))
        ring_color = QColor(ACCENT3)
        ring_color.setAlpha(38)
        ring_pen.setColor(ring_color)
        painter.setPen(ring_pen)
        painter.setBrush(Qt.NoBrush)
        painter.drawEllipse(self.width() - 155, 28, 105, 105)
        painter.drawEllipse(self.width() - 133, 50, 61, 61)

        wave_pen = QPen(QColor(ACCENT))
        wave_pen.setWidth(2)
        wave_color = QColor(ACCENT)
        wave_color.setAlpha(95)
        wave_pen.setColor(wave_color)
        painter.setPen(wave_pen)
        points = [
            QPoint(28, self.height() - 78),
            QPoint(75, self.height() - 96),
            QPoint(122, self.height() - 68),
            QPoint(169, self.height() - 88),
            QPoint(216, self.height() - 58),
            QPoint(263, self.height() - 76),
            QPoint(310, self.height() - 52),
            QPoint(357, self.height() - 68),
            QPoint(404, self.height() - 46),
            QPoint(451, self.height() - 63),
            QPoint(498, self.height() - 42),
        ]
        for start, end in zip(points, points[1:]):
            painter.drawLine(start, end)
        painter.restore()

        painter.setBrush(Qt.NoBrush)
        painter.setPen(QPen(QColor(ACCENT), 1))
        painter.drawRoundedRect(area, 24, 24)
        painter.end()


class AboutDialog(QDialog):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setWindowFlag(Qt.WindowContextHelpButtonHint, False)
        self.setWindowTitle("Sobre o NeonWave Player")
        self.setWindowFlags(Qt.Dialog | Qt.FramelessWindowHint)
        self.setAttribute(Qt.WA_TranslucentBackground, True)
        self.setFixedSize(560, 500)
        self.setStyleSheet("QDialog { background: transparent; border: none; }")
        self._build()

    def _build(self):
        outer = QVBoxLayout(self)
        outer.setContentsMargins(10, 10, 10, 10)

        backdrop = AboutBackdrop()
        backdrop.setObjectName("AboutBackdrop")
        backdrop.setAttribute(Qt.WA_TranslucentBackground, True)
        backdrop.setStyleSheet("QWidget#AboutBackdrop { background: transparent; border: none; }")
        lay = QVBoxLayout(backdrop)
        lay.setContentsMargins(30, 26, 30, 26)
        lay.setSpacing(12)

        brand = QLabel("✦  NEONWAVE")
        brand.setStyleSheet(
            f"color: {ACCENT}; font-size: 11px; font-weight: 900; "
            "letter-spacing: 3px;"
        )
        lay.addWidget(brand)

        title = QLabel("Sobre o NeonWave Player")
        title.setStyleSheet(
            f"color: {TEXT}; font-size: 23px; font-weight: 800;"
        )
        lay.addWidget(title)

        creator = QLabel(
            "Criado por mim, com dedicação, criatividade e muitas horas de trabalho."
        )
        creator.setObjectName("AboutCreator")
        creator.setWordWrap(True)
        creator.setStyleSheet(
            f"QLabel#AboutCreator {{ color: {TEXT}; font-size: 13px; "
            f"font-weight: 700; background: {BG2}; "
            f"border: 1px solid {ACCENT}; border-radius: 10px; "
            "padding: 10px 12px; }}"
        )
        lay.addWidget(creator)

        message = QLabel(
            "O NeonWave Player nasceu para tornar seus momentos com música "
            "mais simples, bonitos e especiais. Cada detalhe foi pensado "
            "para oferecer uma experiência agradável, leve e feita com "
            "carinho para quem usa."
        )
        message.setWordWrap(True)
        message.setStyleSheet(f"color: {TEXT}; font-size: 13px;")
        lay.addWidget(message)

        thanks = QLabel(
            "Obrigado por confiar no meu trabalho e fazer parte dessa história. "
            "Espero que o programa acompanhe muitos momentos bons da sua vida."
        )
        thanks.setWordWrap(True)
        thanks.setStyleSheet(f"color: {TEXT}; font-size: 13px;")
        lay.addWidget(thanks)

        signature = QLabel("Com carinho,\nCauê — CMC")
        signature.setStyleSheet(
            f"color: {ACCENT}; font-size: 14px; font-weight: 800; "
            "padding-top: 4px;"
        )
        lay.addWidget(signature)

        buttons = QHBoxLayout()
        buttons.addStretch()
        close_btn = QPushButton("Fechar")
        close_btn.setObjectName("CancelBtn")
        close_btn.clicked.connect(self.accept)
        buttons.addWidget(close_btn)
        lay.addLayout(buttons)

        outer.addWidget(backdrop)

    def showEvent(self, event):
        super().showEvent(event)
        parent = self.parentWidget()
        if parent is not None:
            self.move(parent.window().frameGeometry().center() - self.rect().center())


class UpdateNoticeDialog(QDialog):
    def __init__(
        self, title, message, kind='info', question=False, parent=None
    ):
        super().__init__(parent)
        self.setWindowFlag(Qt.WindowContextHelpButtonHint, False)
        self.setWindowTitle(title)
        self.setFixedWidth(500)
        self.setModal(True)
        self._question = bool(question)
        self._kind = str(kind or 'info')
        self._build(title, message)

    def _build(self, title, message):
        if self._kind == 'success':
            color, symbol = LIME, "✓"
        elif self._kind == 'warning':
            color, symbol = RED, "!"
        else:
            color, symbol = ACCENT2, "i"

        outer = QVBoxLayout(self)
        outer.setContentsMargins(10, 10, 10, 10)

        card = QFrame()
        card.setObjectName("UpdateNoticeCard")
        card.setStyleSheet(
            f"QFrame#UpdateNoticeCard {{ background: {BG2}; "
            f"border: 1px solid {color}66; border-radius: 16px; }}"
        )
        lay = QVBoxLayout(card)
        lay.setContentsMargins(24, 22, 24, 20)
        lay.setSpacing(16)

        header = QHBoxLayout()
        header.setSpacing(12)

        icon = QLabel(symbol)
        icon.setAlignment(Qt.AlignCenter)
        icon.setFixedSize(40, 40)
        icon.setStyleSheet(
            f"background: {color}22; color: {color}; "
            f"border: 1px solid {color}88; border-radius: 20px; "
            "font-size: 20px; font-weight: 800;"
        )
        header.addWidget(icon)

        heading = QLabel(title)
        heading.setStyleSheet(
            f"color: {TEXT}; font-size: 16px; font-weight: 800;"
        )
        header.addWidget(heading, 1)
        lay.addLayout(header)

        body = QLabel(str(message))
        body.setWordWrap(True)
        body.setTextFormat(Qt.PlainText)
        body.setStyleSheet(f"color: {TEXT2}; font-size: 13px;")
        lay.addWidget(body)

        buttons = QHBoxLayout()
        buttons.addStretch()

        if self._question:
            no_btn = QPushButton("Agora não")
            no_btn.setObjectName("CancelBtn")
            no_btn.clicked.connect(self.reject)
            buttons.addWidget(no_btn)

            yes_btn = QPushButton("Baixar e instalar")
            yes_btn.setObjectName("DialogBtn")
            yes_btn.clicked.connect(self.accept)
            buttons.addWidget(yes_btn)
        else:
            ok_btn = QPushButton("OK")
            ok_btn.setObjectName("DialogBtn")
            ok_btn.clicked.connect(self.accept)
            buttons.addWidget(ok_btn)

        lay.addLayout(buttons)
        outer.addWidget(card)


class PremiumActivationBackdrop(QWidget):
    def paintEvent(self, event):
        painter = QPainter(self)
        painter.setRenderHint(QPainter.Antialiasing)

        area = self.rect().adjusted(1, 1, -1, -1)
        gradient = QLinearGradient(area.topLeft(), area.bottomRight())
        gradient.setColorAt(0.0, QColor("#17152D"))
        gradient.setColorAt(0.48, QColor(BG2))
        gradient.setColorAt(1.0, QColor("#101321"))
        painter.setBrush(gradient)
        painter.setPen(Qt.NoPen)
        painter.drawRoundedRect(area, 24, 24)

        clip_path = QPainterPath()
        clip_path.addRoundedRect(
            float(area.x()), float(area.y()),
            float(area.width()), float(area.height()),
            24.0, 24.0
        )
        painter.save()
        painter.setClipPath(clip_path)

        glow = QColor(ACCENT)
        glow.setAlpha(46)
        painter.setBrush(glow)
        painter.drawEllipse(-110, -105, 300, 300)

        glow = QColor(ACCENT2)
        glow.setAlpha(42)
        painter.setBrush(glow)
        painter.drawEllipse(self.width() - 185, self.height() - 170, 300, 300)

        line_color = QColor(ACCENT2)
        line_color.setAlpha(34)
        painter.setPen(QPen(line_color, 1))
        for offset in range(-self.height(), self.width(), 34):
            painter.drawLine(offset, self.height(), offset + self.height(), 0)

        ring_color = QColor(ACCENT3)
        ring_color.setAlpha(32)
        painter.setPen(QPen(ring_color, 1))
        painter.setBrush(Qt.NoBrush)
        painter.drawEllipse(self.width() - 150, 34, 96, 96)
        painter.drawEllipse(self.width() - 126, 58, 48, 48)

        painter.setPen(Qt.NoPen)
        for x, y, size in ((72, 52, 3), (118, 82, 2), (430, 72, 3), (470, 112, 2)):
            dot = QColor(ACCENT3)
            dot.setAlpha(110)
            painter.setBrush(dot)
            painter.drawEllipse(x, y, size, size)
        painter.restore()

        border = QColor(ACCENT2)
        border.setAlpha(150)
        painter.setBrush(Qt.NoBrush)
        painter.setPen(QPen(border, 1))
        painter.drawRoundedRect(area, 24, 24)
        painter.end()


class PremiumActivationDialog(QDialog):
    def __init__(self, current_serial='', parent=None):
        super().__init__(parent)
        self.setWindowTitle("Ativar Premium")
        self.setWindowFlags(Qt.Dialog | Qt.FramelessWindowHint)
        self.setAttribute(Qt.WA_TranslucentBackground, True)
        self.setFixedSize(560, 430)
        self.setModal(True)
        self.setStyleSheet("QDialog { background: transparent; border: none; }")
        self._build(current_serial)

    def _build(self, current_serial):
        outer = QVBoxLayout(self)
        outer.setContentsMargins(10, 10, 10, 10)

        backdrop = PremiumActivationBackdrop()
        backdrop.setObjectName("PremiumActivationBackdrop")
        backdrop.setAttribute(Qt.WA_TranslucentBackground, True)
        backdrop.setStyleSheet(
            "QWidget#PremiumActivationBackdrop { "
            "background: transparent; border: none; }"
        )
        lay = QVBoxLayout(backdrop)
        lay.setContentsMargins(32, 26, 32, 26)
        lay.setSpacing(10)

        header = QHBoxLayout()
        header.setSpacing(10)

        icon = QLabel()
        icon.setPixmap(_make_menu_icon('premium', ACCENT).pixmap(QSize(28, 28)))
        icon.setFixedSize(32, 32)
        icon.setAlignment(Qt.AlignCenter)
        icon.setStyleSheet("background: transparent; border: none;")
        header.addWidget(icon)

        heading = QVBoxLayout()
        heading.setSpacing(1)
        eyebrow = QLabel("NEONWAVE  /  PREMIUM")
        eyebrow.setStyleSheet(
            f"color: {ACCENT}; background: transparent; "
            "font-size: 10px; font-weight: 800; letter-spacing: 2px;"
        )
        heading.addWidget(eyebrow)
        heading_title = QLabel("Ative sua experiência")
        heading_title.setStyleSheet(
            f"color: {TEXT}; background: transparent; "
            "font-size: 18px; font-weight: 800;"
        )
        heading.addWidget(heading_title)
        header.addLayout(heading, 1)

        close = QPushButton("×")
        close.setObjectName("PremiumCloseBtn")
        close.setFixedSize(30, 30)
        close.setToolTip("Fechar")
        close.clicked.connect(self.reject)
        close.setStyleSheet(
            f"QPushButton#PremiumCloseBtn {{ background: transparent; "
            f"color: {TEXT3}; border: none; font-size: 22px; }}"
            f"QPushButton#PremiumCloseBtn:hover {{ color: {TEXT}; "
            f"background: {BG3}; border-radius: 15px; }}"
        )
        header.addWidget(close, 0, Qt.AlignTop)
        lay.addLayout(header)

        subtitle = QLabel(
            "Digite o serial da sua licença para liberar as funções Premium."
        )
        subtitle.setWordWrap(True)
        subtitle.setStyleSheet(
            f"color: {TEXT2}; background: transparent; font-size: 12px;"
        )
        lay.addWidget(subtitle)

        self.serial_input = QLineEdit()
        self.serial_input.setText(current_serial)
        self.serial_input.setPlaceholderText("Ex.: NW-XXXX-XXXX-XXXX")
        self.serial_input.setClearButtonEnabled(True)
        self.serial_input.setMinimumHeight(48)
        self.serial_input.setStyleSheet(f"""
            QLineEdit {{
                background: {BG3}CC;
                border: 1px solid {ACCENT}99;
                border-radius: 10px;
                padding: 0 14px;
                color: {TEXT};
                font-size: 14px;
                selection-background-color: {ACCENT};
            }}
            QLineEdit:focus {{
                border: 2px solid {ACCENT};
                background: {BG3};
            }}
        """)
        self.serial_input.returnPressed.connect(self.accept)
        lay.addWidget(self.serial_input)

        hint = QLabel("A licença será validada com o servidor.")
        hint.setStyleSheet(
            f"color: {TEXT3}; background: transparent; font-size: 11px;"
        )
        lay.addWidget(hint)

        line = QFrame()
        line.setFixedHeight(1)
        line.setStyleSheet(f"background: {BORDER}; border: none;")
        lay.addWidget(line)

        buttons = QHBoxLayout()
        buttons.setSpacing(10)
        buttons.addStretch()
        cancel = QPushButton("Cancelar")
        cancel.setObjectName("CancelBtn")
        cancel.clicked.connect(self.reject)
        activate = QPushButton("Ativar Premium")
        activate.setObjectName("DialogBtn")
        activate.clicked.connect(self.accept)
        buttons.addWidget(cancel)
        buttons.addWidget(activate)
        lay.addLayout(buttons)

        outer.addWidget(backdrop)
        self.serial_input.setFocus()

    def showEvent(self, event):
        super().showEvent(event)
        parent = self.parentWidget()
        if parent is not None:
            self.move(parent.window().frameGeometry().center() - self.rect().center())

    def serial(self):
        return self.serial_input.text().strip()


class PremiumSuccessDialog(QDialog):
    def __init__(self, message, parent=None):
        super().__init__(parent)
        self.setWindowTitle("Premium ativado")
        self.setWindowFlags(Qt.Dialog | Qt.FramelessWindowHint)
        self.setAttribute(Qt.WA_TranslucentBackground, True)
        self.setFixedSize(520, 300)
        self.setModal(True)
        self.setStyleSheet("QDialog { background: transparent; border: none; }")

        outer = QVBoxLayout(self)
        outer.setContentsMargins(10, 10, 10, 10)

        backdrop = PremiumActivationBackdrop()
        backdrop.setObjectName("PremiumSuccessBackdrop")
        backdrop.setAttribute(Qt.WA_TranslucentBackground, True)
        backdrop.setStyleSheet(
            "QWidget#PremiumSuccessBackdrop { "
            "background: transparent; border: none; }"
        )
        lay = QVBoxLayout(backdrop)
        lay.setContentsMargins(30, 26, 30, 24)
        lay.setSpacing(9)

        header = QHBoxLayout()
        header.setSpacing(12)

        badge = QLabel("✓")
        badge.setFixedSize(42, 42)
        badge.setAlignment(Qt.AlignCenter)
        badge.setStyleSheet(
            f"background: {LIME}22; color: {LIME}; "
            f"border: 1px solid {LIME}88; border-radius: 21px; "
            "font-size: 22px; font-weight: 900;"
        )
        header.addWidget(badge)

        heading = QVBoxLayout()
        heading.setSpacing(1)
        eyebrow = QLabel("STATUS DA CONTA")
        eyebrow.setStyleSheet(
            f"color: {LIME}; background: transparent; "
            "font-size: 10px; font-weight: 800; letter-spacing: 2px;"
        )
        heading.addWidget(eyebrow)
        title = QLabel("Premium ativado")
        title.setStyleSheet(
            f"color: {TEXT}; background: transparent; "
            "font-size: 20px; font-weight: 800;"
        )
        heading.addWidget(title)
        header.addLayout(heading, 1)
        lay.addLayout(header)

        body = QLabel(str(message))
        body.setTextFormat(Qt.PlainText)
        body.setWordWrap(True)
        body.setStyleSheet(
            f"color: {TEXT2}; background: transparent; font-size: 13px;"
        )
        lay.addWidget(body)

        hint = QLabel("Todos os recursos Premium já estão disponíveis.")
        hint.setStyleSheet(
            f"color: {TEXT3}; background: transparent; font-size: 11px;"
        )
        lay.addWidget(hint)

        line = QFrame()
        line.setFixedHeight(1)
        line.setStyleSheet(f"background: {BORDER}; border: none;")
        lay.addWidget(line)

        buttons = QHBoxLayout()
        buttons.addStretch()
        close = QPushButton("Continuar")
        close.setObjectName("DialogBtn")
        close.clicked.connect(self.accept)
        close.setDefault(True)
        buttons.addWidget(close)
        lay.addLayout(buttons)

        outer.addWidget(backdrop)
        close.setFocus()

    def showEvent(self, event):
        super().showEvent(event)
        parent = self.parentWidget()
        if parent is not None:
            self.move(parent.window().frameGeometry().center() - self.rect().center())


class AccountDialog(QDialog):
    activation_requested = pyqtSignal()

    def __init__(self, settings, free_limit, free_used, parent=None):
        super().__init__(parent)
        self.settings = settings
        self.free_limit = int(free_limit)
        self.free_used = int(free_used)
        self.setWindowFlag(Qt.WindowContextHelpButtonHint, False)
        self.setWindowTitle("Minha conta")
        self.setFixedWidth(500)
        self._build()

    def _build(self):
        outer = QVBoxLayout(self)
        outer.setContentsMargins(12, 12, 12, 12)
        card = QFrame()
        card.setObjectName("AccountCard")
        card.setStyleSheet(
            f"QFrame#AccountCard {{ background: {BG2}; "
            f"border: 1px solid {BORDER}; border-radius: 16px; }}"
        )
        lay = QVBoxLayout(card)
        lay.setContentsMargins(26, 24, 26, 24)
        lay.setSpacing(12)

        title = QLabel("Minha conta")
        title.setStyleSheet(f"color: {TEXT}; font-size: 19px; font-weight: 800;")
        lay.addWidget(title)

        premium = bool(self.settings.get('premium'))
        status = QLabel("✓  PREMIUM ATIVO" if premium else "CONTA GRATUITA")
        status.setStyleSheet(
            f"color: {LIME if premium else ACCENT}; font-size: 12px; "
            "font-weight: 800; letter-spacing: 1px;"
        )
        lay.addWidget(status)

        plan = self.settings.get('premium_plan') or (
            "Plano gratuito" if not premium else "Premium"
        )
        expires = self.settings.get('premium_expires_at')
        if premium and expires:
            try:
                expires_text = time.strftime(
                    "%d/%m/%Y %H:%M",
                    time.localtime(float(expires))
                )
            except (TypeError, ValueError, OverflowError):
                expires_text = "Não informado"
        elif premium:
            expires_text = "Sem data de expiração informada"
        else:
            expires_text = "—"

        serial = str(self.settings.get('premium_serial') or '').strip()
        if len(serial) > 8:
            serial_text = f"{serial[:4]}••••{serial[-4:]}"
        else:
            serial_text = serial or "—"
        quota = (
            "Downloads ilimitados"
            if premium else f"{self.free_used}/{self.free_limit} downloads usados"
        )
        details = QLabel(
            f"<b>Plano:</b> {plan}<br>"
            f"<b>Serial:</b> {serial_text}<br>"
            f"<b>Validade:</b> {expires_text}<br>"
            f"<b>Downloads:</b> {quota}"
        )
        details.setTextFormat(Qt.RichText)
        details.setStyleSheet(f"color: {TEXT2}; font-size: 12px;")
        details.setWordWrap(True)
        lay.addWidget(details)

        buttons = QHBoxLayout()
        buttons.addStretch()
        close = QPushButton("Fechar")
        close.setObjectName("CancelBtn")
        close.clicked.connect(self.reject)
        buttons.addWidget(close)
        if not premium:
            activate = QPushButton("Ativar Premium")
            activate.setObjectName("DialogBtn")
            activate.clicked.connect(self._request_activation)
            buttons.addWidget(activate)
        lay.addLayout(buttons)
        outer.addWidget(card)

    def _request_activation(self):
        self.accept()
        self.activation_requested.emit()


class BrowserDialog(QDialog):
    download_requested = pyqtSignal(str, bool)

    def __init__(self, parent=None):
        super().__init__(parent)
        self.setWindowFlag(Qt.WindowContextHelpButtonHint, False)
        self.setWindowTitle("Navegador YouTube — NeonWave")
        self.resize(1000, 700)
        lay = QVBoxLayout(self)
        bar = QHBoxLayout()
        self.url_input = QLineEdit("https://www.youtube.com")
        self.url_input.returnPressed.connect(self._go)
        go = QPushButton("Acessar")
        go.setObjectName("DialogBtn")
        go.clicked.connect(self._go)
        download_audio = QPushButton("Baixar música")
        download_audio.setObjectName("DialogBtn")
        download_audio.clicked.connect(lambda: self._request_download(False))
        download_video = QPushButton("Baixar clipe")
        download_video.setObjectName("DialogBtn")
        download_video.clicked.connect(lambda: self._request_download(True))
        bar.addWidget(self.url_input, 1)
        bar.addWidget(go)
        bar.addWidget(download_audio)
        bar.addWidget(download_video)
        lay.addLayout(bar)
        if HAS_WEBENGINE:
            self.view = QWebEngineView()
            self.view.urlChanged.connect(lambda url: self.url_input.setText(url.toString()))
            self.view.load(QUrl("https://www.youtube.com"))
        else:
            self.view = QLabel("O navegador interno requer o pacote PyQtWebEngine.")
            self.view.setAlignment(Qt.AlignCenter)
        lay.addWidget(self.view, 1)

    def _go(self):
        if HAS_WEBENGINE:
            value = self.url_input.text().strip()
            if not value.startswith(("http://", "https://")):
                value = "https://" + value
            self.view.load(QUrl(value))

    def _request_download(self, is_video):
        url = self.view.url().toString().strip() if HAS_WEBENGINE else self.url_input.text().strip()
        if not url or url == "https://www.youtube.com/":
            url = self.url_input.text().strip()
        if not url.startswith(("http://", "https://")):
            QMessageBox.warning(self, "Endereço inválido", "Abra uma página válida antes de baixar.")
            return
        if HAS_WEBENGINE:
            # Interrompe o YouTube antes de fechar o navegador interno para
            # não deixar o áudio original tocando junto com o arquivo salvo.
            self.view.page().runJavaScript(
                "document.querySelectorAll('video,audio').forEach(function(m){"
                "m.pause();m.currentTime=0;});"
            )
            self.view.stop()
            self.view.setHtml("")
        self.download_requested.emit(url, is_video)
        self.accept()


# ─────────────────────────────────────────────
#  Worker de Download com progresso real
# ─────────────────────────────────────────────
class DownloadWorker(QThread):
    progress  = pyqtSignal(str, int)   # mensagem, percentagem (0-100, -1=indeterminado)
    finished  = pyqtSignal(str, str, str, bool)
    error     = pyqtSignal(str)

    def __init__(
        self, url, is_video=False, cookie_file="", max_height=720,
        video_format='webm', audio_format='best', audio_quality='0'
    ):
        super().__init__()
        self.url = url
        self.is_video = is_video
        self.cookie_file = cookie_file
        self.max_height = int(max_height or 720)
        self.video_format = str(video_format or 'webm').lower()
        self.audio_format = str(audio_format or 'best').lower()
        self.audio_quality = str(audio_quality or '0')
        self._status_counter = 0

    def _emit_download_status(self, pct=-1):
        if pct >= 0:
            index = min(int(pct / 100 * len(DOWNLOAD_STATUS_MESSAGES)),
                        len(DOWNLOAD_STATUS_MESSAGES) - 1)
            message = DOWNLOAD_STATUS_MESSAGES[index] + f" {pct}%"
        else:
            message = (
                DOWNLOAD_STATUS_MESSAGES[
                    self._status_counter % len(DOWNLOAD_STATUS_MESSAGES)
                ] + "..."
            )
            self._status_counter += 1
        self.progress.emit(message, pct)

    @staticmethod
    def _export_browser_cookies(browser):
        try:
            result = subprocess.run(
                [
                    sys.executable, '-m', 'yt_dlp', '--ignore-config',
                    '--no-update', '--cookies-from-browser', browser,
                    '--cookies', str(AUTO_COOKIES_FILE)
                ],
                capture_output=True, text=True,
                encoding='utf-8', errors='replace', timeout=60
            )
            if result.returncode == 0 and AUTO_COOKIES_FILE.exists():
                header = AUTO_COOKIES_FILE.read_text(
                    encoding='utf-8', errors='replace'
                )[:200]
                if '# Netscape HTTP Cookie File' in header or '# HTTP Cookie File' in header:
                    return True
        except (OSError, subprocess.SubprocessError, UnicodeError):
            pass
        return False

    def run(self):
        try:
            global YTDLP_UPDATE_CHECKED
            if not YTDLP_UPDATE_CHECKED:
                try:
                    subprocess.run(
                        [sys.executable, '-c', 'import yt_dlp; import yt_dlp_ejs'],
                        capture_output=True, timeout=15
                    ).check_returncode()
                except Exception:
                    self.progress.emit("Instalando o yt-dlp...", -1)
                    subprocess.run(
                        [sys.executable, '-m', 'pip', 'install', 'yt-dlp[default]'],
                        capture_output=True, text=True, encoding='utf-8',
                        errors='replace', timeout=120
                    )
                YTDLP_UPDATE_CHECKED = True

            self._emit_download_status()
            video_info = None
            selected_client = []
            selected_cookie_args = []
            last_detail = ""
            auth_required = False
            cookie_profiles = []
            if self.cookie_file and os.path.isfile(self.cookie_file):
                cookie_profiles.append(['--cookies', self.cookie_file])
            if AUTO_COOKIES_FILE.exists():
                auto_cookie_args = ['--cookies', str(AUTO_COOKIES_FILE)]
                if auto_cookie_args not in cookie_profiles:
                    cookie_profiles.append(auto_cookie_args)
            for cookie_profile in YTDLP_COOKIE_PROFILES:
                if cookie_profile not in cookie_profiles:
                    cookie_profiles.append(cookie_profile)
            # Tente primeiro todos os clientes sem cookies. Assim, quando o
            # vídeo é público, não há espera desnecessária exportando cookies
            # de navegadores que não estão sendo usados.
            info_profiles = [
                (client_args, [])
                for client_args in YTDLP_CLIENT_PROFILES
            ]
            for cookie_args in cookie_profiles:
                if cookie_args:
                    info_profiles.extend(
                        (client_args, cookie_args)
                        for client_args in YTDLP_CLIENT_PROFILES
                    )

            for client_args, cookie_args in info_profiles:
                if cookie_args:
                    self._emit_download_status()
                info_cmd = [
                    sys.executable, '-m', 'yt_dlp', '--ignore-config',
                    '--dump-json', '--no-playlist', '--no-warnings',
                    '--no-check-certificates', '--force-ipv4',
                    *client_args, *cookie_args, *YTDLP_COMMON_ARGS,
                    *YTDLP_JS_RUNTIME_ARGS, self.url
                ]
                info = subprocess.run(
                    info_cmd, capture_output=True, text=True,
                    encoding='utf-8', errors='replace'
                )
                if info.returncode == 0 and info.stdout.strip():
                    try:
                        video_info = json.loads(info.stdout.splitlines()[-1])
                        selected_client = client_args
                        selected_cookie_args = cookie_args
                        if (
                            len(cookie_args) == 2
                            and cookie_args[0] == '--cookies-from-browser'
                        ):
                            self._emit_download_status()
                            if self._export_browser_cookies(cookie_args[1]):
                                selected_cookie_args = [
                                    '--cookies', str(AUTO_COOKIES_FILE)
                                ]
                        break
                    except (ValueError, TypeError):
                        pass
                last_detail = (info.stderr or info.stdout or "").strip()
                detail_lower = last_detail.casefold()
                if (
                    "sign in to confirm" in detail_lower
                    or "not a bot" in detail_lower
                    or "cookies-from-browser" in detail_lower
                ):
                    auth_required = True

            if not video_info:
                if auth_required:
                    raise RuntimeError(
                        "O YouTube exigiu autenticação para este vídeo. "
                        "O programa tentou os cookies locais dos navegadores, "
                        "mas não encontrou uma sessão válida. "
                        "Abra o Chrome, Edge, Firefox ou Brave, entre no YouTube "
                        "e tente novamente."
                    )
                raise RuntimeError(
                    "Não foi possível acessar este vídeo.\n\n"
                    f"{last_detail[-1200:] or 'O yt-dlp não retornou informações.'}"
                )
            title      = video_info.get('title', 'Faixa')
            thumb_url  = video_info.get('thumbnail')
            # Usa o título original como nome do arquivo, removendo caracteres
            # que não são aceitos pelos sistemas de arquivos do Windows.
            safe_name = re.sub(r'[<>:"/\\|?*\x00-\x1f]', '_', title)
            safe_name = re.sub(r'\s+', ' ', safe_name).strip().rstrip('.')
            safe_name = safe_name[:150].strip().rstrip('.')
            if not safe_name:
                safe_name = 'Faixa'
            if safe_name.upper().split('.')[0] in {
                'CON', 'PRN', 'AUX', 'NUL',
                *[f'COM{n}' for n in range(1, 10)],
                *[f'LPT{n}' for n in range(1, 10)]
            }:
                safe_name = f'_{safe_name}'

            original_name = safe_name
            copy_number = 2
            while any(MUSIC_DIR.glob(f"{safe_name}.*")):
                safe_name = f"{original_name} ({copy_number})"
                copy_number += 1

            # Thumbnail
            thumb_path = None
            if thumb_url:
                try:
                    self._emit_download_status()
                    r = requests.get(thumb_url, timeout=10)
                    thumb_path = THUMB_DIR / f"{safe_name}.jpg"
                    thumb_path.write_bytes(r.content)
                except:
                    thumb_path = None

            target_tpl = MUSIC_DIR / f"{safe_name}.%(ext)s"
            self._emit_download_status(0)

            output_lines = []
            download_ok = False
            download_profiles = [(selected_client, selected_cookie_args)]
            for client_profile in YTDLP_CLIENT_PROFILES:
                for cookie_profile in cookie_profiles:
                    if (
                        len(cookie_profile) == 2
                        and cookie_profile[0] == '--cookies-from-browser'
                    ):
                        continue
                    candidate = (client_profile, cookie_profile)
                    if candidate not in download_profiles:
                        download_profiles.append(candidate)

            def run_ytdlp(format_selector, output_template, progress_label):
                postprocess_args = []
                if not self.is_video and self.audio_format != 'best':
                    postprocess_args = [
                        '-x',
                        '--audio-format', self.audio_format,
                        '--audio-quality', self.audio_quality
                    ]
                proc = subprocess.Popen(
                    [sys.executable, '-m', 'yt_dlp', '--ignore-config',
                     '--no-update', '--no-check-certificates', '--no-playlist',
                     '--force-ipv4', *client_args, *cookie_args, *YTDLP_COMMON_ARGS,
                     *YTDLP_JS_RUNTIME_ARGS,
                     *postprocess_args,
                     '-f', format_selector, '--newline',
                     '-o', str(output_template), self.url],
                    stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
                    text=True, encoding='utf-8', errors='replace'
                )
                lines = []
                pct_re = re.compile(r'\s*([\d.]+)%')
                for line in proc.stdout:
                    lines.append(line)
                    match = pct_re.search(line)
                    if match:
                        try:
                            pct = int(float(match.group(1)))
                            self._emit_download_status(pct)
                        except:
                            pass
                proc.wait()
                return proc.returncode, lines

            if self.is_video:
                # Baixar os fluxos separadamente evita depender da mesclagem
                # automática do yt-dlp, que pode deixar o MP4 apenas com vídeo.
                # A recompressão continua no mesmo formato compatível do
                # original, mas com menos perda de qualidade.
                if self.video_format not in {'webm', 'mp4'}:
                    self.video_format = 'webm'
                final_target = MUSIC_DIR / f"{safe_name}.{self.video_format}"
                video_tpl = MUSIC_DIR / f"{safe_name}.video.%(ext)s"
                audio_tpl = MUSIC_DIR / f"{safe_name}.audio.%(ext)s"
                ffmpeg_bin = _get_ffmpeg_bin()

                for attempt, (client_args, cookie_args) in enumerate(download_profiles):
                    if attempt:
                        self._emit_download_status()

                    if not ffmpeg_bin:
                        video_code, video_lines = run_ytdlp(
                            f'best[height<={self.max_height}][ext=webm]/'
                            f'best[height<={self.max_height}]/best',
                            target_tpl,
                            "Baixando vídeo compatível..."
                        )
                        output_lines.extend(video_lines)
                        if video_code == 0:
                            download_ok = True
                            break
                        continue

                    preferred_video_ext = self.video_format
                    preferred_audio_ext = (
                        'm4a' if self.video_format == 'mp4' else 'webm'
                    )
                    video_code, video_lines = run_ytdlp(
                        f'bestvideo[height<={self.max_height}]'
                        f'[ext={preferred_video_ext}]/'
                        f'bestvideo[height<={self.max_height}]/'
                        f'bestvideo[height<={self.max_height}][ext=mp4]/'
                        f'best[height<={self.max_height}]/'
                        f'best[height<={self.max_height}][ext=mp4]/best',
                        video_tpl,
                        "Baixando vídeo..."
                    )
                    audio_code, audio_lines = run_ytdlp(
                        f'bestaudio[ext={preferred_audio_ext}]/'
                        'bestaudio[ext=m4a]/bestaudio/best',
                        audio_tpl,
                        "Baixando áudio..."
                    )
                    output_lines.extend(video_lines)
                    output_lines.extend(audio_lines)

                    video_files = [
                        path for path in MUSIC_DIR.glob(f"{safe_name}.video.*")
                        if path.is_file() and path.suffix not in ('.part', '.ytdl')
                    ]
                    audio_files = [
                        path for path in MUSIC_DIR.glob(f"{safe_name}.audio.*")
                        if path.is_file() and path.suffix not in ('.part', '.ytdl')
                    ]

                    if video_code == 0 and audio_code == 0 and video_files and audio_files:
                        video_file = max(video_files, key=lambda path: path.stat().st_size)
                        audio_file = max(audio_files, key=lambda path: path.stat().st_size)
                        self.progress.emit("Alinhando as batidas...", -1)
                        mux = subprocess.run(
                            [
                                ffmpeg_bin, '-y',
                                '-i', str(video_file),
                                '-i', str(audio_file),
                                '-map', '0:v:0',
                                '-map', '1:a:0',
                                # Quando os fluxos são compatíveis com WebM,
                                # apenas os une. Isso evita reencodificar o
                                # vídeo e reduz bastante o tempo de finalização.
                                '-c:v', 'copy',
                                '-c:a', 'copy',
                                '-shortest',
                                '-f', self.video_format,
                                str(final_target)
                            ],
                            capture_output=True, text=True,
                            encoding='utf-8', errors='replace'
                        )
                        if mux.returncode != 0 or not final_target.exists():
                            try:
                                final_target.unlink()
                            except OSError:
                                pass
                            self.progress.emit(
                                "Compatibilizando áudio e vídeo...", -1
                            )
                            mux = subprocess.run(
                                [
                                    ffmpeg_bin, '-y',
                                    '-i', str(video_file),
                                    '-i', str(audio_file),
                                    '-map', '0:v:0',
                                    '-map', '1:a:0',
                                    '-c:v', (
                                        'libx264' if self.video_format == 'mp4'
                                        else 'libvpx-vp9'
                                    ),
                                    *(
                                        ['-preset', 'veryfast', '-crf', '18']
                                        if self.video_format == 'mp4'
                                        else [
                                            '-deadline', 'realtime',
                                            '-cpu-used', '4',
                                            '-crf', '24', '-b:v', '0'
                                        ]
                                    ),
                                    '-c:a', (
                                        'aac' if self.video_format == 'mp4'
                                        else 'libopus'
                                    ),
                                    '-b:a', '192k',
                                    *(
                                        ['-movflags', '+faststart']
                                        if self.video_format == 'mp4' else []
                                    ),
                                    '-shortest',
                                    '-f', self.video_format,
                                    str(final_target)
                                ],
                                capture_output=True, text=True,
                                encoding='utf-8', errors='replace'
                            )
                        output_lines.append(mux.stderr or mux.stdout or "")

                        audio_ok = False
                        ffprobe_bin = shutil.which("ffprobe")
                        if mux.returncode == 0 and final_target.exists() and ffprobe_bin:
                            probe = subprocess.run(
                                [
                                    ffprobe_bin, '-v', 'error',
                                    '-select_streams', 'a:0',
                                    '-show_entries', 'stream=codec_name',
                                    '-of', 'default=nw=1:nk=1',
                                    str(final_target)
                                ],
                                capture_output=True, text=True,
                                encoding='utf-8', errors='replace'
                            )
                            audio_ok = probe.returncode == 0 and bool(probe.stdout.strip())
                        elif mux.returncode == 0 and final_target.exists():
                            # O FFmpeg já recebeu -map 1:a:0; sem ffprobe,
                            # considere a mesclagem válida se ele terminou.
                            audio_ok = True

                        if audio_ok:
                            download_ok = True
                            for temp_file in (video_file, audio_file):
                                try:
                                    temp_file.unlink()
                                except OSError:
                                    pass
                            break

                    for temp_file in MUSIC_DIR.glob(f"{safe_name}.video.*"):
                        try:
                            temp_file.unlink()
                        except OSError:
                            pass
                    for temp_file in MUSIC_DIR.glob(f"{safe_name}.audio.*"):
                        try:
                            temp_file.unlink()
                        except OSError:
                            pass
                    if final_target.exists() and not download_ok:
                        try:
                            final_target.unlink()
                        except OSError:
                            pass
            else:
                for attempt, (client_args, cookie_args) in enumerate(download_profiles):
                    if attempt:
                        self.progress.emit("Tentando outro formato do YouTube...", -1)
                    code, lines = run_ytdlp(
                        'bestaudio/best',
                        target_tpl,
                        "Baixando..."
                    )
                    output_lines.extend(lines)
                    if code == 0:
                        download_ok = True
                        break

            if not download_ok:
                raise RuntimeError(
                    "O download foi recusado pelo YouTube ou falhou.\n\n"
                    f"{''.join(output_lines).strip()[-1200:] or 'Verifique se o vídeo está disponível.'}"
                )

            exts = (['mp4', 'webm', 'mkv', 'avi'] if self.is_video
                    else [
                        'webm', 'm4a', 'opus', 'ogg', 'mp3',
                        'aac', 'flac', 'wav'
                    ])
            target = None
            for ext in exts:
                candidate = MUSIC_DIR / f"{safe_name}.{ext}"
                if candidate.exists():
                    target = candidate
                    break
            if target is None:
                # O yt-dlp pode escolher outra extensão ao mesclar os fluxos
                # ou ao usar um formato retornado pelo cliente alternativo.
                # Nesse caso, localize o arquivo real criado nesta tentativa.
                candidates = [
                    path for path in MUSIC_DIR.glob(f"{safe_name}.*")
                    if path.is_file() and path.suffix not in ('.part', '.ytdl')
                ]
                if candidates:
                    target = max(candidates, key=lambda path: path.stat().st_size)

            if target and target.exists():
                self.finished.emit(title, str(target), str(thumb_path) if thumb_path else "", self.is_video)
            else:
                self.error.emit("Arquivo não encontrado após o download.")
        except Exception as e:
            message = str(e)
            try:
                DOWNLOAD_ERROR_FILE.write_text(message, encoding='utf-8')
            except OSError:
                pass
            self.error.emit(message)


# ─────────────────────────────────────────────
#  Slider clicável
# ─────────────────────────────────────────────
class ClickSlider(QSlider):
    def mousePressEvent(self, e):
        if e.button() == Qt.LeftButton:
            opt = QStyleOptionSlider()
            self.initStyleOption(opt)
            gr = self.style().subControlRect(QStyle.CC_Slider, opt, QStyle.SC_SliderGroove, self)
            sr = self.style().subControlRect(QStyle.CC_Slider, opt, QStyle.SC_SliderHandle, self)
            sl = sr.width() if self.orientation() == Qt.Horizontal else sr.height()
            mn = gr.x() if self.orientation() == Qt.Horizontal else gr.y()
            mx = (gr.right() - sl + 1) if self.orientation() == Qt.Horizontal else (gr.bottom() - sl + 1)
            pr = e.pos() - sr.center() + sr.topLeft()
            p  = pr.x() if self.orientation() == Qt.Horizontal else pr.y()
            val = QStyle.sliderValueFromPosition(self.minimum(), self.maximum(), p - mn, mx - mn, opt.upsideDown)
            self.setValue(val)
            self.sliderMoved.emit(val)
        super().mousePressEvent(e)


# ─────────────────────────────────────────────
#  Item da playlist (card com thumbnail)
# ─────────────────────────────────────────────
class TrackCard(QWidget):
    clicked       = pyqtSignal(int)
    delete_req    = pyqtSignal(int)
    favorite_req  = pyqtSignal(int)
    folder_req    = pyqtSignal(int)
    playlist_req  = pyqtSignal(int)

    _THUMB_W = 48
    _THUMB_H = 48

    def __init__(
        self, index, song: Song, is_playing=False, is_locked=False, parent=None
    ):
        super().__init__(parent)
        self.index = index
        self.song  = song
        self.is_playing = is_playing
        self.is_locked = is_locked
        self._hovered = False
        self.setObjectName("TrackCard")
        self.setAttribute(Qt.WA_StyledBackground, True)
        self._build()
        self.setMinimumHeight(70)
        self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)

    def _build(self):
        layout = QHBoxLayout(self)
        layout.setContentsMargins(12, 8, 10, 8)
        layout.setSpacing(10)

        # Thumbnail
        self.thumb_box = QWidget()
        self.thumb_box.setFixedSize(self._THUMB_W, self._THUMB_H)
        self.thumb_box.setAttribute(Qt.WA_TranslucentBackground, True)

        self.thumb_lbl = QLabel(self.thumb_box)
        self.thumb_lbl.setGeometry(
            0, 0, self._THUMB_W, self._THUMB_H
        )
        self.thumb_lbl.setAlignment(Qt.AlignCenter)
        self.thumb_lbl.setAttribute(Qt.WA_TransparentForMouseEvents, True)
        self.thumb_lbl.setStyleSheet(f"""
            background: {BG4};
            border: 1px solid {RED if self.is_locked else 'transparent'};
            border-radius: 8px;
            color: {TEXT3}; font-size: 18px;
        """)
        self._load_thumb()
        layout.addWidget(self.thumb_box)

        self.lock_lbl = QLabel(self.thumb_box)
        self.lock_lbl.setFixedSize(18, 18)
        self.lock_lbl.move(
            self._THUMB_W - self.lock_lbl.width() - 2,
            self._THUMB_H - self.lock_lbl.height() - 2
        )
        self.lock_lbl.setAlignment(Qt.AlignCenter)
        if self.is_locked:
            self.lock_lbl.setPixmap(
                _make_lock_icon(QColor("#FFFFFF")).pixmap(QSize(14, 14))
            )
        self.lock_lbl.setToolTip(
            "Faixa bloqueada: exclusiva do Premium"
            if self.is_locked else ""
        )
        self.lock_lbl.setStyleSheet(f"""
            background: {RED};
            border: 1px solid #FFFFFFAA;
            border-radius: 9px;
        """)
        self.lock_lbl.setVisible(self.is_locked)
        self.lock_lbl.setAttribute(Qt.WA_TransparentForMouseEvents, True)
        self.lock_lbl.raise_()

        # Info
        info = QVBoxLayout()
        info.setSpacing(2)

        title_row = QHBoxLayout()
        title_row.setSpacing(6)

        type_icon = QLabel("▶" if self.song.is_video else "♪")
        type_icon.setStyleSheet(f"color: {ACCENT if self.is_playing else TEXT3}; font-size: 11px;")
        type_icon.setFixedWidth(14)
        type_icon.setAttribute(Qt.WA_TransparentForMouseEvents, True)
        title_row.addWidget(type_icon)

        self.title_lbl = QLabel(self.song.title)
        self.title_lbl.setStyleSheet(f"""
            color: {'white' if self.is_playing else TEXT};
            font-weight: {'700' if self.is_playing else '400'};
            font-size: 13px;
        """)
        self.title_lbl.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Preferred)
        self.title_lbl.setMinimumWidth(0)
        self.title_lbl.setWordWrap(False)
        self.title_lbl.setAttribute(Qt.WA_TransparentForMouseEvents, True)
        title_row.addWidget(self.title_lbl)
        info.addLayout(title_row)

        src_lbl = QLabel("YouTube" if self.song.source == 'youtube' else "Arquivo local")
        src_lbl.setStyleSheet(f"color: {ACCENT if self.is_playing else TEXT3}; font-size: 11px;")
        src_lbl.setAttribute(Qt.WA_TransparentForMouseEvents, True)
        info.addWidget(src_lbl)

        layout.addLayout(info, 1)

        actions = QHBoxLayout()
        actions.setContentsMargins(0, 0, 0, 0)
        actions.setSpacing(5)

        self.playlist_btn = QPushButton(self)
        self.playlist_btn.setObjectName("TrackActionBtn")
        self.playlist_btn.setIcon(_make_playlist_icon(ACCENT2))
        self.playlist_btn.setIconSize(QSize(18, 18))
        self.playlist_btn.setFixedSize(30, 30)
        self.playlist_btn.setToolTip("Adicionar à playlist (Premium)")
        self.playlist_btn.clicked.connect(
            lambda: self.playlist_req.emit(self.index)
        )
        self.playlist_btn.setCursor(Qt.PointingHandCursor)
        actions.addWidget(self.playlist_btn)

        self.folder_btn = QPushButton(self)
        self.folder_btn.setObjectName("TrackActionBtn")
        self.folder_btn.setIcon(_make_folder_icon(ACCENT2))
        self.folder_btn.setIconSize(QSize(18, 18))
        self.folder_btn.setFixedSize(30, 30)
        self.folder_btn.setToolTip("Organizar em pasta (Premium)")
        self.folder_btn.clicked.connect(lambda: self.folder_req.emit(self.index))
        self.folder_btn.setCursor(Qt.PointingHandCursor)
        actions.addWidget(self.folder_btn)

        self.favorite_btn = QPushButton(self)
        self.favorite_btn.setObjectName("TrackFavoriteBtn")
        self.favorite_btn.setIcon(_make_star_icon(self.song.favorite))
        self.favorite_btn.setIconSize(QSize(18, 18))
        self.favorite_btn.setFixedSize(30, 30)
        self.favorite_btn.setToolTip("Favorito (Premium)")
        self.favorite_btn.clicked.connect(
            lambda: self.favorite_req.emit(self.index)
        )
        self.favorite_btn.setCursor(Qt.PointingHandCursor)
        actions.addWidget(self.favorite_btn)

        # Delete btn
        self.del_btn = QPushButton(self)
        self.del_btn.setObjectName("TrackDeleteBtn")
        self.del_btn.setIcon(_make_close_icon())
        self.del_btn.setIconSize(QSize(15, 15))
        self.del_btn.setFixedSize(30, 30)
        self.del_btn.setToolTip("Remover faixa")
        self.del_btn.clicked.connect(lambda: self.delete_req.emit(self.index))
        self.del_btn.setCursor(Qt.PointingHandCursor)
        actions.addWidget(self.del_btn)
        layout.addLayout(actions)

        self._update_bg()

    def resizeEvent(self, e):
        super().resizeEvent(e)

    def _load_thumb(self):
        pix = QPixmap()
        if self.song.thumb and os.path.exists(self.song.thumb):
            pix = QPixmap(self.song.thumb)
        if pix.isNull():
            pix = _get_default_art_pixmap()
        if not pix.isNull():
            pix = pix.scaled(self._THUMB_W, self._THUMB_H, Qt.KeepAspectRatioByExpanding, Qt.SmoothTransformation)
            # Crop to square
            if pix.width() > self._THUMB_W or pix.height() > self._THUMB_H:
                x = (pix.width()  - self._THUMB_W) // 2
                y = (pix.height() - self._THUMB_H) // 2
                pix = pix.copy(x, y, self._THUMB_W, self._THUMB_H)
            # Rounded mask
            rounded = QPixmap(self._THUMB_W, self._THUMB_H)
            rounded.fill(Qt.transparent)
            p = QPainter(rounded)
            p.setRenderHint(QPainter.Antialiasing)
            p.setBrush(QBrush(pix))
            p.setPen(Qt.NoPen)
            p.drawRoundedRect(0, 0, self._THUMB_W, self._THUMB_H, 8, 8)
            p.end()
            self.thumb_lbl.setPixmap(rounded)
            self.thumb_lbl.setText("")
            return
        icon = "▶" if self.song.is_video else "♪"
        self.thumb_lbl.setText(icon)

    def _update_bg(self):
        if self._hovered and not self.is_playing:
            self.setStyleSheet(
                f"QWidget#TrackCard {{ background: {BG3}; border-left: 3px solid transparent; }}")
        elif self.is_playing:
            self.setStyleSheet(
                f"QWidget#TrackCard {{ background: rgba(255,79,154,0.12); border-left: 3px solid {ACCENT}; }}")
        else:
            self.setStyleSheet(
                f"QWidget#TrackCard {{ background: transparent; border-left: 3px solid transparent; }}")

    def enterEvent(self, e):
        self._hovered = True
        self._update_bg()

    def leaveEvent(self, e):
        if not self.rect().contains(self.mapFromGlobal(QtGui.QCursor.pos())):
            self._hovered = False
            self._update_bg()

    def mousePressEvent(self, e):
        if e.button() == Qt.LeftButton:
            self.clicked.emit(self.index)


# ─────────────────────────────────────────────
#  Caixa de arte com fundo desfocado (QPainter)
# ─────────────────────────────────────────────
class ArtDisplay(QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)
        self._pixmap  = None
        self._blurred = None
        self.setMinimumSize(300, 300)

    def set_pixmap(self, pix: QPixmap):
        self._pixmap  = pix
        self._blurred = self._make_blur(pix, 60) if pix else None
        self.update()

    def clear(self):
        self._pixmap  = None
        self._blurred = None
        self.update()

    @staticmethod
    def _make_blur(pix, radius=40):
        small = pix.scaled(200, 200, Qt.IgnoreAspectRatio, Qt.SmoothTransformation)
        big   = small.scaled(pix.width() or 400, pix.height() or 400, Qt.IgnoreAspectRatio, Qt.SmoothTransformation)
        return big

    def paintEvent(self, e):
        p = QPainter(self)
        p.setRenderHint(QPainter.Antialiasing)
        w, h = self.width(), self.height()

        # Background
        p.fillRect(0, 0, w, h, QColor(BG))

        if self._blurred:
            scaled = self._blurred.scaled(w, h, Qt.KeepAspectRatioByExpanding, Qt.SmoothTransformation)
            x = (w - scaled.width())  // 2
            y = (h - scaled.height()) // 2
            p.setOpacity(0.25)
            p.drawPixmap(x, y, scaled)
            p.setOpacity(1.0)

        # Gradient overlay
        grad = QLinearGradient(0, 0, 0, h)
        c0 = QColor(BG); c0.setAlpha(16)
        grad.setColorAt(0, c0)  # near-transparent
        grad.setColorAt(1, QColor(10, 10, 15, 220))
        p.fillRect(0, 0, w, h, grad)

        if self._pixmap:
            size  = min(w, h) - 80
            art   = self._pixmap.scaled(size, size, Qt.KeepAspectRatio, Qt.SmoothTransformation)
            ax    = (w - art.width())  // 2
            ay    = (h - art.height()) // 2 - 10

            # Shadow
            p.setOpacity(0.5)
            shadow_rect = QtCore.QRectF(ax + 6, ay + 14, art.width(), art.height())
            p.setBrush(QColor(0, 0, 0, 120))
            p.setPen(Qt.NoPen)
            p.drawRoundedRect(shadow_rect, 16, 16)
            p.setOpacity(1.0)

            # Art with rounded corners
            rounded = QPixmap(art.width(), art.height())
            rounded.fill(Qt.transparent)
            rp = QPainter(rounded)
            rp.setRenderHint(QPainter.Antialiasing)
            rp.setBrush(QBrush(art))
            rp.setPen(Qt.NoPen)
            rp.drawRoundedRect(0, 0, art.width(), art.height(), 16, 16)
            rp.end()
            p.drawPixmap(ax, ay, rounded)
        else:
            # Placeholder
            grad2 = QLinearGradient(0, 0, w, h)
            grad2.setColorAt(0, QColor(ACCENT).darker(200))
            grad2.setColorAt(1, QColor(BG3))
            size = min(w, h) - 80
            ax   = (w - size) // 2
            ay   = (h - size) // 2 - 10
            p.setBrush(grad2)
            p.setPen(Qt.NoPen)
            p.drawRoundedRect(ax, ay, size, size, 16, 16)
            p.setPen(QColor(ACCENT2))
            p.setFont(QFont("Segoe UI", size // 4))
            p.drawText(QtCore.QRect(ax, ay, size, size), Qt.AlignCenter, "♪")

        p.end()


# ─────────────────────────────────────────────
#  Dialog de Download
# ─────────────────────────────────────────────
class DownloadDialog(QDialog):
    def __init__(self, parent=None, premium=False):
        super().__init__(parent)
        self.setWindowTitle("Baixar do YouTube")
        self.setFixedWidth(440)
        self.setWindowFlags(Qt.Dialog)
        self.setWindowFlag(Qt.WindowContextHelpButtonHint, False)
        self.worker  = None
        self.cookie_file = ""
        self.premium = bool(premium)
        self._build()

    def _build(self):
        outer = QVBoxLayout(self)
        outer.setContentsMargins(10, 10, 10, 10)

        card = QFrame()
        card.setObjectName("DownloadCard")
        card.setStyleSheet(f"""
            QFrame#DownloadCard {{
                background: {BG2};
                border: 1px solid {BORDER};
                border-radius: 16px;
            }}
        """)
        shadow = QGraphicsDropShadowEffect()
        shadow.setBlurRadius(40)
        shadow.setColor(QColor(0, 0, 0, 180))
        shadow.setOffset(0, 8)
        card.setGraphicsEffect(shadow)

        lay = QVBoxLayout(card)
        lay.setContentsMargins(28, 28, 28, 28)
        lay.setSpacing(18)

        # Título
        t = QLabel("Baixar do YouTube")
        t.setStyleSheet(f"color: {TEXT}; font-size: 17px; font-weight: 700;")
        lay.addWidget(t)

        sub = QLabel("Cole o link do vídeo ou playlist abaixo.")
        sub.setStyleSheet(f"color: {TEXT2}; font-size: 12px;")
        lay.addWidget(sub)

        # Input URL
        self.url_input = QLineEdit()
        self.url_input.setPlaceholderText("https://www.youtube.com/watch?v=...")
        lay.addWidget(self.url_input)

        # Tipo
        type_row = QHBoxLayout()
        type_row.setSpacing(16)
        self.rb_audio = QRadioButton("Apenas Audio")
        self.rb_video = QRadioButton("Video (Clipe)")
        self.rb_audio.setChecked(True)
        type_row.addWidget(self.rb_audio)
        type_row.addWidget(self.rb_video)
        type_row.addStretch()
        lay.addLayout(type_row)

        quality_row = QHBoxLayout()
        quality_row.setSpacing(10)
        self.quality_label = QLabel("Qualidade")
        quality_row.addWidget(self.quality_label)
        self.quality_combo = QComboBox()
        for label, height in [
            ("360p — gratuito", 360),
            ("480p — gratuito", 480),
            ("720p — gratuito", 720),
            ("1080p — Premium", 1080),
            ("1440p — Premium", 1440),
            ("2160p / 4K — Premium", 2160),
        ]:
            self.quality_combo.addItem(label, height)
        self.quality_combo.setCurrentIndex(3 if self.premium else 2)
        quality_row.addWidget(self.quality_combo, 1)
        lay.addLayout(quality_row)

        audio_format_row = QHBoxLayout()
        audio_format_row.setSpacing(10)
        self.audio_format_label = QLabel("Formato do áudio")
        audio_format_row.addWidget(self.audio_format_label)
        self.audio_format_combo = QComboBox()
        for label, value in [
            ("Melhor disponível", "best"),
            ("MP3", "mp3"),
            ("M4A", "m4a"),
            ("OGG", "vorbis"),
        ]:
            self.audio_format_combo.addItem(label, value)
        audio_format_row.addWidget(self.audio_format_combo, 1)
        lay.addLayout(audio_format_row)

        audio_quality_row = QHBoxLayout()
        audio_quality_row.setSpacing(10)
        self.audio_quality_label = QLabel("Qualidade do áudio")
        audio_quality_row.addWidget(self.audio_quality_label)
        self.audio_quality_combo = QComboBox()
        for label, value in [
            ("Melhor disponível", "0"),
            ("320 kbps", "320K"),
            ("192 kbps", "192K"),
            ("128 kbps", "128K"),
        ]:
            self.audio_quality_combo.addItem(label, value)
        audio_quality_row.addWidget(self.audio_quality_combo, 1)
        lay.addLayout(audio_quality_row)

        self.rb_audio.toggled.connect(self._update_quality_state)
        self._update_quality_state(self.rb_audio.isChecked())

        # Progress (oculto inicialmente)
        self.prog_frame = QFrame()
        self.prog_frame.setObjectName("ProgressFrame")
        self.prog_frame.setStyleSheet("QFrame#ProgressFrame { background: transparent; }")
        prog_lay = QVBoxLayout(self.prog_frame)
        prog_lay.setContentsMargins(0, 0, 0, 0)
        prog_lay.setSpacing(6)

        self.prog_label = QLabel("Aguardando...")
        self.prog_label.setStyleSheet(f"color: {TEXT2}; font-size: 12px;")
        prog_lay.addWidget(self.prog_label)

        self.prog_bar = QProgressBar()
        self.prog_bar.setRange(0, 100)
        self.prog_bar.setValue(0)
        self.prog_bar.setTextVisible(False)
        self.prog_bar.setFixedHeight(6)
        prog_lay.addWidget(self.prog_bar)

        self.prog_frame.hide()
        lay.addWidget(self.prog_frame)

        # Botões
        btn_row = QHBoxLayout()
        btn_row.setSpacing(10)

        self.cancel_btn = QPushButton("Cancelar")
        self.cancel_btn.setObjectName("CancelBtn")
        self.cancel_btn.clicked.connect(self.reject)

        self.dl_btn = QPushButton("  Baixar  ")
        self.dl_btn.setObjectName("DialogBtn")
        self.dl_btn.clicked.connect(self._start)

        btn_row.addStretch()
        btn_row.addWidget(self.cancel_btn)
        btn_row.addWidget(self.dl_btn)
        lay.addLayout(btn_row)

        outer.addWidget(card)

    def _update_quality_state(self, audio_checked):
        enabled = not audio_checked
        self.quality_label.setEnabled(enabled)
        self.quality_combo.setEnabled(enabled)
        self.audio_format_label.setEnabled(audio_checked)
        self.audio_format_combo.setEnabled(audio_checked)
        self.audio_quality_label.setEnabled(audio_checked)
        self.audio_quality_combo.setEnabled(audio_checked)
        self.adjustSize()

    def _start(self):
        url = _normalize_download_url(self.url_input.text())
        if not url or not url.startswith(("http://", "https://")):
            QMessageBox.warning(
                self, "Endereço inválido",
                "Cole o link do vídeo do YouTube antes de baixar."
            )
            self.url_input.setFocus()
            return
        is_video = self.rb_video.isChecked()
        max_height = int(self.quality_combo.currentData() or 720)
        video_format = 'webm'
        audio_format = self.audio_format_combo.currentData() or 'best'
        audio_quality = self.audio_quality_combo.currentData() or '0'
        if is_video and max_height > 720 and not self.premium:
            QMessageBox.information(
                self, "Recurso Premium",
                "Qualidades acima de 720p estão disponíveis somente no Premium."
            )
            self.quality_combo.setCurrentIndex(2)
            return
        self.dl_btn.setEnabled(False)
        self.url_input.setEnabled(False)
        self.rb_audio.setEnabled(False)
        self.rb_video.setEnabled(False)
        self.prog_frame.show()
        self.prog_bar.setRange(0, 0)  # indeterminado
        self.adjustSize()

        self.worker = DownloadWorker(
            url,
            is_video,
            max_height=max_height,
            video_format=video_format,
            audio_format=audio_format,
            audio_quality=audio_quality
        )
        self.worker.progress.connect(self._on_progress)
        self.worker.finished.connect(self._on_done)
        self.worker.error.connect(self._on_error)
        self.worker.start()

    def _on_progress(self, msg, pct):
        self.prog_label.setText(msg)
        if pct >= 0:
            self.prog_bar.setRange(0, 100)
            self.prog_bar.setValue(pct)
        else:
            self.prog_bar.setRange(0, 0)

    def _on_done(self, title, path, thumb, is_video):
        self._result = (title, path, thumb, is_video)
        self.accept()

    def _on_error(self, err):
        self.dl_btn.setEnabled(True)
        self.url_input.setEnabled(True)
        self.rb_audio.setEnabled(True)
        self.rb_video.setEnabled(True)
        self.prog_frame.hide()
        self.adjustSize()
        QMessageBox.critical(self, "Erro no Download", err)

    def get_result(self):
        return getattr(self, '_result', None)

    # Arrastar janela sem barra de título
    def mousePressEvent(self, e):
        if e.button() == Qt.LeftButton:
            self._drag_pos = e.globalPos() - self.frameGeometry().topLeft()

    def mouseMoveEvent(self, e):
        if e.buttons() == Qt.LeftButton and hasattr(self, '_drag_pos'):
            self.move(e.globalPos() - self._drag_pos)


class AdLabel(QLabel):
    link_clicked = pyqtSignal(str)

    def __init__(self, parent=None):
        super().__init__(parent)
        self.link_url = ''
        self.setMouseTracking(True)

    def set_link_url(self, value):
        self.link_url = str(value or '').strip()
        self.setCursor(
            Qt.PointingHandCursor if self.link_url else Qt.ArrowCursor
        )

    def mousePressEvent(self, event):
        if event.button() == Qt.LeftButton and self.link_url:
            link_url = self.link_url
            self.link_clicked.emit(link_url)
            QDesktopServices.openUrl(QUrl(link_url))

            window = self.window()
            owner = window.parentWidget()
            if owner is not None:
                window = owner.window()
            QTimer.singleShot(150, window.raise_)
            QTimer.singleShot(150, window.activateWindow)
            event.accept()
            return
        super().mousePressEvent(event)


class AdPopup(QDialog):
    def __init__(self, config, image_data, parent=None):
        super().__init__(parent)
        self.setWindowTitle("Propaganda")
        self.setModal(False)
        self.setAttribute(Qt.WA_DeleteOnClose, True)
        self.setAttribute(Qt.WA_TranslucentBackground, True)
        self.setWindowFlags(
            Qt.Window | Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint
        )
        self.setStyleSheet("QDialog { background: transparent; border: none; }")
        self.setFixedSize(560, 430)

        outer = QVBoxLayout(self)
        outer.setContentsMargins(0, 0, 0, 0)
        outer.setSpacing(0)

        card = QFrame()
        card.setObjectName("AdPopupCard")
        card.setStyleSheet(
            f"QFrame#AdPopupCard {{ background: {BG2}; "
            f"border: 1px solid {ACCENT}99; border-radius: 22px; }}"
        )
        card_lay = QVBoxLayout(card)
        card_lay.setContentsMargins(24, 20, 24, 20)
        card_lay.setSpacing(10)

        header = QHBoxLayout()
        title = QLabel("OFERTA ESPECIAL")
        title.setObjectName("AdPopupTitle")
        title.setAlignment(Qt.AlignCenter)
        title.setFixedHeight(30)
        title.setStyleSheet(
            f"QLabel#AdPopupTitle {{ color: {ACCENT}; "
            "font-size: 12px; font-weight: 800; "
            "background: rgba(255, 79, 154, 0.12); "
            f"border: 1px solid {ACCENT}80; border-radius: 11px; "
            "padding: 4px 12px; }}"
        )
        title.setMinimumWidth(150)
        header.addWidget(title)
        header.addStretch()

        close_btn = QPushButton()
        close_btn.setIcon(_make_close_icon())
        close_btn.setIconSize(QSize(14, 14))
        close_btn.setFixedSize(32, 32)
        close_btn.setToolTip("Fechar propaganda")
        close_btn.setStyleSheet(
            f"QPushButton {{ background: {BG4}; color: {TEXT}; "
            f"border: 1px solid {BORDER}; border-radius: 16px; "
            "padding: 0; margin: 0; }"
            f"QPushButton:hover {{ background: {ACCENT}; color: white; "
            f"border: 1px solid {ACCENT}; }}"
        )
        close_btn.clicked.connect(self.close)
        header.addWidget(close_btn)
        card_lay.addLayout(header)

        self.image = AdLabel()
        self.image.setAlignment(Qt.AlignCenter)
        self.image.setFixedHeight(220)
        self.image.setStyleSheet(
            f"background: #151F31; border: 1px solid {BORDER}; "
            "border-radius: 14px; padding: 8px;"
        )
        self.image.link_clicked.connect(self.close)
        self.image.set_link_url(config.get('link_url'))
        pixmap = QPixmap()
        if image_data:
            pixmap.loadFromData(image_data)
        if not pixmap.isNull():
            self.image.setPixmap(
                pixmap.scaled(490, 200, Qt.KeepAspectRatio, Qt.SmoothTransformation)
            )
        else:
            self.image.setText("Imagem da propaganda indisponível")
            self.image.setStyleSheet(
                f"background: {BG3}; color: {TEXT2}; border: 1px solid {BORDER}; "
                "border-radius: 14px; padding: 8px;"
            )
        card_lay.addWidget(self.image)

        text = QLabel(str(config.get('text') or '').strip())
        text.setObjectName("AdPopupText")
        text.setWordWrap(True)
        text.setAlignment(Qt.AlignCenter)
        text.setStyleSheet(
            f"QLabel#AdPopupText {{ color: {TEXT}; font-size: 13px; "
            "font-weight: 600; background: transparent; border: none; "
            "padding-left: 14px; padding-right: 14px; }}"
        )
        card_lay.addWidget(text)

        hint = QLabel("Clique na imagem para abrir o link." if config.get('link_url')
                      else "Feche esta janela para continuar.")
        hint.setObjectName("AdPopupHint")
        hint.setAlignment(Qt.AlignCenter)
        hint.setStyleSheet(
            f"QLabel#AdPopupHint {{ color: {TEXT3}; font-size: 11px; "
            "background: transparent; border: none; padding: 0; }}"
        )
        card_lay.addWidget(hint)

        outer.addWidget(card)

    def showEvent(self, event):
        super().showEvent(event)
        parent = self.parentWidget()
        screen = parent.screen() if parent is not None else self.screen()
        if screen is not None:
            self.move(
                screen.availableGeometry().center() - self.rect().center()
            )
        elif parent is not None:
            self.move(parent.frameGeometry().center() - self.rect().center())


class FreeLimitDialog(QDialog):
    PREMIUM_REQUESTED = 2

    def __init__(self, detail='', parent=None):
        super().__init__(parent)
        self.setWindowTitle("NeonWave Premium")
        self.setWindowFlags(Qt.Dialog | Qt.FramelessWindowHint)
        self.setAttribute(Qt.WA_TranslucentBackground, True)
        self.setModal(True)
        self.setFixedSize(500, 310)
        self._detail = detail or (
            "Você já utilizou os 10 downloads gratuitos. "
            "Adquira o Premium para adicionar músicas ilimitadamente."
        )
        self._build()

    def _build(self):
        outer = QVBoxLayout(self)
        outer.setContentsMargins(10, 10, 10, 10)

        card = QFrame()
        card.setObjectName("FreeLimitCard")
        card.setStyleSheet(f"""
            QFrame#FreeLimitCard {{
                background: qlineargradient(
                    x1: 0, y1: 0, x2: 1, y2: 1,
                    stop: 0 #151D2D, stop: 0.55 {BG2}, stop: 1 #0D1320
                );
                border: 1px solid {ACCENT}99;
                border-radius: 22px;
            }}
            QLabel#FreeLimitBadge {{
                color: {ACCENT3};
                background: {ACCENT}22;
                border: 1px solid {ACCENT}66;
                border-radius: 21px;
                font-size: 21px;
                font-weight: 800;
            }}
            QLabel#FreeLimitTitle {{
                color: {TEXT};
                font-size: 20px;
                font-weight: 800;
            }}
            QLabel#FreeLimitSubtitle {{
                color: {ACCENT};
                font-size: 11px;
                font-weight: 800;
                letter-spacing: 1px;
            }}
            QLabel#FreeLimitText {{
                color: {TEXT2};
                font-size: 13px;
                line-height: 1.4;
            }}
            QPushButton#FreeLimitPremium {{
                color: white;
                background: qlineargradient(
                    x1: 0, y1: 0, x2: 1, y2: 0,
                    stop: 0 {ACCENT}, stop: 1 #FF7A59
                );
                border: 1px solid #FFB5CF;
                border-radius: 10px;
                padding: 11px 20px;
                font-size: 13px;
                font-weight: 800;
            }}
            QPushButton#FreeLimitPremium:hover {{
                background: qlineargradient(
                    x1: 0, y1: 0, x2: 1, y2: 0,
                    stop: 0 #FF79B2, stop: 1 #FF9B69
                );
                border: 1px solid white;
            }}
            QPushButton#FreeLimitClose {{
                color: {TEXT2};
                background: {BG4};
                border: 1px solid {BORDER};
                border-radius: 10px;
                padding: 11px 18px;
                font-size: 13px;
            }}
            QPushButton#FreeLimitClose:hover {{
                color: {TEXT};
                background: {BG3};
                border: 1px solid {TEXT3};
            }}
        """)
        shadow = QGraphicsDropShadowEffect()
        shadow.setBlurRadius(48)
        shadow.setColor(QColor(0, 0, 0, 210))
        shadow.setOffset(0, 12)
        card.setGraphicsEffect(shadow)

        lay = QVBoxLayout(card)
        lay.setContentsMargins(24, 22, 24, 22)
        lay.setSpacing(10)

        top = QHBoxLayout()
        badge = QLabel("★")
        badge.setObjectName("FreeLimitBadge")
        badge.setAlignment(Qt.AlignCenter)
        badge.setFixedSize(42, 42)
        top.addWidget(badge)

        heading = QVBoxLayout()
        heading.setSpacing(1)
        subtitle = QLabel("CONTA GRATUITA")
        subtitle.setObjectName("FreeLimitSubtitle")
        title = QLabel("Seu limite chegou ao fim")
        title.setObjectName("FreeLimitTitle")
        heading.addWidget(subtitle)
        heading.addWidget(title)
        top.addLayout(heading, 1)
        lay.addLayout(top)

        text = QLabel(self._detail)
        text.setObjectName("FreeLimitText")
        text.setWordWrap(True)
        lay.addWidget(text)

        line = QFrame()
        line.setFixedHeight(1)
        line.setStyleSheet(f"background: {BORDER};")
        lay.addWidget(line)

        hint = QLabel("Desbloqueie downloads ilimitados, qualidade superior e uma experiência sem anúncios.")
        hint.setObjectName("FreeLimitText")
        hint.setWordWrap(True)
        lay.addWidget(hint)

        buttons = QHBoxLayout()
        buttons.setSpacing(10)
        buttons.addStretch()
        close = QPushButton("Agora não")
        close.setObjectName("FreeLimitClose")
        close.clicked.connect(self.reject)
        premium = QPushButton("  Ativar Premium  ")
        premium.setObjectName("FreeLimitPremium")
        premium.clicked.connect(lambda: self.done(self.PREMIUM_REQUESTED))
        buttons.addWidget(close)
        buttons.addWidget(premium)
        lay.addLayout(buttons)

        outer.addWidget(card)

    def showEvent(self, event):
        super().showEvent(event)
        parent = self.parentWidget()
        if parent is not None:
            self.move(parent.frameGeometry().center() - self.rect().center())


class DownloadQueueDialog(QDialog):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setWindowFlag(Qt.WindowContextHelpButtonHint, False)
        self.setWindowTitle("Fila Premium")
        self.setFixedWidth(560)
        self.setMinimumHeight(390)
        self._build()

    def _build(self):
        outer = QVBoxLayout(self)
        outer.setContentsMargins(12, 12, 12, 12)
        card = QFrame()
        card.setObjectName("QueueCard")
        card.setStyleSheet(
            f"QFrame#QueueCard {{ background: {BG2}; "
            f"border: 1px solid {BORDER}; border-radius: 16px; }}"
        )
        lay = QVBoxLayout(card)
        lay.setContentsMargins(26, 24, 26, 24)
        lay.setSpacing(12)

        title = QLabel("Fila de downloads Premium")
        title.setStyleSheet(f"color: {TEXT}; font-size: 18px; font-weight: 800;")
        lay.addWidget(title)
        hint = QLabel(
            "Cole um link por linha. A fila será processada uma por vez, "
            "na ordem informada, para preservar estabilidade e permitir retomada."
        )
        hint.setWordWrap(True)
        hint.setStyleSheet(f"color: {TEXT2}; font-size: 12px;")
        lay.addWidget(hint)

        self.url_edit = QPlainTextEdit()
        self.url_edit.setPlaceholderText(
            "https://www.youtube.com/watch?v=...\n"
            "https://www.youtube.com/playlist?list=..."
        )
        self.url_edit.setMinimumHeight(190)
        lay.addWidget(self.url_edit)

        buttons = QHBoxLayout()
        buttons.addStretch()
        cancel = QPushButton("Cancelar")
        cancel.setObjectName("CancelBtn")
        cancel.clicked.connect(self.reject)
        start = QPushButton("Iniciar fila")
        start.setObjectName("DialogBtn")
        start.clicked.connect(self.accept)
        buttons.addWidget(cancel)
        buttons.addWidget(start)
        lay.addLayout(buttons)
        outer.addWidget(card)

    def urls(self):
        return [
            _normalize_download_url(line)
            for line in self.url_edit.toPlainText().splitlines()
            if _normalize_download_url(line).startswith(('http://', 'https://'))
        ]


class StartupSplash(QWidget):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("NeonWave Player")
        self.setWindowFlags(
            Qt.Window | Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint
        )
        self.setAttribute(Qt.WA_TranslucentBackground, True)
        self.setFixedSize(620, 390)
        self._progress_value = 0
        self._build()

        self._timer = QTimer(self)
        self._timer.setInterval(35)
        self._timer.timeout.connect(self._animate_loading)

    def _build(self):
        outer = QVBoxLayout(self)
        outer.setContentsMargins(12, 12, 12, 12)

        card = QFrame()
        card.setObjectName("SplashCard")
        card.setStyleSheet(f"""
            QFrame#SplashCard {{
                background: qlineargradient(
                    x1: 0, y1: 0, x2: 1, y2: 1,
                    stop: 0 #121B2B, stop: 0.5 #090D14, stop: 1 #171225
                );
                border: 1px solid {ACCENT}88;
                border-radius: 30px;
            }}
            QLabel#SplashTitle {{
                color: {TEXT};
                font-size: 26px;
                font-weight: 900;
                letter-spacing: 5px;
            }}
            QLabel#SplashCaption {{
                color: {ACCENT2};
                font-size: 10px;
                font-weight: 700;
                letter-spacing: 3px;
            }}
            QLabel#SplashStatus {{
                color: {TEXT2};
                font-size: 11px;
            }}
            QProgressBar#SplashProgress {{
                background: #253247;
                border: none;
                border-radius: 3px;
                height: 6px;
            }}
            QProgressBar#SplashProgress::chunk {{
                background: qlineargradient(
                    x1: 0, y1: 0, x2: 1, y2: 0,
                    stop: 0 {ACCENT}, stop: 1 {ACCENT2}
                );
                border-radius: 3px;
            }}
        """)
        lay = QVBoxLayout(card)
        lay.setContentsMargins(42, 28, 42, 30)
        lay.setSpacing(5)

        logo = QLabel()
        logo.setAlignment(Qt.AlignCenter)
        logo_path = Path(__file__).resolve().parent / "logo.png"
        logo_pixmap = QPixmap(str(logo_path))
        if not logo_pixmap.isNull():
            logo.setPixmap(
                logo_pixmap.scaled(
                    360, 205, Qt.KeepAspectRatio, Qt.SmoothTransformation
                )
            )
        else:
            logo.setText("NEONWAVE")
            logo.setStyleSheet(
                f"color: {ACCENT2}; font-size: 28px; font-weight: 900;"
            )
        logo.setMinimumHeight(205)
        lay.addWidget(logo)

        caption = QLabel("MUSIC  •  VIDEO  •  YOUR  VIBE")
        caption.setObjectName("SplashCaption")
        caption.setAlignment(Qt.AlignCenter)
        lay.addWidget(caption)
        lay.addSpacing(8)

        self.status = QLabel("Preparando sua experiência...")
        self.status.setObjectName("SplashStatus")
        self.status.setAlignment(Qt.AlignCenter)
        lay.addWidget(self.status)

        self.progress = QProgressBar()
        self.progress.setObjectName("SplashProgress")
        self.progress.setRange(0, 100)
        self.progress.setValue(0)
        self.progress.setTextVisible(False)
        self.progress.setFixedHeight(6)
        lay.addWidget(self.progress)

        outer.addWidget(card)

    def showEvent(self, event):
        super().showEvent(event)
        screen = QApplication.primaryScreen()
        if screen is not None:
            self.move(screen.availableGeometry().center() - self.rect().center())
        self._timer.start()

    def _animate_loading(self):
        self._progress_value = min(self._progress_value + 3, 96)
        self.progress.setValue(self._progress_value)
        messages = (
            "Preparando sua experiência...",
            "Acendendo as luzes...",
            "Organizando sua biblioteca...",
            "Quase pronto..."
        )
        self.status.setText(messages[(self._progress_value // 24) % len(messages)])

    def finish(self):
        self._timer.stop()
        self.progress.setValue(100)
        self.status.setText("Tudo pronto.")
        QApplication.processEvents()
        self.close()


# ─────────────────────────────────────────────
#  Janela principal
# ─────────────────────────────────────────────
class MusicPlayer(QWidget):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("NeonWave Player")
        if APP_ICON_FILE.is_file():
            self.setWindowIcon(QIcon(str(APP_ICON_FILE)))
        screen = QApplication.primaryScreen()
        available = screen.availableGeometry() if screen is not None else None
        if available is not None:
            initial_width = max(1000, min(1400, available.width() - 60))
            initial_height = max(700, min(950, available.height() - 30))
        else:
            initial_width = 1400
            initial_height = 950
        self._initial_window_size = QSize(initial_width, initial_height)
        self.setMinimumSize(760, 360)
        self.setMaximumSize(QSize(16777215, 16777215))
        self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
        self.resize(initial_width, initial_height)
        self._centered_on_start = False

        self.player        = QMediaPlayer()
        self.player.setVolume(40)
        self.playlist      = []
        self.current_index = -1
        self._is_seeking   = False
        self._shuffle      = False
        self._repeat       = False
        self._library_filter = 'Todas'
        self._web_active   = False
        self._sidebar_widget = None
        self._now_playing_panel = None
        self._premium_btn = None
        self._premium_expiry_lbl = None
        self._is_fullscreen = False
        self.settings      = load_settings()
        self.playlists     = load_playlists()
        self._theme_key = _apply_theme_colors(
            self.settings.get('theme', 'neonwave'),
            self.settings.get('premium', False)
        )
        if self.settings.get('theme') != self._theme_key:
            self.settings['theme'] = self._theme_key
            save_settings(self.settings)
        self._ad_config = {}
        self._ad_signature = None
        self._ad_image_data = b''
        self._ad_display_key = None
        self._ad_popup = None
        self._ad_worker = None

        self._web_poll = QTimer()
        self._web_poll.setInterval(500)
        self._web_poll.timeout.connect(self._poll_web)

        self.setStyleSheet(_build_stylesheet())
        self._build_ui()
        self._fullscreen_shortcut = QShortcut(QKeySequence("F11"), self)
        self._fullscreen_shortcut.setContext(Qt.ApplicationShortcut)
        self._fullscreen_shortcut.activated.connect(self._toggle_fullscreen)
        self._escape_shortcut = QShortcut(QKeySequence("Esc"), self)
        self._escape_shortcut.setContext(Qt.ApplicationShortcut)
        self._escape_shortcut.activated.connect(self._exit_fullscreen)
        self._premium_timer = QTimer(self)
        self._premium_timer.setInterval(250)
        self._premium_timer.timeout.connect(self._update_premium_countdown)
        self._premium_timer.start()
        self._ad_refresh_timer = QTimer(self)
        self._ad_refresh_timer.setInterval(3000)
        self._ad_refresh_timer.timeout.connect(self._load_remote_ad)
        self._ad_refresh_timer.start()
        self._connect_player()
        self._load_playlist()
        QTimer.singleShot(150, self._load_remote_ad)

    # ── UI ──────────────────────────────────
    def _build_ui(self):
        root = QHBoxLayout(self)
        root.setContentsMargins(0, 0, 0, 0)
        root.setSpacing(0)

        self._sidebar_widget = self._build_sidebar()
        root.addWidget(self._sidebar_widget)
        root.addWidget(self._build_main(), 1)

    def _build_sidebar(self):
        sb = QFrame()
        sb.setObjectName("Sidebar")
        sb.setFixedWidth(330)
        sb.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Ignored)

        lay = QVBoxLayout(sb)
        lay.setContentsMargins(0, 0, 0, 0)
        lay.setSpacing(0)

        # Logo
        logo_frame = QFrame()
        logo_frame.setStyleSheet(f"background: {BG2}; border-bottom: 1px solid {BORDER};")
        lf = QVBoxLayout(logo_frame)
        lf.setContentsMargins(8, 4, 8, 4)
        lf.setSpacing(0)

        logo = QLabel()
        logo.setAlignment(Qt.AlignCenter)
        logo_path = Path(__file__).resolve().parent / "logo.png"
        logo_pixmap = QPixmap(str(logo_path))
        if not logo_pixmap.isNull():
            logo.setPixmap(
                logo_pixmap.scaled(
                    190, 125, Qt.KeepAspectRatio, Qt.SmoothTransformation
                )
            )
            logo.setFixedHeight(125)
        else:
            # Fallback para manter o cabeçalho utilizável se o PNG não estiver
            # na mesma pasta do arquivo Python.
            logo.setObjectName("AppTitle")
            logo.setText("NEONWAVE")
            logo.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
        lf.addWidget(logo)
        lay.addWidget(logo_frame)

        # Ações
        lay.addSpacing(6)
        sec = QLabel("AÇÕES")
        sec.setObjectName("SectionLabel")
        lay.addWidget(sec)

        for label, slot, icon_kind in [
            ("Adicionar Arquivo", self._add_files, 'file'),
            ("Baixar do YouTube", self._add_youtube, 'download'),
            ("Fila Premium", self._open_download_queue, 'queue'),
            ("Abrir YouTube", self._open_browser, 'browser'),
            ("Configuração", self._open_settings, 'settings'),
            ("Minha conta", self._open_account, 'account'),
            ("Ativar Premium", self._open_premium_activation, 'premium'),
        ]:
            btn = QPushButton(label)
            btn.setObjectName("SideBtn")
            btn.setIcon(_make_menu_icon(icon_kind))
            btn.setIconSize(QSize(20, 20))
            btn.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
            btn.setFixedHeight(40)
            btn.clicked.connect(slot)
            lay.addWidget(btn)
            if icon_kind == 'premium':
                self._premium_btn = btn

        self._mode_lbl = QLabel()
        self._mode_lbl.setStyleSheet(f"color: {GREEN}; font-size: 11px; padding: 5px 24px 2px;")
        lay.addWidget(self._mode_lbl)
        self._quota_lbl = QLabel()
        self._quota_lbl.setWordWrap(True)
        self._quota_lbl.setStyleSheet(
            f"color: {TEXT3}; font-size: 11px; padding: 2px 24px 5px;"
        )
        lay.addWidget(self._quota_lbl)
        self._ad_lbl = AdLabel()
        self._ad_lbl.setWordWrap(True)
        self._ad_lbl.setAlignment(Qt.AlignCenter)
        self._ad_lbl.setFixedHeight(84)
        self._ad_lbl.setStyleSheet(
            f"color: {TEXT2}; background: {BG3}; border: 1px solid {BORDER}; "
            "border-radius: 8px; padding: 7px 10px; margin: 3px 14px 6px;"
        )
        lay.addWidget(self._ad_lbl)
        self._refresh_account_badges()

        lay.addSpacing(4)
        sec2 = QLabel("FILA DE REPRODUÇÃO")
        sec2.setObjectName("SectionLabel")
        lay.addWidget(sec2)

        filter_row = QWidget()
        filter_lay = QHBoxLayout(filter_row)
        filter_lay.setContentsMargins(14, 0, 14, 0)
        filter_lay.setSpacing(6)

        self._library_filter_combo = QComboBox()
        popup = self._library_filter_combo.view().window()
        if isinstance(popup, QFrame):
            popup.setFrameShape(QFrame.NoFrame)
            popup.setFrameShadow(QFrame.Plain)
            popup.setLineWidth(0)
            popup.setMidLineWidth(0)
        self._library_filter_combo.setSizePolicy(
            QSizePolicy.Expanding, QSizePolicy.Fixed
        )
        self._library_filter_combo.addItem("Todas as faixas", "Todas")
        self._library_filter_combo.addItem(
            _make_star_icon(False, ACCENT), "Favoritos", "Favoritos"
        )
        self._library_filter_combo.currentIndexChanged.connect(
            self._on_library_filter_changed
        )
        self._library_filter_combo.setToolTip(
            "Favoritos, pastas e playlists são recursos Premium."
        )
        filter_lay.addWidget(self._library_filter_combo, 1)

        self._playlist_create_btn = QPushButton("＋ Playlist")
        self._playlist_create_btn.setObjectName("PlaylistCreateBtn")
        self._playlist_create_btn.setToolTip(
            "Criar uma playlist personalizada (Premium)"
        )
        self._playlist_create_btn.clicked.connect(self._create_playlist)
        filter_lay.addWidget(self._playlist_create_btn)

        self._playlist_delete_btn = QPushButton("−")
        self._playlist_delete_btn.setObjectName("PlaylistDeleteBtn")
        self._playlist_delete_btn.setToolTip("Remover a playlist selecionada")
        self._playlist_delete_btn.setEnabled(False)
        self._playlist_delete_btn.clicked.connect(self._delete_playlist)
        filter_lay.addWidget(self._playlist_delete_btn)
        lay.addWidget(filter_row)

        # Área scrollable de faixas
        scroll = QScrollArea()
        scroll.setWidgetResizable(True)
        scroll.setMinimumSize(0, 0)
        scroll.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Ignored)
        scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        scroll.setStyleSheet("QScrollArea { border: none; }")

        self._track_container = QWidget()
        self._track_container.setMinimumSize(0, 0)
        self._track_container.setSizePolicy(
            QSizePolicy.Expanding, QSizePolicy.MinimumExpanding
        )
        self._track_layout    = QVBoxLayout(self._track_container)
        self._track_layout.setContentsMargins(0, 4, 0, 0)
        self._track_layout.setSpacing(0)
        self._track_layout.addStretch()
        scroll.setWidget(self._track_container)
        lay.addWidget(scroll, 1)

        # Contador
        self._count_lbl = QLabel("0 faixas")
        self._count_lbl.setStyleSheet(f"color: {TEXT3}; font-size: 11px; padding: 8px 24px;")
        lay.addWidget(self._count_lbl)

        return sb

    def _on_library_filter_changed(self, index):
        self._library_filter = (
            self._library_filter_combo.itemData(index) or 'Todas'
        )
        if (
            self._library_filter != 'Todas'
            and not self.settings.get('premium')
        ):
            self._require_premium(
                "Favoritos, organização por pastas e playlists são funções exclusivas do Premium."
            )
            self._library_filter = 'Todas'
            self._library_filter_combo.blockSignals(True)
            self._library_filter_combo.setCurrentIndex(0)
            self._library_filter_combo.blockSignals(False)
            return
        self._playlist_delete_btn.setEnabled(
            self._library_filter.startswith('playlist:')
        )
        self._refresh_tracks()

    def _refresh_library_filters(self):
        if not hasattr(self, '_library_filter_combo'):
            return
        current = self._library_filter
        self._library_filter_combo.blockSignals(True)
        self._library_filter_combo.clear()
        self._library_filter_combo.addItem("Todas as faixas", "Todas")
        self._library_filter_combo.addItem(
            _make_star_icon(False, ACCENT), "Favoritos", "Favoritos"
        )
        folders = sorted({
            song.folder for song in self.playlist
            if str(song.folder or 'Geral').strip()
        }, key=str.casefold)
        for folder in folders:
            self._library_filter_combo.addItem(
                _make_folder_icon(ACCENT2), folder, folder
            )
        for playlist_name in sorted(self.playlists, key=str.casefold):
            self._library_filter_combo.addItem(
                _make_playlist_icon(ACCENT), playlist_name,
                f"playlist:{playlist_name}"
            )
        idx = self._library_filter_combo.findData(current)
        self._library_filter_combo.setCurrentIndex(idx if idx >= 0 else 0)
        self._library_filter_combo.blockSignals(False)
        self._library_filter = (
            self._library_filter_combo.currentData() or 'Todas'
        )
        self._playlist_delete_btn.setEnabled(
            self._library_filter.startswith('playlist:')
        )

    def _load_remote_ad(self):
        if self._ad_worker is not None:
            try:
                if self._ad_worker.isRunning():
                    return
            except RuntimeError:
                self._ad_worker = None
        self._ad_worker = AdConfigWorker(self)
        self._ad_worker.loaded.connect(self._apply_remote_ad)
        self._ad_worker.finished.connect(self._on_ad_worker_finished)
        self._ad_worker.start()

    def _on_ad_worker_finished(self):
        worker = self.sender()
        if worker is self._ad_worker:
            self._ad_worker = None
        if worker is not None:
            worker.deleteLater()

    def _apply_remote_ad(self, config, image_data):
        self._ad_config = config if isinstance(config, dict) else {}
        self._ad_image_data = image_data or b''
        signature = json.dumps(
            {
                'enabled': bool(self._ad_config.get('enabled', True)),
                'show_free': self._ad_config.get('show_free'),
                'show_premium': self._ad_config.get('show_premium'),
                'text': str(self._ad_config.get('text') or ''),
                'link_url': str(self._ad_config.get('link_url') or ''),
                'image_url': str(self._ad_config.get('image_url') or '')
            },
            ensure_ascii=False,
            sort_keys=True
        )
        self._ad_signature = signature

        pixmap = QPixmap()
        if image_data:
            pixmap.loadFromData(image_data)
        if not pixmap.isNull():
            pixmap = pixmap.scaled(
                224, 68, Qt.KeepAspectRatio, Qt.SmoothTransformation
            )
            self._ad_lbl.setText('')
            self._ad_lbl.setPixmap(pixmap)
            self._ad_lbl.setToolTip(
                str(self._ad_config.get('text') or self.settings.get('ad_text', ''))
            )
        else:
            self._ad_lbl.setPixmap(QPixmap())
            self._ad_lbl.setText('')
            self._ad_lbl.setToolTip('')
        self._ad_lbl.set_link_url(self._ad_config.get('link_url'))
        self._refresh_account_badges()

    def _show_ad_popup(self, config, image_data):
        if self._ad_popup is not None:
            self._ad_popup.close()
        self._ad_popup = AdPopup(config, image_data, self)
        self._ad_popup.finished.connect(self._on_ad_popup_finished)
        self._ad_popup.open()
        QTimer.singleShot(0, self._raise_ad_popup)

    def _raise_ad_popup(self):
        if self._ad_popup is not None:
            self._ad_popup.showNormal()
            self._ad_popup.raise_()
            self._ad_popup.activateWindow()

    def _on_ad_popup_finished(self):
        self._ad_popup = None

    def _should_show_remote_ad(self):
        is_premium = bool(self.settings.get('premium'))
        if (
            'show_free' in self._ad_config
            or 'show_premium' in self._ad_config
        ):
            audience_key = 'show_premium' if is_premium else 'show_free'
            return bool(self._ad_config.get(audience_key, False))

        # Compatibilidade com configurações antigas do servidor.
        return (
            bool(self._ad_config.get(
                'enabled', self.settings.get('ad_enabled', True)
            ))
            and not is_premium
        )

    def _maybe_show_ad_popup(self):
        if self._ad_signature is None:
            return
        if not self.isVisible():
            return

        display_key = (
            self._ad_signature,
            bool(self.settings.get('premium'))
        )
        show_ad = self._should_show_remote_ad()
        if show_ad and display_key != self._ad_display_key:
            self._ad_display_key = display_key
            self._show_ad_popup(self._ad_config, self._ad_image_data)
        elif not show_ad and self._ad_popup is not None:
            self._ad_popup.close()
            self._ad_popup = None
            self._ad_display_key = display_key
        else:
            self._ad_display_key = display_key

    def _show_ad_for_track(self):
        """Exibe a propaganda novamente sempre que uma faixa começa."""
        if self._ad_signature is None:
            return
        self._ad_display_key = None
        self._maybe_show_ad_popup()

    def _refresh_account_badges(self):
        premium_active = bool(self.settings.get('premium'))
        if self._premium_btn is not None:
            if premium_active:
                self._premium_btn.setText("Premium ATIVO")
                self._premium_btn.setIcon(_make_menu_icon('premium', LIME))
                self._premium_btn.setStyleSheet(
                    f"""
                    QPushButton#SideBtn {{
                        background: rgba(183, 255, 60, 0.10);
                        color: {LIME};
                        text-align: left;
                        padding: 9px 18px;
                        font-size: 13px;
                        font-weight: 800;
                        border-radius: 10px;
                        border: 1px solid rgba(183, 255, 60, 0.38);
                        margin: 2px 12px;
                    }}
                    QPushButton#SideBtn:hover {{
                        background: rgba(183, 255, 60, 0.18);
                        color: {LIME};
                        border: 1px solid {LIME};
                    }}
                    QPushButton#SideBtn:disabled {{
                        background: rgba(183, 255, 60, 0.10);
                        color: {LIME};
                        border: 1px solid rgba(183, 255, 60, 0.38);
                    }}
                    """
                )
                self._premium_btn.setEnabled(False)
                self._premium_btn.setToolTip("O Premium já está ativo.")
            else:
                self._premium_btn.setText("Ativar Premium")
                self._premium_btn.setIcon(_make_menu_icon('premium'))
                self._premium_btn.setStyleSheet("")
                self._premium_btn.setEnabled(True)
                self._premium_btn.setToolTip("Ativar Premium")

        if self.settings.get('child_mode'):
            self._mode_lbl.setText("MODO INFANTIL ATIVO")
            self._mode_lbl.show()
        elif premium_active:
            self._mode_lbl.clear()
            self._mode_lbl.hide()
        else:
            self._mode_lbl.setText("CONTA GRATUITA")
            self._mode_lbl.show()
        if premium_active:
            self._quota_lbl.setText("Downloads ilimitados • recursos Premium")
            self._quota_lbl.setStyleSheet(
                f"color: {LIME}; font-size: 11px; padding: 2px 24px 5px;"
            )
        else:
            used = self._get_saved_youtube_count()
            remaining = max(FREE_DOWNLOAD_LIMIT - used, 0)
            if remaining == 0:
                quota_text = "Limite gratuito atingido • Premium ilimitado"
                quota_color = RED
            elif remaining <= 2:
                quota_text = f"Atenção: restam {remaining} download(s) gratuito(s)"
                quota_color = ACCENT
            else:
                quota_text = (
                    f"Downloads gratuitos: {used}/{FREE_DOWNLOAD_LIMIT}"
                )
                quota_color = TEXT3
            self._quota_lbl.setText(quota_text)
            self._quota_lbl.setStyleSheet(
                f"color: {quota_color}; font-size: 11px; padding: 2px 24px 5px;"
            )
        show_ad = self._should_show_remote_ad()
        self._ad_lbl.setVisible(show_ad)
        if show_ad:
            remote_text = str(self._ad_config.get('text') or '').strip()
            local_text = self.settings.get(
                'ad_text', 'Desfrute do NeonWave sem anúncios com o Premium.'
            )
            if self._ad_lbl.pixmap() is None or self._ad_lbl.pixmap().isNull():
                self._ad_lbl.setText(
                    "PROPAGANDA\n" + (remote_text or local_text)
                )
            self._ad_lbl.set_link_url(self._ad_config.get('link_url'))
        self._update_premium_countdown()
        self._maybe_show_ad_popup()

    def _update_premium_countdown(self):
        label = self._premium_expiry_lbl
        if label is None:
            return
        if not self.settings.get('premium'):
            label.hide()
            return

        try:
            expires_at = float(self.settings.get('premium_expires_at') or 0)
            server_offset = float(self.settings.get('premium_server_offset') or 0)
        except (TypeError, ValueError):
            expires_at = 0
            server_offset = 0

        if expires_at <= 0:
            label.setText("PREMIUM ATIVO")
            label.show()
            return

        remaining = expires_at - (time.time() + server_offset)
        if remaining <= 0:
            self.settings['premium'] = False
            self.settings['premium_expires_at'] = ''
            self.settings['premium_plan'] = ''
            save_settings(self.settings)
            self._refresh_account_badges()
            self._stop_locked_current_song()
            self._refresh_tracks()
            return

        total_seconds = int(remaining)
        days, rest = divmod(total_seconds, 86400)
        hours, rest = divmod(rest, 3600)
        minutes, seconds = divmod(rest, 60)
        plan = self.settings.get('premium_plan') or 'Premium'
        label.setText(
            f"{plan}: {days}d {hours:02d}:{minutes:02d}:{seconds:02d}"
        )
        label.show()

    def _open_browser(self):
        if self.settings.get('child_mode'):
            QMessageBox.information(self, "Modo infantil", "O navegador está bloqueado no modo infantil.")
            return
        browser = BrowserDialog(self)
        browser.download_requested.connect(self._download_from_browser)
        browser.exec_()

    def _expand_playlist_url(self, url):
        try:
            result = subprocess.run(
                [
                    sys.executable, '-m', 'yt_dlp', '--ignore-config',
                    '--flat-playlist', '--print', '%(webpage_url)s',
                    '--no-warnings', '--no-check-certificates',
                    '--force-ipv4', *YTDLP_COMMON_ARGS,
                    *YTDLP_JS_RUNTIME_ARGS, '--yes-playlist', url
                ],
                capture_output=True,
                text=True,
                encoding='utf-8',
                errors='replace',
                timeout=180
            )
            urls = []
            for line in result.stdout.splitlines():
                value = _normalize_download_url(line)
                if value.startswith(('http://', 'https://')) and value not in urls:
                    urls.append(value)
            return urls or [url]
        except (OSError, subprocess.SubprocessError):
            return [url]

    def _open_download_queue(self):
        if not self._require_premium(
            "A fila de downloads e o download de playlists são exclusivos do Premium."
        ):
            return
        dlg = DownloadQueueDialog(self)
        if dlg.exec_() != QDialog.Accepted:
            return
        urls = dlg.urls()
        if not urls:
            QMessageBox.warning(
                self, "Fila vazia", "Informe pelo menos um link válido."
            )
            return

        expanded_urls = []
        for url in urls:
            for item in self._expand_playlist_url(url):
                if item not in expanded_urls:
                    expanded_urls.append(item)

        completed = 0
        for url in expanded_urls:
            download_dlg = DownloadDialog(
                self, premium=True
            )
            download_dlg.url_input.setText(url)
            download_dlg.rb_video.setChecked(True)
            download_dlg.rb_audio.setChecked(False)
            if download_dlg.exec_() != QDialog.Accepted:
                break
            self._add_download_result(download_dlg.get_result())
            completed += 1

        if completed:
            QMessageBox.information(
                self,
                "Fila concluída",
                f"{completed} download(s) foram adicionados à biblioteca."
            )

    def _get_saved_youtube_count(self):
        return sum(1 for song in self.playlist if song.source == 'youtube')

    def _can_download_youtube(self):
        if self.settings.get('premium'):
            return True

        download_count = self._get_saved_youtube_count()

        if download_count < FREE_DOWNLOAD_LIMIT:
            return True

        self._show_free_limit_dialog()
        return False

    def _show_free_limit_dialog(self, detail=''):
        dlg = FreeLimitDialog(detail, self)
        if dlg.exec_() == FreeLimitDialog.PREMIUM_REQUESTED:
            self._open_premium_activation()

    def _download_from_browser(self, url, is_video):
        if not self._can_download_youtube():
            return
        dlg = DownloadDialog(self, premium=self.settings.get('premium', False))
        dlg.url_input.setText(url)
        dlg.rb_video.setChecked(is_video)
        dlg.rb_audio.setChecked(not is_video)
        if dlg.exec_() == QDialog.Accepted:
            self._add_download_result(dlg.get_result())

    def _open_settings(self):
        dlg = SettingsDialog(self)
        dlg.controls_requested.connect(self._open_parent_controls)
        dlg.check_update_requested.connect(self._check_for_update)
        dlg.install_update_requested.connect(self._install_update)
        dlg.exec_()

    def _open_account(self):
        dlg = AccountDialog(
            self.settings,
            FREE_DOWNLOAD_LIMIT,
            self._get_saved_youtube_count(),
            self
        )
        dlg.activation_requested.connect(self._open_premium_activation)
        dlg.exec_()

    def _check_for_update(self):
        if getattr(self, '_update_worker', None) is not None:
            try:
                if self._update_worker.isRunning():
                    return
            except RuntimeError:
                self._update_worker = None
        self._update_worker = UpdateWorker(parent=self)
        self._update_worker.checked.connect(self._on_update_checked)
        self._update_worker.failed.connect(self._on_update_failed)
        self._update_worker.finished.connect(self._on_update_worker_finished)
        self._update_worker.start()

    def _install_update(self, manifest=None):
        if getattr(self, '_update_worker', None) is not None:
            try:
                if self._update_worker.isRunning():
                    return
            except RuntimeError:
                self._update_worker = None
        self._update_worker = UpdateWorker(
            install=True,
            manifest=manifest or getattr(self, '_last_update_manifest', None),
            parent=self
        )
        self._update_worker.updated.connect(self._on_update_installed)
        self._update_worker.failed.connect(self._on_update_failed)
        self._update_worker.finished.connect(self._on_update_worker_finished)
        self._update_worker.start()

    def _on_update_checked(self, available, message, manifest):
        self._last_update_manifest = (
            manifest if isinstance(manifest, dict) else None
        )
        if not available:
            UpdateNoticeDialog(
                "Atualização", message, kind='info', parent=self
            ).exec_()
            return
        answer = UpdateNoticeDialog(
            "Atualização disponível",
            f"{message}\n\nDeseja baixar e instalar agora?",
            kind='info',
            question=True,
            parent=self
        ).exec_()
        if answer == QDialog.Accepted:
            self._install_update(self._last_update_manifest)

    def _on_update_installed(self, message):
        UpdateNoticeDialog(
            "Atualização concluída", message, kind='success', parent=self
        ).exec_()

    def _on_update_failed(self, message):
        UpdateNoticeDialog(
            "Atualização indisponível",
            "Não foi possível verificar ou instalar a atualização.\n\n"
            f"{message}",
            kind='warning',
            parent=self
        ).exec_()

    def _on_update_worker_finished(self):
        worker = self.sender()
        if worker is self._update_worker:
            self._update_worker = None
        if worker is not None:
            worker.deleteLater()

    def _apply_theme_live(self, theme_key):
        theme_key = _theme_for_account(
            theme_key, self.settings.get('premium', False)
        )
        if theme_key == self._theme_key:
            return

        old_theme = THEMES.get(self._theme_key, THEMES['neonwave'])
        new_theme = THEMES[theme_key]
        _apply_theme_colors(theme_key, self.settings.get('premium', False))
        self._theme_key = theme_key
        self.settings['theme'] = theme_key
        save_settings(self.settings)

        color_names = (
            'BG', 'BG2', 'BG3', 'BG4', 'ACCENT', 'ACCENT2', 'ACCENT3',
            'TEXT', 'TEXT2', 'TEXT3', 'BORDER', 'PANEL_BORDER', 'RED',
            'GREEN', 'LIME'
        )
        widgets = [self] + self.findChildren(QWidget)
        for widget in widgets:
            style = widget.styleSheet()
            if not style:
                continue
            for name in color_names:
                style = style.replace(old_theme[name], new_theme[name])
            style = style.replace('#FF7A59', new_theme['ACCENT2'])
            style = style.replace('#FFB5CF', new_theme['ACCENT3'])
            style = style.replace('#FF9F43', new_theme['ACCENT2'])
            widget.setStyleSheet(style)

        self.setStyleSheet(_build_stylesheet())
        self._refresh_tracks()
        self._refresh_account_badges()
        if 0 <= self.current_index < len(self.playlist):
            song = self.playlist[self.current_index]
            self.title_lbl.setText(song.title)
            self.src_lbl.setText(
                "YouTube" if song.source == 'youtube' else "Arquivo local"
            )
            self._update_mini_thumb(song)
        self.update()
        QApplication.processEvents()

    def _open_parent_controls(self):
        pin = self.settings.get('parent_pin', '')
        if pin:
            entered, ok = QInputDialog.getText(self, "Acesso dos pais", "Digite o PIN:", QLineEdit.Password)
            if not ok or entered != pin:
                if ok:
                    QMessageBox.warning(self, "Acesso negado", "PIN incorreto.")
                return
        dlg = ParentDialog(self.settings, self)
        if dlg.exec_() == QDialog.Accepted:
            self._refresh_account_badges()
            self._refresh_tracks()

    def _require_premium(self, detail):
        if self.settings.get('premium'):
            return True
        if PremiumPromptDialog(detail, self).exec_() == QDialog.Accepted:
            self._open_premium_activation()
        return False

    @staticmethod
    def _song_key(song):
        return str(song.path or '').strip()

    def _create_playlist(self, song_idx=None):
        if not self._require_premium(
            "Criar playlists personalizadas é uma função exclusiva do Premium."
        ):
            return

        name, ok = QInputDialog.getText(
            self,
            "Criar playlist",
            "Nome da playlist:",
            QLineEdit.Normal,
            ""
        )
        if not ok:
            return

        name = ' '.join(name.strip().split())[:80]
        if not name:
            QMessageBox.warning(
                self, "Nome obrigatório",
                "Digite um nome para criar a playlist."
            )
            return
        if any(existing.casefold() == name.casefold() for existing in self.playlists):
            QMessageBox.warning(
                self, "Playlist já existe",
                "Escolha outro nome para a nova playlist."
            )
            return

        self.playlists[name] = []
        if 0 <= (song_idx if song_idx is not None else -1) < len(self.playlist):
            self.playlists[name].append(
                self._song_key(self.playlist[song_idx])
            )
        save_playlists(self.playlists)
        self._refresh_library_filters()

        playlist_key = f"playlist:{name}"
        selected = self._library_filter_combo.findData(playlist_key)
        if selected >= 0:
            self._library_filter_combo.setCurrentIndex(selected)
        else:
            self._library_filter = playlist_key
            self._refresh_tracks()

    def _delete_playlist(self):
        if not self._require_premium(
            "Remover playlists personalizadas é uma função exclusiva do Premium."
        ):
            return

        playlist_key = str(self._library_filter or '')
        if not playlist_key.startswith('playlist:'):
            return

        name = playlist_key[len('playlist:'):]
        if name not in self.playlists:
            return

        if PlaylistDeleteDialog(name, self).exec_() != QDialog.Accepted:
            return

        del self.playlists[name]
        save_playlists(self.playlists)
        self._library_filter = 'Todas'
        self._refresh_library_filters()
        self._refresh_tracks()

    def _add_song_to_playlist(self, idx):
        if not self._require_premium(
            "Adicionar faixas a playlists é uma função exclusiva do Premium."
        ):
            return
        if not (0 <= idx < len(self.playlist)):
            return

        names = sorted(self.playlists, key=str.casefold)
        if not names:
            if PlaylistCreatePromptDialog(self).exec_() == QDialog.Accepted:
                self._create_playlist(idx)
            return

        name, ok = QInputDialog.getItem(
            self,
            "Adicionar à playlist",
            "Escolha uma playlist:",
            names,
            0,
            False
        )
        if not ok:
            return

        items = self.playlists.setdefault(name, [])
        key = self._song_key(self.playlist[idx])
        if key in items:
            QMessageBox.information(
                self, "Faixa já adicionada",
                "Essa faixa já está nessa playlist."
            )
            return
        items.append(key)
        save_playlists(self.playlists)
        QMessageBox.information(
            self, "Playlist atualizada",
            f'"{self.playlist[idx].title}" foi adicionada à playlist "{name}".'
        )

    def _toggle_favorite(self, idx):
        if not self._require_premium(
            "Favoritos são exclusivos da conta Premium."
        ):
            return
        if not (0 <= idx < len(self.playlist)):
            return
        self.playlist[idx].favorite = not self.playlist[idx].favorite
        self._save_playlist()
        self._refresh_tracks()

    def _choose_song_folder(self, idx):
        if not self._require_premium(
            "A organização por pastas é exclusiva da conta Premium."
        ):
            return
        if not (0 <= idx < len(self.playlist)):
            return
        song = self.playlist[idx]
        folder, ok = QInputDialog.getText(
            self,
            "Organizar faixa",
            "Nome da pasta:",
            QLineEdit.Normal,
            song.folder or "Geral"
        )
        if not ok:
            return
        folder = folder.strip() or "Geral"
        song.folder = folder[:80]
        self._save_playlist()
        self._refresh_library_filters()
        self._refresh_tracks()

    def _open_premium_activation(self):
        if self.settings.get('premium'):
            return

        dlg = PremiumActivationDialog(
            self.settings.get('premium_serial', ''),
            self
        )
        if dlg.exec_() != QDialog.Accepted:
            return

        serial = dlg.serial()
        if not serial:
            QMessageBox.warning(self, "Serial inválido", "Digite um serial para continuar.")
            return

        try:
            response = requests.post(
                PREMIUM_VALIDATE_URL,
                json={'serial': serial},
                headers=PLAYER_HTTP_HEADERS,
                timeout=15
            )
            try:
                payload = response.json()
            except ValueError:
                payload = {}
        except requests.RequestException as exc:
            QMessageBox.critical(
                self,
                "Falha na validação",
                "Não foi possível consultar o servidor de licenças.\n\n"
                f"Detalhes: {exc}"
            )
            return

        if response.status_code != 200 or not payload.get('valid'):
            QMessageBox.warning(
                self,
                "Serial recusado",
                str(payload.get('message', 'Serial inválido ou expirado.'))
            )
            return

        self.settings['premium'] = True
        self.settings['premium_serial'] = serial
        self.settings['premium_plan'] = payload.get('plan_label') or payload.get('plan') or ''
        self.settings['premium_expires_at'] = float(
            payload.get('expires_at_unix') or 0
        )
        self.settings['premium_server_offset'] = float(
            payload.get('server_time_unix') or time.time()
        ) - time.time()
        save_settings(self.settings)
        self._refresh_account_badges()
        self._refresh_tracks()
        PremiumSuccessDialog(
            str(payload.get('message', 'Premium ativado com sucesso.')),
            self
        ).exec_()

    def _build_main(self):
        main = QFrame()
        ml   = QVBoxLayout(main)
        ml.setContentsMargins(0, 0, 0, 0)
        ml.setSpacing(0)

        # ── Arte / Vídeo ──
        self.media_stack = QStackedWidget()

        # Página 0: ArtDisplay
        self.art_widget = ArtDisplay()
        self.media_stack.addWidget(self.art_widget)

        # Página 1: VideoWidget (DirectShow)
        self.video_widget = QVideoWidget()
        self.video_widget.setStyleSheet("background: black;")
        self.media_stack.addWidget(self.video_widget)

        # Página 2: WebEngine
        if HAS_WEBENGINE:
            self.web_view = QWebEngineView()
            self.web_view.setStyleSheet("background: black;")
            s = self.web_view.settings()
            s.setAttribute(QWebEngineSettings.PluginsEnabled, True)
            s.setAttribute(QWebEngineSettings.PlaybackRequiresUserGesture, False)
            s.setAttribute(QWebEngineSettings.LocalContentCanAccessFileUrls, True)
            s.setAttribute(QWebEngineSettings.AllowRunningInsecureContent, True)
        else:
            self.web_view = QLabel("Instale PyQtWebEngine para suporte a vídeo robusto.")
            self.web_view.setAlignment(Qt.AlignCenter)
            self.web_view.setStyleSheet("color: #666; background: black;")
        self.media_stack.addWidget(self.web_view)

        self._fullscreen_exit_btn = QPushButton("⛶", self.media_stack)
        self._fullscreen_exit_btn.setObjectName("CtrlBtn")
        self._fullscreen_exit_btn.setFixedSize(44, 44)
        self._fullscreen_exit_btn.setToolTip("Sair da tela cheia")
        self._fullscreen_exit_btn.clicked.connect(self._exit_fullscreen)
        self._fullscreen_exit_btn.hide()
        self._fullscreen_exit_btn.raise_()

        ml.addWidget(self.media_stack, 1)

        # ── Painel inferior ──
        panel = QFrame()
        panel.setObjectName("NowPlayingPanel")
        panel.setFixedHeight(170)
        pl = QVBoxLayout(panel)
        pl.setContentsMargins(24, 3, 24, 3)
        pl.setSpacing(1)

        # Info da faixa
        info_row = QHBoxLayout()
        info_row.setSpacing(12)

        self._thumb_mini = QLabel()
        self._thumb_mini.setFixedSize(40, 40)
        self._thumb_mini.setAlignment(Qt.AlignCenter)
        self._thumb_mini.setStyleSheet(f"background: {BG4}; border-radius: 8px; color: {TEXT3}; font-size: 20px;")
        self._thumb_mini.setText("♪")
        info_row.addWidget(self._thumb_mini)

        titles = QVBoxLayout()
        titles.setSpacing(1)
        self.title_lbl = QLabel("Nenhuma faixa")
        self.title_lbl.setStyleSheet(
            f"background: transparent; color: {TEXT}; font-size: 15px; font-weight: 700;")
        self.title_lbl.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
        self.src_lbl = QLabel("—")
        self.src_lbl.setStyleSheet(
            f"background: transparent; color: {TEXT2}; font-size: 12px;")
        titles.addWidget(self.title_lbl)
        titles.addWidget(self.src_lbl)
        info_row.addLayout(titles, 1)

        # Shuffle & Repeat (lado direito)
        self._shuf_btn = QPushButton("⇄")
        self._shuf_btn.setObjectName("CtrlBtn")
        self._shuf_btn.setCheckable(True)
        self._shuf_btn.setFixedSize(30, 30)
        self._shuf_btn.setToolTip("Ativar ou desativar reprodução aleatória")
        self._shuf_btn.clicked.connect(self._toggle_shuffle)
        self._rep_btn = QPushButton("↺")
        self._rep_btn.setObjectName("CtrlBtn")
        self._rep_btn.setCheckable(True)
        self._rep_btn.setFixedSize(30, 30)
        self._rep_btn.setToolTip("Ativar ou desativar repetição")
        self._rep_btn.clicked.connect(self._toggle_repeat)
        self._fullscreen_btn = QPushButton("⛶")
        self._fullscreen_btn.setObjectName("CtrlBtn")
        self._fullscreen_btn.setFixedSize(30, 30)
        self._fullscreen_btn.setToolTip("Tela cheia (F11)")
        self._fullscreen_btn.setEnabled(False)
        self._fullscreen_btn.clicked.connect(self._toggle_fullscreen)
        info_row.addWidget(self._shuf_btn)
        info_row.addWidget(self._rep_btn)
        info_row.addWidget(self._fullscreen_btn)

        pl.addLayout(info_row)

        # Seek bar
        seek_row = QHBoxLayout()
        seek_row.setSpacing(10)
        self.lbl_curr = QLabel("0:00")
        self.lbl_curr.setStyleSheet(
            f"background: transparent; color: {TEXT2}; font-size: 11px;")
        self.lbl_curr.setFixedWidth(35)
        self.seek = ClickSlider(Qt.Horizontal)
        self.seek.setRange(0, 1000)
        self.seek.setFixedHeight(14)
        self.seek.setToolTip("Avançar ou voltar na faixa")
        self.seek.sliderPressed.connect(lambda: setattr(self, '_is_seeking', True))
        self.seek.sliderReleased.connect(self._on_seek_release)
        self.lbl_tot = QLabel("0:00")
        self.lbl_tot.setStyleSheet(
            f"background: transparent; color: {TEXT2}; font-size: 11px;")
        self.lbl_tot.setFixedWidth(35)
        seek_row.setContentsMargins(0, 1, 0, 1)
        seek_row.addWidget(self.lbl_curr)
        seek_row.addWidget(self.seek, 1)
        seek_row.addWidget(self.lbl_tot)
        pl.addLayout(seek_row)

        # Controles centrais + volume
        # Três colunas deixam o transporte no centro real do painel,
        # independentemente da largura do volume ou do status Premium.
        ctrl_row = QGridLayout()
        ctrl_row.setContentsMargins(0, 0, 0, 0)
        ctrl_row.setHorizontalSpacing(4)
        ctrl_row.setColumnStretch(0, 1)
        ctrl_row.setColumnStretch(2, 1)

        # Volume
        vol_icon = QLabel("♪")
        vol_icon.setStyleSheet(
            f"background: transparent; color: {TEXT2}; font-size: 14px;")
        self.vol_slider = ClickSlider(Qt.Horizontal)
        self.vol_slider.setObjectName("VolumeSlider")
        self.vol_slider.setRange(0, 100)
        self.vol_slider.setValue(40)
        self.vol_slider.setFixedWidth(90)
        self.vol_slider.setToolTip("Ajustar volume")
        self.vol_slider.valueChanged.connect(self._set_volume)

        volume_box = QWidget()
        volume_box.setAttribute(Qt.WA_TranslucentBackground, True)
        volume_lay = QHBoxLayout(volume_box)
        volume_lay.setContentsMargins(0, 0, 0, 0)
        volume_lay.setSpacing(4)
        volume_lay.addWidget(vol_icon)
        volume_lay.addWidget(self.vol_slider)
        ctrl_row.addWidget(volume_box, 0, 0, Qt.AlignLeft | Qt.AlignVCenter)

        for symbol, slot, size in [
            ("⏮", self._prev, (38, 38)),
            ("⏭", self._next, (38, 38)),
        ]:
            pass  # we'll add these below with play btn in center

        # Prev / Play / Next centered
        self.btn_prev = QPushButton()
        self.btn_prev.setObjectName("CtrlBtn")
        self.btn_prev.setIcon(_make_skip_icon())
        self.btn_prev.setIconSize(QSize(18, 18))
        self.btn_prev.setFixedSize(30, 30)
        self.btn_prev.setToolTip("Faixa anterior")
        self.btn_prev.clicked.connect(self._prev)

        self.btn_play = QPushButton()
        self.btn_play.setObjectName("PlayBtn")
        self.btn_play.setFixedSize(40, 40)
        self.btn_play.setIcon(_make_play_icon())
        self.btn_play.setIconSize(QSize(24, 24))
        self.btn_play.setToolTip("Reproduzir ou pausar")
        self.btn_play.clicked.connect(self._play_pause)

        self.btn_next = QPushButton()
        self.btn_next.setObjectName("CtrlBtn")
        self.btn_next.setIcon(_make_skip_icon(True))
        self.btn_next.setIconSize(QSize(18, 18))
        self.btn_next.setFixedSize(30, 30)
        self.btn_next.setToolTip("Próxima faixa")
        self.btn_next.clicked.connect(self._next)

        transport = QWidget()
        transport.setAttribute(Qt.WA_TranslucentBackground, True)
        transport.setStyleSheet("background: transparent;")
        transport_lay = QHBoxLayout(transport)
        transport_lay.setContentsMargins(0, 0, 0, 0)
        transport_lay.setSpacing(8)
        transport_lay.addWidget(self.btn_prev, 0, Qt.AlignVCenter)
        transport_lay.addWidget(self.btn_play, 0, Qt.AlignVCenter)
        transport_lay.addWidget(self.btn_next, 0, Qt.AlignVCenter)
        ctrl_row.addWidget(transport, 0, 1, Qt.AlignCenter)

        self._premium_expiry_lbl = QLabel()
        self._premium_expiry_lbl.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
        self._premium_expiry_lbl.setMinimumWidth(210)
        self._premium_expiry_lbl.setStyleSheet(
            f"background: transparent; color: #FF9F43; "
            "font-size: 11px; font-weight: 700;"
        )
        self._premium_expiry_lbl.hide()
        ctrl_row.addWidget(
            self._premium_expiry_lbl, 0, 2, Qt.AlignRight | Qt.AlignVCenter
        )

        pl.addLayout(ctrl_row)

        self._now_playing_panel = panel
        ml.addWidget(panel)
        return main

    def _set_play_icon(self, playing):
        self.btn_play.setIcon(_make_play_icon(playing))
        self.btn_play.setText("")

    # ── Player signals ───────────────────────
    def _connect_player(self):
        self.player.mediaStatusChanged.connect(self._on_media_status)
        self.player.positionChanged.connect(self._on_position)
        self.player.durationChanged.connect(self._on_duration)
        self.player.error.connect(self._on_error)
        if HAS_WEBENGINE:
            self.web_view.titleChanged.connect(self._on_web_title)

    # ── Playlist ────────────────────────────
    @staticmethod
    def _is_youtube_song(song):
        return str(getattr(song, 'source', '') or '').strip().casefold() == 'youtube'

    def _is_premium_account(self):
        value = self.settings.get('premium', False)
        if isinstance(value, str):
            value = value.strip().casefold() in {
                '1', 'true', 'sim', 'yes', 'on'
            }
        if value is not True:
            return False

        try:
            expires_at = float(self.settings.get('premium_expires_at') or 0)
            server_offset = float(self.settings.get('premium_server_offset') or 0)
        except (TypeError, ValueError):
            expires_at = 0
            server_offset = 0
        return (
            expires_at <= 0
            or expires_at > time.time() + server_offset
        )

    def _migrate_download_access(self):
        youtube_position = 0
        changed = False
        for song in self.playlist:
            if not self._is_youtube_song(song):
                continue
            premium_only = youtube_position >= FREE_DOWNLOAD_LIMIT
            if getattr(song, 'premium_only', None) != premium_only:
                song.premium_only = premium_only
                changed = True
            youtube_position += 1
        if changed:
            self._save_playlist()

    def _song_requires_premium(self, song, playlist_index=None):
        if self._is_premium_account() or not self._is_youtube_song(song):
            return False

        if playlist_index is not None:
            youtube_position = sum(
                1 for item in self.playlist[:playlist_index]
                if self._is_youtube_song(item)
            )
            return youtube_position >= FREE_DOWNLOAD_LIMIT

        youtube_position = 0
        for item in self.playlist:
            if not self._is_youtube_song(item):
                continue
            if item is song:
                return youtube_position >= FREE_DOWNLOAD_LIMIT
            youtube_position += 1

        return bool(getattr(song, 'premium_only', False))

    def _stop_locked_current_song(self):
        if not (0 <= self.current_index < len(self.playlist)):
            return
        if not self._song_requires_premium(self.playlist[self.current_index]):
            return

        self.current_index = -1
        self.player.stop()
        if HAS_WEBENGINE:
            self._web_active = False
            self._web_poll.stop()
            self.web_view.setHtml("")
        self.art_widget.clear()
        self.title_lbl.setText("Nenhuma faixa")
        self.src_lbl.setText("—")
        self._thumb_mini.setPixmap(QPixmap())
        self._thumb_mini.setText("♪")
        self._set_play_icon(False)
        self.media_stack.setCurrentIndex(0)
        self._refresh_tracks()

    def _load_playlist(self):
        if os.path.exists(PLAYLIST_FILE):
            try:
                data = json.loads(open(PLAYLIST_FILE, encoding='utf-8').read())
                self.playlist = [Song.from_dict(d) for d in data]
                self._migrate_download_access()
                self._refresh_library_filters()
                self._refresh_tracks()
            except:
                pass

    def _save_playlist(self):
        with open(PLAYLIST_FILE, 'w', encoding='utf-8') as f:
            json.dump([s.to_dict() for s in self.playlist], f, ensure_ascii=False, indent=2)

    def _refresh_tracks(self):
        # Remover cards antigos
        while self._track_layout.count() > 1:
            item = self._track_layout.takeAt(0)
            if item.widget():
                item.widget().deleteLater()

        visible = []
        for i, song in enumerate(self.playlist):
            if self._library_filter == 'Favoritos' and not song.favorite:
                continue
            if self._library_filter.startswith('playlist:'):
                playlist_name = self._library_filter[len('playlist:'):]
                if self._song_key(song) not in self.playlists.get(playlist_name, []):
                    continue
            if (
                self._library_filter not in {'Todas', 'Favoritos'}
                and not self._library_filter.startswith('playlist:')
                and song.folder != self._library_filter
            ):
                continue
            visible.append((i, song))

        for i, song in visible:
            card = TrackCard(
                i,
                song,
                is_playing=(i == self.current_index),
                is_locked=self._song_requires_premium(song, i)
            )
            card.clicked.connect(self._play_index)
            card.delete_req.connect(self._delete_song)
            card.favorite_req.connect(self._toggle_favorite)
            card.folder_req.connect(self._choose_song_folder)
            card.playlist_req.connect(self._add_song_to_playlist)
            self._track_layout.insertWidget(self._track_layout.count() - 1, card)

        n = len(visible)
        total = len(self.playlist)
        if n == total:
            self._count_lbl.setText(f"{n} faixa{'s' if n != 1 else ''}")
        else:
            self._count_lbl.setText(
                f"{n} de {total} faixas"
            )

    # ── Reprodução ──────────────────────────
    def _play_index(self, idx):
        if not (0 <= idx < len(self.playlist)):
            return
        song = self.playlist[idx]
        if self._is_premium_account():
            self._update_premium_countdown()
        if self._song_requires_premium(song, idx):
            self._require_premium(
                "Esta faixa foi baixada além das 10 faixas gratuitas. "
                "Ative o Premium para continuar ouvindo."
            )
            return
        self.current_index = idx
        self._fullscreen_btn.setEnabled(bool(song.is_video))
        if not song.is_video and self._is_fullscreen:
            self._exit_fullscreen()

        self.title_lbl.setText(song.title)
        self.src_lbl.setText("YouTube" if song.source == 'youtube' else "Arquivo local")
        self._update_mini_thumb(song)
        self._refresh_tracks()

        # Arte de fundo
        pix = QPixmap()
        if song.thumb and os.path.exists(song.thumb):
            pix = QPixmap(song.thumb)
        if pix.isNull():
            pix = _get_default_art_pixmap()
        if not pix.isNull():
            self.art_widget.set_pixmap(pix)
        else:
            self.art_widget.clear()

        url = QUrl.fromLocalFile(os.path.abspath(song.path))

        if HAS_WEBENGINE:
            self.player.stop()
            self._web_active = True
            media_url = url.toString(QUrl.FullyEncoded)
            if song.is_video:
                body = (f'<video id="m" autoplay preload="auto" playsinline '
                        f'onloadedmetadata="this.muted=false;this.volume={self.vol_slider.value() / 100};this.play()" '
                        f'oncanplay="this.muted=false;this.volume={self.vol_slider.value() / 100};this.play()" '
                        f'style="max-width:100%;max-height:100%;width:100%;height:100%;" '
                        f'src="{media_url}"></video>')
                stack_idx = 2
            else:
                body = (
                    f'<audio id="m" autoplay preload="auto" '
                    f'onloadedmetadata="this.muted=false;this.volume={self.vol_slider.value() / 100};this.play()" '
                    f'oncanplay="this.muted=false;this.volume={self.vol_slider.value() / 100};this.play()" '
                    f'src="{media_url}"></audio>'
                )
                stack_idx = 0
            html = (
                '<!DOCTYPE html><html>'
                '<body style="background:black;margin:0;padding:0;'
                'display:flex;justify-content:center;align-items:center;height:100vh;">'
                + body +
                '<script>var m=document.getElementById("m");'
                'm.muted=false;'
                'm.volume=' + str(self.vol_slider.value() / 100) + ';'
                'm.play().catch(function(){});'
                'm.addEventListener("canplay",function(){'
                'm.muted=false;m.volume=' + str(self.vol_slider.value() / 100) + ';'
                'm.play().catch(function(){});'
                '});'
                'm.addEventListener("ended",function(){document.title="__ENDED__";});'
                '</script></body></html>'
            )
            self.web_view.page().setAudioMuted(False)
            self.web_view.setHtml(
                html,
                QUrl.fromLocalFile(str(MUSIC_DIR.resolve()) + os.sep)
            )
            self.media_stack.setCurrentIndex(stack_idx)
            self._web_poll.start()
            if not song.is_video:
                QTimer.singleShot(
                    350,
                    lambda: self.web_view.page().runJavaScript(
                        "var m=document.getElementById('m');"
                        "if(m){m.muted=false;m.play().catch(function(){});}"
                    )
                )
        else:
            self._web_active = False
            self.player.setVideoOutput(self.video_widget)
            self.player.setVolume(self.vol_slider.value())
            self.player.setMedia(QMediaContent(url))
            self.media_stack.setCurrentIndex(1 if song.is_video else 0)
            self.player.play()

        self._set_play_icon(True)
        self._show_ad_for_track()

    def _update_mini_thumb(self, song):
        pix = QPixmap()
        if song.thumb and os.path.exists(song.thumb):
            pix = QPixmap(song.thumb)
        if pix.isNull():
            pix = _get_default_art_pixmap()
        if not pix.isNull():
            pix = pix.scaled(52, 52, Qt.KeepAspectRatioByExpanding, Qt.SmoothTransformation)
            x = (pix.width() - 52)  // 2
            y = (pix.height() - 52) // 2
            pix = pix.copy(x, y, 52, 52)
            rounded = QPixmap(52, 52)
            rounded.fill(Qt.transparent)
            rp = QPainter(rounded)
            rp.setRenderHint(QPainter.Antialiasing)
            rp.setBrush(QBrush(pix))
            rp.setPen(Qt.NoPen)
            rp.drawRoundedRect(0, 0, 52, 52, 8, 8)
            rp.end()
            self._thumb_mini.setPixmap(rounded)
            self._thumb_mini.setText("")
            return
        self._thumb_mini.setPixmap(QPixmap())
        self._thumb_mini.setText("♪" if not song.is_video else "▶")

    def _play_pause(self):
        if 0 <= self.current_index < len(self.playlist):
            current_song = self.playlist[self.current_index]
            if self._song_requires_premium(current_song, self.current_index):
                self._require_premium(
                    "Esta faixa foi baixada além das 10 faixas gratuitas. "
                    "Ative o Premium para continuar ouvindo."
                )
                self._stop_locked_current_song()
                return
        if self._web_active and HAS_WEBENGINE:
            js = ("var m=document.getElementById('m');"
                  "if(m){if(m.paused){m.play();}else{m.pause();}}")
            self.web_view.page().runJavaScript(js)
            js_state = "var m=document.getElementById('m'); m ? !m.paused : false;"
            self.web_view.page().runJavaScript(
                js_state,
                lambda playing: self._set_play_icon(bool(playing))
            )
            return
        if self.player.state() == QMediaPlayer.PlayingState:
            self.player.pause()
            self._set_play_icon(False)
        elif self.player.mediaStatus() != QMediaPlayer.NoMedia:
            self.player.play()
            self._set_play_icon(True)
        elif self.playlist:
            self._play_index(0)

    def _prev(self):
        if self.current_index > 0:
            self._play_index(self.current_index - 1)

    def _next(self):
        if self._shuffle and len(self.playlist) > 1:
            import random
            idx = random.choice([i for i in range(len(self.playlist)) if i != self.current_index])
            self._play_index(idx)
        elif self.current_index < len(self.playlist) - 1:
            self._play_index(self.current_index + 1)
        elif self._repeat and self.playlist:
            self._play_index(0)

    def _toggle_shuffle(self):
        self._shuffle = not self._shuffle
        self._shuf_btn.setChecked(self._shuffle)

    def _toggle_repeat(self):
        self._repeat = not self._repeat
        self._rep_btn.setChecked(self._repeat)

    # ── Player eventos ──────────────────────
    def _position_fullscreen_button(self):
        if not hasattr(self, '_fullscreen_exit_btn'):
            return
        margin = 18
        self._fullscreen_exit_btn.move(
            self.media_stack.width() - self._fullscreen_exit_btn.width() - margin,
            margin
        )

    def _toggle_fullscreen(self):
        if self._is_fullscreen:
            self._exit_fullscreen()
            return
        if not (0 <= self.current_index < len(self.playlist)):
            return
        if not self.playlist[self.current_index].is_video:
            return
        self._is_fullscreen = True
        self._sidebar_widget.hide()
        self._now_playing_panel.hide()
        self._fullscreen_exit_btn.show()
        self._position_fullscreen_button()
        self._fullscreen_exit_btn.raise_()
        self.showFullScreen()

    def _exit_fullscreen(self):
        if not self._is_fullscreen:
            return
        self._is_fullscreen = False
        self.showNormal()
        self._sidebar_widget.show()
        self._now_playing_panel.show()
        self._fullscreen_exit_btn.hide()

    def resizeEvent(self, event):
        super().resizeEvent(event)
        self._position_fullscreen_button()

    def showEvent(self, event):
        super().showEvent(event)
        if self._centered_on_start:
            return
        self._centered_on_start = True
        self.setMinimumSize(0, 0)
        self.resize(self._initial_window_size)
        self.setMinimumSize(760, 360)
        self._center_on_screen()
        QTimer.singleShot(100, self._center_on_screen)

    def _center_on_screen(self):
        screen = self.screen() or QApplication.primaryScreen()
        if screen is not None:
            monitor = screen.availableGeometry()
            frame = self.frameGeometry()
            frame_width_delta = frame.width() - self.width()
            frame_height_delta = frame.height() - self.height()
            max_width = max(
                760,
                monitor.width() - frame_width_delta - 2
            )
            max_height = max(
                360,
                monitor.height() - frame_height_delta - 2
            )
            target_width = min(self.width(), max_width)
            target_height = min(self.height(), max_height)
            if target_width != self.width() or target_height != self.height():
                self.setMinimumSize(0, 0)
                self.resize(target_width, target_height)
                self.setMinimumSize(760, 360)
                frame = self.frameGeometry()
            frame.moveCenter(monitor.center())
            frame.moveLeft(max(
                monitor.left(),
                min(frame.left(), monitor.right() - frame.width() + 1)
            ))
            frame.moveTop(max(
                monitor.top(),
                min(frame.top(), monitor.bottom() - frame.height() + 1)
            ))
            self.move(frame.topLeft())

    def _on_web_title(self, title):
        if title == "__ENDED__":
            self._next()

    def _poll_web(self):
        if not (self._web_active and HAS_WEBENGINE):
            return
        js = ("var m=document.getElementById('m');"
              "m && m.duration ? [m.currentTime, m.duration, m.paused] : null;")
        self.web_view.page().runJavaScript(js, self._apply_web_pos)

    def _apply_web_pos(self, r):
        if not r or not isinstance(r, list) or len(r) < 3:
            return
        cur, dur, paused = float(r[0]), float(r[1]), bool(r[2])
        if dur > 0:
            if not self._is_seeking:
                self.seek.setValue(int(cur / dur * 1000))
            self.lbl_curr.setText(self._fmt(int(cur * 1000)))
            self.lbl_tot.setText(self._fmt(int(dur * 1000)))
        self._set_play_icon(not paused)

    def _set_volume(self, v):
        self.player.setVolume(v)
        if self._web_active and HAS_WEBENGINE:
            js = f"var m=document.getElementById('m'); if(m) m.volume={v/100};"
            self.web_view.page().runJavaScript(js)

    def _on_media_status(self, status):
        if self._web_active:
            return
        if status == QMediaPlayer.EndOfMedia:
            self._next()

    def _on_position(self, pos):
        if not self._is_seeking:
            dur = self.player.duration()
            if dur > 0:
                self.seek.setValue(int(pos / dur * 1000))
            self.lbl_curr.setText(self._fmt(pos))

    def _on_duration(self, dur):
        self.lbl_tot.setText(self._fmt(dur))

    def _on_seek_release(self):
        self._is_seeking = False
        if self._web_active and HAS_WEBENGINE:
            pct = self.seek.value() / 1000
            js = (f"var m=document.getElementById('m');"
                  f"if(m&&m.duration) m.currentTime=m.duration*{pct};")
            self.web_view.page().runJavaScript(js)
        else:
            dur = self.player.duration()
            if dur > 0:
                self.player.setPosition(int(self.seek.value() / 1000 * dur))

    def _on_error(self):
        if self._web_active:
            return
        err = self.player.errorString()
        QMessageBox.warning(
            self, "Erro de Reprodução",
            f"Não foi possível reproduzir o arquivo.\n\nErro: {err}"
        )

    @staticmethod
    def _fmt(ms):
        s = (ms // 1000) % 60
        m = ms // 60000
        return f"{m}:{s:02d}"

    # ── Adicionar / Remover ─────────────────
    def _add_files(self):
        if not self.settings.get('premium') and (
            self._get_saved_youtube_count() >= FREE_DOWNLOAD_LIMIT
        ):
            self._show_free_limit_dialog(
                "Você já utilizou os 10 downloads gratuitos. "
                "Adquira o Premium para adicionar músicas ilimitadamente."
            )
            return

        files, _ = QFileDialog.getOpenFileNames(
            self, "Abrir Arquivo de Mídia", "",
            "Mídia (*.mp3 *.m4a *.aac *.ogg *.flac *.wav *.webm *.mp4 *.mkv *.avi)"
        )
        if files and not self.settings.get('premium'):
            available = FREE_DOWNLOAD_LIMIT - self._get_saved_youtube_count()
            if len(files) > available:
                self._show_free_limit_dialog(
                    f"Na conta gratuita, você pode adicionar somente "
                    f"{available} arquivo(s) nesta operação."
                )
                files = files[:available]
        for f in files:
            is_video = f.lower().endswith(('.webm', '.mp4', '.mkv', '.avi', '.mov'))
            self.playlist.append(Song(os.path.basename(f), f, 'local', is_video=is_video))
        if files:
            self._refresh_library_filters()
            self._refresh_tracks()
            self._save_playlist()

    def _add_youtube(self):
        if not self._can_download_youtube():
            return
        dlg = DownloadDialog(self, premium=self.settings.get('premium', False))
        if dlg.exec_() == QDialog.Accepted:
            self._add_download_result(dlg.get_result())

    def _add_download_result(self, result):
        if result:
            title, path, thumb, is_video = result
            current_count = self._get_saved_youtube_count()
            if not self.settings.get('premium'):
                if current_count >= FREE_DOWNLOAD_LIMIT:
                    for file_path in (path, thumb):
                        if file_path and os.path.exists(file_path):
                            try:
                                os.remove(file_path)
                            except OSError:
                                pass
                    self._show_free_limit_dialog()
                    return

            if self.settings.get('child_mode'):
                allowed = [str(item).casefold() for item in self.settings.get('allowed_youtube', [])]
                title_allowed = any(item in title.casefold() for item in allowed)
                if not title_allowed:
                    entered, ok = QInputDialog.getText(
                        self, "Autorização dos pais",
                        "Este vídeo não está na lista permitida. Digite o PIN para autorizar:",
                        QLineEdit.Password
                    )
                    if not ok or entered != self.settings.get('parent_pin'):
                        QMessageBox.warning(self, "Download bloqueado", "Esse conteúdo não foi autorizado.")
                        for file_path in (path, thumb):
                            if file_path and os.path.exists(file_path):
                                try:
                                    os.remove(file_path)
                                except OSError:
                                    pass
                        return
                    self.settings.setdefault('allowed_youtube', []).append(title)
                    save_settings(self.settings)
            self.playlist.append(
                Song(
                    title,
                    path,
                    'youtube',
                    thumb or None,
                    is_video,
                    premium_only=(
                        bool(self.settings.get('premium'))
                        and current_count >= FREE_DOWNLOAD_LIMIT
                    )
                )
            )
            self._refresh_library_filters()
            self._refresh_tracks()
            self._save_playlist()
            self._play_index(len(self.playlist) - 1)

    def _delete_song(self, idx):
        if not (0 <= idx < len(self.playlist)):
            return
        song = self.playlist[idx]
        song_key = self._song_key(song)
        for items in self.playlists.values():
            while song_key in items:
                items.remove(song_key)
        try:
            if os.path.exists(song.path):   os.remove(song.path)
            if song.thumb and os.path.exists(song.thumb): os.remove(song.thumb)
        except:
            pass
        self.playlist.pop(idx)
        if self.current_index == idx:
            self.current_index = -1
            self.player.stop()
            if HAS_WEBENGINE:
                self._web_active = False
                self._web_poll.stop()
                self.web_view.setHtml("")
            self.art_widget.clear()
            self.title_lbl.setText("Nenhuma faixa")
            self.src_lbl.setText("—")
            self._thumb_mini.setPixmap(QPixmap())
            self._thumb_mini.setText("♪")
            self._set_play_icon(False)
            self.media_stack.setCurrentIndex(0)
        elif self.current_index > idx:
            self.current_index -= 1
        self._refresh_library_filters()
        self._refresh_tracks()
        self._save_playlist()
        save_playlists(self.playlists)


# ─────────────────────────────────────────────
#  Entry point
# ─────────────────────────────────────────────
if __name__ == "__main__":
    app = QApplication(sys.argv)
    app.setStyle("Fusion")

    if sys.platform == "win32":
        try:
            import ctypes
            ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(
                "NeonWave.Player"
            )
        except (AttributeError, OSError):
            pass

    if APP_ICON_FILE.is_file():
        app.setWindowIcon(QIcon(str(APP_ICON_FILE)))

    # Paleta base escura para diálogos nativos
    palette = QPalette()
    palette.setColor(QPalette.Window,          QColor(17, 24, 36))
    palette.setColor(QPalette.WindowText,      QColor(241, 245, 255))
    palette.setColor(QPalette.Base,            QColor(25, 35, 51))
    palette.setColor(QPalette.AlternateBase,   QColor(37, 50, 71))
    palette.setColor(QPalette.ToolTipBase,     QColor(17, 24, 36))
    palette.setColor(QPalette.ToolTipText,     QColor(241, 245, 255))
    palette.setColor(QPalette.Text,            QColor(241, 245, 255))
    palette.setColor(QPalette.Button,          QColor(25, 35, 51))
    palette.setColor(QPalette.ButtonText,      QColor(241, 245, 255))
    palette.setColor(QPalette.Highlight,       QColor(255, 79, 154))
    palette.setColor(QPalette.HighlightedText, QColor(255, 255, 255))
    app.setPalette(palette)

    splash = StartupSplash()
    splash.show()
    app.processEvents()

    started_at = time.monotonic()
    win = MusicPlayer()
    while time.monotonic() - started_at < 3.20:
        app.processEvents()
        time.sleep(0.02)

    splash.finish()
    win.show()
    app.processEvents()
    win.setMinimumSize(0, 0)
    win.resize(win._initial_window_size)
    win.setMinimumSize(760, 360)
    win._center_on_screen()
    win._maybe_show_ad_popup()
    sys.exit(app.exec_())
