modules/UI/PeakDashboard.psm1

# Peak Dashboard Module
# Shows real-time track insights in a Windows Form window
# Uses standard Spotify API data (no extended quota required)

function Show-PeakDashboard {
    <#
    .SYNOPSIS
    Display track insights dashboard for the currently playing track
 
    .DESCRIPTION
    Creates a non-blocking Windows Form that displays track and artist data
    (popularity, artist popularity, followers, genres, album info, duration)
    with progress bars. Automatically updates when the track changes.
 
    .EXAMPLE
    Show-PeakDashboard
 
    .EXAMPLE
    peak
    #>


    $scriptPath = $PSScriptRoot
    $modulePath = Split-Path -Parent $scriptPath
    $rootPath = Split-Path -Parent $modulePath

    # Get current environment variables for Spotify credentials
    $clientId = $env:SPOTIFY_CLIENT_ID
    $clientSecret = $env:SPOTIFY_CLIENT_SECRET

    # Create temp directory
    $tempDir = Join-Path $env:TEMP "SpotifyPeak"
    if (-not (Test-Path $tempDir)) {
        New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
    }

    $backgroundScript = @"
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
 
# Set credentials for token refresh
`$env:SPOTIFY_CLIENT_ID = '$clientId'
`$env:SPOTIFY_CLIENT_SECRET = '$clientSecret'
 
# Import API client
Import-Module '$rootPath\modules\Core\LegacyApiClient.psm1' -Force -ErrorAction SilentlyContinue
 
# --- State ---
`$script:currentTrackUri = `$null
`$script:popHistory = [System.Collections.Generic.List[int]]::new()
 
# --- Colors ---
`$bgColor = [System.Drawing.ColorTranslator]::FromHtml('#1A1A1A')
`$bgLight = [System.Drawing.ColorTranslator]::FromHtml('#2D2D2D')
`$accent = [System.Drawing.ColorTranslator]::FromHtml('#4ECDC4')
`$textWhite = [System.Drawing.Color]::White
`$textGray = [System.Drawing.ColorTranslator]::FromHtml('#B3B3B3')
`$goldColor = [System.Drawing.ColorTranslator]::FromHtml('#FFD700')
`$pinkColor = [System.Drawing.ColorTranslator]::FromHtml('#FF69B4')
 
# --- Form ---
`$form = New-Object System.Windows.Forms.Form
`$form.Text = "Peak Dashboard"
`$form.Size = New-Object System.Drawing.Size(420, 530)
`$form.StartPosition = 'CenterScreen'
`$form.BackColor = `$bgColor
`$form.ForeColor = `$textWhite
`$form.FormBorderStyle = 'FixedToolWindow'
`$form.TopMost = `$true
 
# --- Header: Song + Artist ---
`$lblSong = New-Object System.Windows.Forms.Label
`$lblSong.Location = New-Object System.Drawing.Point(15, 12)
`$lblSong.Size = New-Object System.Drawing.Size(375, 24)
`$lblSong.Font = New-Object System.Drawing.Font('Segoe UI', 12, [System.Drawing.FontStyle]::Bold)
`$lblSong.ForeColor = `$accent
`$lblSong.Text = "Loading..."
`$form.Controls.Add(`$lblSong)
 
