using MiGu.Server.Configs;
using MiGu.Server.Launcher;

namespace MiGu.Server.Logs;

/// <summary>
/// 对齐 Simple3 LogCleaner：定时删除 log/ 下过期 *.log，并检测日志盘剩余空间（边沿告警，写日志不弹窗）。
/// </summary>
public sealed class LogCleanupService : IHostedService, IDisposable
{
    private readonly ConfigStore _store;
    private readonly Simple3Launcher _launcher;
    private readonly IConfiguration _config;
    private readonly ILogger<LogCleanupService> _log;

    private readonly object _cleanLock = new();
    private readonly object _scheduleLock = new();
    private Timer? _cleanTimer;
    private Timer? _diskTimer;
    private bool _diskBelow;

    public LogCleanupService(
        ConfigStore store,
        Simple3Launcher launcher,
        IConfiguration config,
        ILogger<LogCleanupService> log)
    {
        _store = store;
        _launcher = launcher;
        _config = config;
        _log = log;
    }

    public Task StartAsync(CancellationToken cancellationToken)
    {
        try
        {
            var opt = _store.GetLogCleanup();
            if (opt.Enabled && opt.RunOnStartup)
                RunCleanSafely(opt, "启动清理");
            ApplySchedule();
            CheckDisk(opt);
        }
        catch (Exception ex)
        {
            _log.LogWarning(ex, "LogCleanupService 启动失败");
        }
        return Task.CompletedTask;
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        lock (_scheduleLock)
        {
            _cleanTimer?.Dispose();
            _cleanTimer = null;
            _diskTimer?.Dispose();
            _diskTimer = null;
        }
        return Task.CompletedTask;
    }

    public void Dispose()
    {
        _cleanTimer?.Dispose();
        _diskTimer?.Dispose();
    }

    public LogCleanupOptions GetOptions() => _store.GetLogCleanup();

    public LogCleanupOptions SaveOptions(LogCleanupOptions options)
    {
        var saved = _store.PutLogCleanup(options);
        _ = saved;
        var opt = _store.GetLogCleanup();
        ApplySchedule();
        CheckDisk(opt, forceReeval: true);
        return opt;
    }

    public void ApplySchedule()
    {
        var opt = _store.GetLogCleanup();
        lock (_scheduleLock)
        {
            _cleanTimer?.Dispose();
            _cleanTimer = null;
            if (opt.Enabled)
            {
                var hours = opt.CheckIntervalHours <= 0 ? 1 : opt.CheckIntervalHours;
                var period = TimeSpan.FromHours(hours);
                _cleanTimer = new Timer(_ => RunCleanSafely(_store.GetLogCleanup(), "定时清理"),
                    null, period, period);
            }

            _diskTimer?.Dispose();
            var mins = opt.DiskCheckIntervalMinutes <= 0 ? 30 : opt.DiskCheckIntervalMinutes;
            var diskPeriod = TimeSpan.FromMinutes(mins);
            _diskTimer = new Timer(_ => CheckDisk(_store.GetLogCleanup()),
                null, diskPeriod, diskPeriod);
        }
    }

    public CleanResult CleanNow()
    {
        var opt = _store.GetLogCleanup();
        var r = CleanOnce(opt);
        if (r.Error != null)
            _log.LogWarning("立即清理失败：{Error}", r.Error);
        else if (r.DeletedFiles > 0)
            _log.LogInformation("立即清理删除 {Count} 个日志文件，释放 {Mb:F1} MB", r.DeletedFiles, r.FreedMB);
        else
            _log.LogInformation("立即清理完成：没有过期日志");
        return r;
    }

    public DiskStatus GetDiskStatus()
    {
        var opt = _store.GetLogCleanup();
        var known = TryGetFreeGB(out var freeGB, out var drive);
        var below = known && opt.DiskAlertEnabled && freeGB < opt.DiskFreeAlertGB;
        return new DiskStatus(known, drive, freeGB, opt.DiskAlertEnabled, opt.DiskFreeAlertGB, below);
    }

    private void RunCleanSafely(LogCleanupOptions opt, string reason)
    {
        if (!opt.Enabled && reason != "立即清理") return;
        var r = CleanOnce(opt);
        if (r.Error != null)
            _log.LogWarning("[{Reason}] 失败：{Error}", reason, r.Error);
        else if (r.DeletedFiles > 0)
            _log.LogInformation("[{Reason}] 删除 {Count} 个日志文件，释放 {Mb:F1} MB", reason, r.DeletedFiles, r.FreedMB);
    }

