import sys
import time
import select
import tty
import termios
import spotipy
from spotipy.oauth2 import SpotifyPKCE
from pathlib import Path
from rich.console import Console, Group
from rich.panel import Panel
from rich.text import Text
from rich.ansi import AnsiDecoder
from rich.live import Live
from rich.table import Table
import os
import requests
from io import BytesIO
from PIL import Image
import threading
import math

# Optional audio visualization dependencies
try:
    import numpy as np
    NUMPY_AVAILABLE = True
except ImportError:
    NUMPY_AVAILABLE = False
    np = None

try:
    import sounddevice as sd
    SOUNDDEVICE_AVAILABLE = True
except ImportError:
    SOUNDDEVICE_AVAILABLE = False
    sd = None

# Both are required for audio capture
AUDIO_CAPTURE_AVAILABLE = NUMPY_AVAILABLE and SOUNDDEVICE_AVAILABLE

console = Console()


class AudioCapture:
    """Captures audio from system loopback (BlackHole) for visualization."""

    def __init__(self, device_name="BlackHole", sample_rate=44100, block_size=2048):
        self.sample_rate = sample_rate
        self.block_size = block_size
        self.device_index = None
        self.stream = None
        self.running = False
        self.fft_data = None
        self.lock = threading.Lock()
        self.error = None

        if not AUDIO_CAPTURE_AVAILABLE:
            self.error = "numpy/sounddevice not installed"
            return

        # Find BlackHole device
        try:
            devices = sd.query_devices()
            for i, dev in enumerate(devices):
                if device_name.lower() in dev['name'].lower() and dev['max_input_channels'] > 0:
                    self.device_index = i
                    break

            if self.device_index is None:
                self.error = f"BlackHole not found. Install: brew install blackhole-2ch"
        except Exception as e:
            self.error = str(e)

    def start(self):
        """Start audio capture."""
        if self.error or not AUDIO_CAPTURE_AVAILABLE:
            return False

        if self.running:
            return True

        try:
            self.stream = sd.InputStream(
                device=self.device_index,
                channels=2,
                samplerate=self.sample_rate,
                blocksize=self.block_size,
                callback=self._audio_callback
            )
            self.stream.start()
            self.running = True
            return True
        except Exception as e:
            self.error = str(e)
            return False

    def stop(self):
        """Stop audio capture."""
        if self.stream:
            self.running = False
            self.stream.stop()
            self.stream.close()
            self.stream = None

    def _audio_callback(self, indata, frames, time_info, status):
        """Process incoming audio data."""
        if not self.running:
            return

        # Mix stereo to mono
        mono = indata.mean(axis=1)

        # Apply Hanning window
        windowed = mono * np.hanning(len(mono))

        # Compute FFT
        fft = np.abs(np.fft.rfft(windowed))

        # Normalize and convert to dB scale
        fft = fft / len(fft)
        fft = np.clip(fft, 1e-10, None)  # Avoid log(0)
        fft_db = 20 * np.log10(fft)

        # Normalize to 0-1 range (assuming -60dB to 0dB range)
        fft_normalized = np.clip((fft_db + 60) / 60, 0, 1)

        with self.lock:
            self.fft_data = fft_normalized

    def get_bars(self, num_bars=32):
        """Get frequency bars for visualization."""
        with self.lock:
            if self.fft_data is None:
                return [0.1] * num_bars

            fft = self.fft_data

        # Group FFT bins into bars (logarithmic spacing for better visual)
        bars = []
        fft_len = len(fft)

        for i in range(num_bars):
            # Logarithmic frequency distribution
            low = int((fft_len - 1) * (2 ** (i / num_bars * 4) - 1) / 15)
            high = int((fft_len - 1) * (2 ** ((i + 1) / num_bars * 4) - 1) / 15)
            high = max(high, low + 1)

            # Average the bins in this range
            if low < fft_len and high <= fft_len:
                bar_value = float(np.mean(fft[low:high]))
            else:
                bar_value = 0.1

            bars.append(bar_value)

        return bars


# Global audio capture instance
audio_capture = None


def get_spotify_client():
    # Public client ID - safe to embed (no secret needed with PKCE)
    client_id = "c3c403c0c4914b84af7e486171381b34"
    redirect_uri = "http://127.0.0.1:8888/callback"
    scope = "user-read-playback-state user-modify-playback-state"

    # Store auth cache in user's home directory
    config_dir = Path.home() / ".spotuify"
    config_dir.mkdir(exist_ok=True)
    cache_path = config_dir / ".cache"

    sp = spotipy.Spotify(auth_manager=SpotifyPKCE(
        client_id=client_id,
        redirect_uri=redirect_uri,
        scope=scope,
        cache_path=str(cache_path),
    ))
    return sp


def format_time(ms):
    ms = int(ms)
    seconds = ms // 1000
    minutes = seconds // 60
    seconds = seconds % 60
    return f"{minutes}:{seconds:02d}"


def create_progress_bar(progress_ms, duration_ms, width=30):
    if duration_ms == 0:
        return "─" * width

    ratio = min(progress_ms / duration_ms, 1.0)
    filled = int(width * ratio)
    filled = min(filled, width - 1)

    bar = "━" * filled + "●" + "─" * (width - filled - 1)
    return bar


def fetch_album_art(url, width=30, height=15):
    """Fetch and convert album art to colored Braille characters with edge detection."""
    try:
        import cv2
        import numpy as np

        response = requests.get(url, timeout=3)
        img = Image.open(BytesIO(response.content))

        # Get RGB for color
        img_rgb = img.convert("RGB")

        # Convert to numpy array for OpenCV processing
        img_array = np.array(img_rgb)
        img_gray = cv2.cvtColor(img_array, cv2.COLOR_RGB2GRAY)

        # 1. CLAHE - Contrast Limited Adaptive Histogram Equalization
        clahe = cv2.createCLAHE(clipLimit=20.0, tileGridSize=(8, 8))
        img_clahe = clahe.apply(img_gray)

        # 2. Sharpen using unsharp mask
        gaussian = cv2.GaussianBlur(img_clahe, (0, 0), 2.0)
        img_sharp = cv2.addWeighted(img_clahe, 1.58, gaussian, -1.0, 0)

        # 3. Canny edge detection
        img_edges = cv2.Canny(img_sharp, 50, 150)

        # Convert back to PIL
        img_edges = Image.fromarray(img_edges)

        # Braille is 2x4 dots per character, so multiply dimensions
        img_rgb = img_rgb.resize((width * 2, height * 4), Image.Resampling.LANCZOS)
        img_edges = img_edges.resize((width * 2, height * 4), Image.Resampling.LANCZOS)
        pixels_edges = img_edges.load()
        pixels_rgb = img_rgb.load()

        # Braille dot positions (2x4 grid):
        # 0  3
        # 1  4
        # 2  5
        # 6  7
        dot_values = [
            (0, 0, 0x01), (0, 1, 0x02), (0, 2, 0x04), (0, 3, 0x40),
            (1, 0, 0x08), (1, 1, 0x10), (1, 2, 0x20), (1, 3, 0x80)
        ]

        # Also get grayscale for fill patterns
        img_gray_pil = img_rgb.convert("L")
        img_gray_pil = img_gray_pil.resize((width * 2, height * 4), Image.Resampling.LANCZOS)
        pixels_gray = img_gray_pil.load()

        lines = []
        for cy in range(height):
            row = ""
            for cx in range(width):
                # Calculate braille character for this 2x4 cell
                char_val = 0
                dot_colors = []  # Only collect colors where dots are shown
                brightness_sum = 0

                for dx, dy, bit in dot_values:
                    px = cx * 2 + dx
                    py = cy * 4 + dy
                    if px < img_edges.width and py < img_edges.height:
                        brightness_sum += pixels_gray[px, py]
                        # Bright pixels = edges = dots (threshold at 30)
                        if pixels_edges[px, py] > 30:
                            char_val |= bit
                            # Only collect color where dot is shown
                            r, g, b = pixels_rgb[px, py]
                            dot_colors.append((r, g, b))

                # If no edges detected, fill based on brightness pattern
                if char_val == 0:
                    avg_brightness = brightness_sum // 8
                    # Create fill pattern based on darkness (darker = more dots)
                    fill_bits = []
                    if avg_brightness < 64:
                        fill_bits = [0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80]
                    elif avg_brightness < 96:
                        fill_bits = [0x01, 0x04, 0x10, 0x40]
                    elif avg_brightness < 128:
                        fill_bits = [0x01, 0x10]
                    elif avg_brightness < 160:
                        fill_bits = [0x10]

                    for dx, dy, bit in dot_values:
                        if bit in fill_bits:
                            char_val |= bit
                            px = cx * 2 + dx
                            py = cy * 4 + dy
                            if px < img_rgb.width and py < img_rgb.height:
                                r, g, b = pixels_rgb[px, py]
                                dot_colors.append((r, g, b))

                # Average color only from pixels where dots are shown
                if dot_colors:
                    r_avg = sum(c[0] for c in dot_colors) // len(dot_colors)
                    g_avg = sum(c[1] for c in dot_colors) // len(dot_colors)
                    b_avg = sum(c[2] for c in dot_colors) // len(dot_colors)
                    # Boost intensity
                    intensity = 1.5
                    r_avg = min(255, int(r_avg * intensity))
                    g_avg = min(255, int(g_avg * intensity))
                    b_avg = min(255, int(b_avg * intensity))
                else:
                    r_avg, g_avg, b_avg = 50, 50, 50  # Dim fallback

                # Braille Unicode starts at U+2800, with ANSI color
                row += f"\033[38;2;{r_avg};{g_avg};{b_avg}m{chr(0x2800 + char_val)}"

            row += "\033[0m"  # Reset at end of row
            lines.append(row)

        return "\n".join(lines)
    except Exception as e:
        import traceback
        traceback.print_exc()
        return None


