using System.Security.Cryptography.X509Certificates;
using System.Text.Json;
using System.Text.Json.Serialization;
using NATS.Server.Configuration;

namespace NATS.Server.Tls;

[JsonConverter(typeof(StatusAssertionJsonConverter))]
public enum StatusAssertion
{
    Good = 0,
    Revoked = 1,
    Unknown = 2,
}

public static class StatusAssertionMaps
{
    public static readonly IReadOnlyDictionary<string, StatusAssertion> StatusAssertionStrToVal =
        new Dictionary<string, StatusAssertion>(StringComparer.OrdinalIgnoreCase)
        {
            ["good"] = StatusAssertion.Good,
            ["revoked"] = StatusAssertion.Revoked,
            ["unknown"] = StatusAssertion.Unknown,
        };

    public static readonly IReadOnlyDictionary<StatusAssertion, string> StatusAssertionValToStr =
        new Dictionary<StatusAssertion, string>
        {
            [StatusAssertion.Good] = "good",
            [StatusAssertion.Revoked] = "revoked",
            [StatusAssertion.Unknown] = "unknown",
        };

    public static readonly IReadOnlyDictionary<int, StatusAssertion> StatusAssertionIntToVal =
        new Dictionary<int, StatusAssertion>
        {
            [0] = StatusAssertion.Good,
            [1] = StatusAssertion.Revoked,
            [2] = StatusAssertion.Unknown,
        };

    public static string GetStatusAssertionStr(int sa)
    {
        var value = StatusAssertionIntToVal.TryGetValue(sa, out var mapped)
            ? mapped
            : StatusAssertion.Unknown;
        return StatusAssertionValToStr[value];
    }
}

public sealed class StatusAssertionJsonConverter : JsonConverter<StatusAssertion>
{
    public override StatusAssertion Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        if (reader.TokenType == JsonTokenType.String)
        {
            var str = reader.GetString();
            if (str is not null && StatusAssertionMaps.StatusAssertionStrToVal.TryGetValue(str, out var mapped))
                return mapped;
            return StatusAssertion.Unknown;
        }

        if (reader.TokenType == JsonTokenType.Number && reader.TryGetInt32(out var v))
        {
            return StatusAssertionMaps.StatusAssertionIntToVal.TryGetValue(v, out var mapped)
                ? mapped
                : StatusAssertion.Unknown;
        }

        return StatusAssertion.Unknown;
    }

    public override void Write(Utf8JsonWriter writer, StatusAssertion value, JsonSerializerOptions options)
    {
        if (!StatusAssertionMaps.StatusAssertionValToStr.TryGetValue(value, out var str))
            str = StatusAssertionMaps.StatusAssertionValToStr[StatusAssertion.Unknown];
        writer.WriteStringValue(str);
    }
}

public sealed class ChainLink
{
    public X509Certificate2? Leaf { get; set; }
    public X509Certificate2? Issuer { get; set; }
    public IReadOnlyList<Uri>? OCSPWebEndpoints { get; set; }
}

public sealed class OcspResponseInfo
{
    public DateTime ThisUpdate { get; init; }
    public DateTime? NextUpdate { get; init; }
}

public sealed class CertInfo
{
    [JsonPropertyName("subject")]
    public string Subject { get; init; } = string.Empty;

    [JsonPropertyName("issuer")]
    public string Issuer { get; init; } = string.Empty;

    [JsonPropertyName("fingerprint")]
    public string Fingerprint { get; init; } = string.Empty;

    [JsonPropertyName("raw")]
    public byte[] Raw { get; init; } = [];
}

public sealed class OCSPPeerConfig
{
    public static readonly TimeSpan DefaultAllowedClockSkew = TimeSpan.FromSeconds(30);
    public static readonly TimeSpan DefaultOCSPResponderTimeout = TimeSpan.FromSeconds(2);
    public static readonly TimeSpan DefaultTTLUnsetNextUpdate = TimeSpan.FromHours(1);

    public bool Verify { get; set; }
    public double Timeout { get; set; } = DefaultOCSPResponderTimeout.TotalSeconds;
    public double ClockSkew { get; set; } = DefaultAllowedClockSkew.TotalSeconds;
    public bool WarnOnly { get; set; }
    public bool UnknownIsGood { get; set; }
    public bool AllowWhenCAUnreachable { get; set; }
    public double TTLUnsetNextUpdate { get; set; } = DefaultTTLUnsetNextUpdate.TotalSeconds;

    public static OCSPPeerConfig NewOCSPPeerConfig() => new();

    public static OCSPPeerConfig Parse(IReadOnlyDictionary<string, object?> values)
    {
        var cfg = NewOCSPPeerConfig();
        foreach (var (key, rawValue) in values)
        {
            switch (key.ToLowerInvariant())
            {
                case "verify":
                    cfg.Verify = ParseBool(rawValue, key);
                    break;
                case "allowed_clockskew":
                    ApplyIfNonNegative(rawValue, key, v => cfg.ClockSkew = v);
                    break;
                case "ca_timeout":
                    ApplyIfNonNegative(rawValue, key, v => cfg.Timeout = v);
                    break;
                case "cache_ttl_when_next_update_unset":
                    ApplyIfNonNegative(rawValue, key, v => cfg.TTLUnsetNextUpdate = v);
                    break;
                case "warn_only":
                    cfg.WarnOnly = ParseBool(rawValue, key);
                    break;
                case "unknown_is_good":
                    cfg.UnknownIsGood = ParseBool(rawValue, key);
                    break;
                case "allow_when_ca_unreachable":
                    cfg.AllowWhenCAUnreachable = ParseBool(rawValue, key);
                    break;
                default:
                    throw new FormatException($"error parsing tls peer config, unknown field [{key}]");
            }
        }

        return cfg;
    }

    private static bool ParseBool(object? rawValue, string key)
    {
        if (rawValue is bool b)
            return b;
        throw new FormatException($"error parsing tls peer config, unknown field [{key}]");
    }

    private static void ApplyIfNonNegative(object? rawValue, string key, Action<double> apply)
    {
        var parsed = ParseSeconds(rawValue, key);
        if (parsed >= 0)
            apply(parsed);
    }

    private static double ParseSeconds(object? rawValue, string key)
    {
        try
        {
            return rawValue switch
            {
                long l => l,
                double d => d,
                string s => ConfigProcessor.ParseDuration(s).TotalSeconds,
                _ => throw new FormatException("unexpected type"),
            };
        }
        catch (Exception ex)
        {
            throw new FormatException($"error parsing tls peer config, conversion error: {ex.Message}", ex);
        }
    }
}
