using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

namespace StellaOps.Feedser.Exporter.TrivyDb;

/// <summary>
/// Writes a Trivy DB package to an OCI image layout directory with deterministic content.
/// </summary>
public sealed class TrivyDbOciWriter
{
    private static readonly JsonSerializerOptions SerializerOptions = new()
    {
        PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
        DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull,
        WriteIndented = false,
    };

    private static readonly byte[] OciLayoutBytes = Encoding.UTF8.GetBytes("{\"imageLayoutVersion\":\"1.0.0\"}");

    public async Task<TrivyDbOciWriteResult> WriteAsync(
        TrivyDbPackage package,
        string destination,
        string reference,
        TrivyDbExportPlan plan,
        string? baseLayoutPath,
        CancellationToken cancellationToken)
    {
        if (package is null)
        {
            throw new ArgumentNullException(nameof(package));
        }

        if (string.IsNullOrWhiteSpace(destination))
        {
            throw new ArgumentException("Destination directory must be provided.", nameof(destination));
        }

        if (string.IsNullOrWhiteSpace(reference))
        {
            throw new ArgumentException("Reference tag must be provided.", nameof(reference));
        }

        if (plan is null)
        {
            throw new ArgumentNullException(nameof(plan));
        }

        var root = Path.GetFullPath(destination);
        if (Directory.Exists(root))
        {
            Directory.Delete(root, recursive: true);
        }

        Directory.CreateDirectory(root);
        var timestamp = package.Config.GeneratedAt.UtcDateTime;

        await WriteFileAsync(Path.Combine(root, "metadata.json"), package.MetadataJson, timestamp, cancellationToken).ConfigureAwait(false);
        await WriteFileAsync(Path.Combine(root, "oci-layout"), OciLayoutBytes, timestamp, cancellationToken).ConfigureAwait(false);

        var blobsRoot = Path.Combine(root, "blobs", "sha256");
        Directory.CreateDirectory(blobsRoot);
        Directory.SetLastWriteTimeUtc(Path.GetDirectoryName(blobsRoot)!, timestamp);
        Directory.SetLastWriteTimeUtc(blobsRoot, timestamp);

        var writtenDigests = new HashSet<string>(StringComparer.Ordinal);
        foreach (var pair in package.Blobs)
        {
            if (writtenDigests.Contains(pair.Key))
            {
                continue;
            }

            var reused = await TryReuseExistingBlobAsync(baseLayoutPath, pair.Key, blobsRoot, timestamp, cancellationToken).ConfigureAwait(false);
            if (reused)
            {
                writtenDigests.Add(pair.Key);
                continue;
            }

            if (writtenDigests.Add(pair.Key))
            {
                await WriteBlobAsync(blobsRoot, pair.Key, pair.Value, timestamp, cancellationToken).ConfigureAwait(false);
            }
        }

        var manifestBytes = JsonSerializer.SerializeToUtf8Bytes(package.Manifest, SerializerOptions);
        var manifestDigest = ComputeDigest(manifestBytes);
        if (!writtenDigests.Contains(manifestDigest))
        {
            var reused = await TryReuseExistingBlobAsync(baseLayoutPath, manifestDigest, blobsRoot, timestamp, cancellationToken).ConfigureAwait(false);
            if (!reused)
            {
                await WriteBlobAsync(blobsRoot, manifestDigest, TrivyDbBlob.FromBytes(manifestBytes), timestamp, cancellationToken).ConfigureAwait(false);
            }

            writtenDigests.Add(manifestDigest);
        }

        var manifestDescriptor = new OciDescriptor(
            TrivyDbMediaTypes.OciManifest,
            manifestDigest,
            manifestBytes.LongLength,
            new Dictionary<string, string>
            {
                ["org.opencontainers.image.ref.name"] = reference,
            });
        var index = new OciIndex(2, new[] { manifestDescriptor });
        var indexBytes = JsonSerializer.SerializeToUtf8Bytes(index, SerializerOptions);
        await WriteFileAsync(Path.Combine(root, "index.json"), indexBytes, timestamp, cancellationToken).ConfigureAwait(false);

        if (plan.Mode == TrivyDbExportMode.Delta && !string.IsNullOrWhiteSpace(baseLayoutPath))
        {
            var reuseDigests = await TryReuseBaseBlobsAsync(
                blobsRoot,
                timestamp,
                writtenDigests,
                plan,
                baseLayoutPath,
                cancellationToken).ConfigureAwait(false);
            foreach (var digest in reuseDigests)
            {
                writtenDigests.Add(digest);
            }
        }

        Directory.SetLastWriteTimeUtc(root, timestamp);

        var blobDigests = writtenDigests.ToArray();
        Array.Sort(blobDigests, StringComparer.Ordinal);
        return new TrivyDbOciWriteResult(root, manifestDigest, blobDigests);
    }