def create_command_input(command_text, error=None):
    """Create the command input display at the bottom."""
    input_text = Text()
    input_text.append("/", style="white")
    input_text.append(command_text, style="white")
    if error:
        input_text.append("\n")
        input_text.append(error, style="red")
    else:
        input_text.append("▋", style="white")  # Cursor
    return input_text


def create_tracks_display(tracks, selection, scroll, visible, playlist_name, playlist_art=None, total_tracks=0):
    """Create the tracks view for a playlist with cover art."""
    content = Text()

    # Playlist art at the top (fixed)
    if playlist_art:
        art_text = Text.from_ansi(playlist_art)
        content.append_text(art_text)
        content.append("\n\n")

    # Playlist info
    content.append(f"{playlist_name}\n", style="bold white")
    content.append(f"{total_tracks} tracks\n\n", style="dim")

    max_name_width = 30  # Fixed width for track names

    if not tracks:
        content.append("Loading tracks...", style="dim")
    else:
        total = len(tracks)
        end = min(scroll + visible, total)

        # Show scroll indicator at top
        if scroll > 0:
            content.append(f"  ↑ {scroll} more\n", style="dim")

        # Show only visible window of tracks
        for i in range(scroll, end):
            track = tracks[i]
            # Truncate or pad track name to fixed width
            name = track['name']
            if len(name) > max_name_width:
                name = name[:max_name_width - 1] + "…"
            name = name.ljust(max_name_width)

            artist = track['artist']
            if len(artist) > 20:
                artist = artist[:19] + "…"

            if i == selection:
                content.append("> ", style="green")
                content.append(f"{name}", style="white")
                content.append(f" {artist}\n", style="cyan")
            else:
                content.append("  ", style="dim")
                content.append(f"{name}", style="white")
                content.append(f" {artist}\n", style="dim")

        # Show scroll indicator at bottom
        remaining = total - end
        if remaining > 0:
            content.append(f"  ↓ {remaining} more\n", style="dim")

    content.append("\n[j/k] navigate  [enter] select  [esc] back", style="dim")

    return Panel(
        content,
        title=playlist_name,
        border_style="green",
        padding=(1, 2)
    )


def create_song_display(song, options, selection):
    """Create the song view with options."""
    content = Text()

    if not song:
        content.append("No song selected", style="dim")
    else:
        content.append(f"{song['name']}\n", style="bold white")
        content.append(f"{song['artist']}\n\n", style="cyan")

        for i, option in enumerate(options):
            if i == selection:
                content.append("> ", style="bold green")
                content.append(f"{option}\n", style="bold white")
            else:
                content.append("  ", style="dim")
                content.append(f"{option}\n", style="dim")

    content.append("\n[j/k] navigate  [enter] select  [esc] back  [/] command", style="dim")

    return Panel(
        content,
        title="Song",
        border_style="green",
        padding=(1, 2)
    )


def create_library_display(library_items, selection, scroll, visible):
    """Create the library view with selectable items."""
    content = Text()

    if not library_items:
        content.append("Loading library...", style="dim")
    else:
        total = len(library_items)
        end = min(scroll + visible, total)

        # Show scroll indicator at top
        if scroll > 0:
            content.append(f"  ↑ {scroll} more\n", style="dim")

        # Show only visible window of items
        for i in range(scroll, end):
            item = library_items[i]
            if i == selection:
                content.append("> ", style="bold green")
                content.append(f"{item['name']}\n", style="bold white")
            else:
                content.append("  ", style="dim")
                content.append(f"{item['name']}\n", style="dim")

        # Show scroll indicator at bottom
        remaining = total - end
        if remaining > 0:
            content.append(f"  ↓ {remaining} more\n", style="dim")

    content.append("\n[j/k or ↑/↓] navigate  [enter] select  [/] command", style="dim")

    return Panel(
        content,
        title="Library",
        border_style="green",
        padding=(1, 2)
    )


def create_search_display(player):
    """Create the search results view."""
    content = Text()

    if not player.search_query:
        content.append("Search\n\n", style="bold white")
        content.append("Use /search [query] to search", style="dim")
        content.append("\n\n[/] command", style="dim")
        return Panel(content, title="Search", border_style="yellow", padding=(1, 2))

    categories = [
        ("tracks", "Songs", lambda i: f"{i['name']} - {i['artist']}"),
        ("albums", "Albums", lambda i: f"{i['name']} - {i['artist']}"),
        ("artists", "Artists", lambda i: i['name']),
        ("playlists", "Playlists", lambda i: f"{i['name']} by {i['owner']}")
    ]

    if player.search_expanded is not None:
        # Expanded view - show all items in one category
        cat_key, cat_name, formatter = categories[player.search_expanded]
        items = player.search_results[cat_key]

        content.append(f"{cat_name}\n\n", style="bold white")

        if not items:
            content.append("No results", style="dim")
        else:
            total = len(items)
            end = min(player.search_scroll + player.search_visible, total)

            if player.search_scroll > 0:
                content.append(f"  ↑ {player.search_scroll} more\n", style="dim")

            for i in range(player.search_scroll, end):
                item = items[i]
                if i == player.search_item:
                    content.append("> ", style="bold green")
                    content.append(f"{formatter(item)}\n", style="bold white")
                else:
                    content.append("  ", style="dim")
                    content.append(f"{formatter(item)}\n", style="white")

            remaining = total - end
            if remaining > 0:
                content.append(f"  ↓ {remaining} more\n", style="dim")

        content.append("\n[j/k] navigate  [enter] play  [esc] back", style="dim")
    else:
        # Collapsed view - show 3 items per category + "see more"
        content.append(f'Results for "{player.search_query}"\n\n', style="bold white")

        for cat_idx, (cat_key, cat_name, formatter) in enumerate(categories):
            items = player.search_results[cat_key]
            is_current_cat = (cat_idx == player.search_category)

            content.append(f"{cat_name}\n", style="bold cyan")

            if not items:
                content.append("  No results\n", style="dim")
            else:
                # Show up to 3 items
                for i, item in enumerate(items[:3]):
                    is_selected = is_current_cat and player.search_item == i
                    if is_selected:
                        content.append("> ", style="bold green")
                        content.append(f"{formatter(item)}\n", style="bold white")
                    else:
                        content.append("  ", style="dim")
                        content.append(f"{formatter(item)}\n", style="white")

                # Show "see more" if there are more than 3 items
                if len(items) > 3:
                    is_see_more_selected = is_current_cat and player.search_item == 3
                    if is_see_more_selected:
                        content.append("> ", style="bold green")
                        content.append(f"* see more ({len(items) - 3} more)\n", style="bold yellow")
                    else:
                        content.append("  ", style="dim")
                        content.append(f"* see more ({len(items) - 3} more)\n", style="yellow")

            content.append("\n")

        content.append("[j/k] navigate  [enter] select  [/] new search", style="dim")

    return Panel(
        content,
        title=f"Search: {player.search_query}",
        border_style="green",
        padding=(1, 2)
    )


