using ClawdDotNet.App.Settings;
using ClawdDotNet.Core.Backup;
using ClawdDotNet.Core.Storage;
using Microsoft.Extensions.Logging;

namespace ClawdDotNet.App.Services;

/// <summary>
/// Erstellt einmal täglich zur eingestellten Uhrzeit eine Sicherung.
///
/// Bewusst ohne Zugangsdaten: Die Passphrase müsste dafür gespeichert werden, und
/// neben den Sicherungen abgelegt wäre sie wirkungslos. Wer die Zugangsdaten
/// mitsichern will, macht das von Hand.
///
/// Der Zeitpunkt wird bei jedem Durchlauf neu gegen die Einstellungen geprüft, damit
/// eine Änderung ohne Neustart greift.
///
/// <para><b>Takt.</b> Früher ein <c>System.Windows.Forms.Timer</c> — der braucht eine
/// Nachrichtenschleife und damit ein Fenster. Jetzt <see cref="PeriodicTimer"/> über
/// einen <see cref="TimeProvider"/>: läuft ohne Oberfläche, driftet nicht, und Tests
/// können die Zeit steuern statt zu warten.</para>
/// </summary>
public sealed class BackupScheduler : IAsyncDisposable
{
    private readonly string _instanceDir;
    private readonly string _instanceName;
    private readonly SettingsManager _settings;
    private readonly ILogger _logger;
    private readonly TimeProvider _clock;
    private readonly TimeSpan _tick;
    private readonly BackupService _service = new();
    private readonly CancellationTokenSource _cts = new();

    private Task? _loop;

    /// <summary>Verhindert mehrere Sicherungen am selben Tag.</summary>
    private DateTime? _lastRun;

    public event Action<string>? OnBackupCreated;

    public BackupScheduler(
        string instanceDir,
        string instanceName,
        SettingsManager settings,
        ILogger logger,
        TimeProvider? clock = null,
        TimeSpan? tick = null)
    {
        _instanceDir = instanceDir;
        _instanceName = instanceName;
        _settings = settings;
        _logger = logger;
        _clock = clock ?? TimeProvider.System;

        // Minütlich prüfen reicht — die Uhrzeit ist auf Minuten genau eingestellt.
        _tick = tick ?? TimeSpan.FromMinutes(1);
    }

    public void Start() => _loop ??= RunLoopAsync(_cts.Token);

    private async Task RunLoopAsync(CancellationToken ct)
    {
        using var timer = new PeriodicTimer(_tick, _clock);

        while (await timer.WaitForNextTickAsync(ct))
        {
            try
            {
                await TickAsync();
            }
            catch (Exception ex) when (ex is not OperationCanceledException)
            {
                // Ein Fehlschlag darf die Anwendung nicht stören.
                _logger.LogError(ex, "Automatische Sicherung fehlgeschlagen");
            }
        }
    }

    /// <summary>Ein Durchlauf. Öffentlich, damit Tests ihn deterministisch auslösen können.</summary>
    public async Task TickAsync()
    {
        var settings = _settings.AppSettings;
        if (!settings.AutoBackupEnabled)
            return;

        if (!TimeSpan.TryParse(settings.AutoBackupTime, out var scheduled))
            return;

        var now = _clock.GetLocalNow().DateTime;

        // Fällig, sobald die Uhrzeit erreicht ist und heute noch nichts lief.
        if (now.TimeOfDay < scheduled) return;
        if (_lastRun?.Date == now.Date) return;

        // Vor dem Lauf setzen: Scheitert er, wird nicht jede Minute erneut versucht,
        // sondern morgen wieder. Ein Retry-Sturm über Nacht hilft niemandem.
        _lastRun = now;

        await RunAsync(settings.BackupDirectory, settings.BackupKeepCount, now);
    }

    private async Task RunAsync(string folder, int keepCount, DateTime now)
    {
        // PortableFileName statt Path.GetInvalidFileNameChars: Unter Linux liefert das
        // nur '\0' und '/', ein Instanzname mit ':' ergäbe ein Archiv, das sich unter
        // Windows nicht mehr anlegen lässt.
        var safeName = PortableFileName.Sanitize(_instanceName, "Instanz");

        var file = Path.Combine(
            Path.GetFullPath(folder),
            $"backup_{safeName}_{now:yyyy-MM-dd_HHmm}.zip");

        var result = await _service.CreateAsync(_instanceDir, file, new BackupOptions
        {
            Secrets = SecretMode.Exclude,
            IncludeChatHistory = true,
            IncludeLogs = false
        });

        _logger.LogInformation("Automatische Sicherung erstellt: {Path} ({Size} Bytes)",
            result.ZipPath, result.SizeBytes);

        ApplyRotation(Path.GetFullPath(folder), safeName, keepCount);

        OnBackupCreated?.Invoke(result.ZipPath);
    }

    /// <summary>Behält die neuesten Sicherungen dieser Instanz und entfernt den Rest.</summary>
    private void ApplyRotation(string folder, string safeName, int keepCount)
    {
        if (keepCount <= 0 || !Directory.Exists(folder))
            return;

        try
        {
            var prefix = $"backup_{safeName}_";

            var obsolete = new DirectoryInfo(folder)
                .GetFiles("*.zip")
                .Where(f => f.Name.StartsWith(prefix, PathBoundary.Comparison))
                .OrderByDescending(f => f.LastWriteTime)
                .Skip(keepCount)
                .ToList();

            foreach (var file in obsolete)
            {
                file.Delete();
                _logger.LogInformation("Alte Sicherung entfernt: {Name}", file.Name);
            }
        }
        catch (Exception ex)
        {
            _logger.LogWarning(ex, "Rotation der Sicherungen fehlgeschlagen");
        }
    }

    public async ValueTask DisposeAsync()
    {
        await _cts.CancelAsync();

        if (_loop is not null)
        {
            try { await _loop; }
            catch (OperationCanceledException) { /* erwartet */ }
        }

        _cts.Dispose();
    }
}
