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, IAsyncDisposable
{
    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   _stalled    = false;  // 링크 비정상(silent stall) 감지 여부
    private long            _lastNotificationTicks; // 마지막 데이터 수신 시각(UTC ticks) — 신선도 워치독
    private volatile string _linkFault  = "";     // 최근 감지된 링크 이상 사유

    // 자동 재시작 플래그 파일 경로
    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 Task StartAsync(CancellationToken cancellationToken)
    {
        // 부팅 비블로킹: 자동시작 플래그가 있으면 백그라운드 supervisor만 띄우고 즉시 반환.
        // 연결/재구독/재시도는 supervisor가 앱 부팅과 무관하게 전담한다 (부팅 지연 0).
        if (!File.Exists(FlagPath)) return Task.CompletedTask;

        try
        {
            var json = File.ReadAllText(FlagPath);
            var cfg  = JsonSerializer.Deserialize<ExperionServerConfig>(json);
            if (cfg == null) return Task.CompletedTask;

            StartSupervisor(cfg);
            _logger.LogInformation("[Realtime] 자동시작 — 백그라운드 supervisor 기동 (부팅 비블로킹): {Url}", cfg.EndpointUrl);
        }
        catch (Exception ex)
        {
            _logger.LogWarning(ex, "[Realtime] 자동시작 플래그 읽기 실패 — 무시");
        }
        return Task.CompletedTask;
    }

    public async Task StopAsync(CancellationToken cancellationToken)
    {
        // 앱 종료(Ctrl+C 등): supervisor만 정지하고 플래그는 유지 → 재기동 시 자동 재개.
        await StopInternalAsync(deleteFlag: false, ct: cancellationToken);
        _logger.LogInformation("[Realtime] 구독 중지 완료 (앱 종료 — 자동시작 플래그 유지)");
    }

    /// <summary>의도=ON: 백그라운드 supervisor 루프를 새로 기동한다 (호출 전 기존 것을 정지해 둘 것).</summary>
    private void StartSupervisor(ExperionServerConfig cfg)
    {
        _currentCfg  = cfg;
        _cts         = new CancellationTokenSource();
        _monitorTask = Task.Run(() => SuperviseAsync(_cts.Token));
    }

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

    public async Task StartAsync(ExperionServerConfig cfg)
    {
        // 이미 supervisor가 돌고 있으면 정지 후 교체 (플래그는 아래서 새 cfg로 덮어씀)
        if (_monitorTask is { IsCompleted: false })
        {
            _logger.LogInformation("[Realtime] 이미 실행 중 — 정지 후 재시작합니다.");
            await StopInternalAsync(deleteFlag: false);
        }

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

        StartSupervisor(cfg);
        _logger.LogInformation("[Realtime] 구독 supervisor 시작 요청: {Url}", cfg.EndpointUrl);
    }

    public async Task StopAsync()
    {
        // 운전원 '구독중지': 의도=OFF → 플래그 삭제 + supervisor 정지 (자동 재개 안 함)
        await StopInternalAsync(deleteFlag: true);
        _logger.LogInformation("[Realtime] 구독 중지 완료 (운전원 정지 — 자동시작 비활성화)");
    }

    /// <summary>supervisor·flush 정지 및 세션 정리. deleteFlag=true면 자동시작 플래그도 삭제(의도=OFF).</summary>
    private async Task StopInternalAsync(bool deleteFlag, CancellationToken ct = default)
    {
        if (deleteFlag)
        {
            try
            {
                if (File.Exists(FlagPath)) File.Delete(FlagPath);
                _logger.LogInformation("[Realtime] 자동시작 플래그 삭제");
            }
            catch (Exception ex) { _logger.LogWarning(ex, "[Realtime] 플래그 파일 삭제 실패 (무시)"); }
        }

        _cts?.Cancel();

        var tasks = new[] { _monitorTask, _flushTask }
            .Where(t => t != null).Select(t => t!).ToArray();
        if (tasks.Length > 0)
        {
            try
            {
                await Task.WhenAll(tasks).WaitAsync(TimeSpan.FromSeconds(10), ct).ConfigureAwait(false);
            }
            catch (Exception ex) { _logger.LogDebug(ex, "[Realtime] 정지 대기 중 타임아웃/취소"); }
        }

        await CleanupSessionAsync();
        _pendingUpdates.Clear();
        _cts?.Dispose();
        _cts             = null;
        _monitorTask     = null;
        _flushTask       = null;
        _running         = false;
        _stalled         = false;
        _subscribedCount = 0;
        _statusMsg       = "중지됨";
    }

    /// <summary>링크 건강 점검 — 정상이면 null, 이상이면 사유. silent stall(publish 중단)까지 감지.</summary>
    private string? GetLinkFault()
    {
        var s = _session;
        if (s == null || !s.Connected) return "세션 끊김";
        if (s.KeepAliveStopped)         return "KeepAlive 중단";
        var sub = _subscription;
        if (sub != null && sub.PublishingStopped) return "구독 publish 중단";
        return null;
    }

    public RealtimeServiceStatus GetStatus()
    {
        var ticks = Interlocked.Read(ref _lastNotificationTicks);
        int ageSec = ticks == 0
            ? -1
            : (int)Math.Max(0, (DateTime.UtcNow.Ticks - ticks) / TimeSpan.TicksPerSecond);
        return new(_running, _subscribedCount, _statusMsg, ageSec, _stalled);
    }

    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}");
        }
    }

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

    // supervisor 루프 — 부팅과 무관하게 의도=ON 동안 연결·재구독·재시도를 전담.
    //   성공 → 건강 감시(연결끊김/KeepAlive 중단/구독 publish 중단=silent stall) → 이상 시 재연결
    //   실패/이상 → RetryDelay 후 무한 재시도 (3회-후-포기 없음, 주기적 자동 재개)
    private async Task SuperviseAsync(CancellationToken ct)
    {
        const int RetryDelayMs     = 10_000; // 연결 실패/이상 후 재시도 주기 (리던던트 서버 전환 등 빠른 재포착)
        const int ConnectTimeoutMs = 10_000; // 1회 연결 시도 최대 시간 (죽은 서버에 무한 블로킹 방지)
        const int HealthPollMs     = 5_000;  // 건강 점검 주기

        // flush 루프는 supervisor 수명 동안 단 1회만 기동 (재연결마다 누적되던 버그 차단)
        _flushTask = Task.Run(() => FlushLoopAsync(ct), ct);

        while (!ct.IsCancellationRequested)
        {
            try
            {
                // 1회 연결 시도에 타임아웃을 건다(죽은/전환중 서버에 길게 묶이지 않게).
                // 타임아웃 시 OCE/TimeoutException이 나지만 우리(ct) 취소가 아니므로 아래 일반 catch에서 재시도된다.
                using (var connCts = CancellationTokenSource.CreateLinkedTokenSource(ct))
                {
                    connCts.CancelAfter(ConnectTimeoutMs);
                    await ConnectAndSubscribeAsync(connCts.Token)
                        .WaitAsync(TimeSpan.FromMilliseconds(ConnectTimeoutMs + 2_000), ct); // SDK가 토큰 무시해도 하드 백스톱
                }

                // 연결 유지 동안 건강 감시
                string? fault = null;
                while (!ct.IsCancellationRequested && (fault = GetLinkFault()) == null)
                    await Task.Delay(HealthPollMs, ct);

                if (ct.IsCancellationRequested) break;

                _running   = false;
                _stalled   = true;
                _linkFault = fault ?? "알 수 없음";
                _statusMsg = $"재연결 대기 중: {_linkFault}";
                _logger.LogWarning("[Realtime] 링크 이상 감지 ({Fault}) — {Sec}초 후 재연결", _linkFault, RetryDelayMs / 1000);
                await CleanupSessionAsync();
            }
            // 우리(앱 종료/운전원 정지)가 취소한 경우에만 루프 종료.
            catch (OperationCanceledException) when (ct.IsCancellationRequested) { break; }
            // OPC SDK 연결/세션 타임아웃은 TaskCanceledException(=OCE 하위)으로 던져진다.
            // 우리 취소가 아니면 일반 오류와 동일하게 재시도해야 함.
            // (이전: catch(OCE){break} 가 SDK 타임아웃을 우리 취소로 오인 → '중지됨' 으로 죽어 복구 불가 버그)
            catch (Exception ex)
            {
                _running   = false;
                _stalled   = true;
                _linkFault = ex.Message;
                _statusMsg = $"재연결 대기 중: {ex.Message}";
                _logger.LogWarning(ex, "[Realtime] 연결 오류({Type}) — {Sec}초 후 재시도", ex.GetType().Name, RetryDelayMs / 1000);
                await CleanupSessionAsync();
            }

            try { await Task.Delay(RetryDelayMs, 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, ct);

        _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;
        _stalled         = false;
        _linkFault       = "";
        Interlocked.Exchange(ref _lastNotificationTicks, DateTime.UtcNow.Ticks);
        _statusMsg       = $"구독 중 ({_subscribedCount}개 포인트)";
        _logger.LogInformation("[Realtime] 구독 완료: {Count}개 포인트", _subscribedCount);
        // flush 루프는 SuperviseAsync에서 1회만 기동한다 (여기서 시작하지 않음 — 재연결 누적 방지)
    }

    // 콜백: Task.Run 없이 dictionary에만 기록 (최신 값 덮어쓰기)
    private void OnNotification(MonitoredItem item, MonitoredItemNotificationEventArgs e)
    {
        // 데이터 수신 시각 기록 — 신선도/상태 노출용 (GetLinkFault는 SDK의 PublishingStopped를 사용)
        Interlocked.Exchange(ref _lastNotificationTicks, DateTime.UtcNow.Ticks);
        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,
        CancellationToken         ct = default)
    {
        var identity = new UserIdentity(cfg.UserName, Encoding.UTF8.GetBytes(cfg.Password));
#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type
        return await new DefaultSessionFactory(null).CreateAsync(
            appConfig,
            endpoint,
            false,
            "ExperionRealtimeSession",
            60_000,
            identity,
            null,
            ct);   // 연결 타임아웃 토큰 전달 (이전 CancellationToken.None → 죽은 서버에 ~2분 블로킹 원인)
#pragma warning restore CS8625
    }

    private volatile bool _disposed = false;

    public async ValueTask DisposeAsync()
    {
        if (_disposed) return;
        _disposed = true;

        _cts?.Cancel();
        try
        {
            await CleanupSessionAsync().ConfigureAwait(false);
        }
        catch (Exception ex)
        {
            _logger.LogDebug(ex, "[Realtime] DisposeAsync 중 세션 정리 예외 (무시)");
        }
        _cts?.Dispose();
        GC.SuppressFinalize(this);
    }

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

        _cts?.Cancel();
        // 동기 Dispose 경로 (DI가 IAsyncDisposable 미지원일 경우만 호출됨).
        // GetAwaiter().GetResult() 는 동기 컨텍스트(예: UI 스레드)에서 데드락 위험이 있으므로
        // 별도 스레드에서 실행하고 타임아웃을 둔다.
        try
        {
            Task.Run(async () => await CleanupSessionAsync().ConfigureAwait(false))
                .Wait(TimeSpan.FromSeconds(3));
        }
        catch (Exception ex)
        {
            _logger.LogDebug(ex, "[Realtime] Dispose 중 세션 정리 예외 (무시)");
        }
        _cts?.Dispose();
        GC.SuppressFinalize(this);
    }
}
