// 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.Exceptions;
using FloraMultiPlatformFrontend.Application.Messages;
using FloraMultiPlatformFrontend.Application.Messages.InfoCenter;
using FloraMultiPlatformFrontend.Application.Services.Interfaces;
using FloraMultiPlatformFrontend.Data.Repositories.Interfaces;
using FloraMultiPlatformFrontend.Presentation.Models.Cockpit;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;

namespace FloraMultiPlatformFrontend.Data.Repositories;

public class LiveRepository : ILiveRepository, IDisposable
{
    private readonly IDbContextFactory<FloraDbContext> _contextFactory;
    private readonly ILogger<LiveRepository> _logger;
    private readonly IInfoCenterService _infoCenterService;
    private readonly IAuthService _authService;
    private readonly IConfigurationService _configurationService;
    private readonly INavigationMessenger _navigationMessenger;
    private readonly IFirewallRetryService _firewallRetryService;
    private readonly Timer _cacheRefreshTimer;
    private readonly ConcurrentDictionary<int, SensorData> _sensorCache = new();
    private readonly SemaphoreSlim _cacheLock = new(1, 1);
    private string? _currentMachineCode;

    public LiveRepository(
        IDbContextFactory<FloraDbContext> contextFactory,
        ILogger<LiveRepository> logger,
        IInfoCenterService infoCenterService,
        IConfigurationService configurationService,
        IAuthService authService,
        INavigationMessenger navigationMessenger,
        IFirewallRetryService firewallRetryService
    )
    {
        _contextFactory =
            contextFactory ?? throw new ArgumentNullException(nameof(contextFactory));
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
        _infoCenterService = infoCenterService ?? throw new ArgumentNullException(nameof(infoCenterService));
        _configurationService = configurationService ?? throw new ArgumentNullException(nameof(configurationService));
        _authService = authService ?? throw new ArgumentNullException(nameof(authService));
        _navigationMessenger = navigationMessenger ?? throw new ArgumentNullException(nameof(navigationMessenger));
        _firewallRetryService = firewallRetryService ?? throw new ArgumentNullException(nameof(firewallRetryService));

        _cacheRefreshTimer = new Timer(RefreshCache, null, Timeout.Infinite, Timeout.Infinite);
        _configurationService.SelectedMachineChanged += OnSelectedMachineChanged;

        // Register to listen for authentication ready messages
        _navigationMessenger.RegisterRefresh(this, OnRefreshMessageAsync);

        _ = InitializeCacheAsync();
    }

    private async Task OnRefreshMessageAsync(RefreshDataMessage message)
    {
        if (message.RefreshType == "AuthenticationReady")
        {
            _logger.LogInformation("Received AuthenticationReady message - starting cache refresh timer");
            await StartCacheRefreshTimerAsync();
        }
    }

    private async Task StartCacheRefreshTimerAsync()
    {
        if (string.IsNullOrEmpty(_currentMachineCode))
        {
            var machine = await _configurationService.GetSelectedMachineAsync();
            _currentMachineCode = machine?.Code;
            _logger.LogInformation("Initialized machine code to {MachineCode} for cache refresh", _currentMachineCode);
        }

        if (!string.IsNullOrEmpty(_currentMachineCode))
        {
            _logger.LogInformation("Starting cache refresh timer for machine {MachineCode}", _currentMachineCode);
            _cacheRefreshTimer.Change(TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(3));
        }
        else
        {
            _logger.LogWarning("Cannot start cache refresh timer: Machine code not available");
        }
    }

    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 cache.", _currentMachineCode, newMachineCode);
            _currentMachineCode = newMachineCode;
            _sensorCache.Clear();