`$lblArtist = New-Object System.Windows.Forms.Label
`$lblArtist.Location = New-Object System.Drawing.Point(15, 38)
`$lblArtist.Size = New-Object System.Drawing.Size(375, 20)
`$lblArtist.Font = New-Object System.Drawing.Font('Segoe UI', 9)
`$lblArtist.ForeColor = `$textGray
`$lblArtist.Text = ""
`$form.Controls.Add(`$lblArtist)
 
# --- Metric rows with progress bars ---
`$metrics = @(
    @{ Name = "Track Pop."; Tag = "trackpop" },
    @{ Name = "Artist Pop."; Tag = "artistpop" }
)
 
`$script:bars = @{}
`$script:valueLabels = @{}
`$yStart = 72
`$rowHeight = 42
 
foreach (`$m in `$metrics) {
    `$y = `$yStart + (`$metrics.IndexOf(`$m) * `$rowHeight)
 
    `$nameLabel = New-Object System.Windows.Forms.Label
    `$nameLabel.Location = New-Object System.Drawing.Point(15, `$y)
    `$nameLabel.Size = New-Object System.Drawing.Size(90, 18)
    `$nameLabel.Font = New-Object System.Drawing.Font('Segoe UI', 9)
    `$nameLabel.ForeColor = `$textGray
    `$nameLabel.Text = `$m.Name
    `$form.Controls.Add(`$nameLabel)
 
    `$bar = New-Object System.Windows.Forms.ProgressBar
    `$bar.Location = New-Object System.Drawing.Point(110, `$y)
    `$bar.Size = New-Object System.Drawing.Size(210, 18)
    `$bar.Style = 'Continuous'
    `$bar.ForeColor = `$accent
    `$bar.BackColor = `$bgLight
    `$bar.Minimum = 0
    `$bar.Maximum = 100
    `$bar.Value = 0
    `$form.Controls.Add(`$bar)
    `$script:bars[`$m.Tag] = `$bar
 
    `$valLabel = New-Object System.Windows.Forms.Label
    `$valLabel.Location = New-Object System.Drawing.Point(328, `$y)
    `$valLabel.Size = New-Object System.Drawing.Size(70, 18)
    `$valLabel.Font = New-Object System.Drawing.Font('Consolas', 9)
    `$valLabel.ForeColor = `$textWhite
    `$valLabel.Text = "---"
    `$valLabel.TextAlign = [System.Drawing.ContentAlignment]::MiddleRight
    `$form.Controls.Add(`$valLabel)
    `$script:valueLabels[`$m.Tag] = `$valLabel
}
 
# --- Info section ---
`$yInfo = `$yStart + (`$metrics.Count * `$rowHeight) + 8
 
# Followers row
`$lblFollowersTitle = New-Object System.Windows.Forms.Label
`$lblFollowersTitle.Location = New-Object System.Drawing.Point(15, `$yInfo)
`$lblFollowersTitle.Size = New-Object System.Drawing.Size(90, 18)
`$lblFollowersTitle.Font = New-Object System.Drawing.Font('Segoe UI', 9)
`$lblFollowersTitle.ForeColor = `$textGray
`$lblFollowersTitle.Text = "Followers"
`$form.Controls.Add(`$lblFollowersTitle)
 
`$lblFollowers = New-Object System.Windows.Forms.Label
`$lblFollowers.Location = New-Object System.Drawing.Point(110, `$yInfo)
`$lblFollowers.Size = New-Object System.Drawing.Size(280, 18)
`$lblFollowers.Font = New-Object System.Drawing.Font('Consolas', 9)
`$lblFollowers.ForeColor = `$goldColor
`$lblFollowers.Text = "---"
`$form.Controls.Add(`$lblFollowers)
 
# Duration row
`$yDur = `$yInfo + 28
`$lblDurTitle = New-Object System.Windows.Forms.Label
`$lblDurTitle.Location = New-Object System.Drawing.Point(15, `$yDur)
`$lblDurTitle.Size = New-Object System.Drawing.Size(90, 18)
`$lblDurTitle.Font = New-Object System.Drawing.Font('Segoe UI', 9)
`$lblDurTitle.ForeColor = `$textGray
`$lblDurTitle.Text = "Duration"
`$form.Controls.Add(`$lblDurTitle)
 
`$lblDuration = New-Object System.Windows.Forms.Label
`$lblDuration.Location = New-Object System.Drawing.Point(110, `$yDur)
`$lblDuration.Size = New-Object System.Drawing.Size(280, 18)
`$lblDuration.Font = New-Object System.Drawing.Font('Consolas', 9)
`$lblDuration.ForeColor = `$textWhite
`$lblDuration.Text = "---"
`$form.Controls.Add(`$lblDuration)
 
# Album row
`$yAlbum = `$yDur + 28
`$lblAlbumTitle = New-Object System.Windows.Forms.Label
`$lblAlbumTitle.Location = New-Object System.Drawing.Point(15, `$yAlbum)
`$lblAlbumTitle.Size = New-Object System.Drawing.Size(90, 18)
`$lblAlbumTitle.Font = New-Object System.Drawing.Font('Segoe UI', 9)
`$lblAlbumTitle.ForeColor = `$textGray
`$lblAlbumTitle.Text = "Album"
`$form.Controls.Add(`$lblAlbumTitle)
 
`$lblAlbum = New-Object System.Windows.Forms.Label
`$lblAlbum.Location = New-Object System.Drawing.Point(110, `$yAlbum)
`$lblAlbum.Size = New-Object System.Drawing.Size(280, 18)
`$lblAlbum.Font = New-Object System.Drawing.Font('Segoe UI', 9)
`$lblAlbum.ForeColor = `$textWhite
`$lblAlbum.Text = "---"
`$form.Controls.Add(`$lblAlbum)
 
# Release + Track number row
`$yRelease = `$yAlbum + 22
`$lblRelease = New-Object System.Windows.Forms.Label
`$lblRelease.Location = New-Object System.Drawing.Point(110, `$yRelease)
`$lblRelease.Size = New-Object System.Drawing.Size(280, 18)
`$lblRelease.Font = New-Object System.Drawing.Font('Segoe UI', 8)
`$lblRelease.ForeColor = `$textGray
`$lblRelease.Text = ""
`$form.Controls.Add(`$lblRelease)
 
# Explicit badge
`$lblExplicit = New-Object System.Windows.Forms.Label
`$lblExplicit.Location = New-Object System.Drawing.Point(15, `$yRelease)
`$lblExplicit.Size = New-Object System.Drawing.Size(90, 18)
`$lblExplicit.Font = New-Object System.Drawing.Font('Segoe UI', 8, [System.Drawing.FontStyle]::Bold)
`$lblExplicit.ForeColor = `$pinkColor
`$lblExplicit.Text = ""
`$form.Controls.Add(`$lblExplicit)
 
# --- Genres ---
`$yGenres = `$yRelease + 28
`$lblGenresTitle = New-Object System.Windows.Forms.Label
`$lblGenresTitle.Location = New-Object System.Drawing.Point(15, `$yGenres)
`$lblGenresTitle.Size = New-Object System.Drawing.Size(90, 18)
`$lblGenresTitle.Font = New-Object System.Drawing.Font('Segoe UI', 9)
`$lblGenresTitle.ForeColor = `$textGray
`$lblGenresTitle.Text = "Genres"
`$form.Controls.Add(`$lblGenresTitle)
 
`$lblGenres = New-Object System.Windows.Forms.Label
`$lblGenres.Location = New-Object System.Drawing.Point(110, `$yGenres)
`$lblGenres.Size = New-Object System.Drawing.Size(280, 36)
`$lblGenres.Font = New-Object System.Drawing.Font('Segoe UI', 8)
`$lblGenres.ForeColor = `$accent
`$lblGenres.Text = "---"
`$form.Controls.Add(`$lblGenres)
 
# --- Mini-graph panel (popularity history) ---
`$yGraph = `$yGenres + 44
`$graphPanel = New-Object System.Windows.Forms.Panel
`$graphPanel.Location = New-Object System.Drawing.Point(15, `$yGraph)
`$graphPanel.Size = New-Object System.Drawing.Size(375, 60)
`$graphPanel.BackColor = `$bgLight
`$form.Controls.Add(`$graphPanel)
 
`$lblGraphTitle = New-Object System.Windows.Forms.Label
`$lblGraphTitle.Location = New-Object System.Drawing.Point(5, 2)
`$lblGraphTitle.Size = New-Object System.Drawing.Size(365, 16)
`$lblGraphTitle.Font = New-Object System.Drawing.Font('Segoe UI', 8)
`$lblGraphTitle.ForeColor = `$textGray
`$lblGraphTitle.Text = "Popularity (last 10 tracks)"
`$graphPanel.Controls.Add(`$lblGraphTitle)
 
