using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

namespace CheckDownload
{
    /// <summary>
    /// 自动更新窗体类,负责检查和下载软件更新
    /// </summary>
    public partial class Update : Form
    {
        /// <summary>本地MD5文件名</summary>
        private const string LocalMd5File = "md5.json";
        /// <summary>DNS查询域名,用于获取更新信息</summary>
        private const string DnsQueryDomain = "test.file.ipoi.cn";
        /// <summary>基础下载URL</summary>
        private const string BaseDownloadUrl = "http://localhost:60006/";
        /// <summary>最大并发下载数</summary>
        private const int MaxConcurrentDownloads = 5;

        /// <summary>在线MD5文件名,从DNS查询获取</summary>
        private string _onlineMd5File = "";
        /// <summary>已完成下载的文件数</summary>
        private int _completedFiles = 0;

        /// <summary>
        /// 构造函数,初始化更新窗体
        /// </summary>
        public Update()
        {
            InitializeComponent();
            ConfigureProgressBar();
        }

        /// <summary>
        /// 配置进度条初始设置
        /// </summary>
        private void ConfigureProgressBar()
        {
            Update_Pro.Minimum = 0;  // 设置最小值
            Update_Pro.Maximum = 100; // 设置最大值
            Update_Pro.Value = 0;     // 设置初始值
            Update_Pro.Step = 1;      // 设置步进值
        }

        /// <summary>
        /// 窗体加载事件处理方法,启动更新流程
        /// </summary>
        /// <param name="sender">事件源</param>
        /// <param name="e">事件参数</param>
        public async void Update_Load(object sender, EventArgs e)
        {
            await UpdateFile(); // 执行更新流程
            this.Close();       // 更新完成后关闭窗体
        }

        /// <summary>
        /// 主要更新流程,检查并下载更新文件
        /// </summary>
        /// <returns>更新任务</returns>
        private async Task UpdateFile()
        {
            try
            {
                // 读取本地MD5文件信息
                var localData = await ReadLocalMd5File();
                if (!ValidateLocalData(localData.Version, localData.Md5, localData.Data)) return;

                // 获取在线MD5文件信息
                var onlineData = await GetOnlineMd5File();
                if (!ValidateOnlineData(onlineData.Version, onlineData.Md5)) return;

                // 比较版本,判断是否需要更新
                if (!ShouldUpdate(localData.Version, onlineData.Version)) return;

                // 下载在线MD5文件
                var onlineFileData = await DownloadOnlineMd5File();
                if (onlineFileData == null) return;

                // 比较文件差异
                var differences = CompareDataDifferences(localData.Data, onlineFileData);
                if (differences.Count > 0)
                {
                    // 下载需要更新的文件
                    await DownloadUpdatedFiles(differences);
                }

                // 替换本地MD5文件
                ReplaceLocalMd5File();
            }
            catch (Exception ex)
            {
                UpdateStatus($"发生错误: {ex.Message}");
            }
        }

        /// <summary>
        /// 验证本地数据的有效性
        /// </summary>
        /// <param name="version">版本号</param>
        /// <param name="md5">MD5校验值</param>
        /// <param name="data">文件数据对象</param>
        /// <returns>数据是否有效</returns>
        private bool ValidateLocalData(string version, string md5, JObject data)
        {
            if (string.IsNullOrEmpty(version) || string.IsNullOrEmpty(md5) || data == null)
            {
                UpdateStatus("本地MD5文件无效");
                return false;
            }
            return true;
        }

        /// <summary>
        /// 验证在线数据的有效性
        /// </summary>
        /// <param name="version">版本号</param>
        /// <param name="md5">MD5校验值</param>
        /// <returns>数据是否有效</returns>
        private bool ValidateOnlineData(string version, string md5)
        {
            if (string.IsNullOrEmpty(version) || string.IsNullOrEmpty(md5))
            {
                UpdateStatus("无法获取在线MD5信息");
                return false;
            }
            return true;
        }