    private static async Task WriteFileAsync(string path, ReadOnlyMemory<byte> bytes, DateTime utcTimestamp, CancellationToken cancellationToken)
    {
        var directory = Path.GetDirectoryName(path);
        if (!string.IsNullOrEmpty(directory))
        {
            Directory.CreateDirectory(directory);
            Directory.SetLastWriteTimeUtc(directory, utcTimestamp);
        }

        await using var destination = new FileStream(
            path,
            FileMode.Create,
            FileAccess.Write,
            FileShare.None,
            bufferSize: 81920,
            options: FileOptions.Asynchronous | FileOptions.SequentialScan);
        await destination.WriteAsync(bytes, cancellationToken).ConfigureAwait(false);
        await destination.FlushAsync(cancellationToken).ConfigureAwait(false);
        File.SetLastWriteTimeUtc(path, utcTimestamp);
    }

    private static async Task WriteBlobAsync(string blobsRoot, string digest, TrivyDbBlob blob, DateTime utcTimestamp, CancellationToken cancellationToken)
    {
        var fileName = ResolveDigestFileName(digest);
        var path = Path.Combine(blobsRoot, fileName);
        var directory = Path.GetDirectoryName(path);
        if (!string.IsNullOrEmpty(directory))
        {
            Directory.CreateDirectory(directory);
            Directory.SetLastWriteTimeUtc(directory, utcTimestamp);
        }

        await using var source = await blob.OpenReadAsync(cancellationToken).ConfigureAwait(false);
        await using var destination = new FileStream(
            path,
            FileMode.Create,
            FileAccess.Write,
            FileShare.None,
            bufferSize: 81920,
            options: FileOptions.Asynchronous | FileOptions.SequentialScan);

        await source.CopyToAsync(destination, cancellationToken).ConfigureAwait(false);
        await destination.FlushAsync(cancellationToken).ConfigureAwait(false);
        File.SetLastWriteTimeUtc(path, utcTimestamp);
    }

    private static string ResolveDigestFileName(string digest)
    {
        if (!digest.StartsWith("sha256:", StringComparison.Ordinal))
        {
            throw new InvalidOperationException($"Only sha256 digests are supported. Received '{digest}'.");
        }

        var hex = digest[7..];
        if (hex.Length == 0)
        {
            throw new InvalidOperationException("Digest hex component cannot be empty.");
        }

        return hex;
    }

    private static string ComputeDigest(ReadOnlySpan<byte> payload)
    {
        var hash = System.Security.Cryptography.SHA256.HashData(payload);
        var hex = Convert.ToHexString(hash);
        Span<char> buffer = stackalloc char[7 + hex.Length]; // "sha256:" + hex
        buffer[0] = 's';
        buffer[1] = 'h';
        buffer[2] = 'a';
        buffer[3] = '2';
        buffer[4] = '5';
        buffer[5] = '6';
        buffer[6] = ':';
        for (var i = 0; i < hex.Length; i++)
        {
            buffer[7 + i] = char.ToLowerInvariant(hex[i]);
        }

        return new string(buffer);
    }

