using System.Collections.Immutable;
using System.Runtime.Serialization;

namespace StellaOps.Vexer.Core;

public sealed record VexClaim
{
    public VexClaim(
        string vulnerabilityId,
        string providerId,
        VexProduct product,
        VexClaimStatus status,
        VexClaimDocument document,
        DateTimeOffset firstSeen,
        DateTimeOffset lastSeen,
        VexJustification? justification = null,
        string? detail = null,
        VexConfidence? confidence = null,
        ImmutableDictionary<string, string>? additionalMetadata = null)
    {
        if (string.IsNullOrWhiteSpace(vulnerabilityId))
        {
            throw new ArgumentException("Vulnerability id must be provided.", nameof(vulnerabilityId));
        }

        if (string.IsNullOrWhiteSpace(providerId))
        {
            throw new ArgumentException("Provider id must be provided.", nameof(providerId));
        }

        if (lastSeen < firstSeen)
        {
            throw new ArgumentOutOfRangeException(nameof(lastSeen), "Last seen timestamp cannot be earlier than first seen.");
        }

        VulnerabilityId = vulnerabilityId.Trim();
        ProviderId = providerId.Trim();
        Product = product ?? throw new ArgumentNullException(nameof(product));
        Status = status;
        Document = document ?? throw new ArgumentNullException(nameof(document));
        FirstSeen = firstSeen;
        LastSeen = lastSeen;
        Justification = justification;
        Detail = string.IsNullOrWhiteSpace(detail) ? null : detail.Trim();
        Confidence = confidence;
        AdditionalMetadata = NormalizeMetadata(additionalMetadata);
    }

    public string VulnerabilityId { get; }

    public string ProviderId { get; }

    public VexProduct Product { get; }

    public VexClaimStatus Status { get; }

    public VexJustification? Justification { get; }

    public string? Detail { get; }

    public VexClaimDocument Document { get; }

    public DateTimeOffset FirstSeen { get; }

    public DateTimeOffset LastSeen { get; }

    public VexConfidence? Confidence { get; }

    public ImmutableSortedDictionary<string, string> AdditionalMetadata { get; }

    private static ImmutableSortedDictionary<string, string> NormalizeMetadata(
        ImmutableDictionary<string, string>? additionalMetadata)
    {
        if (additionalMetadata is null || additionalMetadata.Count == 0)
        {
            return ImmutableSortedDictionary<string, string>.Empty;
        }

        var builder = ImmutableSortedDictionary.CreateBuilder<string, string>(StringComparer.Ordinal);
        foreach (var (key, value) in additionalMetadata)
        {
            if (string.IsNullOrWhiteSpace(key))
            {
                continue;
            }

            builder[key.Trim()] = value?.Trim() ?? string.Empty;
        }

        return builder.ToImmutable();
    }
}

public sealed record VexProduct
{
    public VexProduct(
        string key,
        string? name,
        string? version = null,
        string? purl = null,
        string? cpe = null,
        IEnumerable<string>? componentIdentifiers = null)
    {
        if (string.IsNullOrWhiteSpace(key))
        {
            throw new ArgumentException("Product key must be provided.", nameof(key));
        }

        Key = key.Trim();
        Name = string.IsNullOrWhiteSpace(name) ? null : name.Trim();
        Version = string.IsNullOrWhiteSpace(version) ? null : version.Trim();
        Purl = string.IsNullOrWhiteSpace(purl) ? null : purl.Trim();
        Cpe = string.IsNullOrWhiteSpace(cpe) ? null : cpe.Trim();
        ComponentIdentifiers = NormalizeComponentIdentifiers(componentIdentifiers);
    }

    public string Key { get; }

    public string? Name { get; }

    public string? Version { get; }

    public string? Purl { get; }

    public string? Cpe { get; }

    public ImmutableArray<string> ComponentIdentifiers { get; }

    private static ImmutableArray<string> NormalizeComponentIdentifiers(IEnumerable<string>? identifiers)
    {
        if (identifiers is null)
        {
            return ImmutableArray<string>.Empty;
        }

        var set = new SortedSet<string>(StringComparer.Ordinal);
        foreach (var identifier in identifiers)
        {
            if (string.IsNullOrWhiteSpace(identifier))
            {
                continue;
            }

            set.Add(identifier.Trim());
        }

        return set.Count == 0 ? ImmutableArray<string>.Empty : set.ToImmutableArray();
    }
}

