using System.Collections.Concurrent;
using System.Text;
using System.Text.Json;
using ExperionCrawler.Core.Application.Interfaces;
using ExperionCrawler.Core.Domain.Entities;
using ExperionCrawler.Infrastructure.Certificates;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Opc.Ua;
using Opc.Ua.Client;
using ISession = Opc.Ua.Client.ISession;
using StatusCodes = Opc.Ua.StatusCodes;

namespace ExperionCrawler.Infrastructure.OpcUa;

/// <summary>
/// OPC UA Subscription 기반 실시간 livevalue 업데이트 서비스.
/// 값이 변경될 때만 콜백을 받아 realtime_table 을 갱신합니다.
/// </summary>
public class ExperionRealtimeService : IExperionRealtimeService, IHostedService, IDisposable
{
    private readonly IServiceScopeFactory          _scopeFactory;
    private readonly ILogger<ExperionRealtimeService> _logger;
    private readonly IServiceProvider              _sp;
    private readonly IOpcUaConfigProvider              _configProvider;

    private ISession?      _session;
    private Subscription?  _subscription;
    private CancellationTokenSource? _cts;
    private Task?          _monitorTask;
    private Task?          _flushTask;

    // 콜백에서 최신 값만 기록 (노드당 1개 유지) → 500ms 배치 flush
    private readonly ConcurrentDictionary<string, (string? value, DateTime timestamp)>
        _pendingUpdates = new();

    // nodeId → RealtimePoint 매핑 (FlushLoop에서 tagname을 찾기 위해 사용)
    private Dictionary<string, Core.Domain.Entities.RealtimePoint> _pointCache = new();

    // OPC UA 서버 서비스 (순환 참조 방지를 위해 lazy resolve)
    private IExperionOpcServerService? _opcServer;

    private volatile bool   _running;
    private int             _subscribedCount;
    private string          _statusMsg = "중지됨";
    private ExperionServerConfig? _currentCfg;
    private volatile bool   _restarting = false;  // 재진입 방지 플래그

    // 자동 재시작 플래그 파일 경로
    private static readonly string FlagPath =
        Path.GetFullPath("realtime_autostart.json");

    public ExperionRealtimeService(
        IServiceScopeFactory scopeFactory,
        ILogger<ExperionRealtimeService> logger,
        IServiceProvider sp,
        IOpcUaConfigProvider configProvider)
    {
        _scopeFactory = scopeFactory;
        _logger       = logger;
        _sp           = sp;
        _configProvider = configProvider;
    }

    // ── IHostedService ────────────────────────────────────────────────────────

    public async Task StartAsync(CancellationToken cancellationToken)
    {
        // 앱 기동 시 플래그 파일이 있으면 자동 구독 시작
        if (!File.Exists(FlagPath)) return;
        try
        {
            var json = await File.ReadAllTextAsync(FlagPath, cancellationToken);
            var cfg  = JsonSerializer.Deserialize<ExperionServerConfig>(json);
            if (cfg != null)
            {
                _logger.LogInformation("[Realtime] 자동 재시작 플래그 감지 — 구독 자동 시작");
                await StartAsync(cfg);
            }
        }
        catch (Exception ex)
        {
            _logger.LogWarning(ex, "[Realtime] 자동 재시작 플래그 읽기 실패 — 무시");
        }
    }

    public async Task StopAsync(CancellationToken cancellationToken)
    {
        // 앱 종료(Ctrl+C 등) 시: 플래그 파일은 유지 → 재기동 시 자동 재시작
        _cts?.Cancel();
        var tasks = new[] { _monitorTask, _flushTask }
            .Where(t => t != null).Select(t => t!).ToArray();
        if (tasks.Length > 0)
            await Task.WhenAll(tasks).WaitAsync(TimeSpan.FromSeconds(5)).ConfigureAwait(false);
        _running = false;
        _logger.LogInformation("[Realtime] 구독 중지 완료 (앱 종료 — 자동 재시작 플래그 유지)");
    }

    // ── IExperionRealtimeService ──────────────────────────────────────────────

