using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Windows.Threading;
using ConeCalorimeter.Models;
using Serilog;

namespace ConeCalorimeter.Services;

public sealed class ExperimentDataService : IExperimentDataService
{
    private const double TotalHeatReleaseDivisor = 1000;
    private static readonly TimeSpan TimerInterval = TimeSpan.FromMilliseconds(250);
    private static readonly TimeSpan DevicePollInterval = TimeSpan.FromSeconds(1);

    private readonly IRealtimeDataService _realtimeDataService;
    private readonly DispatcherTimer _timer;
    private readonly Stopwatch _testClock = new();
    private DateTime _lastDevicePollStartedAtUtc = DateTime.MinValue;
    private double? _initialMass;
    private double? _previousMass;
    private int? _previousMassSeconds;
    private double _accumulatedTotalHeatRelease;
    private double _accumulatedTotalSmoke;
    private int? _lastAccumulationSeconds;
    private int? _lastRecordedSeconds;
    private bool _isTestRunning;
    private bool _isPollingSnapshot;

    public ExperimentDataService(IRealtimeDataService realtimeDataService)
    {
        _realtimeDataService = realtimeDataService;
        Records = [];
        CurrentSnapshot = BuildIdleSnapshot(_realtimeDataService.GetCurrentSnapshot(TimeSpan.Zero));

        _timer = new DispatcherTimer
        {
            Interval = TimerInterval
        };
        _timer.Tick += async (_, _) => await TickAsync();
        _timer.Start();
    }

    public ObservableCollection<RealtimeDataRecord> Records { get; }

    public RealtimeSnapshot CurrentSnapshot { get; private set; }

    public event EventHandler<RealtimeSnapshot>? SnapshotUpdated;

    public void StartTest()
    {
        Records.Clear();
        ResetComputedState();
        _isTestRunning = true;
        _testClock.Restart();

        if (double.IsFinite(CurrentSnapshot.CurrentMass))
        {
            _initialMass = CurrentSnapshot.CurrentMass;
        }

        RecordElapsedSnapshots();
        QueueImmediateDevicePoll();

        Log.Information(
            "Experiment test started. InitialMass={InitialMass}, SystemSeconds={TestSeconds}.",
            _initialMass,
            CurrentSnapshot.TestSeconds);
    }

    public void StopTest()
    {
        if (_isTestRunning)
        {
            RecordElapsedSnapshots();
        }

        _isTestRunning = false;
        _testClock.Stop();
        PublishSnapshot(BuildIdleSnapshot(CurrentSnapshot));

        Log.Information(
            "Experiment test stopped. Records={RecordCount}, LastRecordedSeconds={LastRecordedSeconds}.",
            Records.Count,
            _lastRecordedSeconds);
    }

    public void ClearRecords()
    {
        Records.Clear();

        if (!_isTestRunning)
        {
            _testClock.Reset();
            ResetComputedState();
            PublishSnapshot(BuildIdleSnapshot(CurrentSnapshot));
        }
    }

    private async Task TickAsync()
    {
        if (_isTestRunning)
        {
            RecordElapsedSnapshots();
        }

        await RefreshDeviceSnapshotAsync();
    }

    private void QueueImmediateDevicePoll()
    {
        _lastDevicePollStartedAtUtc = DateTime.MinValue;
        _ = RefreshDeviceSnapshotAsync();
    }

    private async Task RefreshDeviceSnapshotAsync()
    {
        if (!TryBeginDevicePoll())
        {
            return;
        }

        try
        {
            var elapsed = _isTestRunning ? _testClock.Elapsed : TimeSpan.Zero;
            var snapshot = await Task.Run(() => _realtimeDataService.GetCurrentSnapshot(elapsed));
            PublishSnapshot(BuildDisplaySnapshot(snapshot));
        }
        catch (Exception ex)
        {
            Debug.WriteLine($"Realtime snapshot polling failed: {ex.Message}");
            Log.Warning(ex, "Realtime snapshot polling failed.");
        }
        finally
        {
            _isPollingSnapshot = false;
        }
    }

    private bool TryBeginDevicePoll()
    {
        if (_isPollingSnapshot)
        {
            return false;
        }

        var now = DateTime.UtcNow;
        if (now - _lastDevicePollStartedAtUtc < DevicePollInterval)
        {
            return false;
        }

        _lastDevicePollStartedAtUtc = now;
        _isPollingSnapshot = true;
        return true;
    }

    private void RecordElapsedSnapshots()
    {
        if (!_isTestRunning)
        {
            return;
        }

        var currentSecond = GetElapsedTestSeconds();
        var nextSecond = _lastRecordedSeconds.HasValue ? _lastRecordedSeconds.Value + 1 : 0;

        for (var second = nextSecond; second <= currentSecond; second++)
        {
            var timedSnapshot = CurrentSnapshot with
            {
                TestSeconds = second
            };
            var massSnapshot = ApplyMassLoss(timedSnapshot);
            var accumulatedSnapshot = ApplyAccumulatedTotals(massSnapshot);

            PublishSnapshot(accumulatedSnapshot);
            Records.Add(RealtimeDataRecord.FromSnapshot(accumulatedSnapshot));
            _lastRecordedSeconds = second;
        }
    }

