using CvCleanupJob.Models;
using JobScheduler.Tasks;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Models.Settings;

namespace CvCleanupJob.Tasks;

/// <summary>
/// Deletes oldest cached CV files until the total size of remaining files is at or below the configured budget.
/// </summary>
public sealed class CvStorageCleanupJobTask : IJobTask
{
    private readonly FileStorageSettings _fileStorage;
    private readonly ILogger<CvStorageCleanupJobTask> _logger;

    public CvStorageCleanupJobTask(IOptions<FileStorageSettings> fileStorage, ILogger<CvStorageCleanupJobTask> logger)
    {
        _fileStorage = fileStorage.Value;
        _logger = logger;
    }

    public string TaskType => "CvStorageCleanup";

    public Task ExecuteAsync(IConfiguration parametersSection, CancellationToken cancellationToken)
    {
        cancellationToken.ThrowIfCancellationRequested();

        var parameters = parametersSection.Get<CvStorageCleanupParameters>()
            ?? new CvStorageCleanupParameters();

        if (parameters.MaxTotalSizeMegabytes <= 0)
        {
            _logger.LogWarning(
                "CvStorageCleanup skipped: MaxTotalSizeMegabytes must be positive (got {Value}).",
                parameters.MaxTotalSizeMegabytes);
            return Task.CompletedTask;
        }

        var root = ResolveStorageRoot(_fileStorage.Path);
        if (!Directory.Exists(root))
        {
            _logger.LogDebug("CvStorageCleanup: directory does not exist yet: {Path}", root);
            return Task.CompletedTask;
        }

        var maxBytes = (long)Math.Round(parameters.MaxTotalSizeMegabytes * 1024d * 1024d, MidpointRounding.AwayFromZero);

        var candidatePaths = Directory
            .EnumerateFiles(root, parameters.SearchPattern, SearchOption.TopDirectoryOnly)
            .Where(p => !parameters.RestrictToCvStyleFileNamesOnly || IsCvStyleFileName(Path.GetFileName(p)))
            .Select(p => new FileInfo(p))
            .Where(f => f.Exists)
            .ToList();

        if (candidatePaths.Count == 0)
        {
            _logger.LogDebug("CvStorageCleanup: no files matched under {Path}.", root);
            return Task.CompletedTask;
        }

        long totalBytes = candidatePaths.Sum(f => f.Length);
        if (totalBytes <= maxBytes)
        {
            _logger.LogInformation(
                "CvStorageCleanup: within budget ({TotalMb:F2} MiB / {MaxMb:F2} MiB). No files removed.",
                totalBytes / (1024d * 1024d),
                parameters.MaxTotalSizeMegabytes);
            return Task.CompletedTask;
        }

        var orderedOldestFirst = candidatePaths
            .OrderBy(f => f.LastWriteTimeUtc)
            .ThenBy(f => f.FullName, StringComparer.Ordinal)
            .ToList();

        long remaining = totalBytes;
        var deleted = 0;
        long freedBytes = 0;

        while (remaining > maxBytes && orderedOldestFirst.Count > 0)
        {
            cancellationToken.ThrowIfCancellationRequested();

            var oldest = orderedOldestFirst[0];
            orderedOldestFirst.RemoveAt(0);

            try
            {
                var len = oldest.Length;
                oldest.Delete();
                remaining -= len;
                freedBytes += len;
                deleted++;
                _logger.LogInformation(
                    "CvStorageCleanup: deleted oldest file {File} ({SizeKb} KiB, remaining aggregate ~{RemainMb:F2} MiB).",
                    oldest.Name,
                    len / 1024d,
                    remaining / (1024d * 1024d));
            }
            catch (Exception ex)
            {
                _logger.LogWarning(ex, "CvStorageCleanup: could not delete {File}", oldest.FullName);
            }
        }

        _logger.LogInformation(
            "CvStorageCleanup finished: removed {Deleted} file(s), freed ~{FreedMb:F2} MiB; size now ~{RemainMb:F2} MiB (budget {MaxMb:F2} MiB).",
            deleted,
            freedBytes / (1024d * 1024d),
            remaining / (1024d * 1024d),
            parameters.MaxTotalSizeMegabytes);

        return Task.CompletedTask;
    }

    /// <summary>Matches API behavior for cached CV paths (alphanumeric stem).</summary>
    internal static bool IsCvStyleFileName(string fileName)
    {
        var stem = Path.GetFileNameWithoutExtension(fileName);
        return stem.Length > 0 && stem.All(char.IsLetterOrDigit);
    }

    internal static string ResolveStorageRoot(string configuredPath)
    {
        if (Path.IsPathRooted(configuredPath))
        {
            return configuredPath;
        }

        return Path.GetFullPath(Path.Combine(Directory.GetCurrentDirectory(), configuredPath));
    }
}