    public async Task StartAsync(ExperionServerConfig cfg)
    {
        if (_running || _restarting)
        {
            _logger.LogWarning("[Realtime] 이미 실행 중 또는 재시작 중. 무시합니다.");
            return;
        }
        
        _restarting = true;
        try
        {
            if (_running)
            {
                _logger.LogWarning("[Realtime] 이미 실행 중. 재시작합니다.");
                await StopAsync();
            }
        }
        finally
        {
            _restarting = false;
        }

        // 플래그 파일 저장 (앱 재기동 시 자동 재시작용)
        try
        {
            var json = JsonSerializer.Serialize(cfg);
            await File.WriteAllTextAsync(FlagPath, json);
            _logger.LogInformation("[Realtime] 자동 재시작 플래그 저장: {Path}", FlagPath);
        }
        catch (Exception ex)
        {
            _logger.LogWarning(ex, "[Realtime] 플래그 파일 저장 실패 (무시)");
        }

        _currentCfg  = cfg;
        _cts         = new CancellationTokenSource();
        _monitorTask = Task.Run(() => RunLoopAsync(_cts.Token));
        _logger.LogInformation("[Realtime] 구독 시작 요청: {Url}", cfg.EndpointUrl);
    }

    public async Task StopAsync()
    {
        if (_restarting)
        {
            _logger.LogWarning("[Realtime] 재시작 중이므로 StopAsync 무시 (restarting 플래그 취소)");
            return;
        }
        
        // 플래그 파일 삭제 (자동 재시작 비활성화)
        try
        {
            if (File.Exists(FlagPath)) File.Delete(FlagPath);
            _logger.LogInformation("[Realtime] 자동 재시작 플래그 삭제");
        }
        catch (Exception ex)
        {
            _logger.LogWarning(ex, "[Realtime] 플래그 파일 삭제 실패 (무시)");
        }

        _cts?.Cancel();

        var tasks = new List<Task>();
        if (_monitorTask != null) tasks.Add(_monitorTask);
        if (_flushTask   != null) tasks.Add(_flushTask);
        if (tasks.Count > 0)
            await Task.WhenAll(tasks).WaitAsync(TimeSpan.FromSeconds(10)).ConfigureAwait(false);

        await CleanupSessionAsync();
        _pendingUpdates.Clear();
        _running         = false;
        _subscribedCount = 0;
        _statusMsg       = "중지됨";
        _logger.LogInformation("[Realtime] 구독 중지 완료");
    }

    public RealtimeServiceStatus GetStatus()
        => new(_running, _subscribedCount, _statusMsg);

    public async Task<(bool Success, string Message)> AddMonitoredItemAsync(string nodeId)
    {
        // 구독 중이 아니면 DB에만 저장된 상태 — 다음 구독 시작 시 자동 포함
        if (!_running || _subscription == null)
            return (true, "구독 중 아님 — 다음 구독 시작 시 자동 포함됩니다.");
        await Task.CompletedTask;

        var item = new MonitoredItem(_subscription.DefaultItem)
        {
            StartNodeId      = new NodeId(nodeId),
            AttributeId      = Attributes.Value,
            SamplingInterval = 500,
            QueueSize        = 1,
            DiscardOldest    = true,
            DisplayName      = nodeId
        };
        item.Notification += OnNotification;
        _subscription.AddItem(item);

        try
        {
            // OPC UA 서버에 실제 적용 — 서버가 node_id 유효성 검증
#pragma warning disable CS0618 // 'ApplyChanges()' is obsolete
            _subscription.ApplyChanges();
#pragma warning restore CS0618 // 'ApplyChanges()' is obsolete

            // 서버 응답 상태 확인 (Error가 null이면 정상)
            if (item.Status.Error != null && !StatusCode.IsGood(item.Status.Error.StatusCode))
            {
                // 유효하지 않은 node_id → subscription에서 제거
                _subscription.RemoveItem(item);
#pragma warning disable CS0618 // 'ApplyChanges()' is obsolete
                _subscription.ApplyChanges();
#pragma warning restore CS0618 // 'ApplyChanges()' is obsolete

                var code = item.Status.Error.StatusCode;
                _logger.LogWarning("[Realtime] 잘못된 node_id: {NodeId} — {Code}", nodeId, code);
                return (false, $"OPC UA 서버가 노드를 거부했습니다: {code}");
            }

            _subscribedCount++;
            _statusMsg = $"구독 중 ({_subscribedCount}개 포인트)";
            _logger.LogInformation("[Realtime] 핫 추가 성공: {NodeId}", nodeId);
            return (true, "구독에 즉시 추가되었습니다.");
        }
        catch (Exception ex)
        {
            _subscription.RemoveItem(item);
            _logger.LogError(ex, "[Realtime] MonitoredItem 추가 실패: {NodeId}", nodeId);
            return (false, $"MonitoredItem 추가 중 오류: {ex.Message}");
        }
    }

    // ── 내부 루프 ─────────────────────────────────────────────────────────────

