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

namespace ExperionCrawler.Infrastructure.OpcUa;

/// <summary>
/// fastRecord 데이터 수집 서비스.
/// realtime_table에서 지정한 샘플링 간격마다 태그 값을 복사하여 fast_records 테이블에 저장.
/// OPC UA 직접 연결 없이 기존 실시간 구독 결과(realtime_table)를 재활용.
/// </summary>
public class ExperionFastService : IExperionFastService, IHostedService, IDisposable
{
    private readonly IServiceScopeFactory         _scopeFactory;
    private readonly ILogger<ExperionFastService> _logger;

    private readonly ConcurrentDictionary<int, FastSessionContext> _sessions = new();

    private CancellationTokenSource? _cts;
    private Task?                    _monitorTask;

    private const int MaxConcurrentSessions = 3;
    private const int MaxRowsPerSession     = 5_000_000;
    private const int MonitorIntervalMs     = 1_000;

    private static readonly int[] AllowedSamplingMs = [1000, 5000, 10000, 30000, 60000];

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

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

    public async Task StartAsync(CancellationToken cancellationToken)
    {
        try
        {
            using var scope = _scopeFactory.CreateScope();
            var db = scope.ServiceProvider.GetRequiredService<IExperionDbService>();
            var sessions = await db.GetFastSessionsAsync();

            foreach (var s in sessions.Where(s => s.Status == "Running"))
            {
                _logger.LogWarning("[Fast] 앱 시작 시 Running 세션 {Id} → Failed 마킹", s.Id);
                await db.UpdateFastSessionStatusAsync(s.Id, "Failed");
            }
        }
        catch (Exception ex)
        {
            _logger.LogWarning(ex, "[Fast] DB 접근 실패 — FastTable 서비스 비활성화 (앱 시작 계속)");
            // DB가 없어도 앱은 시작됨, FastTable 기능만 비활성화
            return;
        }

        _cts         = new CancellationTokenSource();
        _monitorTask = Task.Run(() => MonitorLoopAsync(_cts.Token), _cts.Token);
    }

    public async Task StopAsync(CancellationToken cancellationToken)
    {
        _cts?.Cancel();
        if (_monitorTask != null)
        {
            try
            {
                // 종료 시 대기 시간을 2초로 단축하여 빠른 셧다운 유도
                await _monitorTask.WaitAsync(TimeSpan.FromSeconds(2), cancellationToken).ConfigureAwait(false);
            }
            catch (Exception ex) { _logger.LogDebug(ex, "[Fast] StopAsync 대기 중 타임아웃 또는 취소 발생"); }
        }
    }

    public void Dispose() => _cts?.Dispose();

    // ── IExperionFastService ──────────────────────────────────────────────────

    public async Task<FastSessionInfo> StartSessionAsync(FastSessionStartRequest request)
    {
        if (request.TagList.Length == 0 || request.TagList.Length > 8)
            throw new ArgumentException("태그는 1~8개까지 가능합니다.");

        if (!AllowedSamplingMs.Contains(request.SamplingMs))
            throw new ArgumentException(
                $"샘플링 간격은 {string.Join('/', AllowedSamplingMs.Select(ms => ms / 1000 + "s"))} 중 하나여야 합니다.");

        using var scope = _scopeFactory.CreateScope();
        var db = scope.ServiceProvider.GetRequiredService<IExperionDbService>();

        var runningCount = (await db.GetFastSessionsAsync()).Count(s => s.Status == "Running");
        if (runningCount >= MaxConcurrentSessions)
            throw new InvalidOperationException($"동시 실행 가능한 세션은 {MaxConcurrentSessions}개까지입니다.");

        // 태그가 realtime_table에 존재하는지 검증
        var realtimeRecords = (await db.GetRealtimeRecordsByTagNamesAsync(request.TagList)).ToList();
        var found = realtimeRecords.Select(r => r.TagName).ToHashSet();
        foreach (var tag in request.TagList)
        {
            if (!found.Contains(tag))
                throw new ArgumentException($"태그 '{tag}'이 realtime_table에 없습니다. 포인트빌더에서 추가 후 구독을 시작하세요.");
        }

        var session = await db.CreateFastSessionAsync(new FastSessionCreateRequest(
            Name:          request.Name,
            SamplingMs:    request.SamplingMs,
            DurationSec:   request.DurationSec,
            TagList:       request.TagList,
            RetentionDays: request.RetentionDays));

        var ctx = new FastSessionContext
        {
            SessionId     = session.Id,
            TagList       = request.TagList,
            SamplingMs    = request.SamplingMs,
            DurationSec   = request.DurationSec,
            StartedAt     = DateTime.UtcNow,
            LastSampledAt = DateTime.MinValue
        };

        _sessions[session.Id] = ctx;

        _logger.LogInformation("[Fast] 세션 {Id} 시작 — 태그 {Count}개, {Ms}ms, {Sec}s",
            session.Id, request.TagList.Length, request.SamplingMs, request.DurationSec);

        return MapToInfo(session);
    }

