using System.Drawing;
using System.Drawing.Imaging;
using System.Security.Cryptography;
using System.Text;
using Windows.Media.Control;
using Windows.Storage.Streams;

namespace Lumi.Companion.SongOverlay.Providers;

internal sealed class SpotifyWindowsMediaProvider : IMediaProvider
{
    private const int MaxCoverInputBytes = 3 * 1024 * 1024;
    private const int MaxCoverOutputBytes = 500 * 1024;
    private readonly Func<SongOverlaySettings> _settings;
    private readonly Func<MediaTrack, CancellationToken, Task<MediaTrack>> _enrich;
    private readonly Action<string, Exception?> _log;
    private GlobalSystemMediaTransportControlsSessionManager? _manager;
    private GlobalSystemMediaTransportControlsSession? _session;
    private MediaSnapshot? _last;
    private MediaTrack? _trackCache;
    private bool _started;
    private readonly SemaphoreSlim _refreshLock = new(1, 1);

    public SpotifyWindowsMediaProvider(Func<SongOverlaySettings> settings, Func<MediaTrack, CancellationToken, Task<MediaTrack>> enrich, Action<string, Exception?> log)
    {
        _settings = settings;
        _enrich = enrich;
        _log = log;
    }

    public string Id => "spotify";
    public string DisplayName => "Spotify";
    public event EventHandler<ProviderStateChangedEventArgs>? StateChanged;
    public event EventHandler<string>? AvailabilityChanged;

    public async Task StartAsync(CancellationToken cancellationToken)
    {
        if (_started) return;
        _manager = await GlobalSystemMediaTransportControlsSessionManager.RequestAsync();
        _manager.CurrentSessionChanged += OnSessionCollectionChanged;
        _manager.SessionsChanged += OnSessionCollectionChanged;
        _started = true;
        await SelectSessionAsync(cancellationToken).ConfigureAwait(false);
    }

    public async Task StopAsync(CancellationToken cancellationToken)
    {
        if (!_started) return;
        if (_manager is not null)
        {
            _manager.CurrentSessionChanged -= OnSessionCollectionChanged;
            _manager.SessionsChanged -= OnSessionCollectionChanged;
        }
        DetachSession();
        _manager = null;
        _started = false;
        await Task.CompletedTask;
    }

    public Task<MediaSnapshot?> GetSnapshotAsync(CancellationToken cancellationToken) => RefreshAsync("snapshot", true, cancellationToken);

    private async void OnSessionCollectionChanged(GlobalSystemMediaTransportControlsSessionManager sender, object args)
    {
        try { await SelectSessionAsync(CancellationToken.None).ConfigureAwait(false); }
        catch (Exception error) { _log("Could not refresh Spotify media sessions", error); }
    }

    private async Task SelectSessionAsync(CancellationToken cancellationToken)
    {
        var selected = _manager?.GetSessions().FirstOrDefault(IsSpotifySession);
        if (ReferenceEquals(selected, _session))
        {
            if (_session is not null) await RefreshAndPublishAsync("snapshot", false, cancellationToken).ConfigureAwait(false);
            return;
        }
        DetachSession();
        _session = selected;
        if (_session is null)
        {
            AvailabilityChanged?.Invoke(this, "Spotify is not exposing a Windows media session.");
            if (_last is not null)
            {
                _last = null;
                StateChanged?.Invoke(this, new ProviderStateChangedEventArgs("stop", null));
            }
            return;
        }
        _session.MediaPropertiesChanged += OnMediaPropertiesChanged;
        _session.PlaybackInfoChanged += OnPlaybackInfoChanged;
        _session.TimelinePropertiesChanged += OnTimelinePropertiesChanged;
        AvailabilityChanged?.Invoke(this, "Spotify media session connected.");
        await RefreshAndPublishAsync("snapshot", true, cancellationToken).ConfigureAwait(false);
    }

    private void DetachSession()
    {
        if (_session is null) return;
        _session.MediaPropertiesChanged -= OnMediaPropertiesChanged;
        _session.PlaybackInfoChanged -= OnPlaybackInfoChanged;
        _session.TimelinePropertiesChanged -= OnTimelinePropertiesChanged;
        _session = null;
        _trackCache = null;
    }

    private async void OnMediaPropertiesChanged(GlobalSystemMediaTransportControlsSession sender, MediaPropertiesChangedEventArgs args)
    {
        try { await RefreshAndPublishAsync("media", true, CancellationToken.None).ConfigureAwait(false); }
        catch (Exception error) { _log("Spotify media-property update failed", error); }
    }

    private async void OnPlaybackInfoChanged(GlobalSystemMediaTransportControlsSession sender, PlaybackInfoChangedEventArgs args)
    {
        try { await RefreshAndPublishAsync("playback", false, CancellationToken.None).ConfigureAwait(false); }
        catch (Exception error) { _log("Spotify playback update failed", error); }
    }