    private async Task RunLoopAsync(CancellationToken ct)
    {
        while (!ct.IsCancellationRequested)
        {
            try
            {
                await ConnectAndSubscribeAsync(ct);

                // 세션이 살아있는 동안 KeepAlive 대기
                while (!ct.IsCancellationRequested &&
                       _session != null && _session.Connected)
                {
                    await Task.Delay(5_000, ct);
                }
            }
            catch (OperationCanceledException) { break; }
            catch (Exception ex)
            {
                _running   = false;
                _statusMsg = $"재연결 대기 중: {ex.Message}";
                _logger.LogWarning(ex, "[Realtime] 연결 오류, 30초 후 재시도");
                await CleanupSessionAsync();
                try { await Task.Delay(30_000, ct); }
                catch (OperationCanceledException) { break; }
            }
        }

        _running   = false;
        _statusMsg = "중지됨";
    }

    private async Task ConnectAndSubscribeAsync(CancellationToken ct)
    {
        if (_currentCfg == null) return;

        _statusMsg = "연결 중...";
        _logger.LogInformation("[Realtime] OPC UA 접속 시도: {Url}", _currentCfg.EndpointUrl);

        var appConfig = await BuildConfigAsync(_currentCfg);
        var endpoint  = await SelectEndpointAsync(appConfig, _currentCfg.EndpointUrl, ct);
        _session      = await CreateSessionAsync(appConfig, endpoint, _currentCfg);

        _logger.LogInformation("[Realtime] 세션 생성 완료");

        // realtime_table 의 node_id 목록 조회
        List<RealtimePoint> points;
        using (var scope = _scopeFactory.CreateScope())
        {
            var db = scope.ServiceProvider.GetRequiredService<IExperionDbService>();
            points = (await db.GetRealtimePointsAsync()).ToList();
        }

        if (points.Count == 0)
        {
            _statusMsg = "포인트 없음 (포인트빌더에서 먼저 빌드하세요)";
            _logger.LogWarning("[Realtime] realtime_table 이 비어 있습니다.");
            return;
        }

        // Subscription 생성
        _subscription = new Subscription(_session.DefaultSubscription)
        {
            PublishingInterval = 1_000,
            KeepAliveCount     = 10,
            LifetimeCount      = 100,
            MaxNotificationsPerPublish = 1000,
            PublishingEnabled  = true,
            Priority           = 0
        };

        // MonitoredItem 등록
        foreach (var pt in points)
        {
            var item = new MonitoredItem(_subscription.DefaultItem)
            {
                StartNodeId      = new NodeId(pt.NodeId),
                AttributeId      = Attributes.Value,
                SamplingInterval = 500,
                QueueSize        = 1,
                DiscardOldest    = true,
                DisplayName      = pt.NodeId
            };
            item.Notification += OnNotification;
            _subscription.AddItem(item);
        }

        _session.AddSubscription(_subscription);
#pragma warning disable CS0618 // 'Create()' is obsolete
        _subscription.Create();
#pragma warning restore CS0618 // 'Create()' is obsolete

        // nodeId → RealtimePoint 캐시 빌드 (FlushLoop에서 tagname 조회용)
        _pointCache = points.ToDictionary(p => p.NodeId, p => p);

        _subscribedCount = points.Count;
        _running         = true;
        _statusMsg       = $"구독 중 ({_subscribedCount}개 포인트)";
        _logger.LogInformation("[Realtime] 구독 완료: {Count}개 포인트", _subscribedCount);

        // 배치 flush 태스크 시작 (콜백 → dictionary → 500ms 단위 배치 DB 업데이트)
        _flushTask = Task.Run(() => FlushLoopAsync(ct), ct);
    }

    // 콜백: Task.Run 없이 dictionary에만 기록 (최신 값 덮어쓰기)
    private void OnNotification(MonitoredItem item, MonitoredItemNotificationEventArgs e)
    {
        foreach (var val in item.DequeueValues())
        {
            var nodeId    = item.DisplayName;
            var value     = val.Value?.ToString();
            var timestamp = val.SourceTimestamp == DateTime.MinValue ? DateTime.UtcNow : val.SourceTimestamp;
            _pendingUpdates[nodeId] = (value, timestamp);
        }
    }

    // 배치 flush 루프 — 500ms 주기, 단일 DbContext로 일괄 업데이트
    private async Task FlushLoopAsync(CancellationToken ct)
    {
        while (!ct.IsCancellationRequested)
        {
            try { await Task.Delay(500, ct); }
            catch (OperationCanceledException) { break; }
            await FlushPendingAsync();
        }
        // 종료 시 남은 항목 최종 flush
        await FlushPendingAsync();
    }

