using Opc.Ua;
using Opc.Ua.Client;
using Opc.Ua.Configuration;
using System.Security.Cryptography.X509Certificates;

namespace OpcConnectionTest
{
    class Program
    {
        static async Task Main(string[] args)
        {
            Console.WriteLine("=== Experion OPC UA Connection Test ===\n");

            // 1. 기본 경로 및 설정 정보
            string serverUrl = "opc.tcp://192.168.0.20:4840";
            string basePkiPath = Path.GetFullPath("pki");
            string pfxPath = Path.Combine(basePkiPath, "own/certs/OpcTestClient.pfx");

            // 필수 폴더 자동 생성 (Issuer/Rejected 저장소 에러 방지)
            Directory.CreateDirectory(Path.Combine(basePkiPath, "issuers/certs"));
            Directory.CreateDirectory(Path.Combine(basePkiPath, "rejected/certs"));
            Directory.CreateDirectory(Path.Combine(basePkiPath, "trusted/certs"));

            Console.WriteLine($"Server: {serverUrl}");
            Console.WriteLine($"PKI Root: {basePkiPath}\n");

            ISession? session = null;

            try
            {
                // 2. Application Configuration 설정
                var config = new ApplicationConfiguration()
                {
                    ApplicationName = "OpcTestClient",
                    ApplicationType = ApplicationType.Client,
                    ApplicationUri = "urn:OpcTestClient",
                    
                    SecurityConfiguration = new SecurityConfiguration
                    {
                        ApplicationCertificate = new CertificateIdentifier
                        {
                            StoreType = "Directory",
                            StorePath = Path.Combine(basePkiPath, "own"),
                            SubjectName = "CN=OpcTestClient"
                        },
                        TrustedPeerCertificates = new CertificateTrustList
                        {
                            StoreType = "Directory",
                            StorePath = Path.Combine(basePkiPath, "trusted")
                        },
                        // 💡 중요: Issuer와 Rejected 경로를 명시해야 에러가 나지 않습니다.
                        TrustedIssuerCertificates = new CertificateTrustList
                        {
                            StoreType = "Directory",
                            StorePath = Path.Combine(basePkiPath, "issuers")
                        },
                        RejectedCertificateStore = new CertificateTrustList
                        {
                            StoreType = "Directory",
                            StorePath = Path.Combine(basePkiPath, "rejected")
                        },
                        AutoAcceptUntrustedCertificates = true,
                        RejectSHA1SignedCertificates = false,
                        AddAppCertToTrustedStore = true
                    },
                    TransportQuotas = new TransportQuotas { OperationTimeout = 15000 },
                    ClientConfiguration = new ClientConfiguration { DefaultSessionTimeout = 60000 }
                };

                // 설정 검증
                await config.ValidateAsync(ApplicationType.Client);

                // 3. 인증서 수동 주입 (리눅스 PFX 인식 해결)
                if (File.Exists(pfxPath))
                {
                    X509Certificate2 clientCertificate = new X509Certificate2(pfxPath, "");
                    config.SecurityConfiguration.ApplicationCertificate.Certificate = clientCertificate;
                    Console.WriteLine($"✅ 인증서 수동 로드 성공: {clientCertificate.Subject}");
                }
                else
                {
                    Console.WriteLine($"❌ PFX 파일을 찾을 수 없습니다! 경로 확인: {pfxPath}");
                    return;
                }

                // 4. Endpoint 검색
                Console.WriteLine("\nStep 1: Discovering endpoints...");
                var endpointConfig = EndpointConfiguration.Create(config);

                using (var discoveryClient = DiscoveryClient.Create(new Uri(serverUrl), endpointConfig))
                {
                    var endpoints = discoveryClient.GetEndpoints(null);

                    if (endpoints == null || endpoints.Count == 0)
                    {
                        Console.WriteLine("❌ No endpoints found!");
                        return;
                    }

                    Console.WriteLine($"✅ Found {endpoints.Count} endpoint(s)");
                    
                    // 가장 높은 보안 수준의 엔드포인트 선택
                    var selectedEndpoint = endpoints.OrderByDescending(e => e.SecurityLevel).First();
                    Console.WriteLine($"👉 Selected: {selectedEndpoint.SecurityMode} | {selectedEndpoint.SecurityPolicyUri}");

                    // 5. Session 생성 (mngr 계정)
                    Console.WriteLine("\nStep 2: Creating session with mngr account...");

                    var token = new UserNameIdentityToken
                    {
                        UserName = "mngr",
                        DecryptedPassword = System.Text.Encoding.UTF8.GetBytes("mngr") 
                    };

                    var userIdentity = new UserIdentity(token);
                    var configuredEndpoint = new ConfiguredEndpoint(null, selectedEndpoint, endpointConfig);

                    session = await Session.Create(
                        config,
                        configuredEndpoint,
                        updateBeforeConnect: true,
                        "OPC Test Session",
                        60000,
                        userIdentity, 
                        null);
                } 

                Console.WriteLine($"✅ Connected! Session ID: {session.SessionId}");

                // 6. 데이터 읽기 및 브라우징 테스트
                Console.WriteLine("\nStep 3: Reading server info...");
                await ReadServerInfoAsync(session);

                Console.WriteLine("\nStep 4: Checking redundancy...");
                await CheckRedundancyAsync(session);

                Console.WriteLine("\nStep 5: Browsing nodes...");
                await BrowseNodesAsync(session);

                Console.WriteLine("\n✅ All tests completed successfully!");
            }
            catch (Exception ex)
            {
                Console.WriteLine($"\n❌ Error: {ex.Message}");
                if (ex.InnerException != null)
                    Console.WriteLine($"   Inner: {ex.InnerException.Message}");
            }
            finally
            {
                if (session != null)
                {
                    await session.CloseAsync();
                    Console.WriteLine("\nSession closed.");
                }
            }

            Console.WriteLine("\nPress Enter to exit...");
            Console.ReadLine();
        }

        // --- 헬퍼 메서드들 ---
        static async Task ReadServerInfoAsync(ISession session)
        {
            var state = await session.ReadValueAsync(new NodeId(Variables.Server_ServerStatus_State));
            var time = await session.ReadValueAsync(new NodeId(Variables.Server_ServerStatus_CurrentTime));
            Console.WriteLine($"   Server State: {state.Value}");
            Console.WriteLine($"   Server Time:  {time.Value}");
        }

        static async Task CheckRedundancyAsync(ISession session)
        {
            var serviceLevel = await session.ReadValueAsync(new NodeId(Variables.Server_ServiceLevel));
            byte level = Convert.ToByte(serviceLevel.Value);
            string status = level >= 200 ? "PRIMARY" : (level > 0 ? "SECONDARY" : "STANDALONE");
            Console.WriteLine($"   Service Level: {level} ({status})");
        }

        static async Task BrowseNodesAsync(ISession session)
        {
            var browser = new Browser(session)
            {
                BrowseDirection = BrowseDirection.Forward,
                ReferenceTypeId = ReferenceTypeIds.HierarchicalReferences,
                IncludeSubtypes = true,
                NodeClassMask = (int)(NodeClass.Object | NodeClass.Variable)
            };

            var references = await browser.BrowseAsync(ObjectIds.ObjectsFolder);
            Console.WriteLine($"   Found {references.Count} nodes in Objects folder. Top 5:");
            foreach (var r in references.Take(5))
            {
                Console.WriteLine($"      - {r.DisplayName} ({r.NodeClass})");
            }
        }
    }
}