using System.Text;
using ExperionCrawler.Core.Application.Interfaces;
using ExperionCrawler.Core.Domain.Entities;
using ExperionCrawler.Infrastructure.Certificates;
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 서버 접속·읽기·탐색.
/// ApplicationConfiguration 은 원본 Program.cs 의 SecurityConfiguration 구조를 그대로 준수.
/// </summary>
public class ExperionOpcClient : IExperionOpcClient
{
    private readonly ILogger<ExperionOpcClient> _logger;

    public ExperionOpcClient(ILogger<ExperionOpcClient> logger) => _logger = logger;

    // ── 공통 설정 빌더 ────────────────────────────────────────────────────────
    private static async Task<ApplicationConfiguration> BuildConfigAsync(ExperionServerConfig cfg)
    {
        var clientCert = ExperionCertificateService.TryLoadCertificate(cfg.ClientHostName);

        var config = new ApplicationConfiguration
        {
            ApplicationName = "ExperionCrawlerClient",
            ApplicationType = ApplicationType.Client,
            ApplicationUri  = cfg.ApplicationUri,
            SecurityConfiguration = new SecurityConfiguration
            {
                // 원본: ApplicationCertificate = new CertificateIdentifier { Certificate = clientCert }
                ApplicationCertificate = clientCert != null
                    ? new CertificateIdentifier { Certificate = clientCert }
                    : new CertificateIdentifier(),

                // 원본 주석: "⚠️ 에러 발생했던 지점: 아래 3개 경로가 모두 명시되어야 함"
                TrustedPeerCertificates = new CertificateTrustList
                {
                    StoreType = "Directory",
                    StorePath = Path.GetFullPath("pki/trusted")
                },
                TrustedIssuerCertificates = new CertificateTrustList
                {
                    StoreType = "Directory",
                    StorePath = Path.GetFullPath("pki/issuers")
                },
                RejectedCertificateStore = new CertificateTrustList
                {
                    StoreType = "Directory",
                    StorePath = Path.GetFullPath("pki/rejected")
                },
                AutoAcceptUntrustedCertificates = true,
                AddAppCertToTrustedStore        = true
            },
            TransportQuotas    = new TransportQuotas    { OperationTimeout      = 15_000 },
            ClientConfiguration = new ClientConfiguration { DefaultSessionTimeout = 60_000 }
        };

        await config.ValidateAsync(ApplicationType.Client);

        // 원본: config.CertificateValidator.CertificateValidation += (v, e) => { if (...) e.Accept = true; };
        config.CertificateValidator.CertificateValidation += (_, e) =>
        {
            if (e.Error.StatusCode != StatusCodes.Good) e.Accept = true;
        };

        return config;
    }

    // ── 엔드포인트 선택 (원본 로직 동일) ────────────────────────────────────
    private static async Task<ConfiguredEndpoint> SelectEndpointAsync(
        ApplicationConfiguration appConfig, string endpointUrl,
        CancellationToken ct = default)
    {
        // OS TCP SYN 타임아웃(최대 127초)을 피하기 위해 10초 연결 타임아웃 적용
        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);

        // 원본: OrderByDescending SecurityLevel, prefer Basic256Sha256
        var selected = endpoints
            .OrderByDescending(e => e.SecurityLevel)
            .FirstOrDefault(e => e.SecurityPolicyUri.Contains("Basic256Sha256"))
            ?? endpoints[0];

