using System;
using System.Collections.Immutable;

namespace StellaOps.Policy;

/// <summary>
/// Canonical representation of a StellaOps policy document.
/// </summary>
public sealed record PolicyDocument(
    string Version,
    ImmutableArray<PolicyRule> Rules,
    ImmutableDictionary<string, string> Metadata)
{
    public static PolicyDocument Empty { get; } = new(
        PolicySchema.CurrentVersion,
        ImmutableArray<PolicyRule>.Empty,
        ImmutableDictionary<string, string>.Empty);
}

public static class PolicySchema
{
    public const string SchemaId = "https://schemas.stella-ops.org/policy/policy-schema@1.json";
    public const string CurrentVersion = "1.0";

    public static PolicyDocumentFormat DetectFormat(string fileName)
    {
        if (fileName is null)
        {
            throw new ArgumentNullException(nameof(fileName));
        }

        var lower = fileName.Trim().ToLowerInvariant();
        if (lower.EndsWith(".yaml", StringComparison.Ordinal) || lower.EndsWith(".yml", StringComparison.Ordinal))
        {
            return PolicyDocumentFormat.Yaml;
        }

        return PolicyDocumentFormat.Json;
    }
}

public sealed record PolicyRule(
    string Name,
    string? Identifier,
    string? Description,
    PolicyAction Action,
    ImmutableArray<PolicySeverity> Severities,
    ImmutableArray<string> Environments,
    ImmutableArray<string> Sources,
    ImmutableArray<string> Vendors,
    ImmutableArray<string> Licenses,
    ImmutableArray<string> Tags,
    PolicyRuleMatchCriteria Match,
    DateTimeOffset? Expires,
    string? Justification,
    ImmutableDictionary<string, string> Metadata)
{
    public static PolicyRuleMatchCriteria EmptyMatch { get; } = new(
        ImmutableArray<string>.Empty,
        ImmutableArray<string>.Empty,
        ImmutableArray<string>.Empty,
        ImmutableArray<string>.Empty,
        ImmutableArray<string>.Empty,
        ImmutableArray<string>.Empty,
        ImmutableArray<string>.Empty,
        ImmutableArray<string>.Empty);

    public static PolicyRule Create(
        string name,
        PolicyAction action,
        ImmutableArray<PolicySeverity> severities,
        ImmutableArray<string> environments,
        ImmutableArray<string> sources,
        ImmutableArray<string> vendors,
        ImmutableArray<string> licenses,
        ImmutableArray<string> tags,
        PolicyRuleMatchCriteria match,
        DateTimeOffset? expires,
        string? justification,
        string? identifier = null,
        string? description = null,
        ImmutableDictionary<string, string>? metadata = null)
    {
        metadata ??= ImmutableDictionary<string, string>.Empty;
        return new PolicyRule(
            name,
            identifier,
            description,
            action,
            severities,
            environments,
            sources,
            vendors,
            licenses,
            tags,
            match,
            expires,
            justification,
            metadata);
    }

    public bool MatchesAnyEnvironment => Environments.IsDefaultOrEmpty;
}

public sealed record PolicyRuleMatchCriteria(
    ImmutableArray<string> Images,
    ImmutableArray<string> Repositories,
    ImmutableArray<string> Packages,
    ImmutableArray<string> Purls,
    ImmutableArray<string> Cves,
    ImmutableArray<string> Paths,
    ImmutableArray<string> LayerDigests,
    ImmutableArray<string> UsedByEntrypoint)
{
    public static PolicyRuleMatchCriteria Create(
        ImmutableArray<string> images,
        ImmutableArray<string> repositories,
        ImmutableArray<string> packages,
        ImmutableArray<string> purls,
        ImmutableArray<string> cves,
        ImmutableArray<string> paths,
        ImmutableArray<string> layerDigests,
        ImmutableArray<string> usedByEntrypoint)
        => new(
            images,
            repositories,
            packages,
            purls,
            cves,
            paths,
            layerDigests,
            usedByEntrypoint);

    public static PolicyRuleMatchCriteria Empty { get; } = new(
        ImmutableArray<string>.Empty,
        ImmutableArray<string>.Empty,
        ImmutableArray<string>.Empty,
        ImmutableArray<string>.Empty,
        ImmutableArray<string>.Empty,
        ImmutableArray<string>.Empty,
        ImmutableArray<string>.Empty,
        ImmutableArray<string>.Empty);

    public bool IsEmpty =>
        Images.IsDefaultOrEmpty &&
        Repositories.IsDefaultOrEmpty &&
        Packages.IsDefaultOrEmpty &&
        Purls.IsDefaultOrEmpty &&
        Cves.IsDefaultOrEmpty &&
        Paths.IsDefaultOrEmpty &&
        LayerDigests.IsDefaultOrEmpty &&
        UsedByEntrypoint.IsDefaultOrEmpty;
}

public sealed record PolicyAction(
    PolicyActionType Type,
    PolicyIgnoreOptions? Ignore,
    PolicyEscalateOptions? Escalate,
    PolicyRequireVexOptions? RequireVex,
    bool Quiet);

public enum PolicyActionType
{
    Block,
    Ignore,
    Warn,
    Defer,
    Escalate,
    RequireVex,
}

public sealed record PolicyIgnoreOptions(DateTimeOffset? Until, string? Justification);

public sealed record PolicyEscalateOptions(
    PolicySeverity? MinimumSeverity,
    bool RequireKev,
    double? MinimumEpss);

public sealed record PolicyRequireVexOptions(
    ImmutableArray<string> Vendors,
    ImmutableArray<string> Justifications);

public enum PolicySeverity
{
    Critical,
    High,
    Medium,
    Low,
    Informational,
    None,
    Unknown,
}
