// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using FloraMultiPlatformFrontend.Application.Services.Interfaces;
using FloraMultiPlatformFrontend.Data.Repositories.Interfaces;
using FloraMultiPlatformFrontend.Presentation.Models.Diagnosis;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;

namespace FloraMultiPlatformFrontend.Data.Repositories;

public class ReleaseDataRepository : IReleaseDataRepository, IDisposable
{
    private readonly IDbContextFactory<FloraDbContext> _contextFactory;
    private readonly ILogger<ReleaseDataRepository> _logger;
    private readonly IConfigurationService _configurationService;
    private readonly Timer _cacheRefreshTimer;
    private readonly ConcurrentDictionary<int, ReleaseData> _releaseCache = new();
    private readonly SemaphoreSlim _cacheLock = new(1, 1);
    private string? _currentMachineCode;

    public ReleaseDataRepository(
        IDbContextFactory<FloraDbContext> contextFactory,
        IConfigurationService configurationService,
        ILogger<ReleaseDataRepository> logger
    )
    {
        _contextFactory = contextFactory ?? throw new ArgumentNullException(nameof(contextFactory));
        _configurationService = configurationService ?? throw new ArgumentNullException(nameof(configurationService));
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));

        _cacheRefreshTimer = new Timer(RefreshCache, null, TimeSpan.Zero, TimeSpan.FromSeconds(5));
        _configurationService.SelectedMachineChanged += OnSelectedMachineChanged;
        _ = InitializeCacheAsync();
    }

    private async void OnSelectedMachineChanged(object? sender, EventArgs e)
    {
        await InitializeCacheAsync();
    }

    private async Task InitializeCacheAsync()
    {
        var machine = await _configurationService.GetSelectedMachineAsync();
        var newMachineCode = machine?.Code;

        if (_currentMachineCode != newMachineCode)
        {
            _logger.LogInformation("Machine code changed from {OldMachineCode} to {NewMachineCode}. Resetting release data cache.", _currentMachineCode, newMachineCode);
            _currentMachineCode = newMachineCode;
            _releaseCache.Clear();
            _cacheRefreshTimer.Change(TimeSpan.Zero, TimeSpan.FromSeconds(5)); // Start immediately, then every 30 seconds
        }
    }

    private async void RefreshCache(object? state)
    {
        if (string.IsNullOrEmpty(_currentMachineCode))
        {
            _logger.LogWarning("Skipping release data cache refresh because machine code is not set.");
            return;
        }

        if (!await _cacheLock.WaitAsync(TimeSpan.FromSeconds(1)))
        {
            _logger.LogWarning("Could not acquire cache lock to refresh release data. Skipping this cycle.");
            return;
        }

        try
        {
            _logger.LogDebug("Starting release data cache refresh for machine {MachineCode}", _currentMachineCode);
            var serverName = await _configurationService.GetMachineServerNameAsync();
            var tableName = $"{serverName}_{_currentMachineCode}_Release_TBM";
            var query = $@"
                SELECT TagID, TagValue, CAST(Ring AS NVARCHAR(MAX)) AS Ring, CAST(ReleaseOff AS NVARCHAR(MAX)) AS ReleaseOff, CAST(ReleaseOn AS NVARCHAR(MAX)) AS ReleaseOn, CAST(Duration AS NVARCHAR(MAX)) AS Duration
                FROM [{tableName}]";

            using var context = await _contextFactory.CreateDbContextAsync();
            var rawData = await context.Database.SqlQueryRaw<RawReleaseData>(query).ToListAsync();

            foreach (var d in rawData)
            {
                if (_releaseCache.TryGetValue(d.TagID, out var existing))
                {
                    existing.TagValue = d.TagValue == true ? 1 : 0;
                    existing.Ring = int.TryParse(d.Ring, out var ring) ? ring : 0;
                    existing.ReleaseOff = DateTime.TryParse(d.ReleaseOff, out var releaseOff) ? releaseOff : null;
                    existing.ReleaseOn = DateTime.TryParse(d.ReleaseOn, out var releaseOn) ? releaseOn : null;
                    existing.Duration = d.Duration ?? "...";
                }
                else
                {
                    _releaseCache[d.TagID] = MapToReleaseData(d);
                }
            }
            _logger.LogInformation("Release data cache refreshed with {Count} items for machine {MachineCode}", _releaseCache.Count, _currentMachineCode);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error refreshing release data cache for machine {MachineCode}", _currentMachineCode);
        }
        finally
        {
            _cacheLock.Release();
        }
    }

    public Task<IEnumerable<ReleaseData>> GetReleaseDataAsync(
        IEnumerable<int> tagIds,
        string machineCode,
        CancellationToken cancellationToken = default
    )
    {
        // if (_currentMachineCode != machineCode)
        // {
        //     _logger.LogWarning("Requested machine code {RequestedMachineCode} does not match current machine code {CurrentMachineCode}. Returning empty list.", machineCode, _currentMachineCode);
        //     return Task.FromResult(Enumerable.Empty<ReleaseData>());
        // }

        var idList = tagIds.ToList();
        var results = new List<ReleaseData>(idList.Count);
        foreach (var id in idList)
        {
            if (_releaseCache.TryGetValue(id, out var releaseData))
            {
                results.Add(releaseData);
            }
        }
        return Task.FromResult<IEnumerable<ReleaseData>>(results);
    }

    public Task<ReleaseData?> GetReleaseDataByTagIdAsync(
        int tagId,
        string machineCode,
        CancellationToken cancellationToken = default
    )
    {
        if (_currentMachineCode != machineCode)
        {
            _logger.LogWarning("Requested machine code {RequestedMachineCode} does not match current machine code {CurrentMachineCode}. Returning null.", machineCode, _currentMachineCode);
            return Task.FromResult<ReleaseData?>(null);
        }

        _releaseCache.TryGetValue(tagId, out var releaseData);
        return Task.FromResult(releaseData);
    }

    public Task<IEnumerable<ReleaseData>> GetAllReleaseDataAsync(
        string machineCode,
        CancellationToken cancellationToken = default
    )
    {
        // if (_currentMachineCode != machineCode)
        // {
        //     _logger.LogWarning("Requested machine code {RequestedMachineCode} does not match current machine code {CurrentMachineCode}. Returning empty list.", machineCode, _currentMachineCode);
        //     return Task.FromResult(Enumerable.Empty<ReleaseData>());
        // }

        return Task.FromResult<IEnumerable<ReleaseData>>(_releaseCache.Values);
    }

    public Task ResetReleaseFlagsAsync()
    {
        foreach ( var releaseData in _releaseCache.Values)
        {
            releaseData.WasInactive = false;
        }

        return Task.CompletedTask;
    }

    private static ReleaseData MapToReleaseData(RawReleaseData rawData)
    {
        return new ReleaseData
        {
            TagID = rawData.TagID,
            TagValue = rawData.TagValue == true ? 1 : 0,
            Ring = int.TryParse(rawData.Ring, out var ring) ? ring : 0,
            ReleaseOff = DateTime.TryParse(rawData.ReleaseOff, out var releaseOff) ? releaseOff : null,
            ReleaseOn = DateTime.TryParse(rawData.ReleaseOn, out var releaseOn) ? releaseOn : null,
            Duration = rawData.Duration ?? "..."
        };
    }

    public void Dispose()
    {
        _cacheRefreshTimer?.Dispose();
        _configurationService.SelectedMachineChanged -= OnSelectedMachineChanged;
        _cacheLock?.Dispose();
    }

    /// <summary>
    /// Raw data class for mapping SQL query results.
    /// </summary>
    public class RawReleaseData
    {
        public int TagID { get; set; }
        public bool? TagValue { get; set; }
        public string? Ring { get; set; }
        public string? ReleaseOff { get; set; }
        public string? ReleaseOn { get; set; }
        public string? Duration { get; set; }
    }
}
