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

namespace ConeCalorimeter.Services;

public sealed class ExperimentDataService : IExperimentDataService
{
    private const int MaximumRows = 1200;

    private readonly IRealtimeDataService _realtimeDataService;
    private readonly DispatcherTimer _timer;
    private readonly DateTime _startedAt = DateTime.Now;
    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 = _realtimeDataService.GetCurrentSnapshot(TimeSpan.Zero);

        _timer = new DispatcherTimer
        {
            Interval = TimeSpan.FromSeconds(1)
        };
        _timer.Tick += async (_, _) => await RecordCurrentSnapshotAsync();
        _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;

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

    public void StopTest()
    {
        _isTestRunning = false;
    }

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

        if (!_isTestRunning)
        {
            ResetComputedState();
        }
    }

    private async Task RecordCurrentSnapshotAsync()
    {
        if (_isPollingSnapshot)
        {
            return;
        }

        _isPollingSnapshot = true;

        try
        {
            var elapsed = DateTime.Now - _startedAt;
            var snapshot = await Task.Run(() => _realtimeDataService.GetCurrentSnapshot(elapsed));
            RecordSnapshot(snapshot);
        }
        catch (Exception ex)
        {
            Debug.WriteLine($"Realtime snapshot polling failed: {ex.Message}");
        }
        finally
        {
            _isPollingSnapshot = false;
        }
    }

    private void RecordSnapshot(RealtimeSnapshot snapshot)
    {
        var massSnapshot = ApplyMassLoss(snapshot);
        var accumulatedSnapshot = ApplyAccumulatedTotals(massSnapshot);

        CurrentSnapshot = accumulatedSnapshot;

        if (_isTestRunning && ShouldRecordSnapshot(accumulatedSnapshot))
        {
            Records.Add(RealtimeDataRecord.FromSnapshot(accumulatedSnapshot));
            _lastRecordedSeconds = accumulatedSnapshot.TestSeconds;

            while (Records.Count > MaximumRows)
            {
                Records.RemoveAt(0);
            }
        }

        SnapshotUpdated?.Invoke(this, accumulatedSnapshot);
    }

    private bool ShouldRecordSnapshot(RealtimeSnapshot snapshot)
    {
        return snapshot.TestSeconds >= 0
            && (!_lastRecordedSeconds.HasValue || snapshot.TestSeconds > _lastRecordedSeconds.Value);
    }

    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 / 1000;
            }

            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;
    }
}