    private async Task FlushPendingAsync()
    {
        if (_pendingUpdates.IsEmpty) return;

        // 스냅샷 후 제거 (새 콜백은 계속 dictionary에 추가 가능)
        var snapshot = _pendingUpdates.ToArray();
        foreach (var kv in snapshot)
            _pendingUpdates.TryRemove(kv.Key, out _);

        var updates = snapshot
            .Select(kv => new LiveValueUpdate(kv.Key, kv.Value.value, kv.Value.timestamp))
            .ToList();

        try
        {
            using var scope = _scopeFactory.CreateScope();
            var db    = scope.ServiceProvider.GetRequiredService<IExperionDbService>();
            var count = await db.BatchUpdateLiveValuesAsync(updates);
            _logger.LogDebug("[Realtime] 배치 업데이트: {Count}/{Total}건",
                count, updates.Count);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "[Realtime] 배치 DB 업데이트 실패");
        }

        // OPC UA 서버 노드 값 갱신 (lazy resolve — 순환 참조 방지)
        try
        {
            _opcServer ??= _sp.GetService<IExperionOpcServerService>();
            if (_opcServer?.GetStatus().Running == true)
            {
                foreach (var u in updates)
                {
                    if (_pointCache.TryGetValue(u.NodeId, out var pt))
                        _opcServer.UpdateNodeValue(pt.TagName, u.Value, u.Timestamp);
                }
            }
        }
        catch (Exception ex)
        {
            _logger.LogDebug(ex, "[Realtime] OPC 서버 노드 값 갱신 실패 (무시)");
        }
    }

    private async Task CleanupSessionAsync()
    {
        try
        {
            if (_subscription != null)
            {
#pragma warning disable CS0618 // 'Delete()' is obsolete
                _subscription.Delete(true);
#pragma warning restore CS0618 // 'Delete()' is obsolete
                _subscription = null;
            }
            if (_session != null)
            {
                if (_session.Connected)
                    await _session.CloseAsync();
                _session.Dispose();
                _session = null;
            }
        }
        catch (Exception ex)
        {
            _logger.LogWarning(ex, "[Realtime] 세션 정리 중 오류 (무시)");
        }
    }

    // ── OPC UA 헬퍼 ─────────────────────────────────────────────────────────────

    private async Task<ApplicationConfiguration> BuildConfigAsync(ExperionServerConfig cfg)
    {
        return await _configProvider.GetConfigAsync(cfg);
    }

    private static async Task<ConfiguredEndpoint> SelectEndpointAsync(
        ApplicationConfiguration appConfig, string endpointUrl,
        CancellationToken ct = default)
    {
        using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
        timeoutCts.CancelAfter(TimeSpan.FromSeconds(10));

        var endpointConfig = EndpointConfiguration.Create(appConfig);
        using var discovery = await DiscoveryClient.CreateAsync(
            appConfig, new Uri(endpointUrl), DiagnosticsMasks.All, timeoutCts.Token);
        var endpoints = await discovery.GetEndpointsAsync(null);
        var selected  = endpoints
            .OrderByDescending(e => e.SecurityLevel)
            .FirstOrDefault(e => e.SecurityPolicyUri.Contains("Basic256Sha256"))
            ?? endpoints[0];
        return new ConfiguredEndpoint(null, selected, endpointConfig);
    }

    // OPC UA Session 생성 (비동기)
    private static async Task<ISession> CreateSessionAsync(
        ApplicationConfiguration appConfig,
        ConfiguredEndpoint        endpoint,
        ExperionServerConfig      cfg)
    {
        var identity = new UserIdentity(cfg.UserName, Encoding.UTF8.GetBytes(cfg.Password));
        // Session.Create는 동기이므로 Task.Run으로 래핑하여 비동기 실행
#pragma warning disable CS0618 // 'Session.Create()' is obsolete
        return await Task.Run(() => (ISession)Session.Create(
            appConfig,
            endpoint,
            false,
            "ExperionRealtimeSession",
            60_000,
            identity,
            null));
#pragma warning restore CS0618 // 'Session.Create()' is obsolete
    }

    private volatile bool _disposed = false;

    public void Dispose()
    {
        if (_disposed) return;
        _disposed = true;

        _cts?.Cancel();
        // StopAsync에서 이미 Task.WhenAll로 대기하므로, Dispose에서는 await 없이 정리만 수행
        // CleanupSessionAsync는 이미 완료된 상태를 가정
        try
        {
            CleanupSessionAsync().GetAwaiter().GetResult();
        }
        catch
        {
            // Ignore exceptions during disposal
        }
        _cts?.Dispose();
    }
}