def format_followers(num):
    """Format follower count (e.g., 1.2M, 500K)."""
    if num >= 1_000_000:
        return f"{num / 1_000_000:.1f}M"
    elif num >= 1_000:
        return f"{num / 1_000:.1f}K"
    return str(num)


def create_artist_display(player):
    """Create the artist page view."""
    content = Text()

    if not player.artist_data:
        content.append("Loading artist...", style="dim")
        return Panel(content, title="Artist", border_style="green", padding=(1, 2))

    artist = player.artist_data

    # Artist header with info
    content.append(f"{artist['name']}\n", style="bold white")
    content.append(f"{format_followers(artist['followers'])} monthly listeners\n", style="dim")
    if artist['genres']:
        content.append(f"{', '.join(artist['genres'])}\n", style="cyan")
    content.append("\n")

    if player.artist_expanded is not None:
        # Expanded view - show all items in one section
        if player.artist_expanded == 0:
            section_name = "Top Songs"
            items = player.artist_top_tracks
            formatter = lambda i: f"{i['name']}"
        else:
            section_name = "Discography"
            items = player.artist_albums
            formatter = lambda i: f"{i['name']} ({i['year']})" if i['year'] else i['name']

        content.append(f"{section_name}\n\n", style="bold cyan")

        if not items:
            content.append("No results", style="dim")
        else:
            total = len(items)
            end = min(player.artist_scroll + player.artist_visible, total)

            if player.artist_scroll > 0:
                content.append(f"  ↑ {player.artist_scroll} more\n", style="dim")

            for i in range(player.artist_scroll, end):
                item = items[i]
                if i == player.artist_item:
                    content.append("> ", style="bold green")
                    content.append(f"{formatter(item)}\n", style="bold white")
                else:
                    content.append("  ", style="dim")
                    content.append(f"{formatter(item)}\n", style="white")

            remaining = total - end
            if remaining > 0:
                content.append(f"  ↓ {remaining} more\n", style="dim")

        content.append("\n[j/k] navigate  [enter] play  [esc] back", style="dim")
    else:
        # Collapsed view - show sections
        sections = [
            ("Top Songs", player.artist_top_tracks, lambda i: f"{i['name']}"),
            ("Discography", player.artist_albums, lambda i: f"{i['name']} ({i['year']})" if i['year'] else i['name'])
        ]

        for sec_idx, (sec_name, items, formatter) in enumerate(sections):
            is_current_sec = (sec_idx == player.artist_section)

            content.append(f"{sec_name}\n", style="bold cyan")

            if not items:
                content.append("  No results\n", style="dim")
            else:
                # Show up to 3 items
                for i, item in enumerate(items[:3]):
                    is_selected = is_current_sec and player.artist_item == i
                    if is_selected:
                        content.append("> ", style="bold green")
                        content.append(f"{formatter(item)}\n", style="bold white")
                    else:
                        content.append("  ", style="dim")
                        content.append(f"{formatter(item)}\n", style="white")

                # Show "see more" if there are more items
                if len(items) > 3:
                    is_see_more_selected = is_current_sec and player.artist_item == 3
                    if is_see_more_selected:
                        content.append("> ", style="bold green")
                        content.append(f"* see more ({len(items) - 3} more)\n", style="bold yellow")
                    else:
                        content.append("  ", style="dim")
                        content.append(f"* see more ({len(items) - 3} more)\n", style="yellow")

            content.append("\n")

        content.append("[j/k] navigate  [enter] select  [esc] back", style="dim")

    return Panel(
        content,
        title=f"Artist: {artist['name']}",
        border_style="green",
        padding=(1, 2)
    )


def create_album_display(player):
    """Create the album page view with fixed album art and scrollable tracks."""
    content = Text()

    if not player.album_data:
        content.append("Loading album...", style="dim")
        return Panel(content, title="Album", border_style="green", padding=(1, 2))

    album = player.album_data

    # Album art at the top (fixed)
    if player.album_art:
        art_text = Text.from_ansi(player.album_art)
        content.append_text(art_text)
        content.append("\n\n")

    # Album info
    content.append(f"{album['name']}\n", style="bold white")
    content.append(f"{album['artist']}", style="cyan")
    if album['year']:
        content.append(f" • {album['year']}", style="dim")
    content.append(f" • {album['total_tracks']} tracks\n\n", style="dim")

    # Tracks (scrollable)
    tracks = player.album_tracks
    max_name_width = 35  # Fixed width for track names
    if not tracks:
        content.append("No tracks", style="dim")
    else:
        total = len(tracks)
        end = min(player.album_scroll + player.album_visible, total)

        if player.album_scroll > 0:
            content.append(f"  ↑ {player.album_scroll} more\n", style="dim")

        for i in range(player.album_scroll, end):
            track = tracks[i]
            duration = format_time(track['duration_ms'])
            # Truncate or pad track name to fixed width
            name = track['name']
            if len(name) > max_name_width:
                name = name[:max_name_width - 1] + "…"
            name = name.ljust(max_name_width)

            if i == player.album_selection:
                content.append("> ", style="green")
                content.append(f"{track['track_number']:2}. {name}", style="white")
                content.append(f" {duration}\n", style="dim")
            else:
                content.append("  ", style="dim")
                content.append(f"{track['track_number']:2}. {name}", style="white")
                content.append(f" {duration}\n", style="dim")

        remaining = total - end
        if remaining > 0:
            content.append(f"  ↓ {remaining} more\n", style="dim")

    content.append("\n[j/k] navigate  [enter] play  [esc] back", style="dim")

    return Panel(
        content,
        title=f"Album: {album['name']}",
        border_style="green",
        padding=(1, 2)
    )


