using Hc900Crawler.Core.Application.Interfaces;
using Hc900Crawler.Core.Domain.Entities;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System.Collections.Concurrent;
using System.Text.Json;

namespace Hc900Crawler.Infrastructure.Hc900;

public class Hc900DigitalEventDetectorService : BackgroundService
{
    private readonly IServiceScopeFactory _scopeFactory;
    private readonly ILogger<Hc900DigitalEventDetectorService> _logger;
    private readonly ConcurrentDictionary<string, DigitalPointState> _previousStates = new();
    private readonly ConcurrentDictionary<string, string> _areaCache = new();
    private readonly ConcurrentDictionary<string, string?> _subAreaCache = new();
    private readonly int _checkIntervalMs = 1000;
    private readonly int _debounceSeconds = 5;
    private HashSet<string> _knownDigitalTags = new();
    private DateTime _lastTagRefresh = DateTime.MinValue;
    private const int TagRefreshIntervalMinutes = 5;

    private readonly JsonSerializerOptions _jsonOptions = new() { PropertyNamingPolicy = null };

    private record DigitalPointState(string Value, DateTime Timestamp, string? EventType);

    public Hc900DigitalEventDetectorService(
        IServiceScopeFactory scopeFactory,
        ILogger<Hc900DigitalEventDetectorService> logger)
    {
        _scopeFactory = scopeFactory;
        _logger = logger;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation("[DigitalEventDetector] 시작 — 감지 간격: {Interval}ms", _checkIntervalMs);

        try
        {
            await LoadDigitalTagNamesAsync(stoppingToken);
            await LoadCurrentStatesAsync(stoppingToken);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "[DigitalEventDetector] 초기화 실패 — 서비스 계속 실행");
        }

        while (!stoppingToken.IsCancellationRequested)
        {
            try
            {
                await Task.Delay(_checkIntervalMs, stoppingToken);
                await DetectAndRecordChangesAsync(stoppingToken);
            }
            catch (OperationCanceledException) { break; }
            catch (Exception ex)
            {
                _logger.LogError(ex, "[DigitalEventDetector] 감지 루프 오류");
            }
        }

        _logger.LogInformation("[DigitalEventDetector] 종료");
    }

    private async Task LoadDigitalTagNamesAsync(CancellationToken ct)
    {
        using var scope = _scopeFactory.CreateScope();
        var db = scope.ServiceProvider.GetRequiredService<IExperionDbService>();
        var digitalTags = await db.GetDigitalTagNamesAsync();
        foreach (var tag in digitalTags)
            _previousStates.TryAdd(tag, null!);
        _logger.LogInformation("[DigitalEventDetector] 디지털 태그 {Count}개 로드", digitalTags.Count());
    }

    private async Task LoadCurrentStatesAsync(CancellationToken ct)
    {
        using var scope = _scopeFactory.CreateScope();
        var db = scope.ServiceProvider.GetRequiredService<IExperionDbService>();
        var points = await db.GetDigitalPointsAsync();
        foreach (var p in points)
            _previousStates[p.TagName] = new DigitalPointState(p.LiveValue ?? "", DateTime.UtcNow, null);
        _logger.LogInformation("[DigitalEventDetector] 현재 상태 {Count}개 로드", _previousStates.Count);
    }

    private async Task DetectAndRecordChangesAsync(CancellationToken ct)
    {
        using var scope = _scopeFactory.CreateScope();
        var db = scope.ServiceProvider.GetRequiredService<IExperionDbService>();

        if ((DateTime.UtcNow - _lastTagRefresh).TotalMinutes >= TagRefreshIntervalMinutes)
        {
            var tags = await db.GetDigitalTagNamesAsync();
            _knownDigitalTags = new HashSet<string>(tags);
            _lastTagRefresh = DateTime.UtcNow;
            foreach (var tag in _knownDigitalTags)
                _previousStates.TryAdd(tag, null!);
        }

        var queryTags = _knownDigitalTags.Count > 0 ? _knownDigitalTags : _previousStates.Keys.ToHashSet();
        var currentPoints = queryTags.Count > 0
            ? (await db.GetRealtimeRecordsByTagNamesAsync(queryTags)).ToList()
            : Enumerable.Empty<RealtimePoint>();
        var events = new List<DigitalEventRecord>();

        foreach (var point in currentPoints)
        {
            if (ct.IsCancellationRequested) break;

            var tagName = point.TagName;
            var currValue = point.LiveValue ?? "";
            var prevState = _previousStates.GetValueOrDefault(tagName);

            if (prevState == null)
            {
                _previousStates[tagName] = new DigitalPointState(currValue, DateTime.UtcNow, null);
                continue;
            }

            if (prevState.Value != currValue)
            {
                var eventType = DetermineEventType(prevState.Value, currValue);
                var now = DateTime.UtcNow;
                var elapsed = (now - prevState.Timestamp).TotalSeconds;

                if (prevState.EventType == eventType && elapsed < _debounceSeconds)
                {
                    _previousStates[tagName] = new DigitalPointState(currValue, now, eventType);
                    continue;
                }

                var duration = (int)elapsed;
                var area = await GetAreaAsync(db, tagName);
                var subArea = await GetSubAreaAsync(db, tagName);

                // D3: RealtimePoint.ControllerId 전파
                events.Add(new DigitalEventRecord
                {
                    ControllerId     = point.ControllerId,
                    TagName          = tagName,
                    NodeId           = point.NodeId,
                    PrevValue        = prevState.Value,
                    CurrValue        = currValue,
                    EventType        = eventType,
                    EventTime        = now,
                    DurationSeconds  = duration,
                    Area             = area,
                    SubArea          = subArea,
                    Metadata         = BuildMetadata(tagName, eventType, currValue)
                });

                _previousStates[tagName] = new DigitalPointState(currValue, now, eventType);
            }
        }

        if (events.Count > 0)
            await db.BatchRecordDigitalEventsAsync(events);
    }

    private string DetermineEventType(string prevValue, string currValue)
    {
        if (currValue.Contains("FAULT") || currValue.Contains("TRIP")) return "TRIP";
        if (currValue.Contains("ALARM")) return "ALARM";
        if (prevValue.Contains("ALARM") && !currValue.Contains("ALARM")) return "NORMAL";
        if (currValue.Contains("RUN") || currValue.Contains("START")) return "RUN";
        return "CHANGE";
    }

    private async Task<string?> GetAreaAsync(IExperionDbService db, string tagName)
    {
        if (_areaCache.TryGetValue(tagName, out var cached)) return cached;
        var area = await db.GetAreaByTagNameAsync(tagName);
        if (area != null) _areaCache[tagName] = area;
        return area;
    }

    private async Task<string?> GetSubAreaAsync(IExperionDbService db, string tagName)
    {
        if (_subAreaCache.TryGetValue(tagName, out var cached)) return cached;
        var subArea = await db.GetSubAreaByTagNameAsync(tagName);
        _subAreaCache[tagName] = subArea;
        return subArea;
    }

    private string? BuildMetadata(string tagName, string eventType, string currValue)
    {
        if (tagName.Contains("-il-", StringComparison.OrdinalIgnoreCase) || tagName.Contains("-trip", StringComparison.OrdinalIgnoreCase))
        {
            return JsonSerializer.Serialize(new
            {
                interlock_tag = tagName,
                event_type = eventType,
                raw_value = currValue
            }, _jsonOptions);
        }
        return null;
    }
}
