using Hc900.Gateway;
using Hc900Crawler.Core.Application.Interfaces;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Npgsql;
using System.Collections.Concurrent;

namespace Hc900Crawler.Infrastructure.Hc900;

public class Hc900RealtimeService : BackgroundService, IHc900RealtimeService
{
    private readonly ControllerGrpcClientPool _clientPool;
    private readonly ILogger<Hc900RealtimeService> _logger;
    private readonly Hc900Options _options;
    private readonly string _connectionString;

    private readonly ConcurrentDictionary<string, bool> _connected = new();
    private readonly ConcurrentDictionary<string, long> _pollCounts = new();
    private readonly ConcurrentDictionary<string, DateTime?> _lastPollAt = new();

    private Dictionary<string, Dictionary<int, string>> _stateLabels = new();

    public bool IsConnected => _connected.Values.Any(v => v);
    public long PollCount   => _pollCounts.Values.Sum();
    public DateTime? LastPollAt => _lastPollAt.Values.Where(v => v.HasValue).Max();
    public IReadOnlyDictionary<string, bool> ControllerConnected => _connected;

    public Hc900RealtimeService(
        ControllerGrpcClientPool clientPool,
        IOptions<Hc900Options> options,
        IConfiguration config,
        ILogger<Hc900RealtimeService> logger)
    {
        _clientPool = clientPool;
        _options = options.Value;
        _connectionString = config.GetConnectionString("DefaultConnection")
            ?? "Host=localhost;Port=5432;Database=iiot_platform;Username=postgres;Password=postgres;Search Path=hc900";
        _logger = logger;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation("[HC900Realtime] 시작");

        var tasks = _clientPool.EnabledControllerIds
            .Select(id => RunControllerLoopAsync(id, stoppingToken))
            .ToList();
        if (tasks.Count == 0)
            _logger.LogWarning("[HC900Realtime] 활성 컨트롤러 없음");

        await Task.WhenAll(tasks);
        _logger.LogInformation("[HC900Realtime] 종료");
    }

    private async Task RunControllerLoopAsync(string controllerId, CancellationToken ct)
    {
        _logger.LogInformation("[HC900Realtime] [{Id}] 루프 시작", controllerId);

        await WaitForGatewayAsync(controllerId, ct);

        var mapping = await LoadMappingAsync(controllerId, ct);
        _logger.LogInformation("[HC900Realtime] [{Id}] 태그 매핑 {Count}개 로드", controllerId, mapping.Count);

        var stateLabels = await LoadStateLabelsAsync(controllerId, ct);
        _logger.LogInformation("[HC900Realtime] [{Id}] 상태 레이블 {Count}개 로드", controllerId, stateLabels.Count);

        while (!ct.IsCancellationRequested)
        {
            var t0 = DateTime.UtcNow;
            try
            {
                var client = _clientPool.GetClient(controllerId);
                var tagNames = mapping.Select(m => m.Hc900Tag).ToList();
                var resp = tagNames.Count > 0
                    ? await client.ReadTagsAsync(new ReadTagsRequest { TagNames = { tagNames } })
                    : client.ReadTags(new ReadTagsRequest());

                // 프로세스가 살아 있으면 ReadTags는 (캐시 반환으로) 성공하므로 그것만으로는
                // 연결 판정이 안 된다. 실제 Modbus 연결 여부는 게이트웨이 HealthCheck(SERVING)로 판정.
                try
                {
                    var health = await client.HealthCheckAsync(new HealthCheckRequest(),
                        deadline: DateTime.UtcNow.AddSeconds(2));
                    _connected[controllerId] =
                        health.Status == HealthCheckResponse.Types.ServingStatus.Serving;
                }
                catch { _connected[controllerId] = false; }
                _lastPollAt[controllerId] = DateTime.UtcNow;

                if (_connected[controllerId] && resp.Values.Count > 0)
                    await BatchUpdateRealtimeTableAsync(controllerId, resp.Values, mapping, ct);

                // poll count는 gRPC ReadTags + DB write 모두 성공한 후에만 증가
                _pollCounts.AddOrUpdate(controllerId, 1, (_, v) => v + 1);
            }
            catch (OperationCanceledException) { break; }
            catch (Exception ex)
            {
                // ReadTags 실패 → gateway 연결 문제
                // DB write 실패 → gateway와 무관 (index 누락 등) → _connected는 유지
                _logger.LogError(ex, "[HC900Realtime] [{Id}] Poll 실패", controllerId);
            }

            var elapsed = (DateTime.UtcNow - t0).TotalMilliseconds;
            var delay = Math.Max(1, _options.PollIntervalMs - (int)elapsed);
            try { await Task.Delay(delay, ct); } catch (OperationCanceledException) { break; }
        }

        _logger.LogInformation("[HC900Realtime] [{Id}] 루프 종료", controllerId);
    }

