"""
Subify v3.0 Ultra — Commercial Edition with Multi-Player, Chameleon Color Themes, Dual-Language Translation, Full Lyrics Drawer, Global System Hotkeys, and Sleek Corner Floater Resize Grip.
"""

import math
import objc
import AppKit
from AppKit import (
    NSApplication, NSStatusBar, NSMenu, NSMenuItem, NSCursor,
    NSPanel, NSScreen, NSColor, NSFont, NSBezierPath,
    NSAttributedString, NSTextField, NSView, NSVisualEffectView,
    NSVisualEffectMaterialHUDWindow, NSVisualEffectBlendingModeBehindWindow,
    NSVisualEffectStateActive,
    NSTimer, NSImage, NSVariableStatusItemLength,
    NSBackingStoreBuffered, NSLineBreakByWordWrapping,
    NSApplicationActivationPolicyRegular,
    NSPointInRect, NSMakeRect, NSMakePoint, NSMakeSize,
    NSApp, NSNotificationCenter, NSTrackingArea,
    NSTrackingMouseEnteredAndExited, NSTrackingActiveAlways, NSTrackingInVisibleRect
)
from Foundation import NSObject
from Quartz import (
    CATransaction, CGColorCreateGenericRGB,
)

from player_manager import PlayerManager, PlayerState
from lyrics_service import LyricsService
from lrc_parser import LRCParser
from settings import Settings, SettingsWindowController
from theme_engine import get_theme, THEMES
from chameleon_engine import ChameleonEngine
from translation_service import TranslationService
from lyrics_drawer import LyricsDrawerController
from hotkey_manager import HotkeyManager

# Window style constants
NSWindowStyleMaskBorderless = 0
NSWindowStyleMaskNonactivatingPanel = 1 << 7
NSFloatingWindowLevel = 3

# Collection behavior constants
NSWindowCollectionBehaviorCanJoinAllSpaces = 1 << 0
NSWindowCollectionBehaviorFullScreenAuxiliary = 1 << 8
NSWindowCollectionBehaviorStationary = 1 << 4
NSWindowCollectionBehaviorIgnoresCycle = 1 << 6

# Text alignment
NSCenterTextAlignment = 2

# Font weights
NSFontWeightBold = 0.4
NSFontWeightMedium = 0.23


# ─────────────────────────────────────────────────────────────
#  Glassmorphism Background, Drag Handle, Sleek Corner Resize Handle & Close Button
# ─────────────────────────────────────────────────────────────

class OverlayBackgroundView(NSVisualEffectView):
    """A custom NSVisualEffectView providing a native macOS glassmorphism backdrop."""

    def initWithFrame_(self, frame):
        self = objc.super(OverlayBackgroundView, self).initWithFrame_(frame)
        if self is None:
            return None

        self.setMaterial_(NSVisualEffectMaterialHUDWindow)
        self.setBlendingMode_(NSVisualEffectBlendingModeBehindWindow)
        self.setState_(NSVisualEffectStateActive)

        self.setWantsLayer_(True)
        layer = self.layer()
        layer.setCornerRadius_(20)
        layer.setMasksToBounds_(True)

        border = CGColorCreateGenericRGB(1.0, 1.0, 1.0, 0.22)
        layer.setBorderColor_(border)
        layer.setBorderWidth_(0.75)

        layer.setShadowOpacity_(0.45)
        layer.setShadowRadius_(22)
        shadow = CGColorCreateGenericRGB(0.0, 0.0, 0.0, 0.5)
        layer.setShadowColor_(shadow)
        layer.setShadowOffset_(NSMakeSize(0, -6))

        return self

    def apply_theme(self, theme):
        self.setMaterial_(theme.material)
        if self.layer():
            self.layer().setBorderColor_(theme.create_border_color())
            self.layer().setShadowColor_(theme.create_shadow_color())


class DragHandleView(NSView):
    """Small grip handle icon (6 dots) on left side of pill for dragging window."""

    def setApp_(self, app):
        self._app = app

    def initWithFrame_(self, frame):
        self = objc.super(DragHandleView, self).initWithFrame_(frame)
        if self is None:
            return None
        self.setWantsLayer_(True)
        return self

    def resetCursorRects(self):
        self.addCursorRect_cursor_(self.bounds(), NSCursor.openHandCursor())

    def drawRect_(self, rect):
        NSColor.colorWithWhite_alpha_(1.0, 0.55).set()

        dot_r = 1.25
        margin_x = 3.0
        margin_y = 4.0
        spacing_x = 5.0
        spacing_y = 6.0

        for col in range(2):
            for row in range(3):
                cx = margin_x + col * spacing_x
                cy = margin_y + row * spacing_y
                dot_rect = NSMakeRect(cx - dot_r, cy - dot_r, dot_r * 2, dot_r * 2)
                path = NSBezierPath.bezierPathWithOvalInRect_(dot_rect)
                path.fill()

    def mouseDown_(self, event):
        win = self.window()
        if win:
            win.performWindowDragWithEvent_(event)