def create_visualizer_display(player):
    """Create the audio visualizer view with real-time audio capture."""
    global audio_capture

    content = Text()

    if not player.playback or not player.playback.get("item"):
        content.append("No track playing\n\n", style="dim")
        content.append("Play something to see the visualizer", style="dim")
        return Panel(content, title="Visualizer", border_style="yellow", padding=(1, 2))

    track = player.playback["item"]
    is_playing = player.playback.get("is_playing", False)
    progress_ms = player.get_interpolated_progress()
    duration_ms = track.get("duration_ms", 0)
    progress_pct = progress_ms / duration_ms if duration_ms > 0 else 0

    # Track info
    content.append(f"{track['name']}\n", style="bold white")
    content.append(f"{', '.join(a['name'] for a in track['artists'])}\n\n", style="cyan")

    num_bars = 32
    bar_chars = [" ", "▁", "▂", "▃", "▄", "▅", "▆", "▇", "█"]

    # Check if real audio capture is available
    using_real_audio = False
    bars = []

    if audio_capture and audio_capture.running and not audio_capture.error:
        # Real audio visualization
        bars = audio_capture.get_bars(num_bars)
        using_real_audio = True
        mode_text = "♫ Live Audio"
    else:
        # Fallback: simulated visualization
        progress_sec = progress_ms / 1000
        beat_phase = (progress_sec * 2) % 1
        beat_intensity = max(0, 1 - beat_phase * 2)

        for i in range(num_bars):
            wave = math.sin(progress_sec * 4 + i * 0.4) * 0.5 + 0.5
            height = wave * (0.5 + beat_intensity * 0.5) if is_playing else 0.2
            bars.append(height)

        if audio_capture and audio_capture.error:
            mode_text = f"♫ Simulated ({audio_capture.error})"
        else:
            mode_text = "♫ Simulated"

    content.append(f"{mode_text}\n\n", style="dim")

    # Calculate average intensity for beat detection
    avg_intensity = sum(bars) / len(bars) if bars else 0
    beat_intensity = min(1, avg_intensity * 2)

    # Colors based on intensity
    def get_bar_color(height, beat_int):
        if height > 0.7 or beat_int > 0.7:
            return "bold magenta"
        elif height > 0.5:
            return "cyan"
        elif height > 0.3:
            return "blue"
        else:
            return "dim blue"

    # Top half (going up)
    for row in range(4, 0, -1):
        threshold = row / 4
        for i, h in enumerate(bars):
            if h >= threshold:
                char_idx = min(8, int((h - threshold + 0.125) * 8))
                color = get_bar_color(h, beat_intensity) if is_playing else "dim"
                content.append(bar_chars[char_idx], style=color)
            else:
                content.append(" ")
        content.append("\n")

    # Center line
    center_char = "━" if is_playing else "─"
    center_color = "bold white" if beat_intensity > 0.5 else "dim"
    content.append(center_char * num_bars + "\n", style=center_color)

    # Bottom half (mirror)
    for row in range(1, 5):
        threshold = row / 4
        for i, h in enumerate(bars):
            if h >= threshold:
                char_idx = min(8, 8 - int((h - threshold + 0.125) * 8))
                color = get_bar_color(h, beat_intensity) if is_playing else "dim"
                content.append(bar_chars[char_idx], style=color)
            else:
                content.append(" ")
        content.append("\n")

    # Progress bar
    content.append("\n")
    prog_width = 30
    prog_filled = int(prog_width * progress_pct)
    content.append(f"{format_time(progress_ms)} ", style="dim")
    content.append("━" * prog_filled, style="green")
    content.append("─" * (prog_width - prog_filled), style="dim")
    content.append(f" {format_time(duration_ms)}\n", style="dim")

    content.append("\n[x] play/pause  [z/c] prev/next  [/player] back", style="dim")

    return Panel(
        content,
        title="♫ Visualizer",
        border_style="magenta" if is_playing else "dim",
        padding=(1, 2)
    )


def create_help_display():
    """Create the help view with all commands and navigation info."""
    content = Text()

    content.append("Commands\n\n", style="bold white")
    content.append("  /player", style="cyan")
    content.append("    - Now playing view\n", style="dim")
    content.append("  /library", style="cyan")
    content.append("   - Browse playlists\n", style="dim")
    content.append("  /search [query]", style="cyan")
    content.append(" - Search Spotify\n", style="dim")
    content.append("  /test", style="cyan")
    content.append("      - Audio visualizer\n", style="dim")
    content.append("  /volume [0-10]", style="cyan")
    content.append(" - Set volume\n", style="dim")
    content.append("  /help", style="cyan")
    content.append("      - This help screen\n", style="dim")

    content.append("\nPlayer Controls\n\n", style="bold white")
    content.append("  z", style="cyan")
    content.append("  - Previous track\n", style="dim")
    content.append("  x", style="cyan")
    content.append("  - Play/Pause\n", style="dim")
    content.append("  c", style="cyan")
    content.append("  - Next track\n", style="dim")

    content.append("\nNavigation\n\n", style="bold white")
    content.append("  j/↓", style="cyan")
    content.append("  - Move down\n", style="dim")
    content.append("  k/↑", style="cyan")
    content.append("  - Move up\n", style="dim")
    content.append("  enter", style="cyan")
    content.append(" - Select\n", style="dim")
    content.append("  esc", style="cyan")
    content.append("   - Go back\n", style="dim")
    content.append("  /", style="cyan")
    content.append("     - Command mode\n", style="dim")
    content.append("  q", style="cyan")
    content.append("     - Quit\n", style="dim")

    return Panel(
        content,
        title="Help",
        border_style="green",
        padding=(1, 2)
    )


def create_display(player):
    """Create the main display based on current view."""
    progress = player.get_interpolated_progress()

    # Get the appropriate view panel
    if player.current_view == "library":
        if player.song_view:
            view_panel = create_song_display(
                player.current_song, player.song_options,
                player.song_selection
            )
        elif player.tracks_view:
            view_panel = create_tracks_display(
                player.tracks, player.tracks_selection,
                player.tracks_scroll, player.tracks_visible,
                player.current_playlist_name, player.playlist_art,
                player.playlist_total_tracks
            )
        else:
            view_panel = create_library_display(
                player.library_items, player.library_selection,
                player.library_scroll, player.library_visible
            )
    elif player.current_view == "search":
        if player.album_view:
            view_panel = create_album_display(player)
        elif player.artist_view:
            view_panel = create_artist_display(player)
        elif player.tracks_view:
            # Playlist view from search
            view_panel = create_tracks_display(
                player.tracks, player.tracks_selection,
                player.tracks_scroll, player.tracks_visible,
                player.current_playlist_name, player.playlist_art,
                player.playlist_total_tracks
            )
        else:
            view_panel = create_search_display(player)
    elif player.current_view == "help":
        view_panel = create_help_display()
    elif player.current_view == "test":
        view_panel = create_visualizer_display(player)
    else:  # player view
        view_panel = create_player_display(
            player.playback, progress, player.album_art,
            player.last_track, player.audio_features
        )

    # Add command input below panel if in command mode
    if player.command_mode:
        return Group(view_panel, create_command_input(player.command_input))

    # Show error message with the invalid command
    if player.command_error:
        return Group(view_panel, create_command_input(player.last_invalid_command, player.command_error))

    return view_panel


def create_player_display(playback, progress_override=None, album_art=None, last_track=None, audio_features=None):
    # Use current track, or fall back to last known track
    track = None
    is_playing = False

    if playback and playback.get("item"):
        track = playback["item"]
        is_playing = playback["is_playing"]
    elif last_track:
        track = last_track
        is_playing = False

    if not track:
        return Panel(
            Text("Nothing playing\n\nPlay something on Spotify", justify="center", style="dim"),
            title="Spotify",
            border_style="dim",
            padding=(1, 2)
        )

    progress_ms = progress_override if progress_override is not None else 0
    duration_ms = track["duration_ms"]

    title = track["name"]
    artist = ", ".join(a["name"] for a in track["artists"])
    album = track["album"]["name"]

    progress_bar = create_progress_bar(progress_ms, duration_ms)
    time_display = f"{format_time(progress_ms)} / {format_time(duration_ms)}"

    pause_btn = "⏸" if is_playing else "▶"
    controls = f"⏮       {pause_btn}       ⏭"
    controls_hint = "[z]     [x]     [c]"

    # Build content
    content = Text()

    if album_art:
        # Parse ANSI codes from album art
        art_text = Text.from_ansi(album_art)
        content.append_text(art_text)
        content.append("\n\n")

    content.append(f"{title}\n", style="bold white")
    content.append(f"{artist}\n", style="cyan")
    content.append(f"{album}\n\n", style="dim")
    content.append(f"{progress_bar}\n", style="green")
    content.append(f"{time_display}\n\n", style="dim")
    content.append(f"{controls}\n", style="bold")
    content.append(f"{controls_hint}", style="dim")
    content.justify = "center"

    return Panel(
        content,
        title="Spotuify",
        border_style="green" if is_playing else "yellow",
        padding=(1, 2)
    )