        /// <summary>
        /// 读取本地MD5文件并解析内容
        /// </summary>
        /// <returns>包含版本号、MD5值和数据对象的元组</returns>
        private async Task<(string Version, string Md5, JObject Data)> ReadLocalMd5File()
        {
            try
            {
                UpdateStatus("读取md5.json...");
                // 异步读取文件内容
                string json = await Task.Run(() => File.ReadAllText(LocalMd5File));
                // 解析JSON对象
                var obj = JObject.Parse(json);
                // 获取版本号
                string version = obj["version"]?.ToString();
                // 获取数据对象
                var data = (JObject)obj["data"];
                // 计算JSON内容的MD5值
                string jsonMd5 = CalculateMD5(json);
                return (version, jsonMd5, data);
            }
            catch (Exception ex) when (ex is FileNotFoundException || ex is JsonException)
            {
                UpdateStatus($"读取本地MD5文件失败: {ex.Message}");
                return (null, null, null);
            }
        }

        /// <summary>
        /// 获取在线MD5文件信息
        /// </summary>
        /// <returns>包含版本号和MD5值的元组</returns>
        private async Task<(string Version, string Md5)> GetOnlineMd5File()
        {
            try
            {
                UpdateStatus("解析在线md5.json...");
                // 通过DNS查询获取响应数据
                string responseData = await QueryDnsAsync();
                // 反序列化JSON字符串
                string firstUnescaped = JsonConvert.DeserializeObject<string>(responseData);
                // 解析JSON对象
                var dataJson = JObject.Parse(firstUnescaped);

                // 获取版本号和MD5值
                string version = dataJson["version"]?.ToString();
                string md5 = dataJson["md5"]?.ToString();

                // 设置在线MD5文件名
                _onlineMd5File = $"{md5}.json";

                return (version, md5);
            }
            catch (Exception ex)
            {
                UpdateStatus($"获取在线MD5信息失败: {ex.Message}");
                return (null, null);
            }
        }

        /// <summary>
        /// 判断是否需要更新
        /// </summary>
        /// <param name="localVersion">本地版本号</param>
        /// <param name="onlineVersion">在线版本号</param>
        /// <returns>是否需要更新</returns>
        private bool ShouldUpdate(string localVersion, string onlineVersion)
        {
            try
            {
                UpdateStatus("校验信息...");
                // 解析版本号字符串为Version对象
                var localVer = new Version(localVersion);
                var onlineVer = new Version(onlineVersion);
                // 比较版本号,如果本地版本小于在线版本,则需要更新
                return localVer.CompareTo(onlineVer) < 0;
            }
            catch (Exception ex) when (ex is FormatException || ex is ArgumentNullException)
            {
                UpdateStatus($"版本比较失败: {ex.Message}");
                return false;
            }
        }

        /// <summary>
        /// 下载在线MD5文件并解析内容
        /// </summary>
        /// <returns>文件数据对象</returns>
        private async Task<JObject> DownloadOnlineMd5File()
        {
            try
            {
                UpdateStatus("下载在线md5.json文件...");
                // 使用WebClient下载文件
                using (var client = new WebClient())
                {
                    await client.DownloadFileTaskAsync(
                        new Uri($"{BaseDownloadUrl}{_onlineMd5File}"),
                        _onlineMd5File);
                }

                UpdateStatus("读取在线md5.json文件...");
                // 异步读取文件内容
                string json = await Task.Run(() => File.ReadAllText(_onlineMd5File));
                // 解析JSON对象
                var obj = JObject.Parse(json);
                // 返回数据部分
                return (JObject)obj["data"];
            }
            catch (Exception ex)
            {
                UpdateStatus($"下载在线MD5文件失败: {ex.Message}");
                return null;
            }
        }

