using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.IO.Abstractions;
using System.IO.Compression;
using System.Security.Cryptography;
using System.Text.Json;
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 OfflineBundleArtifactStoreOptions
{
    public string RootPath { get; set; } = ".";

    public string ArtifactsFolder { get; set; } = "artifacts";

    public string BundlesFolder { get; set; } = "bundles";

    public string ManifestFileName { get; set; } = "offline-manifest.json";
}

public sealed class OfflineBundleArtifactStore : IVexArtifactStore
{
    private readonly IFileSystem _fileSystem;
    private readonly OfflineBundleArtifactStoreOptions _options;
    private readonly ILogger<OfflineBundleArtifactStore> _logger;
    private readonly JsonSerializerOptions _serializerOptions = new()
    {
        PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
        WriteIndented = true,
    };

    public OfflineBundleArtifactStore(
        IOptions<OfflineBundleArtifactStoreOptions> options,
        ILogger<OfflineBundleArtifactStore> logger,
        IFileSystem? fileSystem = null)
    {
        ArgumentNullException.ThrowIfNull(options);
        _options = options.Value;
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
        _fileSystem = fileSystem ?? new FileSystem();

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

        var root = _fileSystem.Path.GetFullPath(_options.RootPath);
        _fileSystem.Directory.CreateDirectory(root);
        _options.RootPath = root;
        _fileSystem.Directory.CreateDirectory(GetArtifactsRoot());
        _fileSystem.Directory.CreateDirectory(GetBundlesRoot());
    }