class Player:
    def __init__(self, sp):
        self.sp = sp
        self.playback = None
        self.last_track = None  # Keep last track when nothing is playing
        self.last_sync = 0
        self.progress_ms = 0
        self.sync_interval = 5
        self.album_art = None
        self.current_album_url = None
        self.current_track_id = None
        self.audio_features = None
        # Command mode state
        self.command_mode = False
        self.command_input = ""
        self.command_error = ""
        self.last_invalid_command = ""
        self.valid_commands = ["player", "library", "search", "help", "test", "volume"]
        # View state: "player", "library", "search", "help", "test"
        self.current_view = "player"
        # Library state
        self.library_items = []
        self.library_selection = 0
        self.library_scroll = 0
        self.library_visible = 15
        self.library_loaded = False
        # Tracks view state
        self.tracks_view = False
        self.tracks = []
        self.tracks_selection = 0
        self.tracks_scroll = 0
        self.tracks_visible = 10  # Fewer visible since art takes space
        self.current_playlist_name = ""
        self.current_playlist_uri = None  # URI for context playback
        self.playlist_art = None  # Cover art for playlist/liked songs
        self.playlist_total_tracks = 0
        # Song view state
        self.song_view = False
        self.current_song = None
        self.song_options = ["Play", "Add to Queue", "Go to Album", "Go to Artist"]
        self.song_selection = 0
        # Search state
        self.search_query = ""
        self.search_results = {
            "tracks": [],
            "albums": [],
            "artists": [],
            "playlists": []
        }
        self.search_category = 0  # 0=tracks, 1=albums, 2=artists, 3=playlists
        self.search_item = 0  # Which item in current category (0-2 for collapsed, more for expanded)
        self.search_expanded = None  # Which category is expanded (None = collapsed view)
        self.search_scroll = 0
        self.search_visible = 15
        # Artist view state
        self.artist_view = False
        self.artist_data = None  # Artist info (name, followers, etc.)
        self.artist_top_tracks = []
        self.artist_albums = []
        self.artist_section = 0  # 0=top tracks, 1=discography
        self.artist_item = 0
        self.artist_expanded = None  # Which section is expanded
        self.artist_scroll = 0
        self.artist_visible = 15
        # Album view state
        self.album_view = False
        self.album_data = None  # Album info (name, artist, year, etc.)
        self.album_art = None  # Album art for album view
        self.album_tracks = []
        self.album_selection = 0
        self.album_scroll = 0
        self.album_visible = 10  # Fewer visible since album art takes space
        # Visualizer state
        self.audio_analysis = None
        self.audio_features = None
        self.visualizer_track_id = None

    def sync(self):
        try:
            self.playback = self.sp.current_playback()

            # Store last known track
            if self.playback and self.playback.get("item"):
                self.last_track = self.playback["item"]
                self.progress_ms = self.playback.get("progress_ms") or 0

                # Fetch album art if track changed
                images = self.last_track["album"].get("images", [])
                if images:
                    url = images[-1]["url"]
                    if url != self.current_album_url:
                        self.current_album_url = url
                        self.album_art = fetch_album_art(url, width=30, height=15)

            self.last_sync = time.time()
        except Exception:
            pass

    def get_interpolated_progress(self):
        if not self.playback or not self.playback.get("is_playing"):
            return self.progress_ms

        elapsed = (time.time() - self.last_sync) * 1000
        duration = self.playback["item"]["duration_ms"] if self.playback.get("item") else 0
        return min(self.progress_ms + elapsed, duration)

    def needs_sync(self):
        return time.time() - self.last_sync >= self.sync_interval

    def start_audio_capture(self):
        """Start audio capture for visualizer."""
        global audio_capture
        if audio_capture is None:
            audio_capture = AudioCapture()
        if not audio_capture.running:
            audio_capture.start()

    def stop_audio_capture(self):
        """Stop audio capture."""
        global audio_capture
        if audio_capture and audio_capture.running:
            audio_capture.stop()

    def play_pause(self):
        try:
            if self.playback and self.playback["is_playing"]:
                self.sp.pause_playback()
            else:
                self.sp.start_playback()
            time.sleep(0.2)
            self.sync()
        except Exception:
            pass

    def next_track(self):
        try:
            self.sp.next_track()
            time.sleep(0.3)
            self.sync()
        except Exception:
            pass

    def previous_track(self):
        try:
            self.sp.previous_track()
            time.sleep(0.3)
            self.sync()
        except Exception:
            pass

    def enter_command_mode(self):
        self.command_mode = True
        self.command_input = ""
        self.command_error = ""  # Clear any previous error
        self.last_invalid_command = ""

    def exit_command_mode(self):
        self.command_mode = False
        self.command_input = ""

    def find_closest_command(self, cmd):
        """Find the closest matching command based on prefix or similarity."""
        if not cmd:
            return None

        # Check for prefix match first
        for valid_cmd in self.valid_commands:
            if valid_cmd.startswith(cmd):
                return valid_cmd

        # Check if command starts with any valid command's prefix
        for valid_cmd in self.valid_commands:
            if cmd.startswith(valid_cmd[:len(cmd)]) and len(cmd) >= 1:
                return valid_cmd

        # Check for common characters
        best_match = None
        best_score = 0
        for valid_cmd in self.valid_commands:
            # Count matching characters from start
            score = 0
            for i, c in enumerate(cmd):
                if i < len(valid_cmd) and c == valid_cmd[i]:
                    score += 2  # Exact position match worth more
                elif c in valid_cmd:
                    score += 1
            if score > best_score:
                best_score = score
                best_match = valid_cmd

        # Only suggest if there's reasonable similarity
        if best_score >= len(cmd) * 0.5:
            return best_match
        return None

    def execute_command(self):
        """Execute the current command and exit command mode."""
        cmd = self.command_input.strip()
        self.exit_command_mode()
        self.command_error = ""  # Clear previous error

        # Parse command and arguments
        parts = cmd.split(maxsplit=1)
        base_cmd = parts[0].lower() if parts else ""
        args = parts[1] if len(parts) > 1 else ""

        if base_cmd == "player":
            if self.current_view == "test":
                self.stop_audio_capture()
            self.current_view = "player"
        elif base_cmd == "search":
            if args:
                if self.current_view == "test":
                    self.stop_audio_capture()
                self.search_query = args
                self.perform_search(args)
                self.current_view = "search"
            else:
                self.last_invalid_command = cmd
                self.command_error = "Usage: /search [query]"
        elif base_cmd == "library":
            if self.current_view == "test":
                self.stop_audio_capture()
            self.current_view = "library"
            if not self.library_loaded:
                self.load_library()
        elif base_cmd == "help":
            if self.current_view == "test":
                self.stop_audio_capture()
            self.current_view = "help"
        elif base_cmd == "test":
            self.current_view = "test"
            self.start_audio_capture()
        elif base_cmd == "volume":
            if args:
                try:
                    level = int(args)
                    if 0 <= level <= 10:
                        self.sp.volume(level * 10)  # Map 0-10 to 0-100
                    else:
                        self.last_invalid_command = cmd
                        self.command_error = "Volume must be 0-10"
                except ValueError:
                    self.last_invalid_command = cmd
                    self.command_error = "Usage: /volume [0-10]"
            else:
                self.last_invalid_command = cmd
                self.command_error = "Usage: /volume [0-10]"
        elif cmd:  # Non-empty invalid command
            self.last_invalid_command = cmd
            closest = self.find_closest_command(base_cmd)
            if closest:
                self.command_error = f'Invalid command, did you mean "/{closest}"?'
            else:
                self.command_error = "Invalid command, use /help for available commands"

        return cmd

    def load_library(self):
        """Load library items: Liked Songs + playlists."""
        try:
            self.library_items = []
            # Add Liked Songs as first item
            self.library_items.append({
                "type": "liked",
                "name": "Liked Songs",
                "id": None,
                "uri": None  # Liked songs don't have a context URI
            })
            # Fetch user playlists
            playlists = self.sp.current_user_playlists(limit=50)
            for playlist in playlists.get("items", []):
                self.library_items.append({
                    "type": "playlist",
                    "name": playlist["name"],
                    "id": playlist["id"],
                    "uri": playlist["uri"]
                })
            self.library_loaded = True
            self.library_selection = 0
        except Exception:
            pass

    def library_move_up(self):
        if self.song_view:
            if self.song_selection > 0:
                self.song_selection -= 1
        elif self.tracks_view:
            if self.tracks_selection > 0:
                self.tracks_selection -= 1
                if self.tracks_selection < self.tracks_scroll:
                    self.tracks_scroll = self.tracks_selection
        else:
            if self.library_selection > 0:
                self.library_selection -= 1
                if self.library_selection < self.library_scroll:
                    self.library_scroll = self.library_selection

    def library_move_down(self):
        if self.song_view:
            if self.song_selection < len(self.song_options) - 1:
                self.song_selection += 1
        elif self.tracks_view:
            if self.tracks_selection < len(self.tracks) - 1:
                self.tracks_selection += 1
                if self.tracks_selection >= self.tracks_scroll + self.tracks_visible:
                    self.tracks_scroll = self.tracks_selection - self.tracks_visible + 1
        else:
            if self.library_selection < len(self.library_items) - 1:
                self.library_selection += 1
                if self.library_selection >= self.library_scroll + self.library_visible:
                    self.library_scroll = self.library_selection - self.library_visible + 1

    def library_select(self):
        """Select current item - open playlist/liked songs or play track."""
        if self.song_view:
            # Execute the selected song option
            self.execute_song_option()
        elif self.tracks_view:
            # Play the selected track directly
            if self.tracks and self.tracks_selection < len(self.tracks):
                self.current_song = self.tracks[self.tracks_selection]
                try:
                    # Play within playlist context if available
                    if self.current_playlist_uri:
                        self.sp.start_playback(
                            context_uri=self.current_playlist_uri,
                            offset={"position": self.tracks_selection}
                        )
                    else:
                        # Fallback for Liked Songs (no context URI)
                        self.sp.start_playback(uris=[self.current_song["uri"]])
                    time.sleep(0.3)
                    self.sync()
                except Exception:
                    pass
        else:
            if not self.library_items:
                return
            item = self.library_items[self.library_selection]
            self.load_tracks(item)

    def execute_song_option(self):
        """Execute the selected song option."""
        if not self.current_song:
            return

        option = self.song_options[self.song_selection]
        try:
            if option == "Play":
                # Play within playlist context if available
                if self.current_playlist_uri:
                    self.sp.start_playback(
                        context_uri=self.current_playlist_uri,
                        offset={"position": self.tracks_selection}
                    )
                else:
                    # Fallback for Liked Songs (no context URI)
                    self.sp.start_playback(uris=[self.current_song["uri"]])
                time.sleep(0.3)
                self.sync()
                # Go back to tracks view after playing
                self.song_view = False
                self.current_song = None
            elif option == "Add to Queue":
                self.sp.add_to_queue(self.current_song["uri"])
                # Go back to tracks view after adding
                self.song_view = False
                self.current_song = None
            elif option == "Go to Album":
                # TODO: Implement album view
                pass
            elif option == "Go to Artist":
                # TODO: Implement artist view
                pass
        except Exception:
            pass

    def library_back(self):
        """Go back from song view to tracks, or tracks view to library list."""
        if self.song_view:
            self.song_view = False
            self.current_song = None
            self.song_selection = 0
        elif self.tracks_view:
            self.tracks_view = False
            self.tracks = []
            self.tracks_selection = 0
            self.tracks_scroll = 0

    def load_tracks(self, item):
        """Load tracks from a playlist or liked songs."""
        try:
            self.tracks = []
            self.current_playlist_name = item["name"]
            self.current_playlist_uri = item.get("uri")  # Store for context playback
            self.playlist_art = None

            if item["type"] == "liked":
                # Load liked songs
                # Get user ID for collection URI
                user = self.sp.current_user()
                self.current_playlist_uri = f"spotify:user:{user['id']}:collection"

                results = self.sp.current_user_saved_tracks(limit=50)
                self.playlist_total_tracks = results.get("total", len(results.get("items", [])))
                for saved in results.get("items", []):
                    track = saved.get("track")
                    if track:
                        self.tracks.append({
                            "name": track["name"],
                            "artist": ", ".join(a["name"] for a in track["artists"]),
                            "id": track["id"],
                            "uri": track["uri"]
                        })
                # Use first track's album art for liked songs
                if self.tracks and results.get("items"):
                    first_track = results["items"][0].get("track")
                    if first_track:
                        images = first_track.get("album", {}).get("images", [])
                        if images:
                            url = images[-1]["url"]
                            self.playlist_art = fetch_album_art(url, width=25, height=12)
            else:
                # Load playlist info for image
                playlist_info = self.sp.playlist(item["id"], fields="images,tracks.total")
                images = playlist_info.get("images", [])
                if images:
                    url = images[-1]["url"] if len(images) > 0 else images[0]["url"]
                    self.playlist_art = fetch_album_art(url, width=25, height=12)
                self.playlist_total_tracks = playlist_info.get("tracks", {}).get("total", 0)

                # Load playlist tracks
                results = self.sp.playlist_tracks(item["id"], limit=100)
                for track_item in results.get("items", []):
                    track = track_item.get("track")
                    if track:
                        self.tracks.append({
                            "name": track["name"],
                            "artist": ", ".join(a["name"] for a in track["artists"]),
                            "id": track["id"],
                            "uri": track["uri"]
                        })

            self.tracks_view = True
            self.tracks_selection = 0
            self.tracks_scroll = 0
        except Exception:
            pass

    def load_playlist_from_search(self, playlist_id, playlist_name):
        """Load a playlist from search results into the playlist view."""
        try:
            self.tracks = []
            self.current_playlist_name = playlist_name
            self.current_playlist_uri = f"spotify:playlist:{playlist_id}"
            self.playlist_art = None

            # Load playlist info for image
            playlist_info = self.sp.playlist(playlist_id, fields="images,tracks.total,uri")
            images = playlist_info.get("images", [])
            if images:
                url = images[-1]["url"] if len(images) > 0 else images[0]["url"]
                self.playlist_art = fetch_album_art(url, width=25, height=12)
            self.playlist_total_tracks = playlist_info.get("tracks", {}).get("total", 0)

            # Load playlist tracks
            results = self.sp.playlist_tracks(playlist_id, limit=100)
            for track_item in results.get("items", []):
                track = track_item.get("track")
                if track:
                    self.tracks.append({
                        "name": track["name"],
                        "artist": ", ".join(a["name"] for a in track["artists"]),
                        "id": track["id"],
                        "uri": track["uri"]
                    })

            # Enter playlist view within search context
            self.tracks_view = True
            self.tracks_selection = 0
            self.tracks_scroll = 0
            self._playlist_from_search = True
        except Exception:
            pass

    def perform_search(self, query):
        """Search Spotify for tracks, albums, artists, and playlists."""
        try:
            results = self.sp.search(q=query, type="track,album,artist,playlist", limit=20)

            # Parse tracks
            self.search_results["tracks"] = []
            for track in results.get("tracks", {}).get("items", []):
                self.search_results["tracks"].append({
                    "name": track["name"],
                    "artist": ", ".join(a["name"] for a in track["artists"]),
                    "id": track["id"],
                    "uri": track["uri"]
                })

            # Parse albums
            self.search_results["albums"] = []
            for album in results.get("albums", {}).get("items", []):
                self.search_results["albums"].append({
                    "name": album["name"],
                    "artist": ", ".join(a["name"] for a in album["artists"]),
                    "id": album["id"],
                    "uri": album["uri"]
                })

            # Parse artists
            self.search_results["artists"] = []
            for artist in results.get("artists", {}).get("items", []):
                self.search_results["artists"].append({
                    "name": artist["name"],
                    "id": artist["id"],
                    "uri": artist["uri"]
                })

            # Parse playlists
            self.search_results["playlists"] = []
            for playlist in results.get("playlists", {}).get("items", []):
                if playlist:  # Can be None sometimes
                    self.search_results["playlists"].append({
                        "name": playlist["name"],
                        "owner": playlist["owner"]["display_name"] if playlist.get("owner") else "Unknown",
                        "id": playlist["id"],
                        "uri": playlist["uri"]
                    })

            # Reset search navigation state
            self.search_category = 0
            self.search_item = 0
            self.search_expanded = None
            self.search_scroll = 0
        except Exception:
            pass

    def search_move_up(self):
        """Navigate up in search results."""
        if self.search_expanded is not None:
            # Expanded view - navigate within category
            if self.search_item > 0:
                self.search_item -= 1
                if self.search_item < self.search_scroll:
                    self.search_scroll = self.search_item
        else:
            # Collapsed view - navigate between items/categories
            if self.search_item > 0:
                self.search_item -= 1
            elif self.search_category > 0:
                self.search_category -= 1
                # Move to last item in previous category (3 items + "see more" = item 3)
                self.search_item = 3

    def search_move_down(self):
        """Navigate down in search results."""
        categories = ["tracks", "albums", "artists", "playlists"]

        if self.search_expanded is not None:
            # Expanded view - navigate within category
            cat_key = categories[self.search_expanded]
            max_items = len(self.search_results[cat_key])
            if self.search_item < max_items - 1:
                self.search_item += 1
                if self.search_item >= self.search_scroll + self.search_visible:
                    self.search_scroll = self.search_item - self.search_visible + 1
        else:
            # Collapsed view - navigate between items/categories
            cat_key = categories[self.search_category]
            num_items = min(3, len(self.search_results[cat_key]))
            # Items 0-2 are songs, item 3 is "see more"
            max_item = 3 if num_items > 0 else -1

            if self.search_item < max_item:
                self.search_item += 1
            elif self.search_category < 3:
                self.search_category += 1
                self.search_item = 0

    def search_select(self):
        """Select current item in search results."""
        categories = ["tracks", "albums", "artists", "playlists"]

        if self.search_expanded is not None:
            # Expanded view - select item
            cat_key = categories[self.search_expanded]
            if self.search_item < len(self.search_results[cat_key]):
                item = self.search_results[cat_key][self.search_item]
                self.play_search_item(cat_key, item)
        else:
            # Collapsed view
            cat_key = categories[self.search_category]
            if self.search_item == 3:
                # "See more" selected - expand category
                self.search_expanded = self.search_category
                self.search_item = 0
                self.search_scroll = 0
            elif self.search_item < len(self.search_results[cat_key]):
                # Item selected - play/open it
                item = self.search_results[cat_key][self.search_item]
                self.play_search_item(cat_key, item)

    def play_search_item(self, category, item):
        """Play or open a search result item."""
        try:
            if category == "tracks":
                self.sp.start_playback(uris=[item["uri"]])
                time.sleep(0.3)
                self.sync()
            elif category == "albums":
                # Open album page instead of playing
                self.load_album(item["id"], from_search=True)
            elif category == "artists":
                # Open artist page instead of playing
                self.load_artist(item["id"])
            elif category == "playlists":
                # Open playlist view instead of playing
                self.load_playlist_from_search(item["id"], item["name"])
        except Exception:
            pass

    def search_back(self):
        """Go back from expanded view to collapsed view, or from subviews to search."""
        if self.tracks_view and hasattr(self, '_playlist_from_search') and self._playlist_from_search:
            # Go back from playlist view to search results
            self.tracks_view = False
            self.tracks = []
            self.playlist_art = None
            self._playlist_from_search = False
        elif self.artist_view:
            self.artist_view = False
            self.artist_data = None
            self.artist_top_tracks = []
            self.artist_albums = []
        elif self.search_expanded is not None:
            self.search_expanded = None
            self.search_item = 3  # Go to "see more" of the category we were in
            self.search_scroll = 0

    def load_artist(self, artist_id):
        """Load artist page with info, top tracks, and albums."""
        try:
            # Get artist info
            artist = self.sp.artist(artist_id)
            self.artist_data = {
                "name": artist["name"],
                "id": artist["id"],
                "uri": artist["uri"],
                "followers": artist.get("followers", {}).get("total", 0),
                "genres": artist.get("genres", [])[:3],
                "images": artist.get("images", [])
            }

            # Get top tracks
            top_tracks = self.sp.artist_top_tracks(artist_id)
            self.artist_top_tracks = []
            for track in top_tracks.get("tracks", [])[:10]:
                self.artist_top_tracks.append({
                    "name": track["name"],
                    "id": track["id"],
                    "uri": track["uri"],
                    "album": track["album"]["name"]
                })

            # Get albums (discography)
            albums = self.sp.artist_albums(artist_id, album_type="album,single", limit=20)
            self.artist_albums = []
            seen_names = set()  # Avoid duplicate album names
            for album in albums.get("items", []):
                name = album["name"]
                if name not in seen_names:
                    seen_names.add(name)
                    self.artist_albums.append({
                        "name": name,
                        "id": album["id"],
                        "uri": album["uri"],
                        "type": album["album_type"],
                        "year": album["release_date"][:4] if album.get("release_date") else ""
                    })

            # Reset navigation state
            self.artist_view = True
            self.artist_section = 0
            self.artist_item = 0
            self.artist_expanded = None
            self.artist_scroll = 0
        except Exception:
            pass

    def artist_move_up(self):
        """Navigate up in artist view."""
        if self.artist_expanded is not None:
            # Expanded view
            if self.artist_item > 0:
                self.artist_item -= 1
                if self.artist_item < self.artist_scroll:
                    self.artist_scroll = self.artist_item
        else:
            # Collapsed view
            if self.artist_item > 0:
                self.artist_item -= 1
            elif self.artist_section > 0:
                self.artist_section -= 1
                # Move to "see more" of previous section
                self.artist_item = 3

    def artist_move_down(self):
        """Navigate down in artist view."""
        if self.artist_expanded is not None:
            # Expanded view
            items = self.artist_top_tracks if self.artist_expanded == 0 else self.artist_albums
            if self.artist_item < len(items) - 1:
                self.artist_item += 1
                if self.artist_item >= self.artist_scroll + self.artist_visible:
                    self.artist_scroll = self.artist_item - self.artist_visible + 1
        else:
            # Collapsed view - sections: 0=top tracks, 1=discography
            if self.artist_section == 0:
                num_items = min(3, len(self.artist_top_tracks))
            else:
                num_items = min(3, len(self.artist_albums))

            max_item = 3 if num_items > 0 else -1

            if self.artist_item < max_item:
                self.artist_item += 1
            elif self.artist_section < 1:
                self.artist_section += 1
                self.artist_item = 0

    def artist_select(self):
        """Select current item in artist view."""
        if self.artist_expanded is not None:
            # Expanded view - play item or open album
            if self.artist_expanded == 0:
                # Top tracks - play
                if self.artist_item < len(self.artist_top_tracks):
                    track = self.artist_top_tracks[self.artist_item]
                    try:
                        self.sp.start_playback(uris=[track["uri"]])
                        time.sleep(0.3)
                        self.sync()
                    except Exception:
                        pass
            else:
                # Albums - open album page
                if self.artist_item < len(self.artist_albums):
                    album = self.artist_albums[self.artist_item]
                    self.load_album(album["id"], from_search=False)
        else:
            # Collapsed view
            if self.artist_item == 3:
                # "See more" selected
                self.artist_expanded = self.artist_section
                self.artist_item = 0
                self.artist_scroll = 0
            else:
                # Item selected
                if self.artist_section == 0 and self.artist_item < len(self.artist_top_tracks):
                    # Top tracks - play
                    track = self.artist_top_tracks[self.artist_item]
                    try:
                        self.sp.start_playback(uris=[track["uri"]])
                        time.sleep(0.3)
                        self.sync()
                    except Exception:
                        pass
                elif self.artist_section == 1 and self.artist_item < len(self.artist_albums):
                    # Albums - open album page
                    album = self.artist_albums[self.artist_item]
                    self.load_album(album["id"], from_search=False)

    def artist_back(self):
        """Go back from expanded view or exit artist view."""
        if self.album_view:
            # Go back from album view to artist view
            self.album_view = False
            self.album_data = None
            self.album_art = None
        elif self.artist_expanded is not None:
            self.artist_expanded = None
            self.artist_item = 3
            self.artist_scroll = 0
        else:
            self.artist_view = False
            self.artist_data = None

    def load_album(self, album_id, from_search=False):
        """Load album page with art and tracks."""
        try:
            # Get album info
            album = self.sp.album(album_id)
            self.album_data = {
                "name": album["name"],
                "id": album["id"],
                "uri": album["uri"],
                "artist": ", ".join(a["name"] for a in album["artists"]),
                "year": album["release_date"][:4] if album.get("release_date") else "",
                "total_tracks": album.get("total_tracks", 0),
                "images": album.get("images", [])
            }

            # Fetch album art
            images = album.get("images", [])
            if images:
                # Use smaller image for album view
                url = images[-1]["url"] if len(images) > 0 else images[0]["url"]
                self.album_art = fetch_album_art(url, width=25, height=12)
            else:
                self.album_art = None

            # Get tracks
            self.album_tracks = []
            for i, track in enumerate(album.get("tracks", {}).get("items", [])):
                self.album_tracks.append({
                    "name": track["name"],
                    "id": track["id"],
                    "uri": track["uri"],
                    "track_number": track.get("track_number", i + 1),
                    "duration_ms": track.get("duration_ms", 0)
                })

            # Reset navigation state
            self.album_view = True
            self.album_selection = 0
            self.album_scroll = 0
            self._album_from_search = from_search  # Track where we came from
        except Exception:
            pass

    def album_move_up(self):
        """Navigate up in album view."""
        if self.album_selection > 0:
            self.album_selection -= 1
            if self.album_selection < self.album_scroll:
                self.album_scroll = self.album_selection

    def album_move_down(self):
        """Navigate down in album view."""
        if self.album_selection < len(self.album_tracks) - 1:
            self.album_selection += 1
            if self.album_selection >= self.album_scroll + self.album_visible:
                self.album_scroll = self.album_selection - self.album_visible + 1

    def album_select(self):
        """Play selected track from album, continuing with remaining tracks."""
        if self.album_tracks and self.album_selection < len(self.album_tracks) and self.album_data:
            try:
                # Play from album context at the selected position
                self.sp.start_playback(
                    context_uri=self.album_data["uri"],
                    offset={"position": self.album_selection}
                )
                time.sleep(0.3)
                self.sync()
            except Exception:
                pass

    def album_back(self):
        """Go back from album view."""
        self.album_view = False
        self.album_data = None
        self.album_art = None


