using ExperionCrawler.Core.Application.Interfaces;
using ExperionCrawler.Core.Domain.Entities;
using ExperionCrawler.Infrastructure.Database;
using Microsoft.Extensions.Logging;
using Microsoft.EntityFrameworkCore;
using Npgsql;

namespace ExperionCrawler.Infrastructure.OpcUa;

/// <summary>
/// 메타데이터(desc, area, 단위/레인지 euhi·eulo·units)를 OPC UA에서 읽어 tag_metadata에 저장/갱신.
/// 단위·레인지는 corroboration 임계(FS 5%)·향후 SP/제어 계산용. 별도 테이블 없이 EAV(tag_metadata) 재사용 →
/// PointBuilder 작성·수동 메타갱신 트리거에 자동 편승. 타입 접근은 v_instrument_range 뷰.
/// </summary>
public class MetadataLoaderService : IMetadataLoaderService
{
    private readonly IExperionOpcClient _opcClient;
    private readonly ExperionDbContext _ctx;
    private readonly ILogger<MetadataLoaderService> _logger;

    // 로드할 메타데이터 속성 목록 (state0~7descriptor 제거 — pv 값에서 파싱)
    // euhi/eulo/units = 점 레벨 EU 레인지/단위 (node_map_master의 자식 노드명과 일치). 'qv.euhi' 등 서브속성은 정확매칭으로 제외.
    private static readonly string[] MetaAttributes =
    {
        "desc", "area", "euhi", "eulo", "units"
    };

    public MetadataLoaderService(
        IExperionOpcClient opcClient,
        ExperionDbContext ctx,
        ILogger<MetadataLoaderService> logger)
    {
        _opcClient = opcClient;
        _ctx = ctx;
        _logger = logger;
    }

    public async Task<int> LoadMetadataAsync(ExperionServerConfig cfg, IEnumerable<string> baseTags)
    {
        var baseTagSet = baseTags.ToHashSet(StringComparer.OrdinalIgnoreCase);
        if (baseTagSet.Count == 0) return 0;

        // ── Step 0: realtime_table에 없는 orphan base_tag 삭제 ─────────────
        await _ctx.Database.ExecuteSqlRawAsync(@"
            DELETE FROM tag_metadata
            WHERE base_tag NOT IN (
                SELECT DISTINCT LOWER(split_part(tagname, '.', 1)) FROM realtime_table
            )");

        // ── Step 1: node_map_master에서 실제 node_id 조회 ──────────────────
        // hostname을 직접 조성하지 않고 DB에 저장된 원본 node_id를 사용
        var masterNodes = await _ctx.NodeMapMasters
            .Where(n => MetaAttributes.Contains(n.Name))
            .Select(n => new { n.NodeId, n.Name })
            .ToListAsync();

        var nodeMap = new Dictionary<string, (string baseTag, string attr)>();
        foreach (var node in masterNodes)
        {
            var baseTag = ExtractBaseTag(node.NodeId);
            if (baseTagSet.Contains(baseTag))
                nodeMap[node.NodeId] = (baseTag, node.Name);
        }

        // ── Step 2: 배치 읽기 (ReadTagsAsync 사용) ────────────────────────
        var results = await _opcClient.ReadTagsAsync(cfg, nodeMap.Keys);
        var entries = new List<(string baseTag, string attr, string? value, string nodeId)>();

        foreach (var result in results)
        {
            if (result.Success && result.Value != null && nodeMap.TryGetValue(result.NodeId, out var meta))
            {
                entries.Add((meta.baseTag, meta.attr, result.Value?.ToString(), result.NodeId));
            }
        }

        // ── Step 3: 단일 배치 UPSERT ──────────────────────────────────────
        if (entries.Count > 0)
        {
            // VALUES 절을 동적으로 생성하여 한 번에 INSERT
            // CTE 컬럼(5개)과 VALUES 값(5개) 일치: base_tag, attribute, value, node_id, loaded_at
            var valuesSql = string.Join(", ", entries.Select((e, i) =>
                $"(@bt{i}, @attr{i}, @val{i}, @nid{i}, NOW())"));

            await _ctx.Database.ExecuteSqlRawAsync(@"
                WITH new_data (base_tag, attribute, value, node_id, loaded_at) AS (
                    VALUES " + valuesSql + @"
                )
                INSERT INTO tag_metadata (base_tag, attribute, value, node_id, loaded_at)
                SELECT base_tag, attribute, value, node_id, loaded_at FROM new_data
                ON CONFLICT (base_tag, attribute)
                DO UPDATE SET value = excluded.value, node_id = excluded.node_id, loaded_at = NOW()",
                entries.SelectMany((e, i) => new object[] {
                    new NpgsqlParameter($"@bt{i}", e.baseTag),
                    new NpgsqlParameter($"@attr{i}", e.attr),
                    new NpgsqlParameter($"@val{i}", (object?)e.value ?? DBNull.Value),
                    new NpgsqlParameter($"@nid{i}", e.nodeId)
                }).ToArray());
        }

        // v_tag_summary는 일반 VIEW이므로 REFRESH 불필요 (조회 시 실시간 JOIN)
        _logger.LogInformation("[Metadata] 로드 완료: {Count}개 속성 ({TagCount}개 태그)", entries.Count, baseTagSet.Count);
        return entries.Count;
    }

    // "ns=1;s=sinamserver:FIC101.desc" → "FIC101"
    private static string ExtractBaseTag(string nodeId)
    {
        var colon = nodeId.LastIndexOf(':');
        if (colon < 0) return nodeId;
        var afterColon = nodeId[(colon + 1)..];
        var dot = afterColon.IndexOf('.');
        return dot > 0 ? afterColon[..dot] : afterColon;
    }

    public async Task<int> ReloadMetadataAsync(ExperionServerConfig cfg, IEnumerable<string>? baseTags = null)
    {
        List<string> tags;
        if (baseTags != null)
        {
            tags = baseTags.ToList();
        }
        else
        {
            // realtime_table에 등록된 포인트의 tagname에서 base_tag 추출
            // tagname 형식: "FIC101.pv" → base_tag: "FIC101" (첫 번째 '.' 앞)
            var tagnames = await _ctx.RealtimePoints.Select(p => p.TagName).Distinct().ToListAsync();
            tags = tagnames
                .Select(tn => { var d = tn.IndexOf('.'); return d > 0 ? tn[..d] : tn; })
                .Distinct(StringComparer.OrdinalIgnoreCase)
                .ToList();

            if (tags.Count == 0)
                _logger.LogWarning("[Metadata] realtime_table이 비어있습니다. 메타데이터 로드 대상 없음.");
        }
        return await LoadMetadataAsync(cfg, tags);
    }
}