    private static async Task<IReadOnlyCollection<string>> TryReuseBaseBlobsAsync(
        string destinationBlobsRoot,
        DateTime timestamp,
        HashSet<string> written,
        TrivyDbExportPlan plan,
        string baseLayoutPath,
        CancellationToken cancellationToken)
    {
        if (string.IsNullOrWhiteSpace(plan.BaseManifestDigest))
        {
            return Array.Empty<string>();
        }

        var baseRoot = Path.GetFullPath(baseLayoutPath);
        if (!Directory.Exists(baseRoot))
        {
            return Array.Empty<string>();
        }

        var manifestPath = ResolveBlobPath(baseRoot, plan.BaseManifestDigest);
        if (!File.Exists(manifestPath))
        {
            return Array.Empty<string>();
        }

        await using var stream = new FileStream(
            manifestPath,
            FileMode.Open,
            FileAccess.Read,
            FileShare.Read,
            bufferSize: 81920,
            options: FileOptions.Asynchronous | FileOptions.SequentialScan);
        using var document = await JsonDocument.ParseAsync(stream, cancellationToken: cancellationToken).ConfigureAwait(false);
        var root = document.RootElement;

        var digests = new SortedSet<string>(StringComparer.Ordinal)
        {
            plan.BaseManifestDigest,
        };

        if (root.TryGetProperty("config", out var configNode))
        {
            var digest = configNode.GetProperty("digest").GetString();
            if (!string.IsNullOrWhiteSpace(digest))
            {
                digests.Add(digest);
            }
        }

        if (root.TryGetProperty("layers", out var layersNode))
        {
            foreach (var layer in layersNode.EnumerateArray())
            {
                var digest = layer.GetProperty("digest").GetString();
                if (!string.IsNullOrWhiteSpace(digest))
                {
                    digests.Add(digest);
                }
            }
        }

        var copied = new List<string>();
        foreach (var digest in digests)
        {
            if (written.Contains(digest))
            {
                continue;
            }

            var sourcePath = ResolveBlobPath(baseRoot, digest);
            if (!File.Exists(sourcePath))
            {
                continue;
            }

            var destinationPath = Path.Combine(destinationBlobsRoot, ResolveDigestFileName(digest));
            Directory.CreateDirectory(Path.GetDirectoryName(destinationPath)!);
            await using var source = new FileStream(
                sourcePath,
                FileMode.Open,
                FileAccess.Read,
                FileShare.Read,
                bufferSize: 81920,
                options: FileOptions.Asynchronous | FileOptions.SequentialScan);
            await using var destination = new FileStream(
                destinationPath,
                FileMode.Create,
                FileAccess.Write,
                FileShare.None,
                bufferSize: 81920,
                options: FileOptions.Asynchronous | FileOptions.SequentialScan);
            await source.CopyToAsync(destination, cancellationToken).ConfigureAwait(false);
            await destination.FlushAsync(cancellationToken).ConfigureAwait(false);
            File.SetLastWriteTimeUtc(destinationPath, timestamp);
            copied.Add(digest);
        }

        if (copied.Count > 0)
        {
            Directory.SetLastWriteTimeUtc(destinationBlobsRoot, timestamp);
            Directory.SetLastWriteTimeUtc(Path.GetDirectoryName(destinationBlobsRoot)!, timestamp);
        }

        return copied;
    }

    private static string ResolveBlobPath(string layoutRoot, string digest)
    {
        var fileName = ResolveDigestFileName(digest);
        return Path.Combine(layoutRoot, "blobs", "sha256", fileName);
    }

    private static async Task<bool> TryReuseExistingBlobAsync(
        string? baseLayoutPath,
        string digest,
        string destinationBlobsRoot,
        DateTime timestamp,
        CancellationToken cancellationToken)
    {
        if (string.IsNullOrWhiteSpace(baseLayoutPath))
        {
            return false;
        }

        var baseRoot = Path.GetFullPath(baseLayoutPath);
        var sourcePath = ResolveBlobPath(baseRoot, digest);
        if (!File.Exists(sourcePath))
        {
            return false;
        }

        var destinationPath = Path.Combine(destinationBlobsRoot, ResolveDigestFileName(digest));
        Directory.CreateDirectory(Path.GetDirectoryName(destinationPath)!);
        await using var source = new FileStream(
            sourcePath,
            FileMode.Open,
            FileAccess.Read,
            FileShare.Read,
            bufferSize: 81920,
            options: FileOptions.Asynchronous | FileOptions.SequentialScan);
        await using var destination = new FileStream(
            destinationPath,
            FileMode.Create,
            FileAccess.Write,
            FileShare.None,
            bufferSize: 81920,
            options: FileOptions.Asynchronous | FileOptions.SequentialScan);
        await source.CopyToAsync(destination, cancellationToken).ConfigureAwait(false);
        await destination.FlushAsync(cancellationToken).ConfigureAwait(false);
        File.SetLastWriteTimeUtc(destinationPath, timestamp);
        Directory.SetLastWriteTimeUtc(destinationBlobsRoot, timestamp);
        Directory.SetLastWriteTimeUtc(Path.GetDirectoryName(destinationBlobsRoot)!, timestamp);
        return true;
    }
}
