using System.Text.Json;
using MiGu.Server.Infra;
using Microsoft.Extensions.Options;

namespace MiGu.Server.Ota;

public sealed class OtaStore
{
    private readonly object _gate = new();
    private readonly ILogger<OtaStore> _log;
    private readonly JsonSerializerOptions _json = new()
    {
        PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
        WriteIndented = true
    };

    public string Root { get; }
    public string PackagesDir { get; }
    public string JobsDir { get; }
    public string HistoryDir { get; }
    public string TargetFile { get; }
    public string SettingsFile { get; }

    private string? _lastPullId;
    private readonly System.Collections.Concurrent.ConcurrentDictionary<string, string> _pullByIp =
        new(StringComparer.OrdinalIgnoreCase);
    private long _jobSeq;

    public OtaStore(IWebHostEnvironment env, IOptions<OtaOptions> options, ILogger<OtaStore> log)
    {
        _log = log;
        var cfg = options.Value.DataRoot;
        Root = Path.IsPathRooted(cfg) ? cfg : Path.Combine(env.ContentRootPath, cfg);
        PackagesDir = Path.Combine(Root, "packages");
        JobsDir = Path.Combine(Root, "jobs");
        HistoryDir = Path.Combine(Root, "history");
        TargetFile = Path.Combine(Root, "target.json");
        SettingsFile = Path.Combine(Root, "settings.json");
        Directory.CreateDirectory(PackagesDir);
        Directory.CreateDirectory(JobsDir);
        Directory.CreateDirectory(HistoryDir);
    }

    public OtaSettings GetSettings()
    {
        lock (_gate)
        {
            if (!File.Exists(SettingsFile)) return new OtaSettings();
            try
            {
                return JsonSerializer.Deserialize<OtaSettings>(File.ReadAllText(SettingsFile), _json) ?? new OtaSettings();
            }
            catch (Exception ex)
            {
                _log.LogWarning(ex, "OTA settings load failed");
                return new OtaSettings();
            }
        }
    }

    public OtaSettings SaveSettings(OtaSettings settings)
    {
        lock (_gate)
        {
            AtomicFile.WriteAllText(SettingsFile, JsonSerializer.Serialize(settings, _json));
            return settings;
        }
    }

    public OtaTarget? GetTarget()
    {
        lock (_gate)
        {
            if (!File.Exists(TargetFile)) return null;
            try
            {
                return JsonSerializer.Deserialize<OtaTarget>(File.ReadAllText(TargetFile), _json);
            }
            catch (Exception ex)
            {
                _log.LogWarning(ex, "OTA target load failed");
                return null;
            }
        }
    }

    public void SetTarget(OtaTarget target)
    {
        lock (_gate)
        {
            AtomicFile.WriteAllText(TargetFile, JsonSerializer.Serialize(target, _json));
            var settings = GetSettingsUnlocked();
            if (!string.IsNullOrWhiteSpace(target.Name))
            {
                settings.NewVersionName = target.Name;
                AtomicFile.WriteAllText(SettingsFile, JsonSerializer.Serialize(settings, _json));
            }
        }
    }

    private OtaSettings GetSettingsUnlocked()
    {
        if (!File.Exists(SettingsFile)) return new OtaSettings();
        try
        {
            return JsonSerializer.Deserialize<OtaSettings>(File.ReadAllText(SettingsFile), _json) ?? new OtaSettings();
        }
        catch
        {
            return new OtaSettings();
        }
    }

    public string BeginPullPackage(string? sourceIp)
    {
        lock (_gate)
        {
            var baseId = $"{DateTime.UtcNow:yyyyMMddHHmmssfff}({SafeIdPart(sourceIp ?? "upload")})";
            var id = baseId;
            var dir = Path.Combine(PackagesDir, id);
            for (var i = 1; Directory.Exists(dir); i++)
            {
                id = $"{baseId}-{i:D2}";
                dir = Path.Combine(PackagesDir, id);
            }
            Directory.CreateDirectory(dir);
            foreach (var app in new[] { "M", "D", "C" })
                Directory.CreateDirectory(Path.Combine(dir, app));
            Directory.CreateDirectory(Path.Combine(dir, "M", "plugins"));
            var ip = NormalizeIp(sourceIp);
            if (ip != null) _pullByIp[ip] = id;
            _lastPullId = id;
            return id;
        }
    }

