using System.Collections.Concurrent;
using ExperionCrawler.Core.Domain.Entities;
using Opc.Ua;
using Opc.Ua.Server;
using StatusCodes = Opc.Ua.StatusCodes;

namespace ExperionCrawler.Infrastructure.OpcUa;

/// <summary>
/// OPC UA 서버 주소 공간 관리자.
/// ns=2 아래에 ExperionCrawler/ServerInfo/* 와 ExperionCrawler/Realtime/* 노드를 생성한다.
/// </summary>
public class ExperionOpcServerNodeManager : CustomNodeManager2
{
    private const string NamespaceUri = "urn:ExperionCrawler:Realtime";

    private FolderState?               _realtimeFolder;
    private BaseDataVariableState?     _statusNode;
    private BaseDataVariableState?     _pointCountNode;
    private BaseDataVariableState?     _lastUpdateNode;

    private readonly ConcurrentDictionary<string, BaseDataVariableState> _tagNodes = new();

    public ExperionOpcServerNodeManager(
        IServerInternal server,
        ApplicationConfiguration configuration)
        : base(server, configuration, NamespaceUri)
    {
    }

    // ── CreateAddressSpace (서버 시작 시 1회 호출) ────────────────────────────

    public override void CreateAddressSpace(IDictionary<NodeId, IList<IReference>> externalReferences)
    {
        lock (Lock)
        {
            // Root folder: Objects/ExperionCrawler
            var root = CreateFolderNode(null, "ExperionCrawler", "ExperionCrawler", externalReferences);

            // ServerInfo 하위 변수 노드
            var info = CreateFolderNode(root, "ExperionCrawler_ServerInfo", "ServerInfo", null);
            _statusNode     = CreateVarNode(info, "EC_Status",      "Status",         DataTypeIds.String,   ValueRanks.Scalar, (string)"Stopped");
            _pointCountNode = CreateVarNode(info, "EC_PointCount",  "PointCount",     DataTypeIds.Int32,    ValueRanks.Scalar, (int)0);
            _lastUpdateNode = CreateVarNode(info, "EC_LastUpdate",  "LastUpdateTime", DataTypeIds.DateTime, ValueRanks.Scalar, DateTime.UtcNow);

            // Realtime 폴더 — RebuildAddressSpace() 호출 시 태그 노드 추가
            _realtimeFolder = CreateFolderNode(root, "ExperionCrawler_Realtime", "Realtime", null);

            AddPredefinedNode(SystemContext, root);
        }
    }

    // ── RebuildAddressSpace (포인트빌더 빌드 후 호출) ────────────────────────

    /// <summary>realtime_table 의 포인트 목록으로 Realtime 폴더 노드를 재구성한다.</summary>
    public void RebuildAddressSpace(
        IEnumerable<RealtimePoint> points,
        IReadOnlyDictionary<string, string>? dataTypeMap = null)
    {
        if (_realtimeFolder == null) return;

        lock (Lock)
        {
            // 기존 태그 노드 제거
            foreach (var kv in _tagNodes)
            {
                _realtimeFolder.RemoveChild(kv.Value);
                DeleteNode(SystemContext, kv.Value.NodeId);
            }
            _tagNodes.Clear();

            // 새 태그 노드 추가
            foreach (var pt in points)
            {
                var dtStr  = dataTypeMap?.GetValueOrDefault(pt.NodeId) ?? "String";
                var dtId   = MapDataType(dtStr);
                // NodeId는 ns=2;s=tag_{tagname} 형식
                var node   = CreateVarNode(_realtimeFolder, $"tag_{pt.TagName}", pt.TagName, dtId, ValueRanks.Scalar, null);
                node.StatusCode = StatusCodes.BadNoData;
                _tagNodes[pt.TagName] = node;
                AddPredefinedNode(SystemContext, node);
            }

            // ServerInfo 업데이트
            if (_pointCountNode != null)
            {
                _pointCountNode.Value = _tagNodes.Count;
                _pointCountNode.ClearChangeMasks(SystemContext, false);
            }
        }
    }

    // ── UpdateNodeValue (FlushLoop 에서 500ms 마다 호출) ─────────────────────

    public void UpdateNodeValue(string tagname, string? rawValue, DateTime timestamp)
    {
        if (!_tagNodes.TryGetValue(tagname, out var node)) return;

        lock (Lock)
        {
            node.Value      = ParseValue(node.DataType, rawValue);
            node.Timestamp  = timestamp;
            node.StatusCode = rawValue == null ? StatusCodes.BadNoData : StatusCodes.Good;
            node.ClearChangeMasks(SystemContext, false);

            if (_lastUpdateNode != null)
            {
                _lastUpdateNode.Value = timestamp;
                _lastUpdateNode.ClearChangeMasks(SystemContext, false);
            }
        }
    }