    private async void OnTimelinePropertiesChanged(GlobalSystemMediaTransportControlsSession sender, TimelinePropertiesChangedEventArgs args)
    {
        try { await RefreshAndPublishAsync("timeline", false, CancellationToken.None).ConfigureAwait(false); }
        catch (Exception error) { _log("Spotify timeline update failed", error); }
    }

    private async Task RefreshAndPublishAsync(string reason, bool enrichTrack, CancellationToken cancellationToken)
    {
        var snapshot = await RefreshAsync(reason, enrichTrack, cancellationToken).ConfigureAwait(false);
        if (snapshot is null) return;
        StateChanged?.Invoke(this, new ProviderStateChangedEventArgs(reason, snapshot));
    }

    private async Task<MediaSnapshot?> RefreshAsync(string reason, bool enrichTrack, CancellationToken cancellationToken)
    {
        if (_session is null) return null;
        await _refreshLock.WaitAsync(cancellationToken).ConfigureAwait(false);
        try
        {
            var playback = _session.GetPlaybackInfo();
            var timeline = _session.GetTimelineProperties();
            var duration = Math.Max(0, (long)timeline.EndTime.TotalMilliseconds);
            var position = Math.Max(0, (long)timeline.Position.TotalMilliseconds);
            var status = playback.PlaybackStatus switch
            {
                GlobalSystemMediaTransportControlsSessionPlaybackStatus.Playing => "playing",
                GlobalSystemMediaTransportControlsSessionPlaybackStatus.Paused => "paused",
                GlobalSystemMediaTransportControlsSessionPlaybackStatus.Stopped => "stopped",
                GlobalSystemMediaTransportControlsSessionPlaybackStatus.Closed => "closed",
                _ => "unknown"
            };
            var capturedAt = DateTimeOffset.UtcNow;
            var rate = playback.PlaybackRate ?? 1d;
            if (rate <= 0) rate = 1d;
            if (status == "playing" && timeline.LastUpdatedTime != default)
            {
                var elapsed = Math.Max(0, (capturedAt - timeline.LastUpdatedTime).TotalMilliseconds);
                position += (long)(elapsed * rate);
                if (duration > 0) position = Math.Min(duration, position);
            }

            var needsMediaRefresh = _trackCache is null || reason is "media" or "snapshot";
            if (needsMediaRefresh)
            {
                var media = await _session.TryGetMediaPropertiesAsync();
                var title = Clean(media.Title);
                var artist = Clean(media.Artist);
                var album = Clean(media.AlbumTitle);
                if (!string.IsNullOrWhiteSpace(title) || !string.IsNullOrWhiteSpace(artist))
                {
                    var key = Fingerprint(title, artist, album, duration);
                    var cover = _settings().SendCoverArt ? await ReadCoverAsync(media.Thumbnail, cancellationToken).ConfigureAwait(false) : null;
                    var fallbackLink = _settings().UseSearchLinkFallback
                        ? "https://open.spotify.com/search/" + Uri.EscapeDataString(string.Join(" ", new[] { title, artist }.Where(value => value.Length > 0)))
                        : "";
                    var track = new MediaTrack(key, title, artist, album, "", fallbackLink, duration, cover);
                    _trackCache = track;
                    if (enrichTrack) _ = EnrichAndPublishAsync(track);
                }
                else
                {
                    _trackCache = null;
                }
            }
            else if (_trackCache is not null && _trackCache.DurationMilliseconds != duration)
            {
                _trackCache = _trackCache with { DurationMilliseconds = duration };
            }

            var snapshot = new MediaSnapshot(Id, status, position, duration, rate, _trackCache, capturedAt);
            _last = snapshot;
            return snapshot;
        }
        finally { _refreshLock.Release(); }
    }


    private async Task EnrichAndPublishAsync(MediaTrack track)
    {
        try
        {
            var enriched = await _enrich(track, CancellationToken.None).ConfigureAwait(false);
            if (enriched == track) return;
            await _refreshLock.WaitAsync().ConfigureAwait(false);
            try
            {
                if (_trackCache?.Key != track.Key) return;
                _trackCache = enriched;
            }
            finally { _refreshLock.Release(); }
            await RefreshAndPublishAsync("metadata", false, CancellationToken.None).ConfigureAwait(false);
        }
        catch (Exception error) { _log("Spotify metadata enrichment update failed", error); }
    }

    private static bool IsSpotifySession(GlobalSystemMediaTransportControlsSession session)
    {
        var source = session.SourceAppUserModelId ?? "";
        return source.Contains("spotify", StringComparison.OrdinalIgnoreCase);
    }