public sealed record VexClaimDocument
{
    public VexClaimDocument(
        VexDocumentFormat format,
        string digest,
        Uri sourceUri,
        string? revision = null,
        VexSignatureMetadata? signature = null)
    {
        if (string.IsNullOrWhiteSpace(digest))
        {
            throw new ArgumentException("Document digest must be provided.", nameof(digest));
        }

        Format = format;
        Digest = digest.Trim();
        SourceUri = sourceUri ?? throw new ArgumentNullException(nameof(sourceUri));
        Revision = string.IsNullOrWhiteSpace(revision) ? null : revision.Trim();
        Signature = signature;
    }

    public VexDocumentFormat Format { get; }

    public string Digest { get; }

    public Uri SourceUri { get; }

    public string? Revision { get; }

    public VexSignatureMetadata? Signature { get; }
}

public sealed record VexSignatureMetadata
{
    public VexSignatureMetadata(
        string type,
        string? subject = null,
        string? issuer = null,
        string? keyId = null,
        DateTimeOffset? verifiedAt = null,
        string? transparencyLogReference = null)
    {
        if (string.IsNullOrWhiteSpace(type))
        {
            throw new ArgumentException("Signature type must be provided.", nameof(type));
        }

        Type = type.Trim();
        Subject = string.IsNullOrWhiteSpace(subject) ? null : subject.Trim();
        Issuer = string.IsNullOrWhiteSpace(issuer) ? null : issuer.Trim();
        KeyId = string.IsNullOrWhiteSpace(keyId) ? null : keyId.Trim();
        VerifiedAt = verifiedAt;
        TransparencyLogReference = string.IsNullOrWhiteSpace(transparencyLogReference)
            ? null
            : transparencyLogReference.Trim();
    }

    public string Type { get; }

    public string? Subject { get; }

    public string? Issuer { get; }

    public string? KeyId { get; }

    public DateTimeOffset? VerifiedAt { get; }

    public string? TransparencyLogReference { get; }
}

public sealed record VexConfidence
{
    public VexConfidence(string level, double? score = null, string? method = null)
    {
        if (string.IsNullOrWhiteSpace(level))
        {
            throw new ArgumentException("Confidence level must be provided.", nameof(level));
        }

        if (score is not null && (double.IsNaN(score.Value) || double.IsInfinity(score.Value)))
        {
            throw new ArgumentOutOfRangeException(nameof(score), "Confidence score must be a finite number.");
        }

        Level = level.Trim();
        Score = score;
        Method = string.IsNullOrWhiteSpace(method) ? null : method.Trim();
    }

    public string Level { get; }

    public double? Score { get; }

    public string? Method { get; }
}

[DataContract]
public enum VexDocumentFormat
{
    [EnumMember(Value = "csaf")]
    Csaf,

    [EnumMember(Value = "cyclonedx")]
    CycloneDx,

    [EnumMember(Value = "openvex")]
    OpenVex,

    [EnumMember(Value = "oci_attestation")]
    OciAttestation,
}

[DataContract]
public enum VexClaimStatus
{
    [EnumMember(Value = "affected")]
    Affected,

    [EnumMember(Value = "not_affected")]
    NotAffected,

    [EnumMember(Value = "fixed")]
    Fixed,

    [EnumMember(Value = "under_investigation")]
    UnderInvestigation,
}

[DataContract]
public enum VexJustification
{
    [EnumMember(Value = "component_not_present")]
    ComponentNotPresent,

    [EnumMember(Value = "component_not_configured")]
    ComponentNotConfigured,

    [EnumMember(Value = "vulnerable_code_not_present")]
    VulnerableCodeNotPresent,

    [EnumMember(Value = "vulnerable_code_not_in_execute_path")]
    VulnerableCodeNotInExecutePath,

    [EnumMember(Value = "vulnerable_code_cannot_be_controlled_by_adversary")]
    VulnerableCodeCannotBeControlledByAdversary,

    [EnumMember(Value = "inline_mitigations_already_exist")]
    InlineMitigationsAlreadyExist,

    [EnumMember(Value = "protected_by_mitigating_control")]
    ProtectedByMitigatingControl,

    [EnumMember(Value = "code_not_present")]
    CodeNotPresent,

    [EnumMember(Value = "code_not_reachable")]
    CodeNotReachable,

    [EnumMember(Value = "requires_configuration")]
    RequiresConfiguration,

    [EnumMember(Value = "requires_dependency")]
    RequiresDependency,

    [EnumMember(Value = "requires_environment")]
    RequiresEnvironment,

    [EnumMember(Value = "protected_by_compensating_control")]
    ProtectedByCompensatingControl,

    [EnumMember(Value = "protected_at_perimeter")]
    ProtectedAtPerimeter,

    [EnumMember(Value = "protected_at_runtime")]
    ProtectedAtRuntime,
}
