using System.Text;
using System.Text.Json;
using NATS.Server.JetStream.Models;

namespace NATS.Server.JetStream.Api.Handlers;

public static class ConsumerApiHandlers
{
    private const string CreatePrefix = JetStreamApiSubjects.ConsumerCreate;
    private const string InfoPrefix = JetStreamApiSubjects.ConsumerInfo;
    private const string NamesPrefix = JetStreamApiSubjects.ConsumerNames;
    private const string ListPrefix = JetStreamApiSubjects.ConsumerList;
    private const string DeletePrefix = JetStreamApiSubjects.ConsumerDelete;
    private const string PausePrefix = JetStreamApiSubjects.ConsumerPause;
    private const string ResetPrefix = JetStreamApiSubjects.ConsumerReset;
    private const string UnpinPrefix = JetStreamApiSubjects.ConsumerUnpin;
    private const string NextPrefix = JetStreamApiSubjects.ConsumerNext;

    public static JetStreamApiResponse HandleCreate(string subject, ReadOnlySpan<byte> payload, ConsumerManager consumerManager)
    {
        var parsed = ParseSubject(subject, CreatePrefix);
        if (parsed == null)
            return JetStreamApiResponse.NotFound(subject);

        var (stream, durableName) = parsed.Value;
        var config = ParseConfig(payload);
        if (string.IsNullOrWhiteSpace(config.DurableName))
            config.DurableName = durableName;

        return consumerManager.CreateOrUpdate(stream, config);
    }

    public static JetStreamApiResponse HandleInfo(string subject, ConsumerManager consumerManager)
    {
        var parsed = ParseSubject(subject, InfoPrefix);
        if (parsed == null)
            return JetStreamApiResponse.NotFound(subject);

        var (stream, durableName) = parsed.Value;
        return consumerManager.GetInfo(stream, durableName);
    }

    public static JetStreamApiResponse HandleDelete(string subject, ConsumerManager consumerManager)
    {
        var parsed = ParseSubject(subject, DeletePrefix);
        if (parsed == null)
            return JetStreamApiResponse.NotFound(subject);

        var (stream, durableName) = parsed.Value;
        return consumerManager.Delete(stream, durableName)
            ? JetStreamApiResponse.SuccessResponse()
            : JetStreamApiResponse.NotFound(subject);
    }

    public static JetStreamApiResponse HandleNames(string subject, ConsumerManager consumerManager)
    {
        var stream = ParseStreamSubject(subject, NamesPrefix);
        if (stream == null)
            return JetStreamApiResponse.NotFound(subject);

        return new JetStreamApiResponse
        {
            ConsumerNames = consumerManager.ListNames(stream),
        };
    }

    public static JetStreamApiResponse HandleList(string subject, ConsumerManager consumerManager)
    {
        var stream = ParseStreamSubject(subject, ListPrefix);
        if (stream == null)
            return JetStreamApiResponse.NotFound(subject);

        return new JetStreamApiResponse
        {
            ConsumerNames = consumerManager.ListNames(stream),
        };
    }

    public static JetStreamApiResponse HandlePause(string subject, ReadOnlySpan<byte> payload, ConsumerManager consumerManager)
    {
        var parsed = ParseSubject(subject, PausePrefix);
        if (parsed == null)
            return JetStreamApiResponse.NotFound(subject);

        var (stream, durableName) = parsed.Value;
        var paused = ParsePause(payload);
        return consumerManager.Pause(stream, durableName, paused)
            ? JetStreamApiResponse.SuccessResponse()
            : JetStreamApiResponse.NotFound(subject);
    }

    public static JetStreamApiResponse HandleReset(string subject, ConsumerManager consumerManager)
    {
        var parsed = ParseSubject(subject, ResetPrefix);
        if (parsed == null)
            return JetStreamApiResponse.NotFound(subject);

        var (stream, durableName) = parsed.Value;
        return consumerManager.Reset(stream, durableName)
            ? JetStreamApiResponse.SuccessResponse()
            : JetStreamApiResponse.NotFound(subject);
    }

