using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace StellaOps.Scanner.Sbomer.BuildXPlugin.Descriptor;

/// <summary>
/// Builds immutable OCI descriptors enriched with provenance placeholders.
/// </summary>
public sealed class DescriptorGenerator
{
    public const string Schema = "stellaops.buildx.descriptor.v1";

    private readonly TimeProvider timeProvider;

    public DescriptorGenerator(TimeProvider timeProvider)
    {
        timeProvider ??= TimeProvider.System;
        this.timeProvider = timeProvider;
    }

    public async Task<DescriptorDocument> CreateAsync(DescriptorRequest request, CancellationToken cancellationToken)
    {
        if (request is null)
        {
            throw new ArgumentNullException(nameof(request));
        }

        if (string.IsNullOrWhiteSpace(request.ImageDigest))
        {
            throw new BuildxPluginException("Image digest must be provided.");
        }

        if (string.IsNullOrWhiteSpace(request.SbomPath))
        {
            throw new BuildxPluginException("SBOM path must be provided.");
        }

        var sbomFile = new FileInfo(request.SbomPath);
        if (!sbomFile.Exists)
        {
            throw new BuildxPluginException($"SBOM file '{request.SbomPath}' was not found.");
        }

        var sbomDigest = await ComputeFileDigestAsync(sbomFile, cancellationToken).ConfigureAwait(false);

        var nonce = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
        var expectedDsseSha = ComputeExpectedDsseDigest(request.ImageDigest, sbomDigest, nonce);

        var artifactAnnotations = BuildArtifactAnnotations(request, nonce, expectedDsseSha);

        var subject = new DescriptorSubject(
            MediaType: request.SubjectMediaType,
            Digest: request.ImageDigest);

        var artifact = new DescriptorArtifact(
            MediaType: request.SbomMediaType,
            Digest: sbomDigest,
            Size: sbomFile.Length,
            Annotations: artifactAnnotations);

        var provenance = new DescriptorProvenance(
            Status: "pending",
            ExpectedDsseSha256: expectedDsseSha,
            Nonce: nonce,
            AttestorUri: request.AttestorUri,
            PredicateType: request.PredicateType);

        var generatorMetadata = new DescriptorGeneratorMetadata(
            Name: request.GeneratorName ?? "StellaOps.Scanner.Sbomer.BuildXPlugin",
            Version: request.GeneratorVersion);

        var metadata = BuildDocumentMetadata(request, sbomFile, sbomDigest);

        return new DescriptorDocument(
            Schema: Schema,
            GeneratedAt: timeProvider.GetUtcNow(),
            Generator: generatorMetadata,
            Subject: subject,
            Artifact: artifact,
            Provenance: provenance,
            Metadata: metadata);
    }

    private static async Task<string> ComputeFileDigestAsync(FileInfo file, CancellationToken cancellationToken)
    {
        await using var stream = new FileStream(
            file.FullName,
            FileMode.Open,
            FileAccess.Read,
            FileShare.Read,
            bufferSize: 128 * 1024,
            FileOptions.Asynchronous | FileOptions.SequentialScan);

        using var hash = IncrementalHash.CreateHash(HashAlgorithmName.SHA256);

        var buffer = new byte[128 * 1024];
        int bytesRead;
        while ((bytesRead = await stream.ReadAsync(buffer.AsMemory(0, buffer.Length), cancellationToken).ConfigureAwait(false)) > 0)
        {
            hash.AppendData(buffer, 0, bytesRead);
        }

        var digest = hash.GetHashAndReset();
        return $"sha256:{Convert.ToHexString(digest).ToLowerInvariant()}";
    }

    private static string ComputeExpectedDsseDigest(string imageDigest, string sbomDigest, string nonce)
    {
        var payload = $"{imageDigest}\n{sbomDigest}\n{nonce}";
        var bytes = System.Text.Encoding.UTF8.GetBytes(payload);
        Span<byte> hash = stackalloc byte[32];
        SHA256.HashData(bytes, hash);
        return $"sha256:{Convert.ToHexString(hash).ToLowerInvariant()}";
    }

    private static IReadOnlyDictionary<string, string> BuildArtifactAnnotations(DescriptorRequest request, string nonce, string expectedDsse)
    {
        var annotations = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
        {
            ["org.opencontainers.artifact.type"] = request.SbomArtifactType,
            ["org.stellaops.scanner.version"] = request.GeneratorVersion,
            ["org.stellaops.sbom.kind"] = request.SbomKind,
            ["org.stellaops.sbom.format"] = request.SbomFormat,
            ["org.stellaops.provenance.status"] = "pending",
            ["org.stellaops.provenance.dsse.sha256"] = expectedDsse,
            ["org.stellaops.provenance.nonce"] = nonce
        };

        if (!string.IsNullOrWhiteSpace(request.LicenseId))
        {
            annotations["org.stellaops.license.id"] = request.LicenseId!;
        }

        if (!string.IsNullOrWhiteSpace(request.SbomName))
        {
            annotations["org.opencontainers.image.title"] = request.SbomName!;
        }

        if (!string.IsNullOrWhiteSpace(request.Repository))
        {
            annotations["org.stellaops.repository"] = request.Repository!;
        }

        return annotations;
    }

    private static IReadOnlyDictionary<string, string> BuildDocumentMetadata(DescriptorRequest request, FileInfo fileInfo, string sbomDigest)
    {
        var metadata = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
        {
            ["sbomDigest"] = sbomDigest,
            ["sbomPath"] = fileInfo.FullName,
            ["sbomMediaType"] = request.SbomMediaType,
            ["subjectMediaType"] = request.SubjectMediaType
        };

        if (!string.IsNullOrWhiteSpace(request.Repository))
        {
            metadata["repository"] = request.Repository!;
        }

        if (!string.IsNullOrWhiteSpace(request.BuildRef))
        {
            metadata["buildRef"] = request.BuildRef!;
        }

        if (!string.IsNullOrWhiteSpace(request.AttestorUri))
        {
            metadata["attestorUri"] = request.AttestorUri!;
        }

        return metadata;
    }
}