def main():
    sp = get_spotify_client()
    player = Player(sp)

    # Set terminal title (both window and tab)
    sys.stdout.write("\033]0;spotuify\007")
    sys.stdout.write("\033]1;spotuify\007")
    sys.stdout.write("\033]2;spotuify\007")
    sys.stdout.flush()

    # Initial sync
    console.print("[dim]Connecting to Spotify...[/dim]")
    player.sync()

    fd = sys.stdin.fileno()
    old_settings = termios.tcgetattr(fd)

    try:
        tty.setcbreak(fd)

        with Live(console=console, refresh_per_second=30, screen=True) as live:
            while True:
                if player.needs_sync():
                    player.sync()

                live.update(create_display(player))

                if select.select([sys.stdin], [], [], 0.016)[0]:
                    key = sys.stdin.read(1)

                    if player.command_mode:
                        # Handle command mode input
                        if key == '\x1b':  # Escape - check for arrow keys or just escape
                            # Check if it's an arrow key sequence
                            if select.select([sys.stdin], [], [], 0.05)[0]:
                                seq = sys.stdin.read(2)
                                # Ignore arrow keys in command mode
                            else:
                                player.exit_command_mode()
                        elif key == '\n' or key == '\r':  # Enter
                            player.execute_command()
                        elif key == '\x7f':  # Backspace
                            player.command_input = player.command_input[:-1]
                        elif key.isprintable():
                            player.command_input += key
                    else:
                        # Normal mode - handle escape sequences (arrow keys)
                        if key == '\x1b':
                            # Read potential escape sequence
                            is_arrow = False
                            if select.select([sys.stdin], [], [], 0.01)[0]:
                                next_char = sys.stdin.read(1)
                                if next_char == '[':
                                    if select.select([sys.stdin], [], [], 0.01)[0]:
                                        arrow = sys.stdin.read(1)
                                        is_arrow = True
                                        if arrow == 'A':  # Up arrow
                                            if player.current_view == "library":
                                                player.library_move_up()
                                            elif player.current_view == "search":
                                                if player.album_view:
                                                    player.album_move_up()
                                                elif player.artist_view:
                                                    player.artist_move_up()
                                                elif player.tracks_view:
                                                    player.library_move_up()
                                                else:
                                                    player.search_move_up()
                                        elif arrow == 'B':  # Down arrow
                                            if player.current_view == "library":
                                                player.library_move_down()
                                            elif player.current_view == "search":
                                                if player.album_view:
                                                    player.album_move_down()
                                                elif player.artist_view:
                                                    player.artist_move_down()
                                                elif player.tracks_view:
                                                    player.library_move_down()
                                                else:
                                                    player.search_move_down()
                            # Plain escape (no arrow sequence) - go back
                            if not is_arrow:
                                if player.current_view == "library":
                                    player.library_back()
                                elif player.current_view == "search":
                                    if player.album_view:
                                        player.album_back()
                                    elif player.artist_view:
                                        player.artist_back()
                                    elif player.tracks_view:
                                        player.search_back()
                                    else:
                                        player.search_back()
                        elif key == 'q':
                            break
                        elif key == '/':
                            player.enter_command_mode()
                        elif key == '\n' or key == '\r':  # Enter
                            if player.current_view == "library":
                                player.library_select()
                            elif player.current_view == "search":
                                if player.album_view:
                                    player.album_select()
                                elif player.artist_view:
                                    player.artist_select()
                                elif player.tracks_view:
                                    player.library_select()
                                else:
                                    player.search_select()
                        elif player.current_view == "search":
                            # Search/Artist/Album/Playlist-specific keys (j/k for vim-style nav)
                            if player.album_view:
                                if key == 'k':
                                    player.album_move_up()
                                elif key == 'j':
                                    player.album_move_down()
                                elif key == 'h':
                                    player.album_back()
                            elif player.artist_view:
                                if key == 'k':
                                    player.artist_move_up()
                                elif key == 'j':
                                    player.artist_move_down()
                                elif key == 'h':
                                    player.artist_back()
                            elif player.tracks_view:
                                if key == 'k':
                                    player.library_move_up()
                                elif key == 'j':
                                    player.library_move_down()
                                elif key == 'h':
                                    player.search_back()
                            else:
                                if key == 'k':
                                    player.search_move_up()
                                elif key == 'j':
                                    player.search_move_down()
                                elif key == 'h':
                                    player.search_back()
                        elif player.current_view == "library":
                            # Library-specific keys (j/k for vim-style nav)
                            if key == 'k':
                                player.library_move_up()
                            elif key == 'j':
                                player.library_move_down()
                            elif key == 'h':  # h to go back
                                player.library_back()
                        elif player.current_view == "player":
                            # Player-specific keys
                            if key == 'x':
                                player.play_pause()
                            elif key == 'z':
                                player.previous_track()
                            elif key == 'c':
                                player.next_track()
                        elif player.current_view == "test":
                            # Visualizer controls (same as player)
                            if key == 'x':
                                player.play_pause()
                            elif key == 'z':
                                player.previous_track()
                            elif key == 'c':
                                player.next_track()

    finally:
        player.stop_audio_capture()
        termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)


if __name__ == "__main__":
    main()