    private static async Task<CoverPayload?> ReadCoverAsync(IRandomAccessStreamReference? reference, CancellationToken cancellationToken)
    {
        if (reference is null) return null;
        try
        {
            using var stream = await reference.OpenReadAsync();
            var length = checked((uint)Math.Min(stream.Size, MaxCoverInputBytes));
            if (length == 0) return null;
            using var reader = new DataReader(stream.GetInputStreamAt(0));
            var loaded = await reader.LoadAsync(length);
            cancellationToken.ThrowIfCancellationRequested();
            if (loaded == 0) return null;
            var bytes = new byte[checked((int)loaded)];
            reader.ReadBytes(bytes);
            var mime = string.IsNullOrWhiteSpace(stream.ContentType) ? "image/jpeg" : stream.ContentType;
            return CreateCoverPayload(bytes, mime);
        }
        catch { return null; }
    }


    internal static CoverPayload? CreateCoverPayload(byte[] bytes, string sourceMime)
    {
        if (bytes.Length == 0 || bytes.Length > MaxCoverInputBytes) return null;
        try
        {
            var encoded = ResizeJpeg(bytes, 512, 82L);
            if (encoded.Length > MaxCoverOutputBytes) encoded = ResizeJpeg(bytes, 320, 68L);
            if (encoded.Length == 0 || encoded.Length > MaxCoverOutputBytes) return null;
            var colors = ExtractColors(encoded);
            return new CoverPayload("image/jpeg", Convert.ToBase64String(encoded), colors.Primary, colors.Secondary);
        }
        catch { return null; }
    }

    private static byte[] ResizeJpeg(byte[] bytes, int maximumDimension, long quality)
    {
        using var input = new MemoryStream(bytes);
        using var original = new Bitmap(input);
        var ratio = Math.Min(1d, Math.Min((double)maximumDimension / original.Width, (double)maximumDimension / original.Height));
        var width = Math.Max(1, (int)Math.Round(original.Width * ratio));
        var height = Math.Max(1, (int)Math.Round(original.Height * ratio));
        using var resized = new Bitmap(width, height, PixelFormat.Format24bppRgb);
        using (var graphics = Graphics.FromImage(resized))
        {
            graphics.Clear(Color.Black);
            graphics.CompositingQuality = System.Drawing.Drawing2D.CompositingQuality.HighQuality;
            graphics.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
            graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality;
            graphics.PixelOffsetMode = System.Drawing.Drawing2D.PixelOffsetMode.HighQuality;
            graphics.DrawImage(original, 0, 0, width, height);
        }
        using var output = new MemoryStream();
        var codec = ImageCodecInfo.GetImageEncoders().First(item => item.FormatID == ImageFormat.Jpeg.Guid);
        using var parameters = new EncoderParameters(1);
        parameters.Param[0] = new EncoderParameter(System.Drawing.Imaging.Encoder.Quality, quality);
        resized.Save(output, codec, parameters);
        return output.ToArray();
    }

    internal static (string Primary, string Secondary) ExtractColors(byte[] bytes)
    {
        try
        {
            using var memory = new MemoryStream(bytes);
            using var bitmap = new Bitmap(memory);
            var samples = new List<Color>();
            var stepX = Math.Max(1, bitmap.Width / 12);
            var stepY = Math.Max(1, bitmap.Height / 12);
            for (var y = stepY / 2; y < bitmap.Height; y += stepY)
            for (var x = stepX / 2; x < bitmap.Width; x += stepX)
            {
                var color = bitmap.GetPixel(x, y);
                if (color.A > 100) samples.Add(color);
            }
            if (samples.Count == 0) return ("#1f2937", "#111827");
            var primary = Average(samples.OrderByDescending(c => Saturation(c)).Take(Math.Max(1, samples.Count / 3)));
            var secondary = Average(samples.OrderBy(c => Brightness(c)).Take(Math.Max(1, samples.Count / 3)));
            return (Hex(primary), Hex(secondary));
        }
        catch { return ("#1f2937", "#111827"); }
    }

    private static Color Average(IEnumerable<Color> colors)
    {
        var list = colors.ToList();
        return list.Count == 0 ? Color.FromArgb(31, 41, 55) : Color.FromArgb(
            (int)list.Average(c => c.R), (int)list.Average(c => c.G), (int)list.Average(c => c.B));
    }
    private static double Saturation(Color c) => c.GetSaturation();
    private static double Brightness(Color c) => c.GetBrightness();
    private static string Hex(Color c) => $"#{c.R:X2}{c.G:X2}{c.B:X2}";
    private static string Clean(string? value) => string.Join(" ", (value ?? "").Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries)).Trim();
    private static string Fingerprint(string title, string artist, string album, long duration)
    {
        var input = Encoding.UTF8.GetBytes($"{title}\u001f{artist}\u001f{album}\u001f{duration}");
        return Convert.ToHexString(SHA256.HashData(input)).ToLowerInvariant();
    }

    public async ValueTask DisposeAsync()
    {
        await StopAsync(CancellationToken.None).ConfigureAwait(false);
        _refreshLock.Dispose();
    }
}