    private CleanResult CleanOnce(LogCleanupOptions opt)
    {
        opt.Clamp();
        lock (_cleanLock)
        {
            try
            {
                var (root, _, error) = ResolveLogRoot();
                if (root == null)
                    return new CleanResult(0, 0, error ?? "无法定位日志目录");
                if (!Directory.Exists(root))
                    return new CleanResult(0, 0, null);

                var cutoff = DateTime.Now.AddDays(-opt.RetentionDays);
                var deleted = 0;
                long freed = 0;
                foreach (var f in Directory.EnumerateFiles(root, "*.log", SearchOption.AllDirectories))
                {
                    try
                    {
                        var fi = new FileInfo(f);
                        if (fi.LastWriteTime >= cutoff) continue;
                        var len = fi.Length;
                        fi.Delete();
                        deleted++;
                        freed += len;
                    }
                    catch
                    {
                        /* 占用/权限：跳过 */
                    }
                }
                TryRemoveEmptyDirs(root);
                return new CleanResult(deleted, freed, null);
            }
            catch (Exception ex)
            {
                return new CleanResult(0, 0, ex.Message);
            }
        }
    }

    private static void TryRemoveEmptyDirs(string root)
    {
        try
        {
            foreach (var dir in Directory.EnumerateDirectories(root, "*", SearchOption.AllDirectories)
                         .OrderByDescending(d => d.Length))
            {
                try
                {
                    if (!Directory.EnumerateFileSystemEntries(dir).Any())
                        Directory.Delete(dir, false);
                }
                catch { /* 忽略单目录失败 */ }
            }
        }
        catch { /* 忽略枚举失败 */ }
    }

    private void CheckDisk(LogCleanupOptions opt, bool forceReeval = false)
    {
        if (forceReeval) _diskBelow = false;
        if (!opt.DiskAlertEnabled)
        {
            _diskBelow = false;
            return;
        }

        if (!TryGetFreeGB(out var freeGB, out var drive))
            return;

        if (freeGB < opt.DiskFreeAlertGB)
        {
            if (_diskBelow) return;
            _diskBelow = true;
            _log.LogWarning(
                "磁盘剩余空间不足：{Drive} 仅剩 {Free:F1} GB（低于阈值 {Threshold:F0} GB）。请及时清理磁盘，或在系统配置中调低日志保留天数 / 立即清理。",
                drive, freeGB, opt.DiskFreeAlertGB);
        }
        else
        {
            _diskBelow = false;
        }
    }

    private bool TryGetFreeGB(out double freeGB, out string driveName)
    {
        freeGB = 0;
        driveName = "";
        var (root, _, _) = ResolveLogRoot();
        if (root == null) return false;
        try
        {
            var driveRoot = Path.GetPathRoot(root);
            if (string.IsNullOrEmpty(driveRoot)) return false;
            var di = new DriveInfo(driveRoot);
            driveName = di.Name;
            freeGB = di.AvailableFreeSpace / 1024.0 / 1024.0 / 1024.0;
            return true;
        }
        catch
        {
            return false;
        }
    }

    private (string? root, string? workdir, string? error) ResolveLogRoot()
    {
        var overrideRoot = _config["Logs:Root"];
        if (!string.IsNullOrWhiteSpace(overrideRoot))
        {
            var r = Path.GetFullPath(overrideRoot);
            return (r, Path.GetDirectoryName(r), null);
        }

        var wd = _launcher.ResolveWorkingDirectory();
        if (string.IsNullOrWhiteSpace(wd))
            return (null, null, "未能定位 Simple3 工作目录");

        return (Path.GetFullPath(Path.Combine(wd, "log")), wd, null);
    }

    public readonly record struct CleanResult(int DeletedFiles, long FreedBytes, string? Error)
    {
        public double FreedMB => FreedBytes / 1024.0 / 1024.0;
    }

    public readonly record struct DiskStatus(
        bool Known,
        string Drive,
        double FreeGB,
        bool AlertEnabled,
        double AlertGB,
        bool BelowThreshold);
}