    /// <summary>ServerInfo.Status / PointCount 업데이트</summary>
    public void UpdateServerStatus(string status, int pointCount)
    {
        lock (Lock)
        {
            if (_statusNode != null)
            {
                _statusNode.Value = status;
                _statusNode.ClearChangeMasks(SystemContext, false);
            }
            if (_pointCountNode != null)
            {
                _pointCountNode.Value = pointCount;
                _pointCountNode.ClearChangeMasks(SystemContext, false);
            }
        }
    }

    public int TagNodeCount => _tagNodes.Count;

    // ── 헬퍼 ─────────────────────────────────────────────────────────────────

    private FolderState CreateFolderNode(
        NodeState? parent, string nodeIdStr, string displayName,
        IDictionary<NodeId, IList<IReference>>? externalReferences)
    {
        var folder = new FolderState(parent)
        {
            SymbolicName     = displayName,
            ReferenceTypeId  = ReferenceTypes.Organizes,
            TypeDefinitionId = ObjectTypeIds.FolderType,
            NodeId           = new NodeId(nodeIdStr, NamespaceIndex),
            BrowseName       = new QualifiedName(displayName, NamespaceIndex),
            DisplayName      = new LocalizedText("en", displayName),
            WriteMask        = AttributeWriteMask.None,
            UserWriteMask    = AttributeWriteMask.None,
            EventNotifier    = EventNotifiers.None
        };

        if (parent != null)
        {
            parent.AddChild(folder);
        }
        else if (externalReferences != null)
        {
            if (!externalReferences.TryGetValue(ObjectIds.ObjectsFolder, out var refs))
                externalReferences[ObjectIds.ObjectsFolder] = refs = new List<IReference>();
            refs.Add(new NodeStateReference(ReferenceTypes.Organizes, false, folder.NodeId));
            folder.AddReference(ReferenceTypes.Organizes, true, ObjectIds.ObjectsFolder);
        }

        return folder;
    }

    private BaseDataVariableState CreateVarNode(
        NodeState parent, string nodeIdStr, string displayName,
        NodeId dataTypeId, int valueRank, object? initialValue)
    {
        var v = new BaseDataVariableState(parent)
        {
            SymbolicName     = displayName,
            ReferenceTypeId  = ReferenceTypes.HasComponent,
            TypeDefinitionId = VariableTypeIds.BaseDataVariableType,
            NodeId           = new NodeId(nodeIdStr, NamespaceIndex),
            BrowseName       = new QualifiedName(displayName, NamespaceIndex),
            DisplayName      = new LocalizedText("en", displayName),
            WriteMask        = AttributeWriteMask.None,
            UserWriteMask    = AttributeWriteMask.None,
            DataType         = dataTypeId,
            ValueRank        = valueRank,
            ArrayDimensions  = null,
            AccessLevel      = AccessLevels.CurrentRead,
            UserAccessLevel  = AccessLevels.CurrentRead,
            Historizing      = false,
            Value            = initialValue,
            StatusCode       = StatusCodes.Good,
            Timestamp        = DateTime.UtcNow
        };
        parent.AddChild(v);
        return v;
    }

    private static object? ParseValue(NodeId dataTypeId, string? raw)
    {
        if (raw == null) return null;
        try
        {
            if (dataTypeId == DataTypeIds.Double)
                return double.Parse(raw, System.Globalization.CultureInfo.InvariantCulture);
            if (dataTypeId == DataTypeIds.Float)
                return float.Parse(raw, System.Globalization.CultureInfo.InvariantCulture);
            if (dataTypeId == DataTypeIds.Int32)
                return int.Parse(raw, System.Globalization.CultureInfo.InvariantCulture);
            if (dataTypeId == DataTypeIds.Int64)
                return long.Parse(raw, System.Globalization.CultureInfo.InvariantCulture);
            if (dataTypeId == DataTypeIds.Boolean)
                return bool.Parse(raw);
            if (dataTypeId == DataTypeIds.DateTime)
                return DateTime.Parse(raw, System.Globalization.CultureInfo.InvariantCulture,
                    System.Globalization.DateTimeStyles.AdjustToUniversal);
        }
        catch { /* fallback to string */ }
        return raw;
    }

    private static NodeId MapDataType(string? dt) => dt?.ToLowerInvariant() switch
    {
        "double"   => DataTypeIds.Double,
        "float"    => DataTypeIds.Float,
        "int32"    => DataTypeIds.Int32,
        "int64"    => DataTypeIds.Int64,
        "boolean"  => DataTypeIds.Boolean,
        "datetime" => DataTypeIds.DateTime,
        _          => DataTypeIds.String
    };
}
