using System.Text;
using System.Text.Json;
using NATS.Server.JetStream.Cluster;
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, ReadOnlySpan<byte> payload, ConsumerManager consumerManager)
    {
        var stream = ParseStreamSubject(subject, NamesPrefix);
        if (stream == null)
            return JetStreamApiResponse.NotFound(subject);

        var offset = ParseOffset(payload);
        var all = consumerManager.ListNames(stream);
        var page = offset >= all.Count ? [] : all.Skip(offset).ToList();
        return new JetStreamApiResponse
        {
            ConsumerNames = page,
            PaginationTotal = all.Count,
            PaginationOffset = offset,
        };
    }

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

        var offset = ParseOffset(payload);
        var all = consumerManager.ListConsumerInfos(stream);
        var page = offset >= all.Count ? [] : all.Skip(offset).ToList();
        return new JetStreamApiResponse
        {
            ConsumerInfoList = page,
            ConsumerNames = page.Select(c => c.Name ?? string.Empty).ToList(),
            PaginationTotal = all.Count,
            PaginationOffset = offset,
        };
    }

    private static int ParseOffset(ReadOnlySpan<byte> payload)
    {
        if (payload.IsEmpty) return 0;
        try
        {
            using var doc = JsonDocument.Parse(payload.ToArray());
            if (doc.RootElement.TryGetProperty("offset", out var el) && el.TryGetInt32(out var v))
                return Math.Max(v, 0);
        }
        catch (JsonException ex)
        {
            System.Diagnostics.Debug.WriteLine($"Malformed offset payload: {ex.Message}");
        }

        return 0;
    }

    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, pauseUntil) = ParsePauseRequest(payload);

        bool success;
        if (pauseUntil.HasValue)
            success = consumerManager.Pause(stream, durableName, pauseUntil.Value);
        else if (paused)
            success = consumerManager.Pause(stream, durableName, true);
        else
            success = consumerManager.Resume(stream, durableName);

        if (!success)
            return JetStreamApiResponse.NotFound(subject);

        return JetStreamApiResponse.PauseResponse(
            consumerManager.IsPaused(stream, durableName),
            consumerManager.GetPauseUntil(stream, durableName));
    }

    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(),
            },
        };
    }

    // ---------------------------------------------------------------
    // Clustered handlers — propose to meta RAFT group instead of local ConsumerManager.
    // Go reference: jetstream_cluster.go:8100-8265 jsClusteredConsumerRequest and related.
    // ---------------------------------------------------------------

    /// <summary>
    /// Proposes a consumer create to the meta RAFT group.
    /// Validates that this node is the leader and the parent stream exists, then calls
    /// <see cref="JetStreamMetaGroup.ProposeCreateConsumerValidatedAsync"/>.
    /// Go reference: jetstream_cluster.go:8100 jsClusteredConsumerRequest.
    /// </summary>
    public static async Task<JetStreamApiResponse> HandleClusteredCreateAsync(
        string subject,
        byte[] payload,
        JetStreamMetaGroup metaGroup,
        CancellationToken ct)
    {
        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;

        var consumerName = string.IsNullOrWhiteSpace(config.DurableName) ? durableName : config.DurableName;
        var group = new RaftGroup { Name = $"{stream}-{consumerName}" };

        try
        {
            await metaGroup.ProposeCreateConsumerValidatedAsync(stream, consumerName, group, ct);
            return JetStreamApiResponse.SuccessResponse();
        }
        catch (InvalidOperationException ex) when (ex.Message.StartsWith("Not the meta-group leader", StringComparison.Ordinal))
        {
            return JetStreamApiResponse.NotLeader(metaGroup.Leader);
        }
        catch (InvalidOperationException ex)
        {
            return JetStreamApiResponse.ErrorResponse(400, ex.Message);
        }
    }

    /// <summary>
    /// Proposes a consumer deletion to the meta RAFT group.
    /// Validates that this node is the leader and the consumer exists, then calls
    /// <see cref="JetStreamMetaGroup.ProposeDeleteConsumerValidatedAsync"/>.
    /// Go reference: jetstream_cluster.go jsClusteredConsumerDeleteRequest.
    /// </summary>
    public static async Task<JetStreamApiResponse> HandleClusteredDeleteAsync(
        string subject,
        JetStreamMetaGroup metaGroup,
        CancellationToken ct)
    {
        var parsed = ParseSubject(subject, DeletePrefix);
        if (parsed == null)
            return JetStreamApiResponse.NotFound(subject);

        var (stream, consumerName) = parsed.Value;

        // Validate that the consumer assignment exists before proposing deletion.
        if (metaGroup.GetConsumerAssignment(stream, consumerName) == null)
            return JetStreamApiResponse.ErrorResponse(404, $"consumer not found: '{consumerName}' on stream '{stream}'");

        try
        {
            await metaGroup.ProposeDeleteConsumerValidatedAsync(stream, consumerName, ct);
            return JetStreamApiResponse.SuccessResponse();
        }
        catch (InvalidOperationException ex) when (ex.Message.StartsWith("Not the meta-group leader", StringComparison.Ordinal))
        {
            return JetStreamApiResponse.NotLeader(metaGroup.Leader);
        }
        catch (InvalidOperationException ex)
        {
            return JetStreamApiResponse.ErrorResponse(400, ex.Message);
        }
    }

    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;

            // The client wraps config in a "config" property (CreateConsumerRequest).
            // Go reference: consumer.go — CreateConsumerRequest { Config ConsumerConfig `json:"config"` }
            var configEl = root.TryGetProperty("config", out var nested) ? nested : root;
            var config = new ConsumerConfig();

            if (configEl.TryGetProperty("durable_name", out var durableEl))
                config.DurableName = durableEl.GetString() ?? string.Empty;
            else if (configEl.TryGetProperty("name", out var nameEl))
                config.DurableName = nameEl.GetString() ?? string.Empty;

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

            if (configEl.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 (configEl.TryGetProperty("ephemeral", out var ephemeralEl) && ephemeralEl.ValueKind == JsonValueKind.True)
                config.Ephemeral = true;

            // Go: consumer.go — deliver_subject marks a consumer as push-based.
            // Reference: server/consumer.go:deliverSubject field on ConsumerConfig
            if (configEl.TryGetProperty("deliver_subject", out var deliverSubjectEl))
            {
                var ds = deliverSubjectEl.GetString();
                if (!string.IsNullOrWhiteSpace(ds))
                {
                    config.DeliverSubject = ds;
                    config.Push = true; // presence of deliver_subject implies push mode
                }
            }

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

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

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

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

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

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

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

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

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

            if (configEl.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 (configEl.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 (configEl.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 (configEl.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;
    }

    /// <summary>
    /// Parses pause request payload supporting both a boolean <c>pause</c> field and an RFC3339
    /// <c>pause_until</c> deadline. When <c>pause_until</c> is present it implies <c>pause=true</c>.
    /// Go reference: server/consumer.go jsConsumerPauseRequest.
    /// </summary>
    private static (bool Paused, DateTime? PauseUntil) ParsePauseRequest(ReadOnlySpan<byte> payload)
    {
        if (payload.IsEmpty)
            return (false, null);

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

            DateTime? pauseUntil = null;
            if (root.TryGetProperty("pause_until", out var untilEl)
                && untilEl.ValueKind == JsonValueKind.String
                && DateTime.TryParse(untilEl.GetString(), null, System.Globalization.DateTimeStyles.RoundtripKind, out var dt))
            {
                pauseUntil = dt.ToUniversalTime();
            }

            bool paused = false;
            if (root.TryGetProperty("pause", out var pauseEl))
                paused = pauseEl.ValueKind == JsonValueKind.True;

            // pause_until implies pause=true (Go reference: consumer.go pauseConsumer).
            if (pauseUntil.HasValue)
                paused = true;

            return (paused, pauseUntil);
        }
        catch (JsonException)
        {
        }

        return (false, null);
    }

    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;
    }
}