    public async Task StopSessionAsync(int sessionId)
    {
        if (!_sessions.TryGetValue(sessionId, out var ctx))
            throw new InvalidOperationException($"세션 {sessionId}를 찾을 수 없습니다.");

        ctx.Cancel = true;

        using var scope = _scopeFactory.CreateScope();
        var db = scope.ServiceProvider.GetRequiredService<IExperionDbService>();
        await db.UpdateFastSessionStatusAsync(sessionId, "Completed");
        await db.UpdateFastSessionRowCountAsync(sessionId, ctx.TotalRows);

        _sessions.TryRemove(sessionId, out _);
        _logger.LogInformation("[Fast] 세션 {Id} 중지 — 총 {Count}행", sessionId, ctx.TotalRows);
    }

    public async Task DeleteSessionAsync(int sessionId)
    {
        using var scope = _scopeFactory.CreateScope();
        var db = scope.ServiceProvider.GetRequiredService<IExperionDbService>();
        await db.DeleteFastSessionAsync(sessionId);
        _sessions.TryRemove(sessionId, out _);
    }

    public async Task PinSessionAsync(int sessionId, bool pinned)
    {
        using var scope = _scopeFactory.CreateScope();
        var db = scope.ServiceProvider.GetRequiredService<IExperionDbService>();
        await db.UpdateFastSessionPinnedAsync(sessionId, pinned);
    }

    public async Task<FastSessionInfo?> GetSessionAsync(int sessionId)
    {
        using var scope = _scopeFactory.CreateScope();
        var db = scope.ServiceProvider.GetRequiredService<IExperionDbService>();
        var session = await db.GetFastSessionAsync(sessionId);
        return session == null ? null : MapToInfo(session);
    }

    public async Task<IEnumerable<FastSessionInfo>> GetSessionsAsync()
    {
        using var scope = _scopeFactory.CreateScope();
        var db = scope.ServiceProvider.GetRequiredService<IExperionDbService>();
        return (await db.GetFastSessionsAsync()).Select(MapToInfo);
    }

    public async Task<FastQueryResult> GetRecordsAsync(int sessionId, DateTime? from, DateTime? to, string format = "long")
    {
        using var scope = _scopeFactory.CreateScope();
        var db = scope.ServiceProvider.GetRequiredService<IExperionDbService>();
        return await db.GetFastRecordsAsync(sessionId, from, to);
    }

    public async Task ExportCsvAsync(int sessionId, Stream stream, DateTime? from = null, DateTime? to = null)
    {
        using var scope = _scopeFactory.CreateScope();
        var db = scope.ServiceProvider.GetRequiredService<IExperionDbService>();
        await db.ExportFastRecordsToCsvAsync(sessionId, stream, from, to);
    }

    // ── Private ────────────────────────────────────────────────────────────────

    private async Task MonitorLoopAsync(CancellationToken ct)
    {
        while (!ct.IsCancellationRequested)
        {
            try
            {
                await Task.Delay(MonitorIntervalMs, ct);

                foreach (var kvp in _sessions.ToList())
                {
                    var ctx = kvp.Value;
                    if (ctx.Cancel) continue;

                    if ((DateTime.UtcNow - ctx.StartedAt).TotalSeconds >= ctx.DurationSec)
                    {
                        ctx.Cancel = true;
                        await CompleteSessionAsync(ctx.SessionId, ctx.TotalRows, "Completed");
                        continue;
                    }

                    if ((DateTime.UtcNow - ctx.LastSampledAt).TotalMilliseconds >= ctx.SamplingMs)
                    {
                        ctx.LastSampledAt = DateTime.UtcNow;
                        await SampleAsync(ctx);
                    }
                }
            }
            catch (OperationCanceledException) { }
            catch (Exception ex)
            {
                _logger.LogError(ex, "[Fast] 모니터링 루프 오류");
            }
        }
    }