            // Only start timer if authenticated
            if (_authService.CurrentAccount != null)
            {
                _logger.LogInformation("Starting cache refresh timer for machine {MachineCode}", _currentMachineCode);
                _cacheRefreshTimer.Change(TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(3));
            }
            else
            {
                _logger.LogDebug("Skipping cache refresh timer start: User not authenticated");
            }
        }
    }

    private async void RefreshCache(object? state)
    {
        if (string.IsNullOrEmpty(_currentMachineCode))
        {
            _logger.LogWarning("cache refresh because machine code is not set. Trying to fetch");
            var machine = await _configurationService.GetSelectedMachineAsync();
            _currentMachineCode = machine?.Code;
            _logger.LogInformation($"Successfully set machine code to {_currentMachineCode}");
        }

        // Check authentication before attempting database access
        if (_authService.CurrentAccount == null)
        {
            _logger.LogDebug("Skipping cache refresh: User not authenticated");
            return;
        }

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

        try
        {
            _logger.LogDebug("Starting sensor cache refresh for machine {MachineCode}", _currentMachineCode);
            var serverName = await _configurationService.GetMachineServerNameAsync();
            var query = $@"
                SELECT ID, tagValue AS ValueBoolean, NULL AS ValueInteger, NULL AS ValueNumeric FROM dbo.{serverName}_{_currentMachineCode}_Bit
                UNION ALL
                SELECT ID, NULL, tagValue, NULL FROM dbo.{serverName}_{_currentMachineCode}_Int
                UNION ALL
                SELECT ID, NULL, NULL, tagValue FROM dbo.{serverName}_{_currentMachineCode}_Float";

            // Use ExecuteWithoutRetryAsync - this method polls every 3 seconds, so natural retry happens
            List<RawSensorData> rawData = new();
            await _firewallRetryService.ExecuteWithoutRetryAsync(async () =>
            {
                using var context = await _contextFactory.CreateDbContextAsync();
                if (context == null)
                {
                    _logger.LogWarning("DbContext creation returned null, skipping cache refresh");
                    return;
                }

                rawData = await context.Database.SqlQueryRaw<RawSensorData>(query).ToListAsync();
            }, "LiveRepository.RefreshCache");

            foreach (var d in rawData)
            {
                var sensorData = new SensorData
                {
                    SensorId = d.ID,
                    ValueBoolean = d.ValueBoolean,
                    ValueInteger = d.ValueInteger,
                    ValueNumeric = d.ValueNumeric,
                    DataType = d.ValueBoolean.HasValue ? SensorDataType.Boolean : d.ValueInteger.HasValue ? SensorDataType.Integer : SensorDataType.Double,
                    MachineCode = _currentMachineCode,
                    ServerName = serverName,
                    PlcSource = "ValueData",
                    Timestamp = DateTime.UtcNow
                };
                _sensorCache[d.ID] = sensorData;
            }
        }
        catch (NotAuthenticatedException)
        {
            _logger.LogDebug("Cache refresh blocked by authentication exception");
            _sensorCache.Clear();
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error refreshing sensor cache for machine {MachineCode}", _currentMachineCode);
        }
        finally
        {
            _cacheLock.Release();
        }
    }

    public async Task<IEnumerable<SensorData>> GetSensorDataAsync(
        IEnumerable<int> sensorIds,
        string machineCode,
        CancellationToken cancellationToken = default
    )
    {
        if (_currentMachineCode != machineCode)
        {
            _logger.LogWarning("Requested machine code {RequestedMachineCode} does not match current machine code {CurrentMachineCode}. Returning trying to fetch correct one.", machineCode, _currentMachineCode);
            var machine = await _configurationService.GetSelectedMachineAsync();
            _currentMachineCode = machine?.Code;
            // return Task.FromResult(Enumerable.Empty<SensorData>());
        }

        var idList = sensorIds.ToList();
        var results = new List<SensorData>(idList.Count);
        foreach (var id in idList)
        {
            if (_sensorCache.TryGetValue(id, out var sensorData))
            {
                results.Add(sensorData);
            }
            else
            {
                _logger.LogDebug("Sensor ID {SensorId} not found in cache for machine {MachineCode}", id, machineCode);
            }
        }
        return results;
    }

    public async Task<DateTime?> GetLastUpdateTimeAsync(string machineCode, CancellationToken cancellationToken = default)
    {
        if (_authService.CurrentAccount == null)
        {
            _logger.LogDebug("GetLastUpdateTimeAsync blocked: User not authenticated for machine {MachineCode}", machineCode);
            return null;
        }

        var serverName = await _configurationService.GetMachineServerNameAsync();
        var query = $"SELECT TOP 1 tagTime AS Value FROM dbo.{serverName}_{machineCode}_Bit WHERE ID = 32767 ORDER BY ID DESC";

        try
        {
            // Wrap database operation with firewall retry logic
            return await _firewallRetryService.ExecuteWithRetryAsync(async () =>
            {
                using var context = await _contextFactory.CreateDbContextAsync(cancellationToken);
                if (context == null)
                {
                    _logger.LogWarning("DbContext creation returned null");
                    return (DateTime?)null;
                }

                var result = await context.Database.SqlQueryRaw<DateTime>(query).FirstOrDefaultAsync(cancellationToken);
                return result == default(DateTime) ? null : (DateTime?)result;
            }, "LiveRepository.GetLastUpdateTimeAsync");
        }
        catch (NotAuthenticatedException)
        {
            _logger.LogDebug("GetLastUpdateTimeAsync blocked by authentication exception");
            return null;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error fetching last update time for machine {MachineCode}", machineCode);
            return null;
        }
    }

    public void Dispose() => throw new NotImplementedException();

    /// <summary>
    /// Private class for mapping results from the UNION ALL query.
    /// EF Core requires a concrete type for raw SQL query results.
    /// </summary>
    private class RawSensorData
    {
        public int ID { get; set; }
        public bool? ValueBoolean { get; set; }
        public int? ValueInteger { get; set; }
        public double? ValueNumeric { get; set; }
    }
}