    private RealtimeSnapshot BuildDisplaySnapshot(RealtimeSnapshot snapshot)
    {
        if (!_isTestRunning)
        {
            return BuildIdleSnapshot(snapshot);
        }

        var timedSnapshot = snapshot with
        {
            TestSeconds = GetElapsedTestSeconds()
        };

        if (!_initialMass.HasValue && double.IsFinite(timedSnapshot.CurrentMass))
        {
            _initialMass = timedSnapshot.CurrentMass;
        }

        var massLoss = _initialMass.HasValue && double.IsFinite(timedSnapshot.CurrentMass)
            ? _initialMass.Value - timedSnapshot.CurrentMass
            : double.NaN;

        return timedSnapshot with
        {
            InitialMass = _initialMass ?? double.NaN,
            MassLoss = massLoss,
            MassLossRate = CurrentSnapshot.MassLossRate,
            TotalHeatRelease = _accumulatedTotalHeatRelease,
            TotalSmoke = _accumulatedTotalSmoke
        };
    }

    private RealtimeSnapshot BuildIdleSnapshot(RealtimeSnapshot snapshot)
    {
        return snapshot with
        {
            TestSeconds = -1,
            InitialMass = _initialMass ?? double.NaN,
            MassLoss = double.NaN,
            MassLossRate = double.NaN,
            TotalHeatRelease = double.NaN,
            TotalSmoke = double.NaN
        };
    }

    private int GetElapsedTestSeconds()
    {
        return Math.Max(0, (int)Math.Floor(_testClock.Elapsed.TotalSeconds));
    }

    private void PublishSnapshot(RealtimeSnapshot snapshot)
    {
        CurrentSnapshot = snapshot;
        SnapshotUpdated?.Invoke(this, snapshot);
    }

    private RealtimeSnapshot ApplyAccumulatedTotals(RealtimeSnapshot snapshot)
    {
        if (!_isTestRunning)
        {
            return snapshot with
            {
                TotalHeatRelease = double.NaN,
                TotalSmoke = double.NaN
            };
        }

        if (snapshot.TestSeconds < 0)
        {
            return snapshot with
            {
                TotalHeatRelease = _accumulatedTotalHeatRelease,
                TotalSmoke = _accumulatedTotalSmoke
            };
        }

        if (!_lastAccumulationSeconds.HasValue)
        {
            _lastAccumulationSeconds = snapshot.TestSeconds;
            return snapshot with
            {
                TotalHeatRelease = _accumulatedTotalHeatRelease,
                TotalSmoke = _accumulatedTotalSmoke
            };
        }

        var deltaSeconds = snapshot.TestSeconds - _lastAccumulationSeconds.Value;
        if (deltaSeconds > 0)
        {
            if (double.IsFinite(snapshot.HeatReleaseRate))
            {
                _accumulatedTotalHeatRelease += snapshot.HeatReleaseRate * deltaSeconds / TotalHeatReleaseDivisor;
            }

            if (double.IsFinite(snapshot.SmokeProduction))
            {
                _accumulatedTotalSmoke += snapshot.SmokeProduction * deltaSeconds;
            }

            _lastAccumulationSeconds = snapshot.TestSeconds;
        }

        return snapshot with
        {
            TotalHeatRelease = _accumulatedTotalHeatRelease,
            TotalSmoke = _accumulatedTotalSmoke
        };
    }

    private RealtimeSnapshot ApplyMassLoss(RealtimeSnapshot snapshot)
    {
        if (!_isTestRunning)
        {
            return snapshot with
            {
                InitialMass = _initialMass ?? double.NaN,
                MassLoss = double.NaN,
                MassLossRate = double.NaN
            };
        }

        if (!_initialMass.HasValue && double.IsFinite(snapshot.CurrentMass))
        {
            _initialMass = snapshot.CurrentMass;
        }

        var massLoss = _initialMass.HasValue && double.IsFinite(snapshot.CurrentMass)
            ? _initialMass.Value - snapshot.CurrentMass
            : double.NaN;
        var massLossRate = CalculateMassLossRate(snapshot);

        return snapshot with
        {
            InitialMass = _initialMass ?? double.NaN,
            MassLoss = massLoss,
            MassLossRate = massLossRate
        };
    }

    private double CalculateMassLossRate(RealtimeSnapshot snapshot)
    {
        if (snapshot.TestSeconds < 0 || !double.IsFinite(snapshot.CurrentMass))
        {
            return double.NaN;
        }

        if (!_previousMass.HasValue || !_previousMassSeconds.HasValue)
        {
            _previousMass = snapshot.CurrentMass;
            _previousMassSeconds = snapshot.TestSeconds;
            return double.NaN;
        }

        var deltaSeconds = snapshot.TestSeconds - _previousMassSeconds.Value;
        var deltaMass = _previousMass.Value - snapshot.CurrentMass;

        _previousMass = snapshot.CurrentMass;
        _previousMassSeconds = snapshot.TestSeconds;

        return deltaSeconds > 0 && deltaMass >= 0
            ? deltaMass / deltaSeconds
            : double.NaN;
    }

    private void ResetComputedState()
    {
        _initialMass = null;
        _previousMass = null;
        _previousMassSeconds = null;
        _accumulatedTotalHeatRelease = 0;
        _accumulatedTotalSmoke = 0;
        _lastAccumulationSeconds = null;
        _lastRecordedSeconds = null;
    }
}
