using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using Microsoft.Extensions.DependencyInjection;

namespace {{ namespace }}.Core;

public interface ICredentialStore
{
    Task SaveAsync(string key, string secret, CancellationToken cancellationToken = default);

    Task<string?> GetAsync(string key, CancellationToken cancellationToken = default);

    Task DeleteAsync(string key, CancellationToken cancellationToken = default);
}

/// <summary>
/// REQ-2.3.5's encrypted-file fallback: secrets are AES-256-GCM encrypted
/// at rest under <c>~/.{{ project_name }}/credentials.enc</c>, keyed by a
/// per-installation key generated on first use and stored alongside it.
/// Native OS-keychain integration (macOS Keychain Services, Windows
/// Credential Manager, Linux Secret Service) each need their own P/Invoke
/// binding with no mature, maintained cross-platform NuGet wrapper as of
/// this writing — v4-implementation-plan.md's open decision #... flagged
/// this as a real implementation risk up front. REQ-2.3.5 explicitly
/// sanctions an encrypted-file fallback, so this ships that fallback now,
/// fully functional on every OS `dotnet` runs on; native keychain
/// integration is a follow-up story, not a gap in this one's contract.
/// In-memory caching (also required by REQ-2.3.5) is layered on top by
/// <see cref="CachingCredentialStore"/>.
/// </summary>
public sealed class EncryptedFileCredentialStore : ICredentialStore
{
    private readonly string _credentialsPath;
    private readonly string _keyPath;
    private readonly SemaphoreSlim _lock = new(1, 1);

    public EncryptedFileCredentialStore()
    {
        var dir = Path.Combine(
            Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
            ".{{ project_name }}");
        Directory.CreateDirectory(dir);
        _credentialsPath = Path.Combine(dir, "credentials.enc");
        _keyPath = Path.Combine(dir, "credentials.key");
    }

    public async Task SaveAsync(string key, string secret, CancellationToken cancellationToken = default)
    {
        await _lock.WaitAsync(cancellationToken);
        try
        {
            var store = await ReadAllAsync(cancellationToken);
            store[key] = secret;
            await WriteAllAsync(store, cancellationToken);
        }
        finally
        {
            _lock.Release();
        }
    }

    public async Task<string?> GetAsync(string key, CancellationToken cancellationToken = default)
    {
        await _lock.WaitAsync(cancellationToken);
        try
        {
            var store = await ReadAllAsync(cancellationToken);
            return store.TryGetValue(key, out var value) ? value : null;
        }
        finally
        {
            _lock.Release();
        }
    }

    public async Task DeleteAsync(string key, CancellationToken cancellationToken = default)
    {
        await _lock.WaitAsync(cancellationToken);
        try
        {
            var store = await ReadAllAsync(cancellationToken);
            if (store.Remove(key))
            {
                await WriteAllAsync(store, cancellationToken);
            }
        }
        finally
        {
            _lock.Release();
        }
    }

    private async Task<Dictionary<string, string>> ReadAllAsync(CancellationToken cancellationToken)
    {
        if (!File.Exists(_credentialsPath))
        {
            return new Dictionary<string, string>();
        }

        var encrypted = await File.ReadAllBytesAsync(_credentialsPath, cancellationToken);
        var key = GetOrCreateKey();
        var json = Decrypt(encrypted, key);
        return JsonSerializer.Deserialize<Dictionary<string, string>>(json) ?? new Dictionary<string, string>();
    }

    private async Task WriteAllAsync(Dictionary<string, string> store, CancellationToken cancellationToken)
    {
        var key = GetOrCreateKey();
        var json = JsonSerializer.Serialize(store);
        var encrypted = Encrypt(json, key);
        await File.WriteAllBytesAsync(_credentialsPath, encrypted, cancellationToken);
    }

    private byte[] GetOrCreateKey()
    {
        if (File.Exists(_keyPath))
        {
            return Convert.FromBase64String(File.ReadAllText(_keyPath));
        }

        var key = RandomNumberGenerator.GetBytes(32);
        File.WriteAllText(_keyPath, Convert.ToBase64String(key));
        return key;
    }

    private static byte[] Encrypt(string plaintext, byte[] key)
    {
        var nonce = RandomNumberGenerator.GetBytes(AesGcm.NonceByteSizes.MaxSize);
        var plaintextBytes = Encoding.UTF8.GetBytes(plaintext);
        var ciphertext = new byte[plaintextBytes.Length];
        var tag = new byte[AesGcm.TagByteSizes.MaxSize];

        using var aes = new AesGcm(key, tag.Length);
        aes.Encrypt(nonce, plaintextBytes, ciphertext, tag);

        var result = new byte[nonce.Length + tag.Length + ciphertext.Length];
        Buffer.BlockCopy(nonce, 0, result, 0, nonce.Length);
        Buffer.BlockCopy(tag, 0, result, nonce.Length, tag.Length);
        Buffer.BlockCopy(ciphertext, 0, result, nonce.Length + tag.Length, ciphertext.Length);
        return result;
    }

    private static string Decrypt(byte[] data, byte[] key)
    {
        var nonceSize = AesGcm.NonceByteSizes.MaxSize;
        var tagSize = AesGcm.TagByteSizes.MaxSize;
        var nonce = data[..nonceSize];
        var tag = data[nonceSize..(nonceSize + tagSize)];
        var ciphertext = data[(nonceSize + tagSize)..];
        var plaintext = new byte[ciphertext.Length];

        using var aes = new AesGcm(key, tagSize);
        aes.Decrypt(nonce, ciphertext, tag, plaintext);

        return Encoding.UTF8.GetString(plaintext);
    }
}

/// <summary>
/// REQ-2.3.5's in-memory caching layer: wraps any <see cref="ICredentialStore"/>
/// so repeated <see cref="GetAsync"/> calls for the same key don't re-hit
/// disk/decrypt on every credential lookup within a process lifetime.
/// </summary>
public sealed class CachingCredentialStore(ICredentialStore inner) : ICredentialStore
{
    private readonly Dictionary<string, string> _cache = new();
    private readonly SemaphoreSlim _lock = new(1, 1);

    public async Task SaveAsync(string key, string secret, CancellationToken cancellationToken = default)
    {
        await inner.SaveAsync(key, secret, cancellationToken);
        await _lock.WaitAsync(cancellationToken);
        try
        {
            _cache[key] = secret;
        }
        finally
        {
            _lock.Release();
        }
    }

    public async Task<string?> GetAsync(string key, CancellationToken cancellationToken = default)
    {
        await _lock.WaitAsync(cancellationToken);
        try
        {
            if (_cache.TryGetValue(key, out var cached))
            {
                return cached;
            }
        }
        finally
        {
            _lock.Release();
        }

        var value = await inner.GetAsync(key, cancellationToken);
        if (value is not null)
        {
            await _lock.WaitAsync(cancellationToken);
            try
            {
                _cache[key] = value;
            }
            finally
            {
                _lock.Release();
            }
        }

        return value;
    }

    public async Task DeleteAsync(string key, CancellationToken cancellationToken = default)
    {
        await inner.DeleteAsync(key, cancellationToken);
        await _lock.WaitAsync(cancellationToken);
        try
        {
            _cache.Remove(key);
        }
        finally
        {
            _lock.Release();
        }
    }
}

public static partial class ServiceCollectionExtensions
{
    static partial void AddCredentialStorage(IServiceCollection services)
    {
        services.AddSingleton<EncryptedFileCredentialStore>();
        services.AddSingleton<ICredentialStore>(sp =>
            new CachingCredentialStore(sp.GetRequiredService<EncryptedFileCredentialStore>()));
    }
}