        return new ConfiguredEndpoint(null, selected, endpointConfig);
    }

    // ── 세션 생성 ─────────────────────────────────────────────────────────────
    private static async Task<ISession> CreateSessionAsync(
        ApplicationConfiguration appConfig,
        ConfiguredEndpoint        endpoint,
        ExperionServerConfig      cfg,
        string                    sessionName)
    {
        // 원본: new UserIdentity(userName, Encoding.UTF8.GetBytes(password))
        var identity = new UserIdentity(cfg.UserName, Encoding.UTF8.GetBytes(cfg.Password));

        // CS0618: Session.Create는 obsolete이지만 SessionFactory/CreateAsync가 현재 라이브러리에 없음
        // Task.Run으로 래핑하여 비동기 실행
#pragma warning disable CS0618 // 'Session.Create()' is obsolete
        return await Task.Run(() => Session.Create(
            appConfig,
            endpoint,
            false,
            sessionName,
            60_000,
            identity,
            null));
#pragma warning restore CS0618 // 'Session.Create()' is obsolete
    }

    // ── 접속 테스트 ───────────────────────────────────────────────────────────
    public async Task<ExperionConnectResult> TestConnectionAsync(ExperionServerConfig cfg)
    {
        ISession? session = null;
        try
        {
            _logger.LogInformation("[ExperionOpc] TestConnection → {Url}", cfg.EndpointUrl);
            var appConfig = await BuildConfigAsync(cfg);
            var endpoint  = await SelectEndpointAsync(appConfig, cfg.EndpointUrl, CancellationToken.None);

            _logger.LogInformation("정책 선택됨: {Policy}", endpoint.Description.SecurityPolicyUri);

            session = await CreateSessionAsync(appConfig, endpoint, cfg, "ExperionCrawlerSession");
            var sessionId = session.SessionId?.ToString() ?? "N/A";

            return new ExperionConnectResult(true, "연결 성공", sessionId, endpoint.Description.SecurityPolicyUri);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "[ExperionOpc] 연결 실패");
            return new ExperionConnectResult(false, ex.Message);
        }
        finally { await DisposeSessionAsync(session); }
    }

    // ── 단일 태그 읽기 ────────────────────────────────────────────────────────
    public async Task<ExperionReadResult> ReadTagAsync(ExperionServerConfig cfg, string nodeId)
    {
        var results = await ReadTagsAsync(cfg, new[] { nodeId });
        return results.FirstOrDefault()
               ?? new ExperionReadResult(false, nodeId, null, "Error", "결과 없음");
    }

    // ── 복수 태그 읽기 ────────────────────────────────────────────────────────
    public async Task<IEnumerable<ExperionReadResult>> ReadTagsAsync(
        ExperionServerConfig cfg, IEnumerable<string> nodeIds)
    {
        ISession? session = null;
        var nodeList = nodeIds.ToList();
        var results  = new List<ExperionReadResult>();

        try
        {
            var appConfig = await BuildConfigAsync(cfg);
            var endpoint  = await SelectEndpointAsync(appConfig, cfg.EndpointUrl, CancellationToken.None);
            session       = await CreateSessionAsync(appConfig, endpoint, cfg, "ExperionCrawlerReadSession");

            foreach (var nodeId in nodeList)
            {
                try
                {
                    // 원본: session.ReadValueAsync(nodeId)
                    var nodeToRead = new ReadValueId
                    {
                        NodeId      = new NodeId(nodeId),
                        AttributeId = Attributes.Value
                    };
                    var readResponse = await session.ReadAsync(null, 0, TimestampsToReturn.Both, new ReadValueIdCollection { nodeToRead }, CancellationToken.None);
                    var dv = readResponse.Results[0];
                    
                    bool isGood = StatusCode.IsGood(dv.StatusCode);
                    var statusStr = isGood
                        ? "Good"
                        : $"0x{(uint)dv.StatusCode:X8}";
                    string? errorMsg = isGood ? null : $"OPC UA 상태 코드 오류: {statusStr}";

                    results.Add(new ExperionReadResult(
                        isGood, nodeId, isGood ? dv.Value : null, statusStr, errorMsg, dv.SourceTimestamp));
                }
                catch (Exception ex)
                {
                    results.Add(new ExperionReadResult(false, nodeId, null, "Error", ex.Message));
                }
            }
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "[ExperionOpc] ReadTags 실패");
            foreach (var n in nodeList.Where(n => results.All(r => r.NodeId != n)))
                results.Add(new ExperionReadResult(false, n, null, "Error", ex.Message));
        }
        finally { await DisposeSessionAsync(session); }

        return results;
    }

    // ── 전체 노드맵 탐색 (재귀) ───────────────────────────────────────────────
    public async Task<ExperionNodeMapResult> BrowseAllNodesAsync(
        ExperionServerConfig cfg, int maxDepth = 10, CancellationToken ct = default)
    {
        ISession? session = null;
        var results = new List<ExperionNodeMapEntry>();
        var visited = new HashSet<string>();

        try
        {
            var appConfig = await BuildConfigAsync(cfg);
            var endpoint  = await SelectEndpointAsync(appConfig, cfg.EndpointUrl, ct);
            session       = await CreateSessionAsync(appConfig, endpoint, cfg, "ExperionCrawlerNodeMapSession");

            _logger.LogInformation("[ExperionOpc] 전체 노드맵 탐색 시작 (maxDepth={MaxDepth})", maxDepth);

            await BrowseRecursiveAsync(session, ObjectIds.ObjectsFolder, 0, maxDepth, results, visited, ct);

            _logger.LogInformation("[ExperionOpc] 전체 노드맵 탐색 완료: {Count}개", results.Count);
            return new ExperionNodeMapResult(true, results, results.Count);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "[ExperionOpc] BrowseAllNodes 실패");
            return new ExperionNodeMapResult(false, results, results.Count, ex.Message);
        }
        finally { await DisposeSessionAsync(session); }
    }

    private async Task BrowseRecursiveAsync(
        ISession                    session,
        NodeId                      startNode,
        int                         currentLevel,
        int                         maxLevel,
        List<ExperionNodeMapEntry>  results,
        HashSet<string>             visited,
        CancellationToken           ct)
    {
        if (currentLevel > maxLevel || ct.IsCancellationRequested) return;

        ReferenceDescriptionCollection refs;
        try
        {
            var browser = new Browser(session)
            {
                BrowseDirection = BrowseDirection.Forward,
                ReferenceTypeId = ReferenceTypeIds.HierarchicalReferences,
                IncludeSubtypes = true,
                NodeClassMask   = (int)(NodeClass.Object | NodeClass.Variable),
                ResultMask      = (uint)BrowseResultMask.All
            };
            refs = await browser.BrowseAsync(startNode);
        }
        catch (Exception ex)
        {
            _logger.LogWarning("[ExperionOpc] Browse 실패 NodeId={NodeId}: {Msg}", startNode, ex.Message);
            return;
        }

        if (refs.Count == 0) return;

        // Variable 노드들 DataType 배치 읽기
        var variableIds = refs
            .Where(r => r.NodeClass == NodeClass.Variable)
            .Select(r => (NodeId)r.NodeId)
            .ToList();

        var dataTypeMap = await BatchReadDataTypesAsync(session, variableIds);

        foreach (var r in refs)
        {
            if (ct.IsCancellationRequested) return;

            var nodeIdStr = r.NodeId.ToString();
            if (!visited.Add(nodeIdStr)) continue;  // 순환 참조 방지

            var dataType = r.NodeClass == NodeClass.Variable
                ? (dataTypeMap.TryGetValue(nodeIdStr, out var dt) ? dt : "Unknown")
                : "N/A";

            results.Add(new ExperionNodeMapEntry(
                currentLevel,
                r.NodeClass.ToString(),
                r.DisplayName.Text,
                nodeIdStr,
                dataType));

            if (results.Count % 1000 == 0)
                _logger.LogInformation("[ExperionOpc] 탐색 중... {Count}개 수집", results.Count);

            if (r.NodeClass == NodeClass.Object)
                await BrowseRecursiveAsync(session, (NodeId)r.NodeId, currentLevel + 1, maxLevel, results, visited, ct);
        }
    }

    private async Task<Dictionary<string, string>> BatchReadDataTypesAsync(
        ISession session, List<NodeId> variableNodeIds)
    {
        var result = new Dictionary<string, string>(variableNodeIds.Count);
        if (variableNodeIds.Count == 0) return result;

        const int chunkSize = 500;
        for (int i = 0; i < variableNodeIds.Count; i += chunkSize)
        {
            var chunk = variableNodeIds.Skip(i).Take(chunkSize).ToList();
            try
            {
                var readValues = new ReadValueIdCollection(
                    chunk.Select(n => new ReadValueId { NodeId = n, AttributeId = Attributes.DataType }));

                var response = await session.ReadAsync(
                    null, 0, TimestampsToReturn.Neither, readValues, CancellationToken.None);

                for (int j = 0; j < chunk.Count; j++)
                    result[chunk[j].ToString()] = ResolveDataTypeName(response.Results[j]);
            }
            catch (Exception ex)
            {
                _logger.LogWarning("[ExperionOpc] DataType 배치 읽기 실패: {Msg}", ex.Message);
                foreach (var n in chunk)
                    result[n.ToString()] = "Unknown";
            }
        }
        return result;
    }

    private static readonly Dictionary<uint, string> _builtinTypes = new()
    {
        {  1, "Boolean"  }, {  2, "SByte"    }, {  3, "Byte"     }, {  4, "Int16"    },
        {  5, "UInt16"   }, {  6, "Int32"    }, {  7, "UInt32"   }, {  8, "Int64"    },
        {  9, "UInt64"   }, { 10, "Float"    }, { 11, "Double"   }, { 12, "String"   },
        { 13, "DateTime" }, { 15, "ByteString"}, { 17, "StatusCode" }
    };

    private static string ResolveDataTypeName(DataValue dv)
    {
        if (!StatusCode.IsGood(dv.StatusCode)) return "Unknown";

        NodeId? nodeId = dv.Value switch
        {
            NodeId        nid => nid,
            ExpandedNodeId eid => (NodeId)eid,
            _                  => null
        };
        if (nodeId is null) return "Unknown";

        if (nodeId.NamespaceIndex == 0 && nodeId.IdType == IdType.Numeric)
        {
            var id = Convert.ToUInt32(nodeId.Identifier);
            if (_builtinTypes.TryGetValue(id, out var name)) return name;
        }
        return nodeId.ToString() ?? "Unknown";
    }

    // ── 노드 탐색 ─────────────────────────────────────────────────────────────
    public async Task<ExperionBrowseResult> BrowseNodesAsync(ExperionServerConfig cfg, string? startNodeId = null)
    {
        ISession? session = null;
        try
        {
            var appConfig = await BuildConfigAsync(cfg);
            var endpoint  = await SelectEndpointAsync(appConfig, cfg.EndpointUrl);
            session       = await CreateSessionAsync(appConfig, endpoint, cfg, "ExperionCrawlerBrowseSession");

            var startNode = startNodeId != null
                ? new NodeId(startNodeId)
                : ObjectIds.ObjectsFolder;

            var browser = new Browser(session)
            {
                BrowseDirection  = BrowseDirection.Forward,
                ReferenceTypeId  = ReferenceTypeIds.HierarchicalReferences,
                IncludeSubtypes  = true,
                NodeClassMask    = (int)(NodeClass.Object | NodeClass.Variable),
                ResultMask       = (uint)BrowseResultMask.All
            };

            var refs  = await browser.BrowseAsync(startNode);
            var nodes = refs.Select(r => new ExperionNodeInfo(
                r.NodeId.ToString(),
                r.DisplayName.Text,
                r.NodeClass.ToString(),
                r.NodeClass == NodeClass.Object
            )).ToList();

            return new ExperionBrowseResult(true, nodes);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "[ExperionOpc] BrowseNodes 실패");
            return new ExperionBrowseResult(false, Enumerable.Empty<ExperionNodeInfo>(), ex.Message);
        }
        finally { await DisposeSessionAsync(session); }
    }

    // ── 세션 정리 ─────────────────────────────────────────────────────────────
    private static async Task DisposeSessionAsync(ISession? session)
    {
        if (session == null) return;
        try { await session.CloseAsync(); } catch { /* ignore */ }
        session.Dispose();
    }
}