        /// <summary>
        /// 比较本地数据和在线数据的差异
        /// </summary>
        /// <param name="localData">本地数据对象</param>
        /// <param name="onlineData">在线数据对象</param>
        /// <param name="currentPath">当前路径,用于递归调用</param>
        /// <returns>需要更新的文件路径和MD5值的字典</returns>
        private Dictionary<string, string> CompareDataDifferences(JObject localData, JObject onlineData, string currentPath = "")
        {
            var differences = new Dictionary<string, string>();

            foreach (var onlineProperty in onlineData.Properties())
            {
                // 获取属性名称
                string key = onlineProperty.Name;
                // 获取属性值
                JToken onlineValue = onlineProperty.Value;
                // 构建完整路径
                string fullPath = string.IsNullOrEmpty(currentPath) ? key : $"{currentPath}/{key}";

                if (onlineValue.Type == JTokenType.String)
                {
                    // 如果是字符串类型,表示是文件的MD5值
                    string expectedMd5 = onlineValue.ToString();
                    // 判断是否需要下载该文件
                    if (ShouldDownloadFile(localData, key, expectedMd5, fullPath))
                    {
                        differences[fullPath] = expectedMd5;
                    }
                }
                else if (onlineValue.Type == JTokenType.Object)
                {
                    // 如果是对象类型,表示是目录,递归比较
                    JObject localSubData = GetSubData(localData, key);
                    var subDifferences = CompareDataDifferences(localSubData, (JObject)onlineValue, fullPath);
                    foreach (var diff in subDifferences)
                    {
                        differences[diff.Key] = diff.Value;
                    }
                }
            }

            return differences;
        }

        /// <summary>
        /// 判断是否需要下载文件
        /// </summary>
        /// <param name="localData">本地数据对象</param>
        /// <param name="key">文件键名</param>
        /// <param name="expectedMd5">期望的MD5值</param>
        /// <param name="fullPath">文件完整路径</param>
        /// <returns>是否需要下载</returns>
        private bool ShouldDownloadFile(JObject localData, string key, string expectedMd5, string fullPath)
        {
            // 本地数据中不存在该键
            bool fileMissing = !localData.ContainsKey(key);
            // 本地数据中存在该键,但MD5值不匹配
            bool md5Mismatch = localData.ContainsKey(key) &&
                                (localData[key].Type != JTokenType.String ||
                                 localData[key].ToString() != expectedMd5);
            // 物理文件不存在
            bool physicalFileMissing = !File.Exists(Path.Combine(".", fullPath.Replace('/', '\\')));

            // 满足任一条件则需要下载
            return fileMissing || md5Mismatch || physicalFileMissing;
        }

        /// <summary>
        /// 获取本地数据中的子数据对象
        /// </summary>
        /// <param name="localData">本地数据对象</param>
        /// <param name="key">子数据键名</param>
        /// <returns>子数据对象,如果不存在则返回空对象</returns>
        private JObject GetSubData(JObject localData, string key)
        {
            // 如果本地数据中存在该键且类型为对象,则返回该对象,否则返回空对象
            return localData.ContainsKey(key) && localData[key].Type == JTokenType.Object
                ? (JObject)localData[key]
                : new JObject();
        }

        /// <summary>
        /// 下载需要更新的文件
        /// </summary>
        /// <param name="differences">需要更新的文件路径和MD5值的字典</param>
        /// <returns>下载任务</returns>
        private async Task DownloadUpdatedFiles(Dictionary<string, string> differences)
        {
            // 设置进度条最大值为需要更新的文件数量
            Update_Pro.Maximum = differences.Count;
            Update_Pro.Value = 0;

            // 创建信号量控制并发下载数量
            var semaphore = new SemaphoreSlim(MaxConcurrentDownloads);
            var downloadTasks = new List<Task>();

            // 为每个需要更新的文件创建下载任务
            foreach (var file in differences)
            {
                downloadTasks.Add(DownloadFileWithSemaphore(file, semaphore));
            }

            // 等待所有下载任务完成
            await Task.WhenAll(downloadTasks);
        }

        /// <summary>
        /// 使用信号量控制并发下载文件
        /// </summary>
        /// <param name="file">文件路径和MD5值的键值对</param>
        /// <param name="semaphore">控制并发的信号量</param>
        /// <returns>下载任务</returns>
        private async Task DownloadFileWithSemaphore(KeyValuePair<string, string> file, SemaphoreSlim semaphore)
        {
            // 等待信号量,控制并发数量
            await semaphore.WaitAsync();
            try
            {
                // 下载并验证文件
                await DownloadAndVerifyFile(file.Key, file.Value);
                // 更新进度
                UpdateProgress();
            }
            catch (Exception ex)
            {
                UpdateStatus($"下载失败: {file.Key} - {ex.Message}");
            }
            finally
            {
                // 释放信号量,允许其他下载任务执行
                semaphore.Release();
            }
        }

