using Lumi.Companion.Abstractions;
using Lumi.Companion.SongOverlay.Providers;
using Lumi.Companion.SongOverlay.Spotify;

namespace Lumi.Companion.SongOverlay;

public sealed class SongOverlayRuntime : ICompanionPluginContribution, IAsyncDisposable
{
    private const string PluginId = "now_playing";
    private readonly SongOverlaySettingsStore _settingsStore;
    private readonly SongOverlaySecretProtector _secrets = new();
    private readonly SongOverlayTransport _transport;
    private readonly string _logDirectory;
    private IMediaProvider? _provider;
    private SpotifyWebApiEnricher? _spotify;
    private CancellationTokenSource? _lifetime;
    private PeriodicTimer? _heartbeat;
    private Task? _heartbeatTask;
    private readonly SemaphoreSlim _eventLock = new(1, 1);
    private MediaSnapshot? _lastObserved;
    private MediaSnapshot? _lastDelivered;
    private readonly List<string> _trackHistory = [];
    private int _historyIndex = -1;
    private long _sequence;
    private readonly string _sessionId = Guid.NewGuid().ToString("N");
    private bool _initialized;

    public SongOverlayRuntime(
        string pluginDataDirectory,
        string logDirectory,
        ICompanionPluginTransport hostTransport)
    {
        Directory.CreateDirectory(pluginDataDirectory);
        _logDirectory = logDirectory;
        _settingsStore = new SongOverlaySettingsStore(Path.Combine(pluginDataDirectory, "settings.json"));
        _transport = new SongOverlayTransport(hostTransport);
        Actions =
        [
            new CompanionPluginAction("send", () => "Send current song now", SendSnapshotAsync, () => _initialized && Settings.Enabled, 10),
            new CompanionPluginAction("toggle", () => Settings.Enabled ? "Disable monitoring" : "Enable monitoring", ToggleEnabledAsync, () => _initialized, 20)
        ];
    }

    public CompanionPluginDescriptor Descriptor { get; } = new(
        PluginId,
        "Song Overlay",
        new Version(0, 1, 1),
        "Reads provider-neutral Windows media-session events and sends minimal playback changes to Lumi.",
        200);

    public IReadOnlyList<CompanionPluginPage> Pages { get; } =
    [
        new CompanionPluginPage("SongOverlay", "Overview & settings", 10)
    ];

    public IReadOnlyList<CompanionPluginAction> Actions { get; }
    public CompanionPluginStatus Status { get; private set; } = new(CompanionPluginHealth.Ready, "Starting", "Waiting for initialization.");
    public SongOverlaySettings Settings => _settingsStore.Current;
    public bool IsInitialized => _initialized;
    public bool IsSpotifyEnrichmentConnected => _spotify?.IsConfigured == true;
    public bool UsesCompanionAuthentication => _transport.IsConfigured;
    public string ProviderStatus => Status.Detail ?? Status.Summary;
    public string? CurrentTrack => _lastObserved?.Track is null ? null : $"{_lastObserved.Track.Title} — {_lastObserved.Track.Artist}";
    public Uri? EffectiveLumiBaseUri => _transport.BaseUri;
    public event Action? Changed;

    public async Task InitializeAsync(CancellationToken cancellationToken = default)
    {
        await _settingsStore.LoadAsync(cancellationToken).ConfigureAwait(false);
        if (string.IsNullOrWhiteSpace(Settings.ProviderId)) Settings.ProviderId = "spotify";
        // Re-save through the current schema so legacy plugin-specific Lumi host
        // and connection-key fields are removed after the shared transport upgrade.
        await _settingsStore.SaveAsync(Settings, cancellationToken).ConfigureAwait(false);
        _spotify = new SpotifyWebApiEnricher(Settings, _secrets, SaveSettingsSync, Log);
        _initialized = true;
        await RestartProviderAsync(cancellationToken).ConfigureAwait(false);
    }

    public async Task SaveSettingsAsync(bool restartProvider, CancellationToken cancellationToken = default)
    {
        Settings.HeartbeatSeconds = Math.Clamp(Settings.HeartbeatSeconds, 15, 300);
        Settings.SeekThresholdMilliseconds = Math.Clamp(Settings.SeekThresholdMilliseconds, 500, 10000);
        Settings.ProviderId = string.IsNullOrWhiteSpace(Settings.ProviderId) ? "spotify" : Settings.ProviderId.Trim().ToLowerInvariant();
        await _settingsStore.SaveAsync(Settings, cancellationToken).ConfigureAwait(false);
        if (restartProvider) await RestartProviderAsync(cancellationToken).ConfigureAwait(false);
        RaiseChanged();
    }