    private async Task WaitForGatewayAsync(string controllerId, CancellationToken ct)
    {
        // HealthCheck는 Modbus 연결 후 SERVING을 반환하지만, gRPC는 즉시 도달 가능.
        // SERVING을 기다리면 Modbus 연결까지 20-30초 걸리는데, poll loop가 대신 처리하므로
        // gRPC에 도달만 하면 바로 poll loop로 진입한다.
        for (int i = 0; i < 5; i++)
        {
            try
            {
                var client = _clientPool.GetClient(controllerId);
                await client.HealthCheckAsync(new HealthCheckRequest(),
                    deadline: DateTime.UtcNow.AddSeconds(2));
                _logger.LogInformation("[HC900Realtime] [{Id}] Gateway gRPC 도달 — poll 시작", controllerId);
                return;
            }
            catch { }
            if (ct.IsCancellationRequested) return;
            try { await Task.Delay(1000, ct); } catch (OperationCanceledException) { return; }
        }
        _logger.LogWarning("[HC900Realtime] [{Id}] Gateway gRPC 미응답 — 계속 실행", controllerId);
    }

    private async Task<List<(string Tagname, string Hc900Tag)>> LoadMappingAsync(string controllerId, CancellationToken ct)
    {
        var result = new List<(string, string)>();
        try
        {
            await using var conn = new NpgsqlConnection(_connectionString);
            await conn.OpenAsync(ct);
            await using var cmd = new NpgsqlCommand(
                "SELECT tagname, hc900_tag FROM hc900_map_master WHERE is_active = TRUE AND controller_id = $1", conn);
            cmd.Parameters.Add(new NpgsqlParameter { Value = controllerId });
            await using var reader = await cmd.ExecuteReaderAsync(ct);
            while (await reader.ReadAsync(ct))
                result.Add((reader.GetString(0), reader.GetString(1)));
        }
        catch (Exception ex)
        {
            _logger.LogWarning(ex, "[HC900Realtime] [{Id}] hc900_map_master 로드 실패", controllerId);
        }
        return result;
    }

    private async Task<Dictionary<string, Dictionary<int, string>>> LoadStateLabelsAsync(string controllerId, CancellationToken ct)
    {
        var result = new Dictionary<string, Dictionary<int, string>>(StringComparer.OrdinalIgnoreCase);
        try
        {
            await using var conn = new NpgsqlConnection(_connectionString);
            await conn.OpenAsync(ct);
            await using var cmd = new NpgsqlCommand(
                "SELECT base_tag, attribute, value FROM tag_metadata WHERE attribute LIKE 'state%' AND controller_id = $1", conn);
            cmd.Parameters.Add(new NpgsqlParameter { Value = controllerId });
            await using var reader = await cmd.ExecuteReaderAsync(ct);
            while (await reader.ReadAsync(ct))
            {
                var baseTag   = reader.GetString(0);
                var attribute = reader.GetString(1);
                var value     = reader.GetString(2);
                if (!int.TryParse(attribute.Replace("state", ""), out var stateNum)) continue;
                if (!result.TryGetValue(baseTag, out var labels))
                    result[baseTag] = labels = new Dictionary<int, string>();
                labels[stateNum] = value;
            }
        }
        catch (Exception ex)
        {
            _logger.LogWarning(ex, "[HC900Realtime] [{Id}] 상태 레이블 로드 실패", controllerId);
        }
        return result;
    }

