using Ink_Canvas.Helpers;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Net.Http;
using System.Runtime.CompilerServices;
using System.Security.Cryptography;
using System.Threading;
using System.Threading.Tasks;

namespace Ink_Canvas.Plugins
{
    /// <summary>
    /// 插件市场服务，负责索引获取、插件下载安装、本地/远程插件合并、镜像管理。
    /// </summary>
    public class PluginMarketService : INotifyPropertyChanged
    {
        private static readonly Lazy<PluginMarketService> _lazy = new Lazy<PluginMarketService>(() => new PluginMarketService());
        public static PluginMarketService Instance => _lazy.Value;

        // 备用官方索引地址
        private const string OfficialIndexUrl = "https://github.com/InkCanvasForClass/PluginIndex/releases/download/latest/index.json";

        private static readonly string MarketCachePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "PluginMarketCache");
        private static readonly string IndexCachePath = Path.Combine(MarketCachePath, "index.json");
        private static readonly string IndexMetaPath = Path.Combine(MarketCachePath, "meta.json");

        // 缓存有效期：超过 3 天视为过期，需重新获取
        private static readonly TimeSpan IndexCacheTtl = TimeSpan.FromDays(3);

        private readonly HttpClient _httpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(15) };
        private PluginMarketIndex _marketIndex;
        private readonly Dictionary<string, string> _resolvedIcons = new Dictionary<string, string>();
        private PluginMarketSourcesService _sources;

        #region 绑定属性

        private bool _isLoading;
        public bool IsLoading
        {
            get => _isLoading;
            set { if (_isLoading == value) return; _isLoading = value; OnPropertyChanged(); }
        }

        private double _loadProgress;
        public double LoadProgress
        {
            get => _loadProgress;
            set { if (Math.Abs(_loadProgress - value) < 0.01) return; _loadProgress = value; OnPropertyChanged(); }
        }

        private string _loadError;
        public string LoadError
        {
            get => _loadError;
            set { if (_loadError == value) return; _loadError = value; OnPropertyChanged(); }
        }

        private List<MergedPluginInfo> _mergedPlugins = new List<MergedPluginInfo>();
        public List<MergedPluginInfo> MergedPlugins
        {
            get => _mergedPlugins;
            set { _mergedPlugins = value; OnPropertyChanged(); }
        }

        private PluginMarketSourceInfo _activeSource;
        /// <summary>
        /// 当前激活的源（用于 UI 展示）。
        /// </summary>
        public PluginMarketSourceInfo ActiveSource
        {
            get => _activeSource ?? PluginMarketSourcesService.OfficialSource;
            private set { _activeSource = value; OnPropertyChanged(); }
        }

        /// <summary>
        /// 源管理服务。注意初始化时若 basePath 不同则会在第一次 RefreshIndexAsync 之前注入。
        /// </summary>
        public PluginMarketSourcesService Sources
        {
            get
            {
                if (_sources == null)
                {
                    _sources = new PluginMarketSourcesService(AppDomain.CurrentDomain.BaseDirectory);
                    ActiveSource = _sources.GetActiveSource();
                }
                return _sources;
            }
        }

        private readonly Dictionary<string, DownloadTaskInfo> _downloadTasks = new Dictionary<string, DownloadTaskInfo>();

        #endregion

        private PluginMarketService()
        {
            _httpClient.DefaultRequestHeaders.Add("User-Agent", "InkCanvasForClass/1.0");
        }

        #region 索引加载

        /// <summary>
        /// 刷新插件市场索引。优先从网络获取，失败时使用缓存。
        /// </summary>
        public async Task RefreshIndexAsync()
        {
            if (IsLoading) return;
            IsLoading = true;
            LoadError = null;
            LoadProgress = 0;

            try
            {
                // 确保缓存目录存在
                if (!Directory.Exists(MarketCachePath))
                    Directory.CreateDirectory(MarketCachePath);

                PluginMarketIndex index = null;
                var active = Sources.GetActiveSource();
                ActiveSource = active;
                var indexUrl = string.IsNullOrEmpty(active.Url) ? OfficialIndexUrl : active.Url;

                // 尝试从网络获取
                try
                {
                    LoadProgress = 10;
                    var response = await _httpClient.GetAsync(indexUrl);
                    response.EnsureSuccessStatusCode();

                    var data = await response.Content.ReadAsByteArrayAsync();
                    LoadProgress = 50;

                    // 判断是 ZIP 还是 JSON
                    if (IsZipFile(data))
                    {
                        index = ExtractIndexFromZip(data);
                    }
                    else
                    {
                        var json = System.Text.Encoding.UTF8.GetString(data);
                        index = JsonConvert.DeserializeObject<PluginMarketIndex>(json);
                    }

                    LoadProgress = 80;

                    // 写入缓存
                    if (index != null)
                    {
                        File.WriteAllText(IndexCachePath, JsonConvert.SerializeObject(index, Formatting.Indented));
                        File.WriteAllText(IndexMetaPath, JsonConvert.SerializeObject(new
                        {
                            lastRefresh = DateTime.Now.ToString("o"),
                            source = active.Id,
                            sourceUrl = indexUrl
                        }));
                    }
                }
                catch (Exception ex)
                {
                    LogHelper.WriteLogToFile($"PluginMarket | 网络获取索引失败: {ex.Message}", LogHelper.LogType.Warning);

                    // 尝试从缓存加载（仅在未过期时）
                    if (File.Exists(IndexCachePath))
                    {
                        var lastRefresh = GetLastRefreshTime();
                        if (lastRefresh.HasValue && (DateTime.Now - lastRefresh.Value) > IndexCacheTtl)
                        {
                            LogHelper.WriteLogToFile(
                                $"PluginMarket | 缓存索引已过期（{lastRefresh.Value:yyyy-MM-dd HH:mm}，TTL={IndexCacheTtl.TotalDays:F0}天），忽略",
                                LogHelper.LogType.Warning);
                        }
                        else
                        {
                            try
                            {
                                var cached = File.ReadAllText(IndexCachePath);
                                index = JsonConvert.DeserializeObject<PluginMarketIndex>(cached);
                                LogHelper.WriteLogToFile("PluginMarket | 使用缓存索引");
                            }
                            catch
                            {
                                // 缓存也损坏了
                            }
                        }
                    }
                }

                if (index == null)
                {
                    LoadError = "无法加载插件市场索引，请检查网络连接。";
                    _marketIndex = new PluginMarketIndex();
                }
                else
                {
                    _marketIndex = index;
                }

                LoadProgress = 90;
                MergePlugins();
                LoadProgress = 100;
            }
            catch (Exception ex)
            {
                LoadError = ex.Message;
                LogHelper.WriteLogToFile($"PluginMarket | 刷新索引异常: {ex}", LogHelper.LogType.Error);
            }
            finally
            {
                IsLoading = false;
            }
        }

        /// <summary>
        /// 仅从缓存加载索引（不联网）。
        /// </summary>
        public void LoadFromCache()
        {
            try
            {
                CleanupStalePartialFiles();
                if (!File.Exists(IndexCachePath))
                {
                    _marketIndex = new PluginMarketIndex();
                    MergePlugins();
                    return;
                }

                // 检查缓存是否过期：超过 TTL 则视为无效
                var lastRefresh = GetLastRefreshTime();
                if (!lastRefresh.HasValue || (DateTime.Now - lastRefresh.Value) > IndexCacheTtl)
                {
                    LogHelper.WriteLogToFile(
                        lastRefresh.HasValue
                            ? $"PluginMarket | 缓存索引已过期（{lastRefresh.Value:yyyy-MM-dd HH:mm}，TTL={IndexCacheTtl.TotalDays:F0}天），跳过"
                            : "PluginMarket | 缓存索引缺少刷新时间，跳过",
                        LogHelper.LogType.Warning);
                    _marketIndex = new PluginMarketIndex();
                    MergePlugins();
                    return;
                }

                var cached = File.ReadAllText(IndexCachePath);
                _marketIndex = JsonConvert.DeserializeObject<PluginMarketIndex>(cached) ?? new PluginMarketIndex();
                MergePlugins();
            }
            catch (Exception ex)
            {
                LogHelper.WriteLogToFile($"PluginMarket | 加载缓存索引失败: {ex.Message}", LogHelper.LogType.Warning);
                _marketIndex = new PluginMarketIndex();
                MergePlugins();
            }
        }

        /// <summary>
        /// 清理早于 <see cref="PartialFileMaxAge"/> 天的失败下载残留（.partial_/rolledaside_/tmp），
        /// 避免 PluginPackages 目录被早期失败的下载堆满。正常 .icpx 包不受影响。
        /// </summary>
        private static void CleanupStalePartialFiles()
        {
            try
            {
                var packagesDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "PluginPackages");
                if (!Directory.Exists(packagesDir)) return;
                var cutoff = DateTime.Now.AddDays(-PartialFileMaxAgeDays);
                foreach (var path in Directory.GetFiles(packagesDir))
                {
                    var name = Path.GetFileName(path);
                    if (!name.Contains(".partial_") && !name.Contains(".rolledaside_") && !name.EndsWith(".tmp")
                        && !name.Contains(".failed_install_"))
                        continue;
                    try
                    {
                        var ts = File.GetLastWriteTime(path);
                        if (ts < cutoff) File.Delete(path);
                    }
                    catch
                    {
                        // 单文件清理失败不影响其他文件
                    }
                }
            }
            catch (Exception ex)
            {
                LogHelper.WriteLogToFile($"PluginMarket | 清理过期残留包失败: {ex.Message}", LogHelper.LogType.Warning);
            }
        }

        private const int PartialFileMaxAgeDays = 30;

        #endregion

        #region 合并插件

        /// <summary>
        /// 合并本地已加载插件与市场插件列表。
        /// </summary>
        private void MergePlugins()
        {
            var merged = new Dictionary<string, MergedPluginInfo>();
            // RestartRequired 仅表示“还有待安装包没被消费”，不再记忆上一轮 UI 状态。
            // 旧逻辑会把 RestartRequired 从旧 MergedPlugins 拷回新列表，导致热安装成功后仍提示重启，
            // 甚至重启后若包仍残留也会继续提示。
            var restartRequiredPluginIds = PluginManager.Instance.GetPendingPackagePluginIds();
            // 决定镜像：根据当前源自定义选择；若为空则用第一个可用镜像；都没有则用空串。
            string selectedMirror = "";
            string activeSourceId = Sources.GetActiveSource()?.Id ?? PluginMarketSourcesService.OfficialSource.Id;

            if (_marketIndex.DownloadMirrors != null && _marketIndex.DownloadMirrors.Count > 0)
            {
                var srcMirror = Sources.GetActiveSource()?.SelectedMirror;
                if (!string.IsNullOrEmpty(srcMirror) && _marketIndex.DownloadMirrors.ContainsKey(srcMirror))
                {
                    selectedMirror = _marketIndex.DownloadMirrors[srcMirror];
                }
                else
                {
                    selectedMirror = _marketIndex.DownloadMirrors.Values.First();
                }
            }

            // 1. 加入本地已安装的插件
            foreach (var local in PluginManager.Instance.Plugins)
            {
                var info = new MergedPluginInfo
                {
                    Id = local.Id,
                    Name = local.Name,
                    Version = local.Version,
                    Description = local.Description,
                    Author = local.Author,
                    IsLocal = true,
                    LoadStatus = local.LoadStatus,
                    PluginFolderPath = local.PluginFolderPath,
                    PluginConfigFolder = local.PluginConfigFolder,
                    LocalInfo = local,
                    RestartRequired = restartRequiredPluginIds.Contains(local.Id)
                };
                merged[local.Id] = info;
            }

            // 2. 合并市场插件
            if (_marketIndex.Plugins != null)
            {
                foreach (var entry in _marketIndex.Plugins)
                {
                    var id = entry.Manifest?.Id;
                    if (string.IsNullOrEmpty(id)) continue;

                    // 解析模板 URL
                    var resolvedDownloadUrl = ResolveUrl(entry.DownloadUrl, selectedMirror);
                    var resolvedIconUrl = ResolveUrl(entry.IconUrl, selectedMirror);

                    if (merged.TryGetValue(id, out var existing))
                    {
                        // 已安装，检查是否有更新
                        existing.IsOnMarket = true;
                        existing.MarketEntry = entry;
                        existing.DownloadUrl = resolvedDownloadUrl;
                        existing.IconUrl = resolvedIconUrl;
                        existing.ReadmeUrl = ResolveUrl(entry.ReadmeUrl, selectedMirror);

                        if (Version.TryParse(NormalizeVersion(existing.Version), out var localVer) &&
                            Version.TryParse(NormalizeVersion(entry.Manifest?.Version), out var remoteVer) &&
                            remoteVer > localVer)
                        {
                            existing.IsUpdateAvailable = true;
                            existing.MarketVersion = entry.Manifest?.Version;
                        }
                    }
                    else
                    {
                        // 仅市场有
                        var info = new MergedPluginInfo
                        {
                            Id = id,
                            Name = entry.Manifest?.Name ?? "",
                            Version = entry.Manifest?.Version ?? "",
                            Description = entry.Manifest?.Description ?? "",
                            Author = entry.Manifest?.Author ?? "",
                            IsLocal = false,
                            IsOnMarket = true,
                            MarketEntry = entry,
                            DownloadUrl = resolvedDownloadUrl,
                            IconUrl = resolvedIconUrl,
                            ReadmeUrl = ResolveUrl(entry.ReadmeUrl, selectedMirror),
                            DownloadCount = entry.DownloadCount,
                            StarsCount = entry.StarsCount,
                            RestartRequired = restartRequiredPluginIds.Contains(id)
                        };
                        merged[id] = info;
                    }
                }
            }

            MergedPlugins = merged.Values.OrderBy(p => p.IsLocal ? 0 : 1).ThenBy(p => p.Name).ToList();
        }

        public void RefreshMergedPlugins()
        {
            _marketIndex ??= new PluginMarketIndex();
            MergePlugins();
        }

        #endregion

        #region 下载安装

        /// <summary>
        /// 获取指定插件的市场条目。
        /// </summary>
        public PluginMarketEntry ResolveMarketPlugin(string id)
        {
            return _marketIndex?.Plugins?.FirstOrDefault(p => p.Manifest?.Id == id);
        }

        /// <summary>
        /// 获取当前下载任务字典。
        /// </summary>
        public IReadOnlyDictionary<string, DownloadTaskInfo> DownloadTasks => _downloadTasks;

        /// <summary>
        /// 当前索引中声明的可选镜像字典（key → root URL）。
        /// </summary>
        public IReadOnlyDictionary<string, string> AvailableMirrors
        {
            get
            {
                if (_marketIndex?.DownloadMirrors == null) return new Dictionary<string, string>();
                return _marketIndex.DownloadMirrors;
            }
        }

        /// <summary>
        /// 切换当前激活的源并刷新索引。
        /// </summary>
        public async Task SwitchSourceAsync(string sourceId)
        {
            if (string.IsNullOrEmpty(sourceId)) return;
            Sources.ActiveSourceId = sourceId;
            ActiveSource = Sources.GetActiveSource();
            await RefreshIndexAsync();
        }

        /// <summary>
        /// 在当前激活源上选择镜像。传入空串表示使用默认。
        /// </summary>
        public async Task SelectMirrorAsync(string mirrorKey)
        {
            var src = Sources.GetActiveSource();
            if (src == null) return;
            Sources.SelectMirror(src.Id, mirrorKey ?? "");
            MergePlugins();
            await Task.CompletedTask;
        }

        /// <summary>
        /// 请求下载安装/更新指定插件。
        /// </summary>
        public async Task<bool> RequestDownloadPluginAsync(string id)
        {
            var merged = MergedPlugins.FirstOrDefault(p => p.Id == id);
            if (merged == null || string.IsNullOrEmpty(merged.DownloadUrl))
            {
                LogHelper.WriteLogToFile($"PluginMarket | 找不到插件下载信息: {id}", LogHelper.LogType.Warning);
                return false;
            }

            if (_downloadTasks.ContainsKey(id))
            {
                LogHelper.WriteLogToFile($"PluginMarket | 插件正在下载中: {id}", LogHelper.LogType.Warning);
                return false;
            }

            var task = new DownloadTaskInfo { IsDownloading = true };
            _downloadTasks[id] = task;
            merged.DownloadTask = task;
            OnPropertyChanged(nameof(DownloadTasks));

            try
            {
                // 下载到临时文件
                var tempFile = Path.GetTempFileName() + ".icpx.tmp";

                using (var response = await _httpClient.GetAsync(merged.DownloadUrl, HttpCompletionOption.ResponseHeadersRead))
                {
                    response.EnsureSuccessStatusCode();
                    var totalBytes = response.Content.Headers.ContentLength ?? -1;
                    var totalRead = 0L;
                    var buffer = new byte[8192];

                    using (var contentStream = await response.Content.ReadAsStreamAsync())
                    using (var fileStream = new FileStream(tempFile, FileMode.Create, FileAccess.Write, FileShare.None))
                    {
                        int bytesRead;
                        while ((bytesRead = await contentStream.ReadAsync(buffer, 0, buffer.Length)) > 0)
                        {
                            if (task.CancellationToken.IsCancellationRequested)
                            {
                                task.IsCancelled = true;
                                break;
                            }

                            await fileStream.WriteAsync(buffer, 0, bytesRead);
                            totalRead += bytesRead;

                            if (totalBytes > 0)
                            {
                                task.Progress = (double)totalRead / totalBytes * 100;
                            }
                        }
                    }
                }

                if (task.IsCancelled)
                {
                    try { File.Delete(tempFile); } catch { }
                    return false;
                }

                // 校验 SHA256（如果有）
                if (!string.IsNullOrEmpty(merged.MarketEntry?.DownloadSha256))
                {
                    var hash = ComputeSha256(tempFile);
                    if (!string.Equals(hash, merged.MarketEntry.DownloadSha256, StringComparison.OrdinalIgnoreCase))
                    {
                        task.Error = "文件校验失败，可能已损坏。";
                        try { File.Delete(tempFile); } catch { }
                        return false;
                    }
                }

                // 移动到 PluginPackages 目录，随后热安装（新装/更新均尝试立即生效）
                var packagesDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "PluginPackages");
                if (!Directory.Exists(packagesDir))
                    Directory.CreateDirectory(packagesDir);

                var targetPath = Path.Combine(packagesDir, id + ".icpx");
                try
                {
                    ProcessProtectionManager.WithWriteAccess(targetPath, () =>
                    {
                        // 先把可能存在的旧包改名备份（不直接删除），
                        // 防止 File.Delete 后 File.Move 失败导致旧新全没。
                        if (File.Exists(targetPath))
                        {
                            var rolledAside = targetPath + ".rolledaside_" + DateTime.Now.ToString("yyyyMMdd_HHmmss");
                            File.Move(targetPath, rolledAside);
                        }
                        File.Move(tempFile, targetPath);
                    });
                }
                catch
                {
                    // 移动失败：保留 tempFile 改名以便下次重试恢复，**不**删除新下载的包。
                    try
                    {
                        if (File.Exists(tempFile))
                        {
                            var retained = Path.Combine(packagesDir, id + ".icpx.partial_" + DateTime.Now.ToString("yyyyMMdd_HHmmss"));
                            File.Move(tempFile, retained);
                        }
                    }
                    catch
                    {
                        // 两次移动都失败（极小概率）—— 至少保留 tempFile 留给下次启动扫描。
                    }
                    throw;
                }

                task.IsCompleted = true;
                task.Progress = 100;

                // 新装与更新统一走热安装：已加载插件会先卸载再覆盖。
                // 仅当包仍残留在 PluginPackages/ 时才标记 RestartRequired。
                try
                {
                    PluginManager.Instance.InstallPendingPackages();
                }
                catch (Exception installEx)
                {
                    LogHelper.WriteLogToFile(
                        $"PluginMarket | 热安装失败 {id}: {installEx.Message}",
                        LogHelper.LogType.Warning);
                }

                // InstallPendingPackages 内部会 RefreshMergedPlugins；这里取最新对象更新状态。
                var refreshed = MergedPlugins?.FirstOrDefault(p => p.Id == id) ?? merged;
                refreshed.RestartRequired = PluginManager.Instance.GetPendingPackagePluginIds().Contains(id);
                if (!ReferenceEquals(refreshed, merged))
                    merged.RestartRequired = refreshed.RestartRequired;

                LogHelper.WriteLogToFile(
                    refreshed.RestartRequired
                        ? $"PluginMarket | 插件下载完成但热安装未消费: {id}（仍需重启）"
                        : $"PluginMarket | 插件下载并热安装完成: {id}");

                return true;
            }
            catch (Exception ex)
            {
                task.Error = ex.Message;
                LogHelper.WriteLogToFile($"PluginMarket | 下载插件失败 {id}: {ex.Message}", LogHelper.LogType.Error);
                return false;
            }
            finally
            {
                task.IsDownloading = false;
                _downloadTasks.Remove(id);
                merged.DownloadTask = null;
                OnPropertyChanged(nameof(DownloadTasks));
            }
        }

        /// <summary>
        /// 取消指定插件的下载。
        /// </summary>
        public void CancelDownload(string id)
        {
            if (_downloadTasks.TryGetValue(id, out var task))
            {
                task.CancellationTokenSource.Cancel();
            }
        }

        /// <summary>
        /// 解析插件依赖，返回需要一起安装的插件 ID 列表。
        /// </summary>
        public List<string> ResolveDependencies(string id)
        {
            var result = new List<string>();
            var visited = new HashSet<string>();
            ResolveDependenciesRecursive(id, result, visited);
            return result;
        }

        private void ResolveDependenciesRecursive(string id, List<string> result, HashSet<string> visited)
        {
            if (visited.Contains(id)) return;
            visited.Add(id);

            var entry = _marketIndex?.Plugins?.FirstOrDefault(p => p.Manifest?.Id == id);
            if (entry?.Manifest?.Dependencies == null) return;

            foreach (var dep in entry.Manifest.Dependencies)
            {
                if (!dep.IsRequired) continue;
                // 跳过已安装的
                if (PluginManager.Instance.Plugins.Any(p => p.Id == dep.Id)) continue;

                result.Add(dep.Id);
                ResolveDependenciesRecursive(dep.Id, result, visited);
            }
        }

        #endregion

        #region 辅助方法

        private static bool IsZipFile(byte[] data)
        {
            return data.Length >= 4 && data[0] == 0x50 && data[1] == 0x4B && data[2] == 0x03 && data[3] == 0x04;
        }

        private static PluginMarketIndex ExtractIndexFromZip(byte[] data)
        {
            using (var ms = new MemoryStream(data))
            using (var archive = new ZipArchive(ms, ZipArchiveMode.Read))
            {
                var entry = archive.GetEntry("index.json");
                if (entry == null) return null;

                using (var reader = new StreamReader(entry.Open()))
                {
                    var json = reader.ReadToEnd();
                    return JsonConvert.DeserializeObject<PluginMarketIndex>(json);
                }
            }
        }

        private static string ResolveUrl(string url, string mirrorRoot)
        {
            if (string.IsNullOrEmpty(url)) return "";
            // 防止恶意 mirrorRoot 通过在 URL 中注入 @ 或 / 改变路径解析：
            //   https://github.com/{root}/file  + mirrorRoot="@evil.com/x"
            //   → https://github.com/@evil.com/x/file（@ 前是 userinfo，真实 host 变成 evil.com）
            // 白名单字符限定为 host/path 部分：字母、数字、点、冒号、斜杠、横线、下划线。
            // 不允许 @ ? # 等 URL 元字符；URI 必须保持模板的可信结构。
            if (!string.IsNullOrEmpty(mirrorRoot))
            {
                foreach (var ch in mirrorRoot)
                {
                    if (!(char.IsLetterOrDigit(ch) || ch == '.' || ch == ':' || ch == '/' || ch == '-' || ch == '_'))
                        return url; // 含非法字符，保留原模板（不替换）
                }
            }
            return url.Replace("{root}", mirrorRoot ?? "");
        }

        private static string NormalizeVersion(string version)
        {
            if (string.IsNullOrWhiteSpace(version)) return "0.0.0";
            version = version.Trim().TrimStart('v', 'V');
            // 确保至少有 Major.Minor.Build
            var parts = version.Split('.');
            if (parts.Length == 2) version += ".0";
            if (parts.Length == 1) version += ".0.0";
            return version;
        }

        private static string ComputeSha256(string filePath)
        {
            using (var sha = SHA256.Create())
            using (var stream = File.OpenRead(filePath))
            {
                var hash = sha.ComputeHash(stream);
                return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant();
            }
        }

        /// <summary>
        /// 获取缓存的索引最后刷新时间。
        /// </summary>
        public DateTime? GetLastRefreshTime()
        {
            try
            {
                if (!File.Exists(IndexMetaPath)) return null;
                var json = JsonConvert.DeserializeAnonymousType(File.ReadAllText(IndexMetaPath), new { lastRefresh = "" });
                if (DateTime.TryParse(json?.lastRefresh, out var dt)) return dt;
            }
            catch { }
            return null;
        }

        /// <summary>
        /// 获取市场插件的图标本地缓存路径（异步下载）。
        /// </summary>
        public async Task<string> GetCachedIconPathAsync(string iconUrl, string pluginId)
        {
            if (string.IsNullOrEmpty(iconUrl)) return null;

            var cacheDir = Path.Combine(MarketCachePath, "icons");
            if (!Directory.Exists(cacheDir))
                Directory.CreateDirectory(cacheDir);

            var ext = Path.GetExtension(new Uri(iconUrl).AbsolutePath);
            if (string.IsNullOrEmpty(ext)) ext = ".png";
            var localPath = Path.Combine(cacheDir, pluginId + ext);

            if (File.Exists(localPath))
                return localPath;

            try
            {
                var data = await _httpClient.GetByteArrayAsync(iconUrl);
                File.WriteAllBytes(localPath, data);
                return localPath;
            }
            catch
            {
                return null;
            }
        }

        #endregion

        #region INotifyPropertyChanged

        public event PropertyChangedEventHandler PropertyChanged;
        protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }

        #endregion
    }

    /// <summary>
    /// 合并后的插件信息（本地 + 市场）。
    /// </summary>
    public class MergedPluginInfo : INotifyPropertyChanged
    {
        public string Id { get; set; }
        public string Name { get; set; }
        public string Version { get; set; }
        public string Description { get; set; }
        public string Author { get; set; }

        // 本地信息
        public bool IsLocal { get; set; }
        public PluginLoadStatus LoadStatus { get; set; }
        public string PluginFolderPath { get; set; }
        public string PluginConfigFolder { get; set; }
        public PluginInfo LocalInfo { get; set; }

        // 市场信息
        public bool IsOnMarket { get; set; }
        public PluginMarketEntry MarketEntry { get; set; }
        public string DownloadUrl { get; set; }
        public string IconUrl { get; set; }
        public string ReadmeUrl { get; set; }
        public long DownloadCount { get; set; }
        public long StarsCount { get; set; }

        // 状态
        public bool IsUpdateAvailable { get; set; }
        public string MarketVersion { get; set; }

        private bool _restartRequired;
        public bool RestartRequired
        {
            get => _restartRequired;
            set { if (_restartRequired == value) return; _restartRequired = value; OnPropertyChanged(); }
        }

        public DownloadTaskInfo DownloadTask { get; set; }

        // UI 辅助属性
        public string VersionText => $"v{Version}";
        public string DownloadCountText => DownloadCount > 0 ? FormatCount(DownloadCount) : null;
        public bool IsDownloading => DownloadTask?.IsDownloading == true;
        public bool ShowInstallButton => IsOnMarket && !IsLocal && !RestartRequired && !IsDownloading;
        public bool ShowInstalledBadge => IsLocal && !IsOnMarket;
        public string IconDisplayPath { get; set; }

        private static string FormatCount(long count)
        {
            if (count >= 10000) return $"{count / 10000.0:F1}w";
            if (count >= 1000) return $"{count / 1000.0:F1}k";
            return count.ToString();
        }

        public event PropertyChangedEventHandler PropertyChanged;
        protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }

    /// <summary>
    /// 下载任务状态。
    /// </summary>
    public class DownloadTaskInfo : INotifyPropertyChanged
    {
        private double _progress;
        public double Progress
        {
            get => _progress;
            set { if (Math.Abs(_progress - value) < 0.01) return; _progress = value; OnPropertyChanged(); }
        }

        private bool _isDownloading;
        public bool IsDownloading
        {
            get => _isDownloading;
            set { if (_isDownloading == value) return; _isDownloading = value; OnPropertyChanged(); }
        }

        public bool IsCompleted { get; set; }
        public bool IsCancelled { get; set; }
        public string Error { get; set; }

        public CancellationTokenSource CancellationTokenSource { get; } = new CancellationTokenSource();
        public CancellationToken CancellationToken => CancellationTokenSource.Token;

        public event PropertyChangedEventHandler PropertyChanged;
        protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}