        /// <summary>
        /// 下载并验证文件
        /// </summary>
        /// <param name="relativePath">文件的相对路径</param>
        /// <param name="expectedMd5">期望的MD5值</param>
        /// <returns>下载任务</returns>
        private async Task DownloadAndVerifyFile(string relativePath, string expectedMd5)
        {
            // 更新状态显示当前下载进度
            UpdateStatus($"正在下载 ({Update_Pro.Value + 1}/{Update_Pro.Maximum}): {relativePath}");

            // 获取本地路径和临时文件路径
            string localPath = GetLocalPath(relativePath);
            string tempPath = $"{localPath}.tmp";

            // 确保目录存在
            EnsureDirectoryExists(localPath);

            // 下载文件到临时路径
            using (var client = new WebClient())
            {
                await client.DownloadFileTaskAsync(
                    new Uri($"{BaseDownloadUrl}File/{relativePath}"),
                    tempPath);
            }

            // 验证文件MD5
            VerifyFileMd5(tempPath, expectedMd5, relativePath);
            // 替换现有文件
            ReplaceExistingFile(tempPath, localPath);
        }

        /// <summary>
        /// 获取文件的本地路径
        /// </summary>
        /// <param name="relativePath">文件的相对路径</param>
        /// <returns>本地文件路径</returns>
        private string GetLocalPath(string relativePath)
        {
            // 将相对路径转换为本地路径,替换斜杠为反斜杠
            return Path.Combine(".", relativePath.Replace('/', '\\'));
        }

        /// <summary>
        /// 确保文件所在目录存在
        /// </summary>
        /// <param name="filePath">文件路径</param>
        private void EnsureDirectoryExists(string filePath)
        {
            // 获取文件所在目录
            string directory = Path.GetDirectoryName(filePath);
            // 如果目录不存在,则创建目录
            if (!Directory.Exists(directory))
            {
                Directory.CreateDirectory(directory);
            }
        }

        /// <summary>
        /// 验证文件的MD5值
        /// </summary>
        /// <param name="filePath">文件路径</param>
        /// <param name="expectedMd5">期望的MD5值</param>
        /// <param name="relativePath">文件的相对路径,用于错误提示</param>
        /// <exception cref="Exception">当MD5校验失败时抛出异常</exception>
        private void VerifyFileMd5(string filePath, string expectedMd5, string relativePath)
        {
            // 计算文件的实际MD5值
            string actualMd5 = CalculateFileMD5(filePath);
            // 如果MD5值不匹配,删除文件并抛出异常
            if (actualMd5 != expectedMd5)
            {
                File.Delete(filePath);
                throw new Exception($"MD5校验失败: {relativePath}");
            }
        }

        /// <summary>
        /// 替换现有文件
        /// </summary>
        /// <param name="tempPath">临时文件路径</param>
        /// <param name="localPath">目标文件路径</param>
        private void ReplaceExistingFile(string tempPath, string localPath)
        {
            // 如果目标文件已存在,则先删除
            if (File.Exists(localPath))
            {
                File.Delete(localPath);
            }
            // 将临时文件移动到目标位置
            File.Move(tempPath, localPath);
        }

        /// <summary>
        /// 更新进度条
        /// </summary>
        private void UpdateProgress()
        {
            // 在UI线程上更新进度条值
            this.Invoke((MethodInvoker)delegate
            {
                Update_Pro.Value++;
            });
        }

        /// <summary>
        /// 替换本地MD5文件为在线下载的新版本
        /// </summary>
        private void ReplaceLocalMd5File()
        {
            try
            {
                // 如果本地MD5文件已存在,则先删除
                if (File.Exists(LocalMd5File))
                {
                    File.Delete(LocalMd5File);
                }
                // 将下载的在线MD5文件移动到本地MD5文件位置
                File.Move(_onlineMd5File, LocalMd5File);
                UpdateStatus("已更新本地版本信息");
            }
            catch (Exception ex)
            {
                UpdateStatus($"更新本地版本信息失败: {ex.Message}");
            }
        }