    private async Task BatchUpdateRealtimeTableAsync(
        string controllerId,
        IEnumerable<TagValue> values,
        List<(string Tagname, string Hc900Tag)> mapping,
        CancellationToken ct)
    {
        var tagLookup = mapping
            .GroupBy(m => m.Hc900Tag, StringComparer.OrdinalIgnoreCase)
            .ToDictionary(g => g.Key, g => g.Select(x => x.Tagname).ToList(),
                          StringComparer.OrdinalIgnoreCase);

        await using var conn = new NpgsqlConnection(_connectionString);
        await conn.OpenAsync(ct);

        var rows = new List<(string tagname, string livevalue, DateTime ts)>();
        foreach (var tv in values)
        {
            var ts = tv.Timestamp?.ToDateTime() ?? DateTime.UtcNow;
            if (tagLookup.TryGetValue(tv.TagName, out var tagnames))
            {
                foreach (var tagname in tagnames)
                {
                    var livevalue = FormatValue(tv, tagname);
                    rows.Add((tagname, livevalue, ts));
                }
            }
            else
            {
                // No mapping row → use the gateway tag as-is (= the register/Experion name).
                var tagname = tv.TagName;
                rows.Add((tagname, FormatValue(tv, tagname), ts));
            }
        }

        const int BatchSize = 500;
        for (int start = 0; start < rows.Count; start += BatchSize)
        {
            var chunk = rows.Skip(start).Take(BatchSize).ToList();
            await using var batch = new NpgsqlBatch(conn);
            foreach (var (tagname, livevalue, ts) in chunk)
            {
                var batchCmd = new NpgsqlBatchCommand(
                    @"INSERT INTO realtime_table (controller_id, tagname, node_id, livevalue, timestamp)
                      VALUES ($1, $2, '', $3, $4)
                      ON CONFLICT (controller_id, tagname) DO UPDATE
                        SET livevalue = EXCLUDED.livevalue,
                            timestamp = EXCLUDED.timestamp");
                batchCmd.Parameters.Add(new NpgsqlParameter { Value = controllerId });
                batchCmd.Parameters.Add(new NpgsqlParameter { Value = tagname });
                batchCmd.Parameters.Add(new NpgsqlParameter { Value = livevalue });
                batchCmd.Parameters.Add(new NpgsqlParameter { Value = ts });
                batch.BatchCommands.Add(batchCmd);
            }
            await batch.ExecuteNonQueryAsync(ct);
        }
    }

    private string FormatValue(TagValue tv, string dbTagname)
    {
        float rawFloat = tv.ValueCase switch
        {
            TagValue.ValueOneofCase.Float32Val => tv.Float32Val,
            TagValue.ValueOneofCase.Uint16Val  => tv.Uint16Val,
            _                                  => tv.Float32Val
        };

        var baseTag = dbTagname.Contains('.')
            ? dbTagname[..dbTagname.IndexOf('.')]
            : dbTagname;

        if (_stateLabels.TryGetValue(baseTag, out var labels))
        {
            var stateNum = (int)Math.Round(rawFloat);
            var label = labels.TryGetValue(stateNum, out var l) ? l : stateNum.ToString();
            return $"{{{stateNum} | {label} | }}";
        }

        return tv.ValueCase == TagValue.ValueOneofCase.Uint16Val
            ? tv.Uint16Val.ToString()
            : tv.Float32Val.ToString("G");
    }
}