    private async Task SampleAsync(FastSessionContext ctx)
    {
        try
        {
            using var scope = _scopeFactory.CreateScope();
            var db = scope.ServiceProvider.GetRequiredService<IExperionDbService>();

            var realtimeRecords = await db.GetRealtimeRecordsByTagNamesAsync(ctx.TagList);
            var now     = DateTime.UtcNow;
            var records = realtimeRecords
                .Select(r => new FastRecord
                {
                    SessionId  = ctx.SessionId,
                    RecordedAt = now,
                    TagName    = r.TagName,
                    Value      = r.LiveValue
                })
                .ToList();

            if (records.Count == 0) return;

            await db.BatchInsertFastRecordsAsync(records);
            ctx.TotalRows += records.Count;
            await db.UpdateFastSessionRowCountAsync(ctx.SessionId, ctx.TotalRows);

            if (ctx.TotalRows >= MaxRowsPerSession)
            {
                ctx.Cancel = true;
                await db.UpdateFastSessionStatusAsync(ctx.SessionId, "RowLimitReached");
                _sessions.TryRemove(ctx.SessionId, out _);
                _logger.LogWarning("[Fast] 세션 {Id} RowLimitReached ({Max}행)", ctx.SessionId, MaxRowsPerSession);
            }
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "[Fast] 세션 {Id} 샘플링 오류", ctx.SessionId);
        }
    }

    private async Task CompleteSessionAsync(int sessionId, int totalRows, string status)
    {
        using var scope = _scopeFactory.CreateScope();
        var db = scope.ServiceProvider.GetRequiredService<IExperionDbService>();
        await db.UpdateFastSessionStatusAsync(sessionId, status);
        await db.UpdateFastSessionRowCountAsync(sessionId, totalRows);
        _sessions.TryRemove(sessionId, out _);
        _logger.LogInformation("[Fast] 세션 {Id} {Status} — 총 {Count}행", sessionId, status, totalRows);
    }

    private static FastSessionInfo MapToInfo(FastSession s) => new(
        Id:            s.Id,
        Name:          s.Name,
        StartedAt:     s.StartedAt,
        EndedAt:       s.EndedAt,
        Status:        s.Status,
        SamplingMs:    s.SamplingMs,
        DurationSec:   s.DurationSec,
        TagList:       JsonSerializer.Deserialize<string[]>(s.TagList) ?? [],
        RowCount:      s.RowCount,
        RetentionDays: s.RetentionDays,
        Pinned:        s.Pinned);

    private sealed class FastSessionContext
    {
        public int      SessionId     { get; set; }
        public string[] TagList       { get; set; } = [];
        public int      SamplingMs    { get; set; }
        public int      DurationSec   { get; set; }
        public DateTime StartedAt     { get; set; }
        public DateTime LastSampledAt { get; set; }
        public int      TotalRows     { get; set; }
        public bool     Cancel        { get; set; }
    }
}

/// <summary>
/// 만료된 FastSession을 정리하는 BackgroundService.
/// 매일 03:00 UTC에 실행. pinned = true 세션과 retention_days = null 세션은 제외.
/// </summary>
public class ExperionFastCleanupService : BackgroundService
{
    private readonly IServiceProvider                     _sp;
    private readonly ILogger<ExperionFastCleanupService> _logger;

    public ExperionFastCleanupService(
        IServiceProvider sp,
        ILogger<ExperionFastCleanupService> logger)
    {
        _sp     = sp;
        _logger = logger;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            var now   = DateTime.UtcNow;
            var next  = now.Date.AddDays(1).AddHours(3);
            var delay = next - now;
            if (delay < TimeSpan.Zero) delay = TimeSpan.Zero;

            try { await Task.Delay(delay, stoppingToken); }
            catch (OperationCanceledException) { break; }

            try
            {
                using var scope = _sp.CreateScope();
                var db = scope.ServiceProvider.GetRequiredService<IExperionDbService>();
                var sessions = await db.GetFastSessionsAsync();
                var cutoff   = DateTime.UtcNow;

                foreach (var s in sessions.Where(s =>
                    !s.Pinned &&
                    s.RetentionDays.HasValue &&
                    s.StartedAt.AddDays(s.RetentionDays.Value) < cutoff))
                {
                    _logger.LogInformation("[FastCleanup] 세션 {Id} 삭제 (retention {Days}일 초과)", s.Id, s.RetentionDays);
                    await db.DeleteFastSessionAsync(s.Id);
                }
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "[FastCleanup] 정리 작업 오류");
            }
        }
    }
}
