using System;
using System.IO;
using System.Security.Cryptography;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;

namespace StellaOps.Auth.Client;

/// <summary>
/// File-based token cache suitable for CLI/offline usage.
/// </summary>
public sealed class FileTokenCache : IStellaOpsTokenCache
{
    private readonly string cacheDirectory;
    private readonly TimeProvider timeProvider;
    private readonly TimeSpan expirationSkew;
    private readonly ILogger<FileTokenCache>? logger;
    private readonly JsonSerializerOptions serializerOptions = new(JsonSerializerDefaults.Web)
    {
        WriteIndented = false
    };

    public FileTokenCache(string cacheDirectory, TimeProvider? timeProvider = null, TimeSpan? expirationSkew = null, ILogger<FileTokenCache>? logger = null)
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(cacheDirectory);

        this.cacheDirectory = cacheDirectory;
        this.timeProvider = timeProvider ?? TimeProvider.System;
        this.expirationSkew = expirationSkew ?? TimeSpan.FromSeconds(30);
        this.logger = logger;
    }

    public async ValueTask<StellaOpsTokenCacheEntry?> GetAsync(string key, CancellationToken cancellationToken = default)
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(key);

        var path = GetPath(key);
        if (!File.Exists(path))
        {
            return null;
        }

        try
        {
            await using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, FileOptions.Asynchronous);
            var entry = await JsonSerializer.DeserializeAsync<StellaOpsTokenCacheEntry>(stream, serializerOptions, cancellationToken).ConfigureAwait(false);

            if (entry is null)
            {
                return null;
            }

            entry = entry.NormalizeScopes();

            if (entry.IsExpired(timeProvider, expirationSkew))
            {
                await RemoveInternalAsync(path).ConfigureAwait(false);
                return null;
            }

            return entry;
        }
        catch (Exception ex) when (ex is IOException or JsonException or UnauthorizedAccessException)
        {
            logger?.LogWarning(ex, "Failed to read token cache entry '{CacheKey}'.", key);
            return null;
        }
    }

    public async ValueTask SetAsync(string key, StellaOpsTokenCacheEntry entry, CancellationToken cancellationToken = default)
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(key);
        ArgumentNullException.ThrowIfNull(entry);

        Directory.CreateDirectory(cacheDirectory);

        var path = GetPath(key);
        var payload = entry.NormalizeScopes();

        try
        {
            await using var stream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, 4096, FileOptions.Asynchronous);
            await JsonSerializer.SerializeAsync(stream, payload, serializerOptions, cancellationToken).ConfigureAwait(false);
        }
        catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
        {
            logger?.LogWarning(ex, "Failed to persist token cache entry '{CacheKey}'.", key);
        }
    }

    public ValueTask RemoveAsync(string key, CancellationToken cancellationToken = default)
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(key);
        var path = GetPath(key);
        return new ValueTask(RemoveInternalAsync(path));
    }

    private async Task RemoveInternalAsync(string path)
    {
        try
        {
            if (File.Exists(path))
            {
                await Task.Run(() => File.Delete(path)).ConfigureAwait(false);
            }
        }
        catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
        {
            logger?.LogDebug(ex, "Failed to remove cache file '{Path}'.", path);
        }
    }

    private string GetPath(string key)
    {
        using var sha = SHA256.Create();
        var bytes = System.Text.Encoding.UTF8.GetBytes(key);
        var hash = Convert.ToHexString(sha.ComputeHash(bytes));
        return Path.Combine(cacheDirectory, $"{hash}.json");
    }
}