    public static JetStreamApiResponse HandleUnpin(string subject, ConsumerManager consumerManager)
    {
        var parsed = ParseSubject(subject, UnpinPrefix);
        if (parsed == null)
            return JetStreamApiResponse.NotFound(subject);

        var (stream, durableName) = parsed.Value;
        return consumerManager.Unpin(stream, durableName)
            ? JetStreamApiResponse.SuccessResponse()
            : JetStreamApiResponse.NotFound(subject);
    }

    public static JetStreamApiResponse HandleNext(string subject, ReadOnlySpan<byte> payload, ConsumerManager consumerManager, StreamManager streamManager)
    {
        var parsed = ParseSubject(subject, NextPrefix);
        if (parsed == null)
            return JetStreamApiResponse.NotFound(subject);

        var (stream, durableName) = parsed.Value;
        var batch = ParseBatch(payload);
        var pullBatch = consumerManager.FetchAsync(stream, durableName, batch, streamManager, default).GetAwaiter().GetResult();
        return new JetStreamApiResponse
        {
            PullBatch = new JetStreamPullBatch
            {
                Messages = pullBatch.Messages
                    .Select(m => new JetStreamDirectMessage
                    {
                        Sequence = m.Sequence,
                        Subject = m.Subject,
                        Payload = Encoding.UTF8.GetString(m.Payload.Span),
                    })
                    .ToArray(),
            },
        };
    }

    private static (string Stream, string Durable)? ParseSubject(string subject, string prefix)
    {
        if (!subject.StartsWith(prefix, StringComparison.Ordinal))
            return null;

        var remainder = subject[prefix.Length..];
        var split = remainder.Split('.', 2, StringSplitOptions.RemoveEmptyEntries);
        if (split.Length != 2)
            return null;

        return (split[0], split[1]);
    }

