using DNS.Client;
using DNS.Protocol;
using FastGithub.Configuration;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
namespace FastGithub.DomainResolve
{
    /// 
    /// 域名解析器
    ///  
    sealed class DomainResolver : IDomainResolver
    {
        private readonly IMemoryCache memoryCache;
        private readonly FastGithubConfig fastGithubConfig;
        private readonly DnscryptProxy dnscryptProxy;
        private readonly ILogger logger;
        private readonly TimeSpan lookupTimeout = TimeSpan.FromSeconds(2d);
        private readonly TimeSpan connectTimeout = TimeSpan.FromSeconds(2d);
        private readonly TimeSpan dnscryptExpiration = TimeSpan.FromMinutes(5d);
        private readonly TimeSpan fallbackExpiration = TimeSpan.FromMinutes(1d);
        private readonly TimeSpan loopbackExpiration = TimeSpan.FromSeconds(5d);
        private readonly ConcurrentDictionary semaphoreSlims = new();
        /// 
        /// 域名解析器
        /// 
        /// 
        /// 
        /// 
        /// 
        public DomainResolver(
            IMemoryCache memoryCache,
            FastGithubConfig fastGithubConfig,
            DnscryptProxy dnscryptProxy,
            ILogger logger)
        {
            this.memoryCache = memoryCache;
            this.fastGithubConfig = fastGithubConfig;
            this.dnscryptProxy = dnscryptProxy;
            this.logger = logger;
        }
        /// 
        /// 解析域名
        /// 
        /// 
        /// 
        /// 
        public async Task ResolveAsync(DnsEndPoint domain, CancellationToken cancellationToken = default)
        {
            var semaphore = this.semaphoreSlims.GetOrAdd(domain, _ => new SemaphoreSlim(1, 1));
            try
            {
                await semaphore.WaitAsync();
                return await this.LookupAsync(domain, cancellationToken);
            }
            finally
            {
                semaphore.Release();
            }
        }
        /// 
        /// 查找ip
        /// 
        /// 
        /// 
        /// 
        private async Task LookupAsync(DnsEndPoint domain, CancellationToken cancellationToken)
        {
            if (this.memoryCache.TryGetValue(domain, out var address))
            {
                return address;
            }
            var expiration = this.dnscryptExpiration;
            if (this.dnscryptProxy.LocalEndPoint != null)
            {
                address = await this.LookupCoreAsync(this.dnscryptProxy.LocalEndPoint, domain, cancellationToken);
            }
            if (address == null)
            {
                expiration = this.fallbackExpiration;
                address = await this.FallbackLookupAsync(domain, cancellationToken);
            }
            if (address == null)
            {
                throw new FastGithubException($"当前解析不到{domain.Host}可用的ip,请刷新重试");
            }
            // 往往是被污染的dns
            if (address.Equals(IPAddress.Loopback) == true)
            {
                expiration = this.loopbackExpiration;
            }
            this.logger.LogInformation($"[{domain.Host}->{address}]");
            this.memoryCache.Set(domain, address, expiration);
            return address;
        }
        /// 
        /// 回退查找ip
        /// 
        /// 
        /// 
        /// 
        private async Task FallbackLookupAsync(DnsEndPoint domain, CancellationToken cancellationToken)
        {
            foreach (var dns in this.fastGithubConfig.FallbackDns)
            {
                var address = await this.LookupCoreAsync(dns, domain, cancellationToken);
                if (address != null)
                {
                    return address;
                }
            }
            return default;
        }
        /// 
        /// 查找ip
        /// 
        /// 
        /// 
        /// 
        /// 
        private async Task LookupCoreAsync(IPEndPoint dns, DnsEndPoint domain, CancellationToken cancellationToken)
        {
            try
            {
                var dnsClient = new DnsClient(dns);
                using var timeoutTokenSource = new CancellationTokenSource(this.lookupTimeout);
                using var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutTokenSource.Token);
                var addresses = await dnsClient.Lookup(domain.Host, RecordType.A, linkedTokenSource.Token);
                return await this.FindFastValueAsync(addresses, domain.Port, cancellationToken);
            }
            catch (Exception ex)
            {
                this.logger.LogWarning($"dns({dns})无法解析{domain.Host}:{ex.Message}");
                return default;
            }
        }
        /// 
        /// 获取最快的ip
        /// 
        /// 
        /// 
        /// 
        /// 
        private async Task FindFastValueAsync(IEnumerable addresses, int port, CancellationToken cancellationToken)
        {
            if (addresses.Any() == false)
            {
                return default;
            }
            if (port <= 0)
            {
                return addresses.FirstOrDefault();
            }
            var tasks = addresses.Select(address => this.IsAvailableAsync(address, port, cancellationToken));
            var fastTask = await Task.WhenAny(tasks);
            return await fastTask;
        }
        /// 
        /// 验证远程节点是否可连接
        /// 
        /// 
        /// 
        /// 
        /// 
        private async Task IsAvailableAsync(IPAddress address, int port, CancellationToken cancellationToken)
        {
            try
            {
                using var socket = new Socket(SocketType.Stream, ProtocolType.Tcp);
                using var timeoutTokenSource = new CancellationTokenSource(this.connectTimeout);
                using var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutTokenSource.Token);
                await socket.ConnectAsync(address, port, linkedTokenSource.Token);
                return address;
            }
            catch (OperationCanceledException)
            {
                return default;
            }
            catch (Exception)
            {
                await Task.Delay(this.connectTimeout, cancellationToken);
                return default;
            }
        }
    }
}