class ResizeHandleView(NSView):
    """Refined, subtle corner resize grip designed to sit seamlessly inside rounded glass HUD pills."""

    def setApp_(self, app):
        self._app = app

    def initWithFrame_(self, frame):
        self = objc.super(ResizeHandleView, self).initWithFrame_(frame)
        if self is None:
            return None
        self._initial_mouse_point = NSMakePoint(0, 0)
        self._initial_font_size = 22.0
        self._is_hovered = False
        self.setWantsLayer_(True)

        tracking_options = NSTrackingMouseEnteredAndExited | NSTrackingActiveAlways | NSTrackingInVisibleRect
        tracking_area = NSTrackingArea.alloc().initWithRect_options_owner_userInfo_(
            self.bounds(), tracking_options, self, None
        )
        self.addTrackingArea_(tracking_area)

        return self

    def resetCursorRects(self):
        self.addCursorRect_cursor_(self.bounds(), NSCursor.crosshairCursor())

    def mouseEntered_(self, event):
        self._is_hovered = True
        self.setNeedsDisplay_(True)

    def mouseExited_(self, event):
        self._is_hovered = False
        self.setNeedsDisplay_(True)

    def drawRect_(self, rect):
        alpha = 0.85 if self._is_hovered else 0.30
        NSColor.colorWithWhite_alpha_(1.0, alpha).set()
        b = self.bounds()
        w = b.size.width
        h = b.size.height

        path = NSBezierPath.bezierPath()
        path.setLineWidth_(1.35)
        path.setLineCapStyle_(AppKit.NSLineCapStyleRound)

        # Inner subtle arc
        path.moveToPoint_(NSMakePoint(w - 3, 7))
        path.lineToPoint_(NSMakePoint(w - 7, 3))

        # Outer subtle arc
        path.moveToPoint_(NSMakePoint(w - 3, 11))
        path.lineToPoint_(NSMakePoint(w - 11, 3))

        path.stroke()

    def mouseDown_(self, event):
        self._initial_mouse_point = event.locationInWindow()
        if hasattr(self, '_app') and self._app is not None:
            self._initial_font_size = self._app._settings.font_size

    def mouseDragged_(self, event):
        if hasattr(self, '_app') and self._app is not None:
            curr_pt = event.locationInWindow()
            delta_x = curr_pt.x - self._initial_mouse_point.x
            delta_y = self._initial_mouse_point.y - curr_pt.y
            delta = delta_x + delta_y
            new_size = max(14.0, min(44.0, round(self._initial_font_size + (delta * 0.25), 1)))
            if new_size != self._app._settings.font_size:
                self._app._settings.font_size = new_size
                self._app._settings.save()
                self._app.applySettings()


class CloseButtonView(NSView):
    """Small close button (✕) on top-right of the floater."""

    def setApp_(self, app):
        self._app = app

    def initWithFrame_(self, frame):
        self = objc.super(CloseButtonView, self).initWithFrame_(frame)
        if self is None:
            return None
        self.setWantsLayer_(True)
        return self

    def resetCursorRects(self):
        self.addCursorRect_cursor_(self.bounds(), NSCursor.pointingHandCursor())

    def drawRect_(self, rect):
        NSColor.colorWithWhite_alpha_(1.0, 0.55).set()
        b = self.bounds()
        cx = b.size.width / 2.0
        cy = b.size.height / 2.0
        r = 3.5

        path = NSBezierPath.bezierPath()
        path.setLineWidth_(1.5)
        path.moveToPoint_(NSMakePoint(cx - r, cy - r))
        path.lineToPoint_(NSMakePoint(cx + r, cy + r))
        path.moveToPoint_(NSMakePoint(cx + r, cy - r))
        path.lineToPoint_(NSMakePoint(cx - r, cy + r))
        path.stroke()

    def mouseDown_(self, event):
        if hasattr(self, '_app') and self._app is not None:
            self._app.hideOverlay()


class OverlayContentView(NSView):
    """Custom content view that routes hits ONLY to drag handle, resize handle, and close button when visible."""

    def setBgView_(self, bg):
        self._bg_view = bg

    def setDragHandle_(self, handle):
        self._drag_handle = handle

    def setResizeHandle_(self, handle):
        self._resize_handle = handle

    def setCloseButton_(self, btn):
        self._close_button = btn

    def hitTest_(self, point):
        if hasattr(self, '_bg_view') and (self._bg_view is None or self._bg_view.isHidden()):
            return None

        if hasattr(self, '_close_button') and self._close_button is not None and not self._close_button.isHidden():
            local_p = self._close_button.convertPoint_fromView_(point, self)
            if NSPointInRect(local_p, self._close_button.bounds()):
                return self._close_button

        if hasattr(self, '_resize_handle') and self._resize_handle is not None and not self._resize_handle.isHidden():
            local_p = self._resize_handle.convertPoint_fromView_(point, self)
            if NSPointInRect(local_p, self._resize_handle.bounds()):
                return self._resize_handle

        if hasattr(self, '_drag_handle') and self._drag_handle is not None and not self._drag_handle.isHidden():
            local_p = self._drag_handle.convertPoint_fromView_(point, self)
            if NSPointInRect(local_p, self._drag_handle.bounds()):
                return self._drag_handle

        return None