    private static ConsumerConfig ParseConfig(ReadOnlySpan<byte> payload)
    {
        if (payload.IsEmpty)
            return new ConsumerConfig();

        try
        {
            using var doc = JsonDocument.Parse(payload.ToArray());
            var root = doc.RootElement;
            var config = new ConsumerConfig();

            if (root.TryGetProperty("durable_name", out var durableEl))
                config.DurableName = durableEl.GetString() ?? string.Empty;

            if (root.TryGetProperty("filter_subject", out var filterEl))
                config.FilterSubject = filterEl.GetString();

            if (root.TryGetProperty("filter_subjects", out var filterSubjectsEl) && filterSubjectsEl.ValueKind == JsonValueKind.Array)
            {
                foreach (var item in filterSubjectsEl.EnumerateArray())
                {
                    var filter = item.GetString();
                    if (!string.IsNullOrWhiteSpace(filter))
                        config.FilterSubjects.Add(filter);
                }
            }

            if (root.TryGetProperty("ephemeral", out var ephemeralEl) && ephemeralEl.ValueKind == JsonValueKind.True)
                config.Ephemeral = true;

            if (root.TryGetProperty("push", out var pushEl) && pushEl.ValueKind == JsonValueKind.True)
                config.Push = true;

            if (root.TryGetProperty("heartbeat_ms", out var hbEl) && hbEl.TryGetInt32(out var hbMs))
                config.HeartbeatMs = hbMs;

            if (root.TryGetProperty("ack_wait_ms", out var ackWaitEl) && ackWaitEl.TryGetInt32(out var ackWait))
                config.AckWaitMs = ackWait;

            if (root.TryGetProperty("max_deliver", out var maxDeliverEl) && maxDeliverEl.TryGetInt32(out var maxDeliver))
                config.MaxDeliver = Math.Max(maxDeliver, 0);

            if (root.TryGetProperty("max_ack_pending", out var maxAckPendingEl) && maxAckPendingEl.TryGetInt32(out var maxAckPending))
                config.MaxAckPending = Math.Max(maxAckPending, 0);

            if (root.TryGetProperty("flow_control", out var flowControlEl) && flowControlEl.ValueKind is JsonValueKind.True or JsonValueKind.False)
                config.FlowControl = flowControlEl.GetBoolean();

            if (root.TryGetProperty("rate_limit_bps", out var rateLimitEl) && rateLimitEl.TryGetInt64(out var rateLimit))
                config.RateLimitBps = Math.Max(rateLimit, 0);

            if (root.TryGetProperty("opt_start_seq", out var optStartSeqEl) && optStartSeqEl.TryGetUInt64(out var optStartSeq))
                config.OptStartSeq = optStartSeq;

            if (root.TryGetProperty("opt_start_time_utc", out var optStartTimeEl)
                && optStartTimeEl.ValueKind == JsonValueKind.String
                && DateTime.TryParse(optStartTimeEl.GetString(), out var optStartTime))
            {
                config.OptStartTimeUtc = optStartTime.ToUniversalTime();
            }

            if (root.TryGetProperty("backoff_ms", out var backoffEl) && backoffEl.ValueKind == JsonValueKind.Array)
            {
                foreach (var item in backoffEl.EnumerateArray())
                {
                    if (item.TryGetInt32(out var backoffValue))
                        config.BackOffMs.Add(Math.Max(backoffValue, 0));
                }
            }

            if (root.TryGetProperty("ack_policy", out var ackPolicyEl))
            {
                var ackPolicy = ackPolicyEl.GetString();
                if (string.Equals(ackPolicy, "explicit", StringComparison.OrdinalIgnoreCase))
                    config.AckPolicy = AckPolicy.Explicit;
                else if (string.Equals(ackPolicy, "all", StringComparison.OrdinalIgnoreCase))
                    config.AckPolicy = AckPolicy.All;
            }

            if (root.TryGetProperty("deliver_policy", out var deliverPolicyEl))
            {
                var deliver = deliverPolicyEl.GetString();
                if (string.Equals(deliver, "last", StringComparison.OrdinalIgnoreCase))
                    config.DeliverPolicy = DeliverPolicy.Last;
                else if (string.Equals(deliver, "new", StringComparison.OrdinalIgnoreCase))
                    config.DeliverPolicy = DeliverPolicy.New;
                else if (string.Equals(deliver, "by_start_sequence", StringComparison.OrdinalIgnoreCase))
                    config.DeliverPolicy = DeliverPolicy.ByStartSequence;
                else if (string.Equals(deliver, "by_start_time", StringComparison.OrdinalIgnoreCase))
                    config.DeliverPolicy = DeliverPolicy.ByStartTime;
                else if (string.Equals(deliver, "last_per_subject", StringComparison.OrdinalIgnoreCase))
                    config.DeliverPolicy = DeliverPolicy.LastPerSubject;
            }

            if (root.TryGetProperty("replay_policy", out var replayPolicyEl))
            {
                var replay = replayPolicyEl.GetString();
                if (string.Equals(replay, "original", StringComparison.OrdinalIgnoreCase))
                    config.ReplayPolicy = ReplayPolicy.Original;
            }

            return config;
        }
        catch (JsonException)
        {
            return new ConsumerConfig();
        }
    }

    private static int ParseBatch(ReadOnlySpan<byte> payload)
    {
        if (payload.IsEmpty)
            return 1;

        try
        {
            using var doc = JsonDocument.Parse(payload.ToArray());
            if (doc.RootElement.TryGetProperty("batch", out var batchEl) && batchEl.TryGetInt32(out var batch))
                return Math.Max(batch, 1);
        }
        catch (JsonException)
        {
        }

        return 1;
    }

    private static bool ParsePause(ReadOnlySpan<byte> payload)
    {
        if (payload.IsEmpty)
            return false;

        try
        {
            using var doc = JsonDocument.Parse(payload.ToArray());
            if (doc.RootElement.TryGetProperty("pause", out var pauseEl))
                return pauseEl.ValueKind == JsonValueKind.True;
        }
        catch (JsonException)
        {
        }

        return false;
    }

    private static string? ParseStreamSubject(string subject, string prefix)
    {
        if (!subject.StartsWith(prefix, StringComparison.Ordinal))
            return null;

        var stream = subject[prefix.Length..].Trim();
        return stream.Length == 0 ? null : stream;
    }
}
