using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using StellaOps.Vexer.Core;

namespace StellaOps.Vexer.Export;

public sealed class S3ArtifactStoreOptions
{
    public string BucketName { get; set; } = string.Empty;

    public string? Prefix { get; set; }
        = null;

    public bool OverwriteExisting { get; set; }
        = true;
}

public interface IS3ArtifactClient
{
    Task<bool> ObjectExistsAsync(string bucketName, string key, CancellationToken cancellationToken);

    Task PutObjectAsync(string bucketName, string key, Stream content, IDictionary<string, string> metadata, CancellationToken cancellationToken);

    Task<Stream?> GetObjectAsync(string bucketName, string key, CancellationToken cancellationToken);

    Task DeleteObjectAsync(string bucketName, string key, CancellationToken cancellationToken);
}

public sealed class S3ArtifactStore : IVexArtifactStore
{
    private readonly IS3ArtifactClient _client;
    private readonly S3ArtifactStoreOptions _options;
    private readonly ILogger<S3ArtifactStore> _logger;

    public S3ArtifactStore(
        IS3ArtifactClient client,
        IOptions<S3ArtifactStoreOptions> options,
        ILogger<S3ArtifactStore> logger)
    {
        _client = client ?? throw new ArgumentNullException(nameof(client));
        ArgumentNullException.ThrowIfNull(options);
        _options = options.Value;
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));

        if (string.IsNullOrWhiteSpace(_options.BucketName))
        {
            throw new ArgumentException("BucketName must be provided for S3ArtifactStore.", nameof(options));
        }
    }

    public async ValueTask<VexStoredArtifact> SaveAsync(VexExportArtifact artifact, CancellationToken cancellationToken)
    {
        ArgumentNullException.ThrowIfNull(artifact);
        var key = BuildObjectKey(artifact.ContentAddress, artifact.Format);

        if (!_options.OverwriteExisting)
        {
            var exists = await _client.ObjectExistsAsync(_options.BucketName, key, cancellationToken).ConfigureAwait(false);
            if (exists)
            {
                _logger.LogInformation("S3 object {Bucket}/{Key} already exists; skipping upload.", _options.BucketName, key);
                return new VexStoredArtifact(artifact.ContentAddress, key, artifact.Content.Length, artifact.Metadata);
            }
        }

        using var contentStream = new MemoryStream(artifact.Content.ToArray());
        await _client.PutObjectAsync(
            _options.BucketName,
            key,
            contentStream,
            BuildObjectMetadata(artifact),
            cancellationToken).ConfigureAwait(false);

        _logger.LogInformation("Uploaded export artifact {Digest} to {Bucket}/{Key}", artifact.ContentAddress.ToUri(), _options.BucketName, key);

        return new VexStoredArtifact(
            artifact.ContentAddress,
            key,
            artifact.Content.Length,
            artifact.Metadata);
    }

    public async ValueTask DeleteAsync(VexContentAddress contentAddress, CancellationToken cancellationToken)
    {
        ArgumentNullException.ThrowIfNull(contentAddress);
        foreach (var key in BuildCandidateKeys(contentAddress))
        {
            await _client.DeleteObjectAsync(_options.BucketName, key, cancellationToken).ConfigureAwait(false);
        }
        _logger.LogInformation("Deleted export artifact {Digest} from {Bucket}", contentAddress.ToUri(), _options.BucketName);
    }

    public async ValueTask<Stream?> OpenReadAsync(VexContentAddress contentAddress, CancellationToken cancellationToken)
    {
        ArgumentNullException.ThrowIfNull(contentAddress);
        foreach (var key in BuildCandidateKeys(contentAddress))
        {
            var stream = await _client.GetObjectAsync(_options.BucketName, key, cancellationToken).ConfigureAwait(false);
            if (stream is not null)
            {
                return stream;
            }
        }

        return null;
    }

    private string BuildObjectKey(VexContentAddress address, VexExportFormat format)
    {
        var sanitizedDigest = address.Digest.Replace(':', '_');
        var prefix = string.IsNullOrWhiteSpace(_options.Prefix) ? string.Empty : _options.Prefix.TrimEnd('/') + "/";
        var formatSegment = format.ToString().ToLowerInvariant();
        return $"{prefix}{formatSegment}/{sanitizedDigest}{GetExtension(format)}";
    }

    private IEnumerable<string> BuildCandidateKeys(VexContentAddress address)
    {
        foreach (VexExportFormat format in Enum.GetValues(typeof(VexExportFormat)))
        {
            yield return BuildObjectKey(address, format);
        }

        if (!string.IsNullOrWhiteSpace(_options.Prefix))
        {
            yield return $"{_options.Prefix.TrimEnd('/')}/{address.Digest.Replace(':', '_')}";
        }

        yield return address.Digest.Replace(':', '_');
    }

    private static IDictionary<string, string> BuildObjectMetadata(VexExportArtifact artifact)
    {
        var metadata = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
        {
            ["vex-format"] = artifact.Format.ToString().ToLowerInvariant(),
            ["vex-digest"] = artifact.ContentAddress.ToUri(),
            ["content-type"] = artifact.Format switch
            {
                VexExportFormat.Json => "application/json",
                VexExportFormat.JsonLines => "application/json",
                VexExportFormat.OpenVex => "application/vnd.openvex+json",
                VexExportFormat.Csaf => "application/json",
                _ => "application/octet-stream",
            },
        };

        foreach (var kvp in artifact.Metadata)
        {
            metadata[$"meta-{kvp.Key}"] = kvp.Value;
        }

        return metadata;
    }

    private static string GetExtension(VexExportFormat format)
        => format switch
        {
            VexExportFormat.Json => ".json",
            VexExportFormat.JsonLines => ".jsonl",
            VexExportFormat.OpenVex => ".json",
            VexExportFormat.Csaf => ".json",
            _ => ".bin",
        };
}

public static class S3ArtifactStoreServiceCollectionExtensions
{
    public static IServiceCollection AddVexS3ArtifactStore(this IServiceCollection services, Action<S3ArtifactStoreOptions> configure)
    {
        ArgumentNullException.ThrowIfNull(configure);
        services.Configure(configure);
        services.AddSingleton<IVexArtifactStore, S3ArtifactStore>();
        return services;
    }
}