    public async Task ToggleEnabledAsync(CancellationToken cancellationToken = default)
    {
        Settings.Enabled = !Settings.Enabled;
        await SaveSettingsAsync(restartProvider: true, cancellationToken).ConfigureAwait(false);
    }

    public async Task ConnectSpotifyAsync(CancellationToken cancellationToken = default)
    {
        EnsureInitialized();
        await _spotify!.AuthorizeAsync(cancellationToken).ConfigureAwait(false);
        SaveSettingsSync();
        RaiseChanged();
        if (_transport.IsConfigured)
            await SendSnapshotAsync(cancellationToken).ConfigureAwait(false);
    }

    public void DisconnectSpotify()
    {
        _spotify?.Disconnect();
        RaiseChanged();
    }

    public async Task SendSnapshotAsync(CancellationToken cancellationToken = default)
    {
        EnsureInitialized();
        if (!_transport.IsConfigured) throw new InvalidOperationException("Pair Companion with Lumi before sending song updates.");
        if (_provider is null) throw new InvalidOperationException("No media provider is running.");
        var snapshot = await _provider.GetSnapshotAsync(cancellationToken).ConfigureAwait(false);
        if (snapshot is null) throw new InvalidOperationException("Spotify is not currently exposing playback information.");
        await SendAsync("snapshot", snapshot, includeTrack: true, cancellationToken).ConfigureAwait(false);
    }

    private async Task RestartProviderAsync(CancellationToken cancellationToken)
    {
        await StopProviderAsync().ConfigureAwait(false);
        if (!Settings.Enabled)
        {
            SetStatus(CompanionPluginHealth.Ready, "Disabled", "Song Overlay monitoring is disabled.");
            return;
        }
        _lastObserved = null;
        _lastDelivered = null;
        _lifetime = new CancellationTokenSource();
        _provider = Settings.ProviderId switch
        {
            "spotify" => new SpotifyWindowsMediaProvider(() => Settings, EnrichTrackAsync, Log),
            _ => throw new InvalidOperationException($"Provider '{Settings.ProviderId}' is not installed.")
        };
        _provider.StateChanged += OnProviderStateChanged;
        _provider.AvailabilityChanged += OnAvailabilityChanged;
        try
        {
            await _provider.StartAsync(cancellationToken).ConfigureAwait(false);
            StartHeartbeat();
            var initial = await _provider.GetSnapshotAsync(cancellationToken).ConfigureAwait(false);
            if (!_transport.IsConfigured)
                SetStatus(CompanionPluginHealth.Warning, "Pairing required", "Spotify monitoring is ready, but Companion must be paired before Lumi can receive updates.");
            else if (initial is null)
                SetStatus(CompanionPluginHealth.Warning, "Waiting for Spotify", "Spotify is not currently exposing a Windows media session.");
            else
                SetStatus(CompanionPluginHealth.Healthy, "Monitoring", Describe(initial));
        }
        catch (Exception error)
        {
            SetStatus(CompanionPluginHealth.Error, "Provider failed", "Could not start the Spotify media provider: " + error.Message);
            throw;
        }
    }

    private async Task StopProviderAsync()
    {
        _lifetime?.Cancel();
        _heartbeat?.Dispose();
        if (_heartbeatTask is not null)
        {
            try { await _heartbeatTask.ConfigureAwait(false); } catch { }
        }
        _heartbeat = null;
        _heartbeatTask = null;
        if (_provider is not null)
        {
            _provider.StateChanged -= OnProviderStateChanged;
            _provider.AvailabilityChanged -= OnAvailabilityChanged;
            await _provider.DisposeAsync().ConfigureAwait(false);
        }
        _provider = null;
        _lifetime?.Dispose();
        _lifetime = null;
    }

    private void StartHeartbeat()
    {
        _heartbeat = new PeriodicTimer(TimeSpan.FromSeconds(Math.Clamp(Settings.HeartbeatSeconds, 15, 300)));
        var token = _lifetime?.Token ?? CancellationToken.None;
        _heartbeatTask = Task.Run(async () =>
        {
            try
            {
                while (_heartbeat is not null && await _heartbeat.WaitForNextTickAsync(token).ConfigureAwait(false))
                {
                    var current = _lastObserved;
                    if (current is null || !_transport.IsConfigured) continue;
                    await SendAsync("heartbeat", current, includeTrack: _lastDelivered?.Track?.Key != current.Track?.Key, token).ConfigureAwait(false);
                }
            }
            catch (OperationCanceledException) { }
            catch (Exception error) { Log("Song Overlay heartbeat failed", error); }
        }, token);
    }

