using DNS.Client;
using DNS.Protocol;
using FastGithub.Configuration;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
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 domainResolveCache = new MemoryCache(Options.Create(new MemoryCacheOptions()));
        private readonly IMemoryCache disableIPAddressCache = new MemoryCache(Options.Create(new MemoryCacheOptions()));
        private readonly FastGithubConfig fastGithubConfig;
        private readonly DnscryptProxy dnscryptProxy;
        private readonly ILogger logger;
        private readonly TimeSpan connectTimeout = TimeSpan.FromSeconds(5d);
        private readonly TimeSpan disableIPExpiration = TimeSpan.FromMinutes(2d);
        private readonly TimeSpan dnscryptExpiration = TimeSpan.FromMinutes(10d);
        private readonly TimeSpan fallbackExpiration = TimeSpan.FromMinutes(2d);
        private readonly TimeSpan loopbackExpiration = TimeSpan.FromSeconds(5d);
        private readonly ConcurrentDictionary semaphoreSlims = new();
        /// 
        /// 域名解析器
        ///  
        /// 
        /// 
        /// 
        public DomainResolver(
            FastGithubConfig fastGithubConfig,
            DnscryptProxy dnscryptProxy,
            ILogger logger)
        {
            this.fastGithubConfig = fastGithubConfig;
            this.dnscryptProxy = dnscryptProxy;
            this.logger = logger;
        }
        /// 
        /// 设置ip不可用
        /// 
        /// ip
        public void SetDisabled(IPAddress address)
        {
            this.disableIPAddressCache.Set(address, address, this.disableIPExpiration);
        }
        /// 
        /// 刷新域名解析结果
        /// 
        /// 域名
        public void FlushDomain(DnsEndPoint domain)
        {
            this.domainResolveCache.Remove(domain);
        }
        /// 
        /// 解析域名
        /// 
        /// 
        /// 
        /// 
        /// 
        /// 
        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.ResolveCoreAsync(domain, cancellationToken);
            }
            finally
            {
                semaphore.Release();
            }
        }
        /// 
        /// 解析域名
        /// 
        /// 
        /// 
        /// 
        /// 
        /// 
        private async Task ResolveCoreAsync(DnsEndPoint domain, CancellationToken cancellationToken)
        {
            if (this.domainResolveCache.TryGetValue(domain, out var address) && address != null)
            {
                return address;
            }
            var expiration = this.dnscryptExpiration;
            address = await this.LookupByDnscryptAsync(domain, cancellationToken);
            if (address == null)
            {
                expiration = this.fallbackExpiration;
                address = await this.LookupByFallbackAsync(domain, cancellationToken);
            }
            if (address == null)
            {
                throw new FastGithubException($"当前解析不到{domain.Host}可用的ip,请刷新重试");
            }
            // 往往是被污染的dns
            if (address.Equals(IPAddress.Loopback) == true)
            {
                expiration = this.loopbackExpiration;
            }
            this.domainResolveCache.Set(domain, address, expiration);
            return address;
        }
        /// 
        /// Dnscrypt查找ip
        /// 
        /// 
        /// 
        /// 
        /// 
        private async Task LookupByDnscryptAsync(DnsEndPoint domain, CancellationToken cancellationToken, int maxTryCount = 2)
        {
            if (this.dnscryptProxy.LocalEndPoint != null)
            {
                for (var i = 0; i < maxTryCount; i++)
                {
                    var address = await this.LookupAsync(this.dnscryptProxy.LocalEndPoint, domain, cancellationToken);
                    if (address != null)
                    {
                        return address;
                    }
                }
            }
            return default;
        }
        /// 
        /// 回退查找ip
        /// 
        /// 
        /// 
        /// 
        /// 
        private async Task LookupByFallbackAsync(DnsEndPoint domain, CancellationToken cancellationToken)
        {
            foreach (var dns in this.fastGithubConfig.FallbackDns)
            {
                var address = await this.LookupAsync(dns, domain, cancellationToken);
                if (address != null)
                {
                    return address;
                }
            }
            return default;
        }
        /// 
        /// 查找最快的可用ip
        /// 
        /// 
        /// 
        /// 
        /// 
        /// 
        private async Task LookupAsync(IPEndPoint dns, DnsEndPoint domain, CancellationToken cancellationToken)
        {
            try
            {
                var dnsClient = new DnsClient(dns);
                var addresses = await dnsClient.Lookup(domain.Host, RecordType.A, cancellationToken);
                addresses = addresses.Where(address => this.disableIPAddressCache.TryGetValue(address, out _) == false).ToList();
                var address = await this.FindFastValueAsync(addresses, domain.Port, cancellationToken);
                if (address == null)
                {
                    this.logger.LogWarning($"dns({dns})解析不到{domain.Host}可用的ip解析");
                }
                else
                {
                    this.logger.LogInformation($"dns({dns}): {domain.Host}->{address}");
                }
                return address;
            }
            catch (Exception ex)
            {
                cancellationToken.ThrowIfCancellationRequested();
                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)
            {
                cancellationToken.ThrowIfCancellationRequested();
                this.SetDisabled(address);
                return default;
            }
            catch (Exception)
            {
                this.SetDisabled(address);
                await Task.Delay(this.connectTimeout, cancellationToken);
                return default;
            }
        }
    }
}