    public string? ActivePullId
    {
        get { lock (_gate) return _lastPullId; }
    }

    public bool TryGetActivePullId(string? sourceIp, out string? id)
    {
        lock (_gate)
        {
            var ip = NormalizeIp(sourceIp);
            if (ip != null && _pullByIp.TryGetValue(ip, out var byIp))
            {
                id = byIp;
                return true;
            }

            if (ip == null && _lastPullId != null)
            {
                id = _lastPullId;
                return true;
            }

            id = null;
            return false;
        }
    }

    public void ClearActivePull(string? sourceIp = null)
    {
        lock (_gate)
        {
            var ip = NormalizeIp(sourceIp);
            if (ip != null) _pullByIp.TryRemove(ip, out _);
            if (_pullByIp.IsEmpty) _lastPullId = null;
        }
    }

    /// <summary>把 WatchDog 回传连接的远端 IP 归一（去掉 IPv6 映射前缀，如 ::ffff:192.168.1.13）。</summary>
    private static string? NormalizeIp(string? ip)
    {
        if (string.IsNullOrWhiteSpace(ip)) return null;
        if (System.Net.IPAddress.TryParse(ip, out var addr))
            return (addr.IsIPv4MappedToIPv6 ? addr.MapToIPv4() : addr).ToString();
        return ip.Trim();
    }

    public string ResolveReceivePath(string routeKey, string? clientIp = null)
    {
        // routeKey examples: Medullaexe, Medulladll, Detourexe, ClumsyConsoleexe, ClumsyConsoledll
        // Do not fall back to the latest session for a known-but-unregistered client.
        if (!TryGetActivePullId(clientIp, out var id) || id == null)
            throw new InvalidOperationException("无进行中的拉取会话");
        var dir = Path.Combine(PackagesDir, id);
        return routeKey.ToLowerInvariant() switch
        {
            "medullaexe" => Path.Combine(dir, "M", "Medulla.exe"),
            "medulladll" => Path.Combine(dir, "M", "plugins", "CartActivator.dll"),
            "medullapdb" => Path.Combine(dir, "M", "plugins", "CartActivator.pdb"),
            "detourexe" => Path.Combine(dir, "D", "Detour.exe"),
            "clumsyconsoleexe" or "clumsyexe" => Path.Combine(dir, "C", "ClumsyConsole.exe"),
            "clumsyconsoledll" or "clumsydll" => Path.Combine(dir, "C", "FG2305014_C.dll"),
            "clumsyconsolepdb" or "clumsypdb" => Path.Combine(dir, "C", "FG2305014_C.pdb"),
            _ => throw new ArgumentException($"未知接收路由: {routeKey}")
        };
    }

    public OtaPackageInfo ScanPackage(string id)
    {
        var dir = ResolveUnder(PackagesDir, id);
        if (!Directory.Exists(dir)) throw new DirectoryNotFoundException(id);
        var info = new OtaPackageInfo
        {
            Id = id,
            CreatedAt = Directory.GetCreationTimeUtc(dir),
            SourceIp = ExtractSourceIp(id)
        };
        long total = 0;
        foreach (var key in OtaPathMap.ComponentKeys)
        {
            var rel = OtaPathMap.RelPathFor(key);
            if (rel == null) continue;
            var path = Path.Combine(dir, rel.Replace('/', Path.DirectorySeparatorChar));
            if (!File.Exists(path)) continue;
            var fi = new FileInfo(path);
            total += fi.Length;
            info.Components[key] = new OtaFileArtifact
            {
                Hash = OtaHash.OfFile(path),
                Path = path,
                FileName = Path.GetFileName(path),
                Time = fi.LastWriteTime.ToString("yyyy-MM-dd HH:mm:ss"),
                Size = fi.Length
            };
        }
        info.TotalBytes = total;
        var target = GetTarget();
        info.IsTarget = target != null && string.Equals(target.PackageId, id, StringComparison.Ordinal);
        return info;
    }

    public List<OtaPackageInfo> ListPackages()
    {
        if (!Directory.Exists(PackagesDir)) return new();
        var list = new List<OtaPackageInfo>();
        foreach (var dir in Directory.GetDirectories(PackagesDir).OrderByDescending(d => d))
        {
            try { list.Add(ScanPackage(Path.GetFileName(dir))); }
            catch (Exception ex) { _log.LogDebug(ex, "skip package {Dir}", dir); }
        }
        return list;
    }