    private void OnAvailabilityChanged(object? sender, string message)
    {
        if (!_transport.IsConfigured)
        {
            SetStatus(CompanionPluginHealth.Warning, "Pairing required", message);
            return;
        }
        var waiting = message.Contains("not exposing", StringComparison.OrdinalIgnoreCase);
        SetStatus(waiting ? CompanionPluginHealth.Warning : CompanionPluginHealth.Healthy,
            waiting ? "Waiting for Spotify" : "Monitoring", message);
    }

    private void OnProviderStateChanged(object? sender, ProviderStateChangedEventArgs args) =>
        _ = HandleStateChangeAsync(args.Reason, args.Snapshot, _lifetime?.Token ?? CancellationToken.None);

    private async Task HandleStateChangeAsync(string reason, MediaSnapshot? snapshot, CancellationToken cancellationToken)
    {
        await _eventLock.WaitAsync(cancellationToken).ConfigureAwait(false);
        try
        {
            if (snapshot is null)
            {
                if (_lastObserved is null) return;
                var stopped = _lastObserved with { Status = "stopped", PositionMilliseconds = 0, Track = null, CapturedAt = DateTimeOffset.UtcNow };
                _lastObserved = null;
                if (_transport.IsConfigured) await SendAsync("stop", stopped, false, cancellationToken).ConfigureAwait(false);
                else RaiseChanged();
                return;
            }
            var previous = _lastObserved;
            _lastObserved = snapshot;
            RaiseChanged();
            if (!_transport.IsConfigured)
            {
                SetStatus(CompanionPluginHealth.Warning, "Pairing required", Describe(snapshot));
                return;
            }
            var currentTrackKey = snapshot.Track?.Key;
            var trackChanged = !string.IsNullOrWhiteSpace(currentTrackKey)
                && !string.Equals(currentTrackKey, previous?.Track?.Key, StringComparison.Ordinal);
            if (trackChanged)
            {
                await SendAsync(ClassifyTrackDirection(currentTrackKey!), snapshot, true, cancellationToken).ConfigureAwait(false);
                return;
            }
            if (previous is null)
            {
                await SendAsync("snapshot", snapshot, true, cancellationToken).ConfigureAwait(false);
                return;
            }
            if (reason == "metadata")
            {
                await SendAsync("snapshot", snapshot, true, cancellationToken).ConfigureAwait(false);
                return;
            }
            if (!string.Equals(previous.Status, snapshot.Status, StringComparison.Ordinal))
            {
                var playbackEvent = snapshot.Status switch
                {
                    "paused" => "pause",
                    "stopped" or "closed" => "stop",
                    "playing" when previous.Status == "paused" => "resume",
                    "playing" => "play",
                    _ => "snapshot"
                };
                await SendAsync(playbackEvent, snapshot, false, cancellationToken).ConfigureAwait(false);
                return;
            }
            if (reason == "timeline")
            {
                var expected = ProjectPosition(previous, snapshot.CapturedAt);
                if (Math.Abs(snapshot.PositionMilliseconds - expected) >= Math.Clamp(Settings.SeekThresholdMilliseconds, 500, 10000))
                    await SendAsync("seek", snapshot, false, cancellationToken).ConfigureAwait(false);
            }
        }
        catch (OperationCanceledException) { }
        catch (Exception error)
        {
            Log("Could not process a Song Overlay event", error);
            SetStatus(CompanionPluginHealth.Warning, "Delivery delayed", "Playback was detected, but Lumi could not be updated: " + error.Message);
        }
        finally { _eventLock.Release(); }
    }