`$lblGraph = New-Object System.Windows.Forms.Label
`$lblGraph.Location = New-Object System.Drawing.Point(5, 22)
`$lblGraph.Size = New-Object System.Drawing.Size(365, 34)
`$lblGraph.Font = New-Object System.Drawing.Font('Consolas', 18)
`$lblGraph.ForeColor = `$accent
`$lblGraph.Text = ""
`$graphPanel.Controls.Add(`$lblGraph)
 
# --- Status label ---
`$yStatus = `$yGraph + 65
`$lblStatus = New-Object System.Windows.Forms.Label
`$lblStatus.Location = New-Object System.Drawing.Point(15, `$yStatus)
`$lblStatus.Size = New-Object System.Drawing.Size(375, 18)
`$lblStatus.Font = New-Object System.Drawing.Font('Segoe UI', 8)
`$lblStatus.ForeColor = `$textGray
`$lblStatus.Text = "Waiting for track..."
`$form.Controls.Add(`$lblStatus)
 
# --- Helper: value to block char ---
function Get-BlockChar {
    param([double]`$val)
    `$blocks = @([char]0x2581, [char]0x2582, [char]0x2583, [char]0x2584, [char]0x2585, [char]0x2586, [char]0x2587, [char]0x2588)
    `$idx = [Math]::Min(7, [Math]::Max(0, [int](`$val * 7.99)))
    return `$blocks[`$idx]
}
 
# --- Helper: format follower count ---
function Format-Number {
    param([long]`$n)
    if (`$n -ge 1000000) { return "{0:N1}M" -f (`$n / 1000000) }
    if (`$n -ge 1000) { return "{0:N1}K" -f (`$n / 1000) }
    return `$n.ToString("N0")
}
 
# --- Helper: update dashboard from playback + artist data ---
function Update-Dashboard {
    param(`$playback, `$artistData)
 
    `$item = `$playback.item
 
    # Track popularity (0-100)
    `$trackPop = [int]`$item.popularity
    `$script:bars['trackpop'].Value = [Math]::Min(100, [Math]::Max(0, `$trackPop))
    `$script:valueLabels['trackpop'].Text = "`$trackPop/100"
 
    # Artist popularity (0-100)
    if (`$artistData) {
        `$artistPop = [int]`$artistData.popularity
        `$script:bars['artistpop'].Value = [Math]::Min(100, [Math]::Max(0, `$artistPop))
        `$script:valueLabels['artistpop'].Text = "`$artistPop/100"
 
        # Followers
        `$followers = [long]`$artistData.followers.total
        `$lblFollowers.Text = Format-Number `$followers
 
        # Genres
        `$genres = `$artistData.genres
        if (`$genres -and `$genres.Count -gt 0) {
            `$lblGenres.Text = (`$genres | Select-Object -First 4) -join ", "
        } else {
            `$lblGenres.Text = "n/a"
        }
    }
 
    # Duration
    `$durMs = [long]`$item.duration_ms
    `$durMin = [int][Math]::Floor(`$durMs / 60000)
    `$durSec = [int][Math]::Floor((`$durMs % 60000) / 1000)
    `$lblDuration.Text = "{0}:{1:D2}" -f `$durMin, `$durSec
 
    # Album
    `$lblAlbum.Text = `$item.album.name
    `$releaseDate = `$item.album.release_date
    `$trackNum = `$item.track_number
    `$totalTracks = `$item.album.total_tracks
    `$lblRelease.Text = "Released: `$releaseDate | Track `$trackNum of `$totalTracks"
 
    # Explicit
    `$lblExplicit.Text = if (`$item.explicit) { "EXPLICIT" } else { "" }
 
    # Update popularity history graph
    `$script:popHistory.Add(`$trackPop)
    if (`$script:popHistory.Count -gt 10) {
        `$script:popHistory.RemoveAt(0)
    }
    `$graphChars = (`$script:popHistory | ForEach-Object { Get-BlockChar (`$_ / 100.0) }) -join " "
    `$lblGraph.Text = `$graphChars
 
    `$lblStatus.Text = "Updated: `$(Get-Date -Format 'HH:mm:ss')"
}
 
# --- Timer: poll every 2 seconds ---
`$timer = New-Object System.Windows.Forms.Timer
`$timer.Interval = 2000
 
`$timer.Add_Tick({
    try {
        `$playback = Invoke-SpotifyApi -Method GET -Path '/me/player' -ErrorAction SilentlyContinue
        if (-not `$playback -or -not `$playback.item) {
            `$lblStatus.Text = "No track playing"
            return
        }
 
        `$trackUri = `$playback.item.uri
        `$isPlaying = `$playback.is_playing
 
        if (-not `$isPlaying) {
            `$lblStatus.Text = "Paused"
        }
 
        # Only update on track change
        if (`$trackUri -ne `$script:currentTrackUri) {
            `$script:currentTrackUri = `$trackUri
 
            # Update header
            `$songName = `$playback.item.name
            `$artists = (`$playback.item.artists | ForEach-Object { `$_.name }) -join ", "
            `$lblSong.Text = `$songName
            `$lblArtist.Text = `$artists
            `$form.Text = "Peak - `$songName"
 
            # Fetch artist details (popularity, followers, genres)
            `$artistData = `$null
            try {
                `$artistId = `$playback.item.artists[0].id
                `$artistData = Invoke-SpotifyApi -Method GET -Path "/artists/`$artistId" -ErrorAction SilentlyContinue
            } catch { }
 
            Update-Dashboard `$playback `$artistData
        }
    } catch {
        # Silently ignore errors
    }
}.GetNewClosure())
 
`$timer.Start()
 
# --- Do initial fetch immediately ---
try {
    `$initPlayback = Invoke-SpotifyApi -Method GET -Path '/me/player' -ErrorAction SilentlyContinue
    if (`$initPlayback -and `$initPlayback.item) {
        `$script:currentTrackUri = `$initPlayback.item.uri
        `$lblSong.Text = `$initPlayback.item.name
        `$lblArtist.Text = (`$initPlayback.item.artists | ForEach-Object { `$_.name }) -join ", "
        `$form.Text = "Peak - `$(`$initPlayback.item.name)"
 
        `$artistData = `$null
        try {
            `$artistId = `$initPlayback.item.artists[0].id
            `$artistData = Invoke-SpotifyApi -Method GET -Path "/artists/`$artistId" -ErrorAction SilentlyContinue
        } catch { }
 
        Update-Dashboard `$initPlayback `$artistData
    }
} catch {
    `$lblStatus.Text = "Init error: `$(`$_.Exception.Message)"
}
 
# --- Cleanup ---
`$form.Add_FormClosing({
    `$timer.Stop()
    `$timer.Dispose()
})
 
`$form.Add_FormClosed({
    Remove-Item '$scriptFile' -Force -ErrorAction SilentlyContinue
})
 
[void]`$form.ShowDialog()
"@


    # Save script to temp file
    $scriptFile = Join-Path $tempDir "peak_dashboard_$(Get-Date -Format 'yyyyMMddHHmmss').ps1"
    $backgroundScript | Out-File -FilePath $scriptFile -Encoding UTF8

    # Start background process
    Start-Process pwsh -ArgumentList @(
        '-NoProfile',
        '-WindowStyle', 'Hidden',
        '-ExecutionPolicy', 'Bypass',
        '-File', $scriptFile
    )

    Write-Host "Peak Dashboard launched!" -ForegroundColor Green
    Write-Host "Dashboard updates automatically when song changes" -ForegroundColor Cyan
}

function Get-SpotifyPeakInfo {
    <#
    .SYNOPSIS
    Get track and artist insights for the currently playing track
    .DESCRIPTION
    Returns $null when nothing is playing or the item is not a music track. Pass the same -Cache
    hashtable between calls (as peak -Watch does) so artist data, your top lists and per-track
    extras are only fetched once.
    #>

    param([hashtable]$Cache = @{})

    $playback = Invoke-SpotifyApi -Method GET -Path '/me/player' -ErrorAction SilentlyContinue
    if (-not $playback -or -not $playback.item -or -not $playback.item.artists) { return $null }
    $item = $playback.item
    $artistId = $item.artists[0].id

    function Get-Cached([string]$key, [scriptblock]$fetch) {
        if (-not $Cache.ContainsKey($key)) {
            $value = $null
            try { $value = & $fetch } catch { }
            $Cache[$key] = $value
        }
        return $Cache[$key]
    }

    $artistData = Get-Cached "artist:$artistId" { Invoke-SpotifyApi -Method GET -Path "/artists/$artistId" -ErrorAction SilentlyContinue }
    $topTracks  = Get-Cached 'top:tracks'  { (Invoke-SpotifyApi -Method GET -Path '/me/top/tracks'  -Query @{ limit = 50; time_range = 'short_term' }).items }
    $topArtists = Get-Cached 'top:artists' { (Invoke-SpotifyApi -Method GET -Path '/me/top/artists' -Query @{ limit = 50; time_range = 'short_term' }).items }
    $artistTop  = Get-Cached "artisttop:$artistId" { (Invoke-SpotifyApi -Method GET -Path "/artists/$artistId/top-tracks" -Query @{ market = 'from_token' }).tracks }
    $album      = Get-Cached "album:$($item.album.id)" { Invoke-SpotifyApi -Method GET -Path "/albums/$($item.album.id)" }
    $liked      = Get-Cached "liked:$($item.id)" { @(Invoke-SpotifyApi -Method GET -Path '/me/tracks/contains' -Query @{ ids = $item.id })[0] }

    # Rankings are 1-based; $null when not in the list
    $rankIn = { param($list, $id) $i = 0; foreach ($x in @($list)) { $i++; if ($x.id -eq $id) { return $i } }; return $null }
    $artistTopRank = & $rankIn $artistTop $item.id
    $artistTopAvg = if (@($artistTop).Count) { [int](@($artistTop) | Measure-Object -Property popularity -Average).Average } else { $null }
    $trackPop = [int]$item.popularity

    [PSCustomObject]@{
        Uri           = $item.uri
        Track         = $item.name
        Artists       = ($item.artists | ForEach-Object { $_.name }) -join ', '
        ArtistName    = $item.artists[0].name
        IsPlaying     = [bool]$playback.is_playing
        TrackPop      = $trackPop
        ArtistPop     = if ($artistData) { [int]$artistData.popularity } else { $null }
        Followers     = if ($artistData) { [long]$artistData.followers.total } else { $null }
        Genres        = if ($artistData) { @($artistData.genres) } else { @() }
        DurationMs    = [long]$item.duration_ms
        Album         = $item.album.name
        ReleaseDate   = $item.album.release_date
        TrackNumber   = $item.track_number
        TotalTracks   = $item.album.total_tracks
        Explicit      = [bool]$item.explicit
        TopTrackRank  = & $rankIn $topTracks $item.id
        TopArtistRank = & $rankIn $topArtists $artistId
        ArtistTopRank = $artistTopRank
        ArtistTopAvg  = $artistTopAvg
        IsHiddenGem   = ($null -eq $artistTopRank) -and ($null -ne $artistTopAvg) -and ($trackPop -le $artistTopAvg - 25)
        AlbumPop      = if ($album) { [int]$album.popularity } else { $null }
        Label         = if ($album) { $album.label } else { $null }
        Liked         = if ($null -ne $liked) { [bool]$liked } else { $null }
    }
}

function Write-SpotifyPeakInfo {
    param($Info, [int[]]$History)

    function Format-PeakNumber([long]$n) {
        if ($n -ge 1000000) { return "{0:N1}M" -f ($n / 1000000) }
        if ($n -ge 1000) { return "{0:N1}K" -f ($n / 1000) }
        return $n.ToString('N0')
    }
    function Write-PeakBar([string]$label, $value) {
        $width = 30
        Write-Host (" {0,-12} " -f $label) -ForegroundColor DarkGray -NoNewline
        if ($null -eq $value) { Write-Host '---' -ForegroundColor DarkGray; return }
        $filled = [int][Math]::Round([Math]::Min(100, [Math]::Max(0, $value)) / 100 * $width)
        Write-Host ([string][char]0x2588 * $filled) -ForegroundColor Cyan -NoNewline
        Write-Host ([string][char]0x2591 * ($width - $filled)) -ForegroundColor DarkGray -NoNewline
        Write-Host (" {0,3}/100" -f $value) -ForegroundColor White
    }
    function Write-PeakRow([string]$label, [string]$text, [string]$color = 'White') {
        Write-Host (" {0,-12} " -f $label) -ForegroundColor DarkGray -NoNewline
        Write-Host $text -ForegroundColor $color
    }

    Write-Host ''
    Write-Host " $($Info.Track)" -ForegroundColor Cyan -NoNewline
    if ($Info.Explicit) { Write-Host ' EXPLICIT' -ForegroundColor Magenta -NoNewline }
    Write-Host ''
    Write-Host " $($Info.Artists)" -ForegroundColor Gray
    Write-Host ''
    Write-PeakBar 'Track pop.' $Info.TrackPop
    Write-PeakBar 'Artist pop.' $Info.ArtistPop
    Write-Host ''
    Write-PeakRow 'Followers' $(if ($null -ne $Info.Followers) { Format-PeakNumber $Info.Followers } else { '---' }) 'Yellow'
    $duration = [TimeSpan]::FromMilliseconds($Info.DurationMs)
    Write-PeakRow 'Duration' ('{0}:{1:D2}' -f [int][Math]::Floor($duration.TotalMinutes), $duration.Seconds)
    Write-PeakRow 'Album' $Info.Album
    Write-PeakRow '' "Released $($Info.ReleaseDate) | Track $($Info.TrackNumber) of $($Info.TotalTracks)" 'DarkGray'
    Write-PeakRow 'Genres' $(if ($Info.Genres.Count) { ($Info.Genres | Select-Object -First 4) -join ', ' } else { 'n/a' }) 'Cyan'

    # --- You & this track ---
    Write-Host ''
    $topParts = @()
    $topParts += if ($Info.TopTrackRank) { "#$($Info.TopTrackRank) of your tracks" } else { 'not in your top 50 tracks' }
    if ($Info.TopArtistRank) { $topParts += "$($Info.ArtistName) is your #$($Info.TopArtistRank) artist" }
    Write-PeakRow 'Your top' (($topParts -join ' | ') + ' (last 4 weeks)') $(if ($Info.TopTrackRank -or $Info.TopArtistRank) { 'Green' } else { 'DarkGray' })
    if ($Info.ArtistTopRank) {
        Write-PeakRow 'Artist rank' "#$($Info.ArtistTopRank) of $($Info.ArtistName)'s 10 most popular" 'Green'
    } else {
        Write-PeakRow 'Artist rank' "not in $($Info.ArtistName)'s top 10" 'DarkGray'
    }
    if ($Info.IsHiddenGem) {
        Write-PeakRow 'Hidden gem' "$([char]::ConvertFromUtf32(0x1F48E)) Track pop $($Info.TrackPop) vs artist avg $($Info.ArtistTopAvg) - a deep cut" 'Magenta'
    }
    if ($null -ne $Info.AlbumPop) {
        $albumText = "Popularity $($Info.AlbumPop)"
        if ($Info.Label) { $albumText += " | $($Info.Label)" }
        Write-PeakRow 'Album info' $albumText 'Gray'
    }
    if ($null -ne $Info.Liked) {
        if ($Info.Liked) { Write-PeakRow 'Liked' "$([char]0x2665) in your library" 'Red' }
        else { Write-PeakRow 'Liked' 'not saved (save-track to like it)' 'DarkGray' }
    }

    if ($History -and $History.Count) {
        $blocks = 0x2581..0x2588 | ForEach-Object { [char]$_ }
        $graph = ($History | ForEach-Object { $blocks[[Math]::Min(7, [Math]::Max(0, [int]($_ / 100 * 7.99)))] }) -join ' '
        Write-Host ''
        Write-PeakRow 'Last tracks' $graph 'Cyan'
    }
    Write-Host ''
}

function Show-SpotifyPeak {
    <#
    .SYNOPSIS
    Show track insights for the current track in the terminal (popularity, followers, album, genres)
    .PARAMETER Watch
    Keep updating every 2 seconds, with a popularity graph for recent tracks. Ctrl+C to stop.
    .EXAMPLE
    peak
    .EXAMPLE
    peak -Watch
    .EXAMPLE
    showpeak
    Opens the same information in a window instead
    #>

    param([switch]$Watch)

    if (-not $Watch) {
        $info = Get-SpotifyPeakInfo
        if (-not $info) { Write-Host 'No track playing' -ForegroundColor Yellow; return }
        Write-SpotifyPeakInfo $info
        return
    }

    $history = [System.Collections.Generic.List[int]]::new()
    $cache = @{}
    $lastUri = $null
    $info = $null
    while ($true) {
        try {
            $fresh = Get-SpotifyPeakInfo -Cache $cache
            if ($fresh) {
                $info = $fresh
                if ($fresh.Uri -ne $lastUri) {
                    $lastUri = $fresh.Uri
                    $history.Add($fresh.TrackPop)
                    if ($history.Count -gt 10) { $history.RemoveAt(0) }
                }
            }
        } catch { }

        Clear-Host
        if ($info) {
            Write-SpotifyPeakInfo $info $history.ToArray()
            $state = if ($info.IsPlaying) { 'Playing' } else { 'Paused' }
            Write-Host " $state | Updated $(Get-Date -Format 'HH:mm:ss') | Ctrl+C to stop" -ForegroundColor DarkGray
        } else {
            Write-Host ''
            Write-Host ' No track playing | Ctrl+C to stop' -ForegroundColor Yellow
        }
        Start-Sleep -Seconds 2
    }
}

Set-Alias -Name peak -Value Show-SpotifyPeak -ErrorAction SilentlyContinue
Set-Alias -Name showpeak -Value Show-PeakDashboard -ErrorAction SilentlyContinue

Export-ModuleMember -Function @('Show-PeakDashboard', 'Show-SpotifyPeak', 'Get-SpotifyPeakInfo') -Alias @('peak', 'showpeak')