        /// <summary>
        /// 更新状态信息显示
        /// </summary>
        /// <param name="message">要显示的状态信息</param>
        private void UpdateStatus(string message)
        {
            // 在UI线程上更新状态文本框
            this.Invoke((MethodInvoker)delegate
            {
                Status_Box.Text = message;
            });
        }

        /// <summary>
        /// 计算字符串的MD5值
        /// </summary>
        /// <param name="input">输入字符串</param>
        /// <returns>MD5哈希值(小写十六进制字符串)</returns>
        private string CalculateMD5(string input)
        {
            using (var md5 = MD5.Create())
            {
                // 将字符串转换为字节数组
                byte[] inputBytes = Encoding.UTF8.GetBytes(input);
                // 计算哈希值
                byte[] hashBytes = md5.ComputeHash(inputBytes);
                // 将字节数组转换为十六进制字符串并返回
                return BitConverter.ToString(hashBytes).Replace("-", "").ToLowerInvariant();
            }
        }

        /// <summary>
        /// 计算文件的MD5值
        /// </summary>
        /// <param name="filePath">文件路径</param>
        /// <returns>MD5哈希值(小写十六进制字符串)</returns>
        private string CalculateFileMD5(string filePath)
        {
            using (var md5 = MD5.Create())
            using (var stream = File.OpenRead(filePath))
            {
                // 计算文件流的哈希值
                byte[] hashBytes = md5.ComputeHash(stream);
                // 将字节数组转换为十六进制字符串并返回
                return BitConverter.ToString(hashBytes).Replace("-", "").ToLowerInvariant();
            }
        }

        /// <summary>
        /// 通过多个DNS over HTTPS服务器查询TXT记录
        /// </summary>
        /// <returns>查询到的TXT记录内容</returns>
        private static async Task<string> QueryDnsAsync()
        {
            // DNS over HTTPS服务器列表
            var dohServers = new List<string>
            {
                "https://cloudflare-dns.com/dns-query",
                "https://dns.cloudflare.com/dns-query",
                "https://1.1.1.1/dns-query",
                "https://1.0.0.1/dns-query",
                "https://dns.google/resolve",
                "https://sm2.doh.pub/dns-query",
                "https://doh.pub/dns-query",
                "https://dns.alidns.com/resolve",
                "https://223.5.5.5/resolve",
                "https://223.6.6.6/resolve",
                "https://doh.360.cn/resolve"
            };

            using (var httpClient = new HttpClient())
            {
                // 创建取消令牌源,用于取消其他任务
                var cts = new CancellationTokenSource();
                var tasks = new List<Task<string>>();

                // 并行查询所有DNS服务器
                foreach (var server in dohServers)
                {
                    tasks.Add(QueryDnsServer(httpClient, server, cts.Token));
                }

                // 等待任意一个任务完成
                var completedTask = await Task.WhenAny(tasks);
                // 取消其他正在进行的任务
                cts.Cancel();
                // 返回第一个完成的任务结果
                return await completedTask;
            }
        }

        /// <summary>
        /// 查询指定DNS over HTTPS服务器的TXT记录
        /// </summary>
        /// <param name="client">HTTP客户端</param>
        /// <param name="server">DNS服务器URL</param>
        /// <param name="token">取消令牌</param>
        /// <returns>查询到的TXT记录内容,失败则返回空字符串</returns>
        private static async Task<string> QueryDnsServer(HttpClient client, string server, CancellationToken token)
        {
            try
            {
                // 构建DNS查询URL
                var url = $"{server}?name={DnsQueryDomain}&type=TXT";
                // 发送HTTP请求获取响应
                var response = await client.GetStringAsync(url);
                // 解析JSON响应
                var jsonResponse = JObject.Parse(response);

                // 遍历Answer部分查找TXT记录
                foreach (var record in jsonResponse["Answer"])
                {
                    string txtRecord = record["data"]?.ToString();
                    // 如果找到有效的TXT记录,则返回
                    if (!string.IsNullOrEmpty(txtRecord))
                    {
                        return txtRecord;
                    }
                }
                return string.Empty;
            }
            catch
            {
                // 查询失败时返回空字符串
                return string.Empty;
            }
        }
    }
}