// 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 Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;

namespace FloraMultiPlatformFrontend.Data.Repositories;

public class IoTagDataRepository : IIoTagDataRepository, IDisposable
{
    private readonly IDbContextFactory<FloraDbContext> _contextFactory;
    private readonly ILogger<IoTagDataRepository> _logger;
    private readonly IConfigurationService _configurationService;
    private readonly Timer _cacheRefreshTimer;
    private readonly ConcurrentDictionary<int, Presentation.Models.IoTag.IoTagData> _ioTagCache = new();
    private readonly SemaphoreSlim _cacheLock = new(1, 1);
    private string? _currentMachineCode;

    public IoTagDataRepository(
        IDbContextFactory<FloraDbContext> contextFactory,
        IConfigurationService configurationService,
        ILogger<IoTagDataRepository> 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 IO tag data cache.", _currentMachineCode, newMachineCode);
            _currentMachineCode = newMachineCode;
            _ioTagCache.Clear();
            _cacheRefreshTimer.Change(TimeSpan.Zero, TimeSpan.FromSeconds(5));
        }
    }

    private async void RefreshCache(object? state)
    {
        if (string.IsNullOrEmpty(_currentMachineCode))
        {
            _logger.LogWarning("Skipping IO tag 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 IO tag data. Skipping this cycle.");
            return;
        }

        try
        {
            _logger.LogDebug("Starting IO tag data cache refresh for machine {MachineCode}", _currentMachineCode);
            var serverName = await _configurationService.GetMachineServerNameAsync();
            var tagConfig = await _configurationService.GetTagConfigurationAsync();
            var ioTablePrefix = tagConfig?.IOTablePrefix ?? "IO";
            var tableName = $"{serverName}_{_currentMachineCode}_{ioTablePrefix}";
            var query = $@"
                SELECT ID, tagValue, tagTime
                FROM [{tableName}]";

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

            foreach (var d in rawData)
            {
                _ioTagCache[d.ID] = MapToIoTagData(d);
            }
            _logger.LogInformation("IO tag data cache refreshed with {Count} items for machine {MachineCode}", _ioTagCache.Count, _currentMachineCode);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error refreshing IO tag data cache for machine {MachineCode}", _currentMachineCode);
        }
        finally
        {
            _cacheLock.Release();
        }
    }

    public Task<IEnumerable<Presentation.Models.IoTag.IoTagData>> GetIoTagDataAsync(
        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<Presentation.Models.IoTag.IoTagData>());
        // }

        var idList = tagIds.ToList();
        var results = new List<Presentation.Models.IoTag.IoTagData>(idList.Count);
        foreach (var id in idList)
        {
            if (_ioTagCache.TryGetValue(id, out var ioTagData))
            {
                results.Add(ioTagData);
            }
        }
        return Task.FromResult<IEnumerable<Presentation.Models.IoTag.IoTagData>>(results);
    }

    public Task<Presentation.Models.IoTag.IoTagData?> GetIoTagDataByIdAsync(
        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<Presentation.Models.IoTag.IoTagData?>(null);
        }

        _ioTagCache.TryGetValue(tagId, out var ioTagData);
        return Task.FromResult(ioTagData);
    }

    public Task<IEnumerable<Presentation.Models.IoTag.IoTagData>> GetAllIoTagDataAsync(
        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<Presentation.Models.IoTag.IoTagData>());
        // }

        return Task.FromResult<IEnumerable<Presentation.Models.IoTag.IoTagData>>(_ioTagCache.Values);
    }

    private static Presentation.Models.IoTag.IoTagData MapToIoTagData(RawIoTagData rawData)
    {
        return new Presentation.Models.IoTag.IoTagData
        {
            Id = rawData.ID,
            TagValue = rawData.tagValue ?? false,
            TagTime = rawData.tagTime
        };
    }

    /// <summary>
    /// Raw data class for mapping SQL query results.
    /// </summary>
    private class RawIoTagData
    {
        public int ID { get; set; }
        public bool? tagValue { get; set; }
        public DateTime? tagTime { get; set; }
    }
    public void Dispose()
    {
        _cacheRefreshTimer?.Dispose();
        _configurationService.SelectedMachineChanged -= OnSelectedMachineChanged;
        _cacheLock?.Dispose();
    }
}