# ─────────────────────────────────────────────────────────────
#  Main Application
# ─────────────────────────────────────────────────────────────

class SpotifyLyricsApp(NSObject):
    """Main application class managing the menu bar, overlay, preferences window, and lyrics sync."""

    def init(self):
        self = objc.super(SpotifyLyricsApp, self).init()
        if self is None:
            return None

        self._app = NSApplication.sharedApplication()
        self._app.setActivationPolicy_(NSApplicationActivationPolicyRegular)

        # Settings & Controllers
        self._settings = Settings()
        self._settings_controller = None
        self._drawer_controller = LyricsDrawerController.alloc().initWithApp_(self)
        self._hotkey_manager = HotkeyManager(self)

        # State
        self._current_track_key = ''
        self._current_track_info = ''
        self._current_player_name = 'Spotify'
        self._lyric_lines = []
        self._translated_map = {}
        self._current_line_index = -1
        self._is_playing = False
        self._overlay_visible = True
        self._show_welcome_on_launch = True
        self._last_fetched_key = ''
        self._is_updating_layout = False

        # UI
        self._status_item = None
        self._overlay_panel = None
        self._current_label = None
        self._next_label = None
        self._status_label = None
        self._bg_view = None
        self._content_view = None
        self._drag_handle = None
        self._resize_handle = None
        self._close_button = None
        self._poll_timer = None
        self._track_info_item = None
        self._toggle_item = None
        self._sync_info_item = None

        return self

    def run(self):
        self._setup_menu_bar()
        self._setup_overlay_panel()
        self.applySettings()
        self._start_polling()

        # Start Global Hotkeys
        self._hotkey_manager.start()

        # Open Preferences / Control Panel on launch for instant UI feedback
        self.openPreferences_(None)

        self._app.run()

    # ─────────────────────────────────────────────────
    #  Menu Bar
    # ─────────────────────────────────────────────────

    def _setup_menu_bar(self):
        self._status_item = NSStatusBar.systemStatusBar().statusItemWithLength_(
            NSVariableStatusItemLength
        )
        button = self._status_item.button()

        img = NSImage.imageWithSystemSymbolName_accessibilityDescription_(
            'music.note', 'Subify'
        )
        if img:
            button.setImage_(img)
        else:
            button.setTitle_('♪')

        menu = NSMenu.alloc().init()

        # Track info (disabled)
        self._track_info_item = NSMenuItem.alloc().initWithTitle_action_keyEquivalent_(
            '♪ Waiting for Player...', None, ''
        )
        self._track_info_item.setEnabled_(False)
        menu.addItem_(self._track_info_item)

        menu.addItem_(NSMenuItem.separatorItem())

        # Toggle overlay visibility
        self._toggle_item = NSMenuItem.alloc().initWithTitle_action_keyEquivalent_(
            'Hide Subtitles', 'toggleOverlay:', 'l'
        )
        self._toggle_item.setKeyEquivalentModifierMask_(
            AppKit.NSEventModifierFlagCommand | AppKit.NSEventModifierFlagShift
        )
        self._toggle_item.setTarget_(self)
        menu.addItem_(self._toggle_item)

        # Full Lyrics Drawer
        drawer_item = NSMenuItem.alloc().initWithTitle_action_keyEquivalent_(
            'Full Lyrics Drawer', 'toggleLyricsDrawer:', 'd'
        )
        drawer_item.setKeyEquivalentModifierMask_(
            AppKit.NSEventModifierFlagCommand | AppKit.NSEventModifierFlagOption
        )
        drawer_item.setTarget_(self)
        menu.addItem_(drawer_item)

        # ── Quick Adjust Submenu in Menu Bar ──
        adjust_menu = NSMenu.alloc().init()

        # 1. Font Size Submenu
        font_menu = NSMenu.alloc().init()
        for font_lbl, font_val in [("Small (18pt)", 18), ("Medium (22pt)", 22), ("Large (26pt)", 26), ("Extra Large (30pt)", 30)]:
            item = NSMenuItem.alloc().initWithTitle_action_keyEquivalent_(font_lbl, "setFontSizeFromTag:", "")
            item.setTag_(font_val)
            item.setTarget_(self)
            font_menu.addItem_(item)

        font_sub = NSMenuItem.alloc().initWithTitle_action_keyEquivalent_("Font Size", None, "")
        font_sub.setSubmenu_(font_menu)
        adjust_menu.addItem_(font_sub)

        # 2. Glass Opacity Submenu
        opacity_menu = NSMenu.alloc().init()
        for op_lbl, op_val in [("Subtle (50%)", 50), ("Standard (85%)", 85), ("Opaque (100%)", 100)]:
            item = NSMenuItem.alloc().initWithTitle_action_keyEquivalent_(op_lbl, "setOpacityFromTag:", "")
            item.setTag_(op_val)
            item.setTarget_(self)
            opacity_menu.addItem_(item)

        opacity_sub = NSMenuItem.alloc().initWithTitle_action_keyEquivalent_("Glass Opacity", None, "")
        opacity_sub.setSubmenu_(opacity_menu)
        adjust_menu.addItem_(opacity_sub)

        # 3. Position Margin Submenu
        margin_menu = NSMenu.alloc().init()
        for m_lbl, m_val in [("Low (30px)", 30), ("Default (45px)", 45), ("High (80px)", 80), ("Top (150px)", 150)]:
            item = NSMenuItem.alloc().initWithTitle_action_keyEquivalent_(m_lbl, "setMarginFromTag:", "")
            item.setTag_(m_val)
            item.setTarget_(self)
            margin_menu.addItem_(item)

        margin_sub = NSMenuItem.alloc().initWithTitle_action_keyEquivalent_("Position Margin", None, "")
        margin_sub.setSubmenu_(margin_menu)
        adjust_menu.addItem_(margin_sub)

        # 4. Lyrics Timing Sync Submenu
        sync_menu = NSMenu.alloc().init()

        self._sync_info_item = NSMenuItem.alloc().initWithTitle_action_keyEquivalent_("Offset: 0.0s", None, "")
        self._sync_info_item.setEnabled_(False)
        sync_menu.addItem_(self._sync_info_item)
        sync_menu.addItem_(NSMenuItem.separatorItem())

        delay_item = NSMenuItem.alloc().initWithTitle_action_keyEquivalent_("+0.5s (Delay Lyrics)", "adjustSyncOffset:", "")
        delay_item.setTag_(5)  # +0.5s
        delay_item.setTarget_(self)
        sync_menu.addItem_(delay_item)

        advance_item = NSMenuItem.alloc().initWithTitle_action_keyEquivalent_("-0.5s (Advance Lyrics)", "adjustSyncOffset:", "")
        advance_item.setTag_(-5)  # -0.5s
        advance_item.setTarget_(self)
        sync_menu.addItem_(advance_item)

        reset_sync_item = NSMenuItem.alloc().initWithTitle_action_keyEquivalent_("Reset Sync (0.0s)", "adjustSyncOffset:", "")
        reset_sync_item.setTag_(0)
        reset_sync_item.setTarget_(self)
        sync_menu.addItem_(reset_sync_item)

        sync_sub = NSMenuItem.alloc().initWithTitle_action_keyEquivalent_("Lyrics Timing Sync", None, "")
        sync_sub.setSubmenu_(sync_menu)
        adjust_menu.addItem_(sync_sub)

        adjust_item = NSMenuItem.alloc().initWithTitle_action_keyEquivalent_("Quick Adjust", None, "")
        adjust_item.setSubmenu_(adjust_menu)
        menu.addItem_(adjust_item)

        menu.addItem_(NSMenuItem.separatorItem())

        # Preferences / Settings
        pref_item = NSMenuItem.alloc().initWithTitle_action_keyEquivalent_(
            'Preferences...', 'openPreferences:', ','
        )
        pref_item.setTarget_(self)
        menu.addItem_(pref_item)

        menu.addItem_(NSMenuItem.separatorItem())

        # Quit
        quit_item = NSMenuItem.alloc().initWithTitle_action_keyEquivalent_(
            'Quit Subify', 'quitApp:', 'q'
        )
        quit_item.setTarget_(self)
        menu.addItem_(quit_item)

        self._status_item.setMenu_(menu)

    @objc.IBAction
    def toggleLyricsDrawer_(self, sender):
        if self._drawer_controller:
            self._drawer_controller.toggleWindow()

    @objc.IBAction
    def quickDelaySync_(self, sender):
        self._settings.sync_offset = round(self._settings.sync_offset + 0.5, 1)
        self._settings.save()
        self.applySettings()

    @objc.IBAction
    def quickAdvanceSync_(self, sender):
        self._settings.sync_offset = round(self._settings.sync_offset - 0.5, 1)
        self._settings.save()
        self.applySettings()

    @objc.IBAction
    def adjustSyncOffset_(self, sender):
        tag = sender.tag()
        if tag == 0:
            self._settings.sync_offset = 0.0
        else:
            delta = float(tag) / 10.0
            self._settings.sync_offset = round(self._settings.sync_offset + delta, 1)
        self._settings.save()
        self.applySettings()

    @objc.IBAction
    def setFontSizeFromTag_(self, sender):
        val = float(sender.tag())
        self._settings.font_size = val
        self._settings.save()
        self.applySettings()

    @objc.IBAction
    def setOpacityFromTag_(self, sender):
        val = float(sender.tag()) / 100.0
        self._settings.glass_opacity = val
        self._settings.save()
        self.applySettings()

    @objc.IBAction
    def setMarginFromTag_(self, sender):
        val = float(sender.tag())
        self._settings.bottom_margin = val
        self._settings.custom_center_x = None
        self._settings.custom_center_y = None
        self._settings.save()
        self.applySettings()

    @objc.IBAction
    def openPreferences_(self, sender):
        if self._settings_controller is None:
            self._settings_controller = SettingsWindowController.alloc().initWithApp_settings_(self, self._settings)
        self._settings_controller.showWindow()

    def hideOverlay(self):
        """Hide the overlay window and update menu bar label."""
        if self._overlay_panel:
            self._overlay_panel.orderOut_(None)
            self._toggle_item.setTitle_('Show Subtitles')
            self._overlay_visible = False

    @objc.IBAction
    def toggleOverlay_(self, sender):
        if self._overlay_visible:
            self.hideOverlay()
        else:
            self._overlay_panel.orderFront_(None)
            self._toggle_item.setTitle_('Hide Subtitles')
            self._overlay_visible = True
            self._update_overlay_layout()

    @objc.IBAction
    def quitApp_(self, sender):
        if self._hotkey_manager:
            self._hotkey_manager.stop()
        if self._poll_timer:
            self._poll_timer.invalidate()
        NSApplication.sharedApplication().terminate_(None)

    # ─────────────────────────────────────────────────
    #  Overlay Panel
    # ─────────────────────────────────────────────────

    def _setup_overlay_panel(self):
        screen = NSScreen.mainScreen()
        if screen is None:
            return
        visible = screen.visibleFrame()

        panel_width = 300.0
        panel_height = 50.0

        if self._settings.custom_center_x is not None and self._settings.custom_center_y is not None:
            x = self._settings.custom_center_x - (panel_width / 2.0)
            y = self._settings.custom_center_y
        else:
            x = visible.origin.x + (visible.size.width - panel_width) / 2.0
            y = visible.origin.y + self._settings.bottom_margin

        style = NSWindowStyleMaskBorderless | NSWindowStyleMaskNonactivatingPanel

        panel = NSPanel.alloc().initWithContentRect_styleMask_backing_defer_(
            NSMakeRect(x, y, panel_width, panel_height),
            style,
            NSBackingStoreBuffered,
            False
        )

        panel.setLevel_(NSFloatingWindowLevel)
        panel.setOpaque_(False)
        panel.setBackgroundColor_(NSColor.clearColor())
        panel.setIgnoresMouseEvents_(False)
        panel.setMovableByWindowBackground_(False)
        panel.setHasShadow_(False)
        panel.setHidesOnDeactivate_(False)
        panel.setBecomesKeyOnlyIfNeeded_(True)

        behavior = (
            NSWindowCollectionBehaviorCanJoinAllSpaces |
            NSWindowCollectionBehaviorFullScreenAuxiliary |
            NSWindowCollectionBehaviorStationary |
            NSWindowCollectionBehaviorIgnoresCycle
        )
        panel.setCollectionBehavior_(behavior)

        # Content view with custom hitTesting
        content = OverlayContentView.alloc().initWithFrame_(
            NSMakeRect(0, 0, panel_width, panel_height)
        )
        content.setWantsLayer_(True)
        panel.setContentView_(content)
        self._content_view = content

        # Background glass pill
        self._bg_view = OverlayBackgroundView.alloc().initWithFrame_(
            NSMakeRect(0, 0, panel_width, panel_height)
        )
        self._bg_view.setHidden_(True)
        content.addSubview_(self._bg_view)
        content.setBgView_(self._bg_view)

        # Drag handle icon (small 6-dot grip on left side of pill)
        self._drag_handle = DragHandleView.alloc().initWithFrame_(
            NSMakeRect(9, 15, 14, 20)
        )
        self._drag_handle.setApp_(self)
        self._bg_view.addSubview_(self._drag_handle)
        content.setDragHandle_(self._drag_handle)

        # Resize handle icon (positioned cleanly inside bottom-right corner)
        self._resize_handle = ResizeHandleView.alloc().initWithFrame_(
            NSMakeRect(panel_width - 24, 7, 16, 16)
        )
        self._resize_handle.setApp_(self)
        self._bg_view.addSubview_(self._resize_handle)
        content.setResizeHandle_(self._resize_handle)

        # Close button (small ✕ on top-right of pill)
        self._close_button = CloseButtonView.alloc().initWithFrame_(
            NSMakeRect(panel_width - 23, 15, 16, 16)
        )
        self._close_button.setApp_(self)
        self._bg_view.addSubview_(self._close_button)
        content.setCloseButton_(self._close_button)

        # Labels are subviews of background glass pill
        self._current_label = self._make_label(
            font_size=self._settings.font_size,
            font_weight=NSFontWeightBold,
            alpha=1.0
        )
        self._bg_view.addSubview_(self._current_label)

        self._next_label = self._make_label(
            font_size=max(self._settings.font_size - 8, 12),
            font_weight=NSFontWeightMedium,
            alpha=0.5
        )
        self._bg_view.addSubview_(self._next_label)

        self._status_label = self._make_label(
            font_size=14,
            font_weight=NSFontWeightMedium,
            alpha=0.75
        )
        self._status_label.setStringValue_('')
        self._status_label.setHidden_(True)
        self._bg_view.addSubview_(self._status_label)

        # Register window move observer for drag tracking
        NSNotificationCenter.defaultCenter().addObserver_selector_name_object_(
            self, 'windowDidMove:', AppKit.NSWindowDidMoveNotification, panel
        )

        panel.orderFront_(None)
        self._overlay_panel = panel
        self._update_overlay_layout()

    @objc.IBAction
    def windowDidMove_(self, notification):
        if self._is_updating_layout:
            return
        win = notification.object()
        if win == self._overlay_panel:
            f = win.frame()
            center_x = f.origin.x + (f.size.width / 2.0)
            center_y = f.origin.y
            self._settings.custom_center_x = float(center_x)
            self._settings.custom_center_y = float(center_y)
            self._settings.save()

    def applySettings(self):
        """Apply current settings live to the UI."""
        theme = get_theme(self._settings.theme_name)
        if self._bg_view:
            self._bg_view.apply_theme(theme)

        font_size = self._settings.font_size
        self._current_label.setFont_(NSFont.systemFontOfSize_weight_(font_size, NSFontWeightBold))
        self._current_label.setTextColor_(theme.text_color)

        self._next_label.setFont_(NSFont.systemFontOfSize_weight_(max(font_size - 8, 12), NSFontWeightMedium))
        self._next_label.setTextColor_(theme.next_text_color)
        self._status_label.setTextColor_(theme.text_color)

        if self._sync_info_item:
            so = self._settings.sync_offset
            sync_str = f"Offset: {so:+.1f}s" if so != 0 else "Offset: 0.0s (Exact)"
            self._sync_info_item.setTitle_(sync_str)

        if self._bg_view and self._bg_view.layer():
            self._bg_view.layer().setOpacity_(self._settings.glass_opacity)

        screen = NSScreen.mainScreen()
        if screen and self._overlay_panel:
            visible = screen.visibleFrame()
            p_frame = self._overlay_panel.frame()
            if self._settings.custom_center_x is None:
                new_y = visible.origin.y + self._settings.bottom_margin
                self._overlay_panel.setFrameOrigin_(NSMakePoint(p_frame.origin.x, new_y))

        self._update_overlay_layout()

    def _make_label(self, font_size, font_weight, alpha):
        """Create a styled NSTextField label for the overlay."""
        label = NSTextField.alloc().initWithFrame_(NSMakeRect(0, 0, 10, 10))
        label.setEditable_(False)
        label.setBezeled_(False)
        label.setDrawsBackground_(False)
        label.setSelectable_(False)

        font = NSFont.systemFontOfSize_weight_(font_size, font_weight)
        label.setFont_(font)
        label.setTextColor_(NSColor.colorWithWhite_alpha_(1.0, alpha))
        label.setAlignment_(NSCenterTextAlignment)
        label.setLineBreakMode_(NSLineBreakByWordWrapping)
        label.setMaximumNumberOfLines_(2)

        cell = label.cell()
        cell.setTruncatesLastVisibleLine_(False)

        label.setStringValue_('')
        label.setWantsLayer_(True)

        return label

    def _measure_text(self, text, font, max_w):
        """Measure text dimensions given maximum width, adding safety margin for cell insets."""
        if not text:
            return 0.0, 0.0
        attrs = {AppKit.NSFontAttributeName: font}
        attr_str = NSAttributedString.alloc().initWithString_attributes_(text, attrs)
        options = AppKit.NSStringDrawingUsesLineFragmentOrigin | AppKit.NSStringDrawingUsesFontLeading
        rect = attr_str.boundingRectWithSize_options_(NSMakeSize(max_w, 1000), options)
        w = float(math.ceil(rect.size.width)) + 20.0
        h = float(math.ceil(rect.size.height))
        return w, h

    def _update_overlay_layout(self):
        """Dynamically shrink-wrap the panel window frame to the exact pixel bounds of the glass pill."""
        if not self._overlay_panel or not self._overlay_visible:
            return

        screen = NSScreen.mainScreen()
        if screen is None:
            return
        visible = screen.visibleFrame()
        max_text_w = visible.size.width * 0.70 - 84.0

        current_text = self._current_label.stringValue()
        next_text = self._next_label.stringValue() if self._settings.show_next_line else ''
        status_text = self._status_label.stringValue() if not self._status_label.isHidden() else ''

        padding_left = 30.0   # Space for drag handle
        padding_right = 30.0  # Space for close & resize buttons
        padding_v = 16.0
        spacing = 6.0

        self._is_updating_layout = True

        if status_text:
            self._current_label.setHidden_(True)
            self._next_label.setHidden_(True)

            w, h = self._measure_text(status_text, self._status_label.font(), max_text_w)
            pill_w = max(w + padding_left + padding_right, 220.0)
            pill_h = max(h + 22.0, 48.0)

            if self._settings.custom_center_x is not None and self._settings.custom_center_y is not None:
                pill_x = self._settings.custom_center_x - (pill_w / 2.0)
                pill_y = self._settings.custom_center_y
            else:
                pill_x = visible.origin.x + (visible.size.width - pill_w) / 2.0
                pill_y = visible.origin.y + self._settings.bottom_margin

            CATransaction.begin()
            CATransaction.setAnimationDuration_(0.15)
            self._overlay_panel.setFrame_display_animate_(NSMakeRect(pill_x, pill_y, pill_w, pill_h), True, False)
            self._content_view.setFrame_(NSMakeRect(0, 0, pill_w, pill_h))
            self._bg_view.setFrame_(NSMakeRect(0, 0, pill_w, pill_h))

            self._drag_handle.setFrame_(NSMakeRect(9.0, (pill_h - 20.0) / 2.0, 14.0, 20.0))
            self._close_button.setFrame_(NSMakeRect(pill_w - 23.0, pill_h - 20.0, 16.0, 16.0))
            self._resize_handle.setFrame_(NSMakeRect(pill_w - 24.0, 7.0, 16.0, 16.0))
            self._status_label.setFrame_(NSMakeRect(padding_left, (pill_h - h) / 2.0, pill_w - (padding_left + padding_right), h))
            self._bg_view.setHidden_(False)
            self._overlay_panel.orderFront_(None)
            CATransaction.commit()
            self._is_updating_layout = False
            return

        if not current_text:
            self._bg_view.setHidden_(True)
            self._overlay_panel.orderOut_(None)
            self._is_updating_layout = False
            return

        self._status_label.setHidden_(True)
        self._current_label.setHidden_(False)
        self._next_label.setHidden_(not bool(next_text))

        cur_w, cur_h = self._measure_text(current_text, self._current_label.font(), max_text_w)
        next_w, next_h = (0.0, 0.0)
        if next_text:
            next_w, next_h = self._measure_text(next_text, self._next_label.font(), max_text_w)

        content_w = max(cur_w, next_w)
        pill_w = max(content_w + padding_left + padding_right, 230.0)

        total_text_h = cur_h + (next_h + spacing if next_h > 0 else 0.0)
        pill_h = total_text_h + (padding_v * 2.0)

        if self._settings.custom_center_x is not None and self._settings.custom_center_y is not None:
            pill_x = self._settings.custom_center_x - (pill_w / 2.0)
            pill_y = self._settings.custom_center_y
        else:
            pill_x = visible.origin.x + (visible.size.width - pill_w) / 2.0
            pill_y = visible.origin.y + self._settings.bottom_margin

        label_w = pill_w - (padding_left + padding_right)

        CATransaction.begin()
        CATransaction.setAnimationDuration_(0.15)

        self._overlay_panel.setFrame_display_animate_(NSMakeRect(pill_x, pill_y, pill_w, pill_h), True, False)
        self._content_view.setFrame_(NSMakeRect(0, 0, pill_w, pill_h))
        self._bg_view.setFrame_(NSMakeRect(0, 0, pill_w, pill_h))

        self._drag_handle.setFrame_(NSMakeRect(9.0, (pill_h - 20.0) / 2.0, 14.0, 20.0))
        self._close_button.setFrame_(NSMakeRect(pill_w - 23.0, pill_h - 20.0, 16.0, 16.0))
        self._resize_handle.setFrame_(NSMakeRect(pill_w - 24.0, 7.0, 16.0, 16.0))

        if next_h > 0:
            self._next_label.setFrame_(NSMakeRect(padding_left, padding_v, label_w, next_h))
            self._current_label.setFrame_(NSMakeRect(padding_left, padding_v + next_h + spacing, label_w, cur_h))
        else:
            self._current_label.setFrame_(NSMakeRect(padding_left, padding_v, label_w, cur_h))

        self._bg_view.setHidden_(False)
        self._overlay_panel.orderFront_(None)
        CATransaction.commit()
        self._is_updating_layout = False

    # ─────────────────────────────────────────────────
    #  Polling & Lyrics Sync (Spotify + Apple Music)
    # ─────────────────────────────────────────────────

    def _start_polling(self):
        self._poll_timer = NSTimer.scheduledTimerWithTimeInterval_target_selector_userInfo_repeats_(
            0.5, self, 'pollPlayer:', None, True
        )

    @objc.IBAction
    def pollPlayer_(self, timer):
        """Called every 0.5s — check Spotify or Apple Music and update lyrics display."""
        state = PlayerManager.get_current_state()

        if state is None or not state.is_playing:
            if self._show_welcome_on_launch:
                self._show_welcome_on_launch = False
                self._status_label.setStringValue_('♪ Subify is active! Play a song on Spotify or Apple Music')
                self._status_label.setHidden_(False)
                self._update_overlay_layout()
                NSTimer.scheduledTimerWithTimeInterval_target_selector_userInfo_repeats_(
                    3.5, self, 'hideWelcome:', None, False
                )
                return

            if self._is_playing or not self._bg_view.isHidden():
                self._clear_display()
                if state:
                    self._track_info_item.setTitle_(f'♪ Paused ({state.player_name}) — {state.display_info}')
                else:
                    self._track_info_item.setTitle_('♪ Waiting for Player...')
            return

        self._show_welcome_on_launch = False
        self._is_playing = True
        self._current_track_info = state.display_info
        self._current_player_name = state.player_name

        display = f"{state.player_name}: {state.display_info}"
        truncated = (display[:42] + '...') if len(display) > 45 else display
        self._track_info_item.setTitle_(f'♪ {truncated}')

        if state.track_key != self._current_track_key:
            self._current_track_key = state.track_key
            self._current_line_index = -1
            self._current_label.setStringValue_('')
            self._next_label.setStringValue_('')
            self._status_label.setStringValue_(f'🎵 [{state.player_name}] {state.display_info}')
            self._status_label.setHidden_(False)
            self._update_overlay_layout()

            if state.track_key != self._last_fetched_key:
                self._last_fetched_key = state.track_key
                self._fetch_lyrics(state)
            return

        if self._lyric_lines:
            effective_pos = max(state.player_position + self._settings.sync_offset, 0.0)
            self._update_current_line(effective_pos)
        else:
            self._status_label.setStringValue_(f'🎵 [{state.player_name}] {state.display_info}')
            self._status_label.setHidden_(False)
            self._current_label.setStringValue_('')
            self._next_label.setStringValue_('')
            self._update_overlay_layout()

    @objc.IBAction
    def hideWelcome_(self, timer):
        if not self._is_playing:
            self._clear_display()

    def _clear_display(self):
        self._current_label.setStringValue_('')
        self._next_label.setStringValue_('')
        self._status_label.setStringValue_('')
        self._status_label.setHidden_(True)
        self._bg_view.setHidden_(True)
        if self._overlay_panel:
            self._overlay_panel.orderOut_(None)
        self._is_playing = False
        self._current_track_key = ''
        self._current_track_info = ''
        self._current_line_index = -1
        self._lyric_lines = []

    def _fetch_lyrics(self, state):
        def on_lyrics(lines):
            self.performSelectorOnMainThread_withObject_waitUntilDone_(
                'applyLyrics:', lines, False
            )

        LyricsService.shared().fetch_lyrics(
            track_name=state.track_name,
            artist_name=state.artist_name,
            album_name=state.album_name,
            duration=state.duration,
            callback=on_lyrics
        )

    @objc.IBAction
    def applyLyrics_(self, lines):
        """Apply fetched lyrics on the main thread."""
        self._lyric_lines = lines if lines else []

        if self._drawer_controller:
            self._drawer_controller.updateLyrics_player_(self._lyric_lines, self._current_player_name)

        if not self._is_playing:
            self._clear_display()
            return

        if not self._lyric_lines:
            self._status_label.setStringValue_(f'🎵 [{self._current_player_name}] {self._current_track_info}')
            self._status_label.setHidden_(False)
            self._current_label.setStringValue_('')
            self._next_label.setStringValue_('')
        else:
            self._status_label.setHidden_(True)
            self._current_line_index = -1
            # Pre-trigger translations for non-English songs
            self._translate_lines_async()

        self._update_overlay_layout()

    def _translate_lines_async(self):
        """Translate lyric lines in background for dual-line mode."""
        for line in self._lyric_lines[:15]:
            txt = line.text
            if txt and len(txt) > 2 and txt not in self._translated_map:
                TranslationService.shared().translate_text(
                    txt, 'en',
                    callback=lambda src, trans: self.performSelectorOnMainThread_withObject_waitUntilDone_(
                        'onTranslationResult:', [src, trans], False
                    )
                )

    @objc.IBAction
    def onTranslationResult_(self, pair):
        if pair and len(pair) == 2:
            src, trans = pair
            if trans:
                self._translated_map[src] = trans
                self._update_overlay_layout()

    def _update_current_line(self, position):
        """Sync the displayed lyric to the current playback position."""
        idx = LRCParser.current_line_index(position, self._lyric_lines)

        if idx is not None and idx != self._current_line_index:
            self._current_line_index = idx
            new_text = self._lyric_lines[idx].text

            if self._drawer_controller:
                self._drawer_controller.highlightLineIndex_(idx)

            CATransaction.begin()
            CATransaction.setAnimationDuration_(0.15)
            self._current_label.layer().setOpacity_(0.0)
            CATransaction.commit()

            self._current_label.setStringValue_(new_text)

            CATransaction.begin()
            CATransaction.setAnimationDuration_(0.25)
            self._current_label.layer().setOpacity_(1.0)
            CATransaction.commit()

            # Dual-line subtitle mode (Translation line if available, or next line preview)
            translated = self._translated_map.get(new_text, '')
            if translated:
                self._next_label.setStringValue_(f"💬 {translated}")
            elif idx + 1 < len(self._lyric_lines) and self._settings.show_next_line:
                self._next_label.setStringValue_(self._lyric_lines[idx + 1].text)
            else:
                self._next_label.setStringValue_('')

            self._status_label.setHidden_(True)
            self._update_overlay_layout()

        elif idx is None and self._current_line_index != -1:
            self._current_line_index = -1
            self._current_label.setStringValue_('')
            if self._lyric_lines and self._settings.show_next_line:
                self._next_label.setStringValue_(self._lyric_lines[0].text)
            else:
                self._next_label.setStringValue_('')
            self._update_overlay_layout()


def create_app():
    """Factory function to create and return the app instance."""
    return SpotifyLyricsApp.alloc().init()