    public async ValueTask<VexStoredArtifact> SaveAsync(VexExportArtifact artifact, CancellationToken cancellationToken)
    {
        ArgumentNullException.ThrowIfNull(artifact);
        EnforceDigestMatch(artifact);

        var artifactRelativePath = BuildArtifactRelativePath(artifact);
        var artifactFullPath = _fileSystem.Path.Combine(_options.RootPath, artifactRelativePath);
        var artifactDirectory = _fileSystem.Path.GetDirectoryName(artifactFullPath);
        if (!string.IsNullOrEmpty(artifactDirectory))
        {
            _fileSystem.Directory.CreateDirectory(artifactDirectory);
        }

        await using (var stream = _fileSystem.File.Create(artifactFullPath))
        {
            await stream.WriteAsync(artifact.Content, cancellationToken).ConfigureAwait(false);
        }

        WriteOfflineBundle(artifactRelativePath, artifact, cancellationToken);
        await UpdateManifestAsync(artifactRelativePath, artifact, cancellationToken).ConfigureAwait(false);

        _logger.LogInformation("Stored offline artifact {Digest} at {Path}", artifact.ContentAddress.ToUri(), artifactRelativePath);

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

    public ValueTask DeleteAsync(VexContentAddress contentAddress, CancellationToken cancellationToken)
    {
        ArgumentNullException.ThrowIfNull(contentAddress);
        var sanitized = contentAddress.Digest.Replace(':', '_');
        var artifactsRoot = GetArtifactsRoot();

        foreach (VexExportFormat format in Enum.GetValues(typeof(VexExportFormat)))
        {
            var extension = GetExtension(format);
            var path = _fileSystem.Path.Combine(artifactsRoot, format.ToString().ToLowerInvariant(), sanitized + extension);
            if (_fileSystem.File.Exists(path))
            {
                _fileSystem.File.Delete(path);
            }

            var bundlePath = _fileSystem.Path.Combine(GetBundlesRoot(), sanitized + ".zip");
            if (_fileSystem.File.Exists(bundlePath))
            {
                _fileSystem.File.Delete(bundlePath);
            }
        }

        return ValueTask.CompletedTask;
    }

    public ValueTask<Stream?> OpenReadAsync(VexContentAddress contentAddress, CancellationToken cancellationToken)
    {
        ArgumentNullException.ThrowIfNull(contentAddress);
        var artifactsRoot = GetArtifactsRoot();
        var sanitized = contentAddress.Digest.Replace(':', '_');

        foreach (VexExportFormat format in Enum.GetValues(typeof(VexExportFormat)))
        {
            var candidate = _fileSystem.Path.Combine(artifactsRoot, format.ToString().ToLowerInvariant(), sanitized + GetExtension(format));
            if (_fileSystem.File.Exists(candidate))
            {
                return ValueTask.FromResult<Stream?>(_fileSystem.File.OpenRead(candidate));
            }
        }

        return ValueTask.FromResult<Stream?>(null);
    }

    private void EnforceDigestMatch(VexExportArtifact artifact)
    {
        if (!artifact.ContentAddress.Algorithm.Equals("sha256", StringComparison.OrdinalIgnoreCase))
        {
            return;
        }

        using var sha = SHA256.Create();
        var computed = "sha256:" + Convert.ToHexString(sha.ComputeHash(artifact.Content.ToArray())).ToLowerInvariant();
        if (!string.Equals(computed, artifact.ContentAddress.ToUri(), StringComparison.OrdinalIgnoreCase))
        {
            throw new InvalidOperationException($"Artifact content digest mismatch. Expected {artifact.ContentAddress.ToUri()} but computed {computed}.");
        }
    }

    private string BuildArtifactRelativePath(VexExportArtifact artifact)
    {
        var sanitized = artifact.ContentAddress.Digest.Replace(':', '_');
        var folder = _fileSystem.Path.Combine(_options.ArtifactsFolder, artifact.Format.ToString().ToLowerInvariant());
        return _fileSystem.Path.Combine(folder, sanitized + GetExtension(artifact.Format));
    }

    private void WriteOfflineBundle(string artifactRelativePath, VexExportArtifact artifact, CancellationToken cancellationToken)
    {
        var zipPath = _fileSystem.Path.Combine(GetBundlesRoot(), artifact.ContentAddress.Digest.Replace(':', '_') + ".zip");
        using var zipStream = _fileSystem.File.Create(zipPath);
        using var archive = new ZipArchive(zipStream, ZipArchiveMode.Create);
        var entry = archive.CreateEntry(artifactRelativePath, CompressionLevel.Optimal);
        using (var entryStream = entry.Open())
        {
            entryStream.Write(artifact.Content.Span);
        }

        // embed metadata file
        var metadataEntry = archive.CreateEntry("metadata.json", CompressionLevel.Optimal);
        using var metadataStream = new StreamWriter(metadataEntry.Open());
        var metadata = new Dictionary<string, object?>
        {
            ["digest"] = artifact.ContentAddress.ToUri(),
            ["format"] = artifact.Format.ToString().ToLowerInvariant(),
            ["sizeBytes"] = artifact.Content.Length,
            ["metadata"] = artifact.Metadata,
        };
        metadataStream.Write(JsonSerializer.Serialize(metadata, _serializerOptions));
    }

    private async Task UpdateManifestAsync(string artifactRelativePath, VexExportArtifact artifact, CancellationToken cancellationToken)
    {
        var manifestPath = _fileSystem.Path.Combine(_options.RootPath, _options.ManifestFileName);
        var records = new List<ManifestEntry>();

        if (_fileSystem.File.Exists(manifestPath))
        {
            await using var existingStream = _fileSystem.File.OpenRead(manifestPath);
            var existing = await JsonSerializer.DeserializeAsync<ManifestDocument>(existingStream, _serializerOptions, cancellationToken).ConfigureAwait(false);
            if (existing is not null)
            {
                records.AddRange(existing.Artifacts);
            }
        }

        records.RemoveAll(x => string.Equals(x.Digest, artifact.ContentAddress.ToUri(), StringComparison.OrdinalIgnoreCase));
        records.Add(new ManifestEntry(
            artifact.ContentAddress.ToUri(),
            artifact.Format.ToString().ToLowerInvariant(),
            artifactRelativePath.Replace("\\", "/"),
            artifact.Content.Length,
            artifact.Metadata));

        records.Sort(static (a, b) => string.CompareOrdinal(a.Digest, b.Digest));

        var doc = new ManifestDocument(records.ToImmutableArray());

        await using var stream = _fileSystem.File.Create(manifestPath);
        await JsonSerializer.SerializeAsync(stream, doc, _serializerOptions, cancellationToken).ConfigureAwait(false);
    }

    private string GetArtifactsRoot() => _fileSystem.Path.Combine(_options.RootPath, _options.ArtifactsFolder);

    private string GetBundlesRoot() => _fileSystem.Path.Combine(_options.RootPath, _options.BundlesFolder);

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

    private sealed record ManifestDocument(ImmutableArray<ManifestEntry> Artifacts);

    private sealed record ManifestEntry(string Digest, string Format, string Path, long SizeBytes, IReadOnlyDictionary<string, string> Metadata);
}

public static class OfflineBundleArtifactStoreServiceCollectionExtensions
{
    public static IServiceCollection AddVexOfflineBundleArtifactStore(this IServiceCollection services, Action<OfflineBundleArtifactStoreOptions>? configure = null)
    {
        if (configure is not null)
        {
            services.Configure(configure);
        }

        services.AddSingleton<IVexArtifactStore, OfflineBundleArtifactStore>();
        return services;
    }
}