    public void DeletePackage(string id)
    {
        lock (_gate)
        {
            var target = GetTarget();
            if (target != null && string.Equals(target.PackageId, id, StringComparison.Ordinal))
                throw new InvalidOperationException("不能删除当前目标版本");
            var dir = ResolveUnder(PackagesDir, id);
            if (Directory.Exists(dir)) Directory.Delete(dir, true);
        }
    }

    public OtaTarget ActivatePackage(string id, string? name)
    {
        var pkg = ScanPackage(id);
        if (pkg.Components.Count == 0)
            throw new InvalidOperationException("包内无有效组件文件");
        var safeId = RequireSafeId(id);
        var target = new OtaTarget
        {
            PackageId = safeId,
            Name = name ?? safeId,
            ActivatedAt = DateTimeOffset.UtcNow,
            Components = pkg.Components
        };
        SetTarget(target);
        return target;
    }

    public string PackageDir(string id) => ResolveUnder(PackagesDir, id);

    public void SaveJob(OtaJob job)
    {
        lock (_gate)
        {
            var path = ResolveUnder(JobsDir, $"{RequireSafeId(job.Id)}.json");
            AtomicFile.WriteAllText(path, JsonSerializer.Serialize(job, _json));
        }
    }

    public OtaJob? GetJob(string id)
    {
        var path = ResolveUnder(JobsDir, $"{RequireSafeId(id)}.json");
        if (!File.Exists(path)) return null;
        try
        {
            return JsonSerializer.Deserialize<OtaJob>(File.ReadAllText(path), _json);
        }
        catch (Exception ex)
        {
            _log.LogWarning(ex, "job load failed {Id}", id);
            return null;
        }
    }

    public List<OtaJob> ListJobs(int take = 100)
    {
        if (!Directory.Exists(JobsDir)) return new();
        return Directory.GetFiles(JobsDir, "*.json")
            .Select(f =>
            {
                try { return JsonSerializer.Deserialize<OtaJob>(File.ReadAllText(f), _json); }
                catch { return null; }
            })
            .Where(j => j != null)
            .Cast<OtaJob>()
            .OrderByDescending(j => j.CreatedAt)
            .Take(take)
            .ToList();
    }

    public string NextJobId()
    {
        // 进程内自增序号保证唯一（同秒也不会撞 ID → 不会两个 job 写同一文件）。
        var seq = System.Threading.Interlocked.Increment(ref _jobSeq);
        return $"J{DateTime.UtcNow:yyyyMMddHHmmss}-{seq:D4}";
    }

    private static string? ExtractSourceIp(string id)
    {
        var open = id.IndexOf('(');
        var close = id.IndexOf(')');
        if (open >= 0 && close > open) return id[(open + 1)..close];
        return null;
    }

    private static string SafeIdPart(string raw)
    {
        var safe = raw.Trim();
        foreach (var ch in Path.GetInvalidFileNameChars())
            safe = safe.Replace(ch, '_');
        return string.IsNullOrWhiteSpace(safe) ? "upload" : safe;
    }

    /// <summary>拒绝路径段（含 .. / 分隔符），只允许单层文件名。</summary>
    private static string RequireSafeId(string id)
    {
        if (string.IsNullOrWhiteSpace(id))
            throw new ArgumentException("无效标识");
        var trimmed = id.Trim();
        if (trimmed is "." or ".."
            || trimmed.Contains('/') || trimmed.Contains('\\')
            || trimmed.IndexOfAny(Path.GetInvalidFileNameChars()) >= 0)
            throw new ArgumentException("无效标识");
        return trimmed;
    }

    private string ResolveUnder(string root, string id)
    {
        var safe = RequireSafeId(id);
        var fullRoot = Path.GetFullPath(root);
        var full = Path.GetFullPath(Path.Combine(fullRoot, safe));
        var prefix = fullRoot.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)
                     + Path.DirectorySeparatorChar;
        if (!full.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)
            && !string.Equals(full, fullRoot, StringComparison.OrdinalIgnoreCase))
            throw new ArgumentException("无效标识");
        return full;
    }
}