    private async Task SendAsync(string eventName, MediaSnapshot snapshot, bool includeTrack, CancellationToken cancellationToken)
    {
        var track = includeTrack && snapshot.Track is not null
            ? new TrackPayload(snapshot.Track.Key, snapshot.Track.Title, snapshot.Track.Artist, snapshot.Track.Album,
                snapshot.Track.ReleaseYear, snapshot.Track.Link, snapshot.Track.Cover)
            : null;
        var payload = new NowPlayingEventPayload(
            1,
            snapshot.Provider,
            _sessionId,
            Interlocked.Increment(ref _sequence),
            eventName,
            snapshot.CapturedAt.ToUnixTimeMilliseconds(),
            new PlaybackPayload(snapshot.Status, snapshot.PositionMilliseconds, snapshot.DurationMilliseconds, snapshot.PlaybackRate),
            track);
        var response = await _transport.SendJsonAsync("/plugins/now_playing/api/companion/state", payload, cancellationToken).ConfigureAwait(false);
        if (!response.IsSuccessStatusCode)
        {
            throw new InvalidOperationException($"Lumi rejected the update with HTTP {response.StatusCode}: {Trim(response.Body, 240)}");
        }
        _lastDelivered = snapshot;
        SetStatus(CompanionPluginHealth.Healthy, snapshot.Status == "playing" ? "Playing" : "Connected", Describe(snapshot));
    }

    private async Task<MediaTrack> EnrichTrackAsync(MediaTrack track, CancellationToken cancellationToken)
    {
        if (_spotify?.IsConfigured != true) return track;
        SpotifyEnrichment? enriched = null;
        foreach (var delay in new[] { 0, 500, 1000 })
        {
            if (delay > 0) await Task.Delay(delay, cancellationToken).ConfigureAwait(false);
            enriched = await _spotify.EnrichAsync(track, cancellationToken).ConfigureAwait(false);
            if (enriched is not null) break;
        }
        if (enriched is null) return track;
        CoverPayload? cover = track.Cover;
        if (enriched.CoverBytes is { Length: > 0 } bytes && Settings.SendCoverArt)
            cover = SpotifyWindowsMediaProvider.CreateCoverPayload(bytes, enriched.CoverMime) ?? cover;
        return track with
        {
            Link = string.IsNullOrWhiteSpace(enriched.Link) ? track.Link : enriched.Link,
            ReleaseYear = string.IsNullOrWhiteSpace(enriched.ReleaseYear) ? track.ReleaseYear : enriched.ReleaseYear,
            Cover = cover
        };
    }

    private string ClassifyTrackDirection(string key)
    {
        if (_historyIndex > 0 && _trackHistory[_historyIndex - 1] == key) { _historyIndex--; return "previous"; }
        if (_historyIndex >= 0 && _historyIndex + 1 < _trackHistory.Count && _trackHistory[_historyIndex + 1] == key) { _historyIndex++; return "next"; }
        if (_historyIndex + 1 < _trackHistory.Count) _trackHistory.RemoveRange(_historyIndex + 1, _trackHistory.Count - _historyIndex - 1);
        _trackHistory.Add(key);
        if (_trackHistory.Count > 50) _trackHistory.RemoveAt(0);
        _historyIndex = _trackHistory.Count - 1;
        return "track_changed";
    }

    private static long ProjectPosition(MediaSnapshot snapshot, DateTimeOffset at)
    {
        if (snapshot.Status != "playing") return snapshot.PositionMilliseconds;
        var elapsed = Math.Max(0, (at - snapshot.CapturedAt).TotalMilliseconds);
        return Math.Min(snapshot.DurationMilliseconds > 0 ? snapshot.DurationMilliseconds : long.MaxValue,
            snapshot.PositionMilliseconds + (long)(elapsed * snapshot.PlaybackRate));
    }

    private void SaveSettingsSync() => _settingsStore.Save(Settings);
    private void EnsureInitialized()
    {
        if (!_initialized) throw new InvalidOperationException("The Song Overlay companion plugin has not initialized yet.");
    }

    private void SetStatus(CompanionPluginHealth health, string summary, string detail)
    {
        Status = new CompanionPluginStatus(health, summary, detail);
        RaiseChanged();
    }

    private void RaiseChanged() => Changed?.Invoke();
    private void Log(string message, Exception? error = null)
    {
        try
        {
            Directory.CreateDirectory(_logDirectory);
            File.AppendAllText(Path.Combine(_logDirectory, $"song-overlay-{DateTime.UtcNow:yyyyMMdd}.log"),
                $"{DateTimeOffset.Now:O}\t{message}{(error is null ? "" : "\t" + error)}{Environment.NewLine}");
        }
        catch { }
    }

    private static string Describe(MediaSnapshot snapshot) => snapshot.Track is null
        ? "Connected; no active song."
        : $"{snapshot.Status}: {snapshot.Track.Title} — {snapshot.Track.Artist}";
    private static string Trim(string value, int max) => value.Length <= max ? value : value[..max];

    public async ValueTask DisposeAsync()
    {
        await StopProviderAsync().ConfigureAwait(false);
        _spotify?.Dispose();
        _eventLock.Dispose();
    }
}
