using System;
using System.Buffers;
using System.Collections.Immutable;
using System.Linq;
using System.Security.Cryptography;
using System.Text.Json;

namespace StellaOps.Policy;

public static class PolicyDigest
{
    public static string Compute(PolicyDocument document)
    {
        if (document is null)
        {
            throw new ArgumentNullException(nameof(document));
        }

        var buffer = new ArrayBufferWriter<byte>();
        using (var writer = new Utf8JsonWriter(buffer, new JsonWriterOptions
        {
            SkipValidation = true,
        }))
        {
            WriteDocument(writer, document);
        }

        var hash = SHA256.HashData(buffer.WrittenSpan);
        return Convert.ToHexString(hash).ToLowerInvariant();
    }

    private static void WriteDocument(Utf8JsonWriter writer, PolicyDocument document)
    {
        writer.WriteStartObject();
        writer.WriteString("version", document.Version);

        if (!document.Metadata.IsEmpty)
        {
            writer.WritePropertyName("metadata");
            writer.WriteStartObject();
            foreach (var pair in document.Metadata.OrderBy(static kvp => kvp.Key, StringComparer.Ordinal))
            {
                writer.WriteString(pair.Key, pair.Value);
            }
            writer.WriteEndObject();
        }

        writer.WritePropertyName("rules");
        writer.WriteStartArray();
        foreach (var rule in document.Rules)
        {
            WriteRule(writer, rule);
        }
        writer.WriteEndArray();

        writer.WriteEndObject();
        writer.Flush();
    }

    private static void WriteRule(Utf8JsonWriter writer, PolicyRule rule)
    {
        writer.WriteStartObject();
        writer.WriteString("name", rule.Name);

        if (!string.IsNullOrWhiteSpace(rule.Identifier))
        {
            writer.WriteString("id", rule.Identifier);
        }

        if (!string.IsNullOrWhiteSpace(rule.Description))
        {
            writer.WriteString("description", rule.Description);
        }

        WriteMetadata(writer, rule.Metadata);
        WriteSeverities(writer, rule.Severities);
        WriteStringArray(writer, "environments", rule.Environments);
        WriteStringArray(writer, "sources", rule.Sources);
        WriteStringArray(writer, "vendors", rule.Vendors);
        WriteStringArray(writer, "licenses", rule.Licenses);
        WriteStringArray(writer, "tags", rule.Tags);

        if (!rule.Match.IsEmpty)
        {
            writer.WritePropertyName("match");
            writer.WriteStartObject();
            WriteStringArray(writer, "images", rule.Match.Images);
            WriteStringArray(writer, "repositories", rule.Match.Repositories);
            WriteStringArray(writer, "packages", rule.Match.Packages);
            WriteStringArray(writer, "purls", rule.Match.Purls);
            WriteStringArray(writer, "cves", rule.Match.Cves);
            WriteStringArray(writer, "paths", rule.Match.Paths);
            WriteStringArray(writer, "layerDigests", rule.Match.LayerDigests);
            WriteStringArray(writer, "usedByEntrypoint", rule.Match.UsedByEntrypoint);
            writer.WriteEndObject();
        }

        WriteAction(writer, rule.Action);

        if (rule.Expires is DateTimeOffset expires)
        {
            writer.WriteString("expires", expires.ToUniversalTime().ToString("O"));
        }

        if (!string.IsNullOrWhiteSpace(rule.Justification))
        {
            writer.WriteString("justification", rule.Justification);
        }

        writer.WriteEndObject();
    }

    private static void WriteAction(Utf8JsonWriter writer, PolicyAction action)
    {
        writer.WritePropertyName("action");
        writer.WriteStartObject();
        writer.WriteString("type", action.Type.ToString().ToLowerInvariant());

        if (action.Quiet)
        {
            writer.WriteBoolean("quiet", true);
        }

        if (action.Ignore is { } ignore)
        {
            if (ignore.Until is DateTimeOffset until)
            {
                writer.WriteString("until", until.ToUniversalTime().ToString("O"));
            }

            if (!string.IsNullOrWhiteSpace(ignore.Justification))
            {
                writer.WriteString("justification", ignore.Justification);
            }
        }

        if (action.Escalate is { } escalate)
        {
            if (escalate.MinimumSeverity is { } severity)
            {
                writer.WriteString("severity", severity.ToString());
            }

            if (escalate.RequireKev)
            {
                writer.WriteBoolean("kev", true);
            }

            if (escalate.MinimumEpss is double epss)
            {
                writer.WriteNumber("epss", epss);
            }
        }

        if (action.RequireVex is { } requireVex)
        {
            WriteStringArray(writer, "vendors", requireVex.Vendors);
            WriteStringArray(writer, "justifications", requireVex.Justifications);
        }

        writer.WriteEndObject();
    }

    private static void WriteMetadata(Utf8JsonWriter writer, ImmutableDictionary<string, string> metadata)
    {
        if (metadata.IsEmpty)
        {
            return;
        }

        writer.WritePropertyName("metadata");
        writer.WriteStartObject();
        foreach (var pair in metadata.OrderBy(static kvp => kvp.Key, StringComparer.Ordinal))
        {
            writer.WriteString(pair.Key, pair.Value);
        }
        writer.WriteEndObject();
    }

    private static void WriteSeverities(Utf8JsonWriter writer, ImmutableArray<PolicySeverity> severities)
    {
        if (severities.IsDefaultOrEmpty)
        {
            return;
        }

        writer.WritePropertyName("severity");
        writer.WriteStartArray();
        foreach (var severity in severities)
        {
            writer.WriteStringValue(severity.ToString());
        }
        writer.WriteEndArray();
    }

    private static void WriteStringArray(Utf8JsonWriter writer, string propertyName, ImmutableArray<string> values)
    {
        if (values.IsDefaultOrEmpty)
        {
            return;
        }

        writer.WritePropertyName(propertyName);
        writer.WriteStartArray();
        foreach (var value in values)
        {
            writer.WriteStringValue(value);
        }
        writer.WriteEndArray();
    }
}
