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

namespace NATS.Server.JetStream.Api.Handlers;

/// <summary>
/// Purge request options. Go reference: jetstream_api.go:1200-1350.
/// </summary>
public sealed record PurgeRequest
{
    /// <summary>Subject filter — only purge messages matching this subject pattern.</summary>
    public string? Filter { get; init; }

    /// <summary>Purge all messages with sequence strictly less than this value.</summary>
    public ulong? Seq { get; init; }

    /// <summary>Keep the last N messages (per matching subject if filter is set).</summary>
    public ulong? Keep { get; init; }
}

public static class StreamApiHandlers
{
    private const string CreatePrefix = JetStreamApiSubjects.StreamCreate;
    private const string InfoPrefix = JetStreamApiSubjects.StreamInfo;
    private const string UpdatePrefix = JetStreamApiSubjects.StreamUpdate;
    private const string DeletePrefix = JetStreamApiSubjects.StreamDelete;
    private const string PurgePrefix = JetStreamApiSubjects.StreamPurge;
    private const string MessageGetPrefix = JetStreamApiSubjects.StreamMessageGet;
    private const string MessageDeletePrefix = JetStreamApiSubjects.StreamMessageDelete;
    private const string SnapshotPrefix = JetStreamApiSubjects.StreamSnapshot;
    private const string RestorePrefix = JetStreamApiSubjects.StreamRestore;

    public static JetStreamApiResponse HandleCreate(string subject, ReadOnlySpan<byte> payload, StreamManager streamManager)
    {
        var streamName = ExtractTrailingToken(subject, CreatePrefix);
        if (streamName == null)
            return JetStreamApiResponse.NotFound(subject);

        var config = ParseConfig(payload);
        if (string.IsNullOrWhiteSpace(config.Name))
            config.Name = streamName;

        if (config.Subjects.Count == 0)
            config.Subjects.Add(streamName.ToLowerInvariant() + ".>");

        return streamManager.CreateOrUpdate(config);
    }

    public static JetStreamApiResponse HandleInfo(string subject, StreamManager streamManager)
    {
        var streamName = ExtractTrailingToken(subject, InfoPrefix);
        if (streamName == null)
            return JetStreamApiResponse.NotFound(subject);

        return streamManager.GetInfo(streamName);
    }

    public static JetStreamApiResponse HandleUpdate(string subject, ReadOnlySpan<byte> payload, StreamManager streamManager)
    {
        var streamName = ExtractTrailingToken(subject, UpdatePrefix);
        if (streamName == null)
            return JetStreamApiResponse.NotFound(subject);

        var config = ParseConfig(payload);
        if (string.IsNullOrWhiteSpace(config.Name))
            config.Name = streamName;

        if (config.Subjects.Count == 0)
            config.Subjects.Add(streamName.ToLowerInvariant() + ".>");

        return streamManager.CreateOrUpdate(config);
    }

    public static JetStreamApiResponse HandleDelete(string subject, StreamManager streamManager)
    {
        var streamName = ExtractTrailingToken(subject, DeletePrefix);
        if (streamName == null)
            return JetStreamApiResponse.NotFound(subject);

        return streamManager.Delete(streamName)
            ? JetStreamApiResponse.SuccessResponse()
            : JetStreamApiResponse.NotFound(subject);
    }

    /// <summary>
    /// Handles stream purge with optional filter, seq, and keep options.
    /// Go reference: jetstream_api.go:1200-1350.
    /// </summary>
    public static JetStreamApiResponse HandlePurge(string subject, ReadOnlySpan<byte> payload, StreamManager streamManager)
    {
        var streamName = ExtractTrailingToken(subject, PurgePrefix);
        if (streamName == null)
            return JetStreamApiResponse.NotFound(subject);

        var request = ParsePurgeRequest(payload);
        var purged = streamManager.PurgeEx(streamName, request.Filter, request.Seq, request.Keep);
        if (purged < 0)
            return JetStreamApiResponse.NotFound(subject);

        return JetStreamApiResponse.PurgeResponse((ulong)purged);
    }

    public static JetStreamApiResponse HandleNames(ReadOnlySpan<byte> payload, StreamManager streamManager)
    {
        var offset = ParseOffset(payload);
        var all = streamManager.ListNames();
        var page = offset >= all.Count ? [] : all.Skip(offset).ToList();
        return new JetStreamApiResponse
        {
            StreamNames = page,
            PaginationTotal = all.Count,
            PaginationOffset = offset,
        };
    }

    public static JetStreamApiResponse HandleList(ReadOnlySpan<byte> payload, StreamManager streamManager)
    {
        var offset = ParseOffset(payload);
        var all = streamManager.ListStreamInfos();
        var page = offset >= all.Count ? [] : all.Skip(offset).ToList();
        return new JetStreamApiResponse
        {
            StreamInfoList = page,
            PaginationTotal = all.Count,
            PaginationOffset = offset,
        };
    }

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

        return 0;
    }

    public static JetStreamApiResponse HandleMessageGet(string subject, ReadOnlySpan<byte> payload, StreamManager streamManager)
    {
        var streamName = ExtractTrailingToken(subject, MessageGetPrefix);
        if (streamName == null)
            return JetStreamApiResponse.NotFound(subject);

        var sequence = ParseSequence(payload);
        if (sequence == 0)
            return JetStreamApiResponse.ErrorResponse(400, "sequence required");

        var message = streamManager.GetMessage(streamName, sequence);
        if (message == null)
            return JetStreamApiResponse.NotFound(subject);

        return new JetStreamApiResponse
        {
            StreamMessage = new JetStreamStreamMessage
            {
                Sequence = message.Sequence,
                Subject = message.Subject,
                Payload = Encoding.UTF8.GetString(message.Payload.Span),
            },
        };
    }

    public static JetStreamApiResponse HandleMessageDelete(string subject, ReadOnlySpan<byte> payload, StreamManager streamManager)
    {
        var streamName = ExtractTrailingToken(subject, MessageDeletePrefix);
        if (streamName == null)
            return JetStreamApiResponse.NotFound(subject);

        var sequence = ParseSequence(payload);
        if (sequence == 0)
            return JetStreamApiResponse.ErrorResponse(400, "sequence required");

        return streamManager.DeleteMessage(streamName, sequence)
            ? JetStreamApiResponse.SuccessResponse()
            : JetStreamApiResponse.NotFound(subject);
    }

    public static JetStreamApiResponse HandleSnapshot(string subject, StreamManager streamManager)
    {
        var streamName = ExtractTrailingToken(subject, SnapshotPrefix);
        if (streamName == null)
            return JetStreamApiResponse.NotFound(subject);

        var snapshot = streamManager.CreateSnapshot(streamName);
        if (snapshot == null)
            return JetStreamApiResponse.NotFound(subject);

        return new JetStreamApiResponse
        {
            Snapshot = new JetStreamSnapshot
            {
                Payload = Convert.ToBase64String(snapshot),
            },
        };
    }

    public static JetStreamApiResponse HandleRestore(string subject, ReadOnlySpan<byte> payload, StreamManager streamManager)
    {
        var streamName = ExtractTrailingToken(subject, RestorePrefix);
        if (streamName == null)
            return JetStreamApiResponse.NotFound(subject);

        var snapshotBytes = ParseRestorePayload(payload);
        if (snapshotBytes == null)
            return JetStreamApiResponse.ErrorResponse(400, "snapshot payload required");

        return streamManager.RestoreSnapshot(streamName, snapshotBytes)
            ? JetStreamApiResponse.SuccessResponse()
            : JetStreamApiResponse.NotFound(subject);
    }

    /// <summary>
    /// Async snapshot handler that validates stream existence before creating the snapshot,
    /// and enriches the response with stream name and chunk metadata.
    /// Go reference: server/jetstream_api.go — jsStreamSnapshotT handler.
    /// </summary>
    public static async Task<JetStreamApiResponse> HandleSnapshotAsync(
        string subject,
        StreamManager streamManager,
        CancellationToken ct)
    {
        _ = ct;

        var streamName = ExtractTrailingToken(subject, SnapshotPrefix);
        if (streamName == null)
            return JetStreamApiResponse.NotFound(subject);

        if (!streamManager.Exists(streamName))
            return JetStreamApiResponse.NotFound(subject);

        var snapshot = streamManager.CreateSnapshot(streamName);
        if (snapshot == null)
            return JetStreamApiResponse.ErrorResponse(500, "snapshot creation failed");

        return new JetStreamApiResponse
        {
            Snapshot = new JetStreamSnapshot
            {
                Payload = Convert.ToBase64String(snapshot),
                StreamName = streamName,
                NumChunks = 1,
                BlkSize = snapshot.Length,
            },
        };
    }

    /// <summary>
    /// Async restore handler that validates the payload and returns a structured error on failure.
    /// Go reference: server/jetstream_api.go — jsStreamRestoreT handler.
    /// </summary>
    public static async Task<JetStreamApiResponse> HandleRestoreAsync(
        string subject,
        byte[] payload,
        StreamManager streamManager,
        CancellationToken ct)
    {
        _ = ct;

        var streamName = ExtractTrailingToken(subject, RestorePrefix);
        if (streamName == null)
            return JetStreamApiResponse.NotFound(subject);

        var snapshotBytes = ParseRestorePayload(payload);
        if (snapshotBytes == null)
            return JetStreamApiResponse.ErrorResponse(400, "snapshot payload required");

        return streamManager.RestoreSnapshot(streamName, snapshotBytes)
            ? JetStreamApiResponse.SuccessResponse()
            : JetStreamApiResponse.ErrorResponse(500, "restore failed");
    }

    // ---------------------------------------------------------------
    // Clustered handlers — propose to meta RAFT group instead of local StreamManager.
    // Go reference: jetstream_cluster.go:7620-7900 jsClusteredStreamRequest and related.
    // ---------------------------------------------------------------

    /// <summary>
    /// Proposes a stream create to the meta RAFT group.
    /// Validates that this node is the leader, then calls
    /// <see cref="JetStreamMetaGroup.ProposeCreateStreamValidatedAsync"/>.
    /// Go reference: jetstream_cluster.go:7620 jsClusteredStreamRequest.
    /// </summary>
    public static async Task<JetStreamApiResponse> HandleClusteredCreateAsync(
        string subject,
        byte[] payload,
        JetStreamMetaGroup metaGroup,
        CancellationToken ct)
    {
        var streamName = ExtractTrailingToken(subject, CreatePrefix);
        if (streamName == null)
            return JetStreamApiResponse.NotFound(subject);

        var config = ParseConfig(payload);
        if (string.IsNullOrWhiteSpace(config.Name))
            config.Name = streamName;

        try
        {
            await metaGroup.ProposeCreateStreamValidatedAsync(config, group: null, 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 stream config update to the meta RAFT group.
    /// Calls <see cref="JetStreamMetaGroup.ProcessUpdateStreamAssignment"/> after validating leadership.
    /// Go reference: jetstream_cluster.go jsClusteredStreamUpdateRequest.
    /// </summary>
    public static async Task<JetStreamApiResponse> HandleClusteredUpdateAsync(
        string subject,
        byte[] payload,
        JetStreamMetaGroup metaGroup,
        CancellationToken ct)
    {
        _ = ct;

        var streamName = ExtractTrailingToken(subject, UpdatePrefix);
        if (streamName == null)
            return JetStreamApiResponse.NotFound(subject);

        if (!metaGroup.IsLeader())
            return JetStreamApiResponse.NotLeader(metaGroup.Leader);

        var config = ParseConfig(payload);
        if (string.IsNullOrWhiteSpace(config.Name))
            config.Name = streamName;

        var existing = metaGroup.GetStreamAssignment(streamName);
        if (existing == null)
            return JetStreamApiResponse.NotFound(subject);

        var sa = new StreamAssignment
        {
            StreamName = streamName,
            Group = existing.Group,
        };

        var updated = metaGroup.ProcessUpdateStreamAssignment(sa);
        if (!updated)
            return JetStreamApiResponse.NotFound(subject);

        return JetStreamApiResponse.SuccessResponse();
    }

    /// <summary>
    /// Proposes a stream deletion to the meta RAFT group.
    /// Calls <see cref="JetStreamMetaGroup.ProposeDeleteStreamValidatedAsync"/> after validating leadership.
    /// Go reference: jetstream_cluster.go jsClusteredStreamDeleteRequest.
    /// </summary>
    public static async Task<JetStreamApiResponse> HandleClusteredDeleteAsync(
        string subject,
        JetStreamMetaGroup metaGroup,
        CancellationToken ct)
    {
        var streamName = ExtractTrailingToken(subject, DeletePrefix);
        if (streamName == null)
            return JetStreamApiResponse.NotFound(subject);

        // Check stream exists before attempting deletion.
        if (metaGroup.GetStreamAssignment(streamName) == null)
            return JetStreamApiResponse.ErrorResponse(404, $"stream not found: '{streamName}'");

        try
        {
            await metaGroup.ProposeDeleteStreamValidatedAsync(streamName, 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? ExtractTrailingToken(string subject, string prefix)
    {
        if (!subject.StartsWith(prefix, StringComparison.Ordinal))
            return null;

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

    internal static PurgeRequest ParsePurgeRequest(ReadOnlySpan<byte> payload)
    {
        if (payload.IsEmpty)
            return new PurgeRequest();

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

            string? filter = null;
            ulong? seq = null;
            ulong? keep = null;

            if (root.TryGetProperty("filter", out var filterEl) && filterEl.ValueKind == JsonValueKind.String)
                filter = filterEl.GetString();

            if (root.TryGetProperty("seq", out var seqEl) && seqEl.TryGetUInt64(out var seqVal))
                seq = seqVal;

            if (root.TryGetProperty("keep", out var keepEl) && keepEl.TryGetUInt64(out var keepVal))
                keep = keepVal;

            return new PurgeRequest { Filter = filter, Seq = seq, Keep = keep };
        }
        catch (JsonException)
        {
            return new PurgeRequest();
        }
    }

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

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

            if (root.TryGetProperty("name", out var nameEl))
                config.Name = nameEl.GetString() ?? string.Empty;

            if (root.TryGetProperty("subjects", out var subjectsEl))
            {
                if (subjectsEl.ValueKind == JsonValueKind.Array)
                {
                    foreach (var item in subjectsEl.EnumerateArray())
                    {
                        var value = item.GetString();
                        if (!string.IsNullOrWhiteSpace(value))
                            config.Subjects.Add(value);
                    }
                }
                else if (subjectsEl.ValueKind == JsonValueKind.String)
                {
                    var value = subjectsEl.GetString();
                    if (!string.IsNullOrWhiteSpace(value))
                        config.Subjects.Add(value);
                }
            }

            if (root.TryGetProperty("max_msgs", out var maxMsgsEl) && maxMsgsEl.TryGetInt32(out var maxMsgs))
                config.MaxMsgs = maxMsgs;

            if (root.TryGetProperty("max_bytes", out var maxBytesEl) && maxBytesEl.TryGetInt64(out var maxBytes))
                config.MaxBytes = maxBytes;

            if (root.TryGetProperty("max_msgs_per", out var maxMsgsPerEl) && maxMsgsPerEl.TryGetInt32(out var maxMsgsPer))
                config.MaxMsgsPer = maxMsgsPer;

            if (root.TryGetProperty("max_age", out var maxAgeNsEl) && maxAgeNsEl.TryGetInt64(out var maxAgeNs))
                config.MaxAge = maxAgeNs;
            else if (root.TryGetProperty("max_age_ms", out var maxAgeMsEl) && maxAgeMsEl.TryGetInt32(out var maxAgeMs))
                config.MaxAgeMs = maxAgeMs;

            if (root.TryGetProperty("max_msg_size", out var maxMsgSizeEl) && maxMsgSizeEl.TryGetInt32(out var maxMsgSize))
                config.MaxMsgSize = maxMsgSize;

            if (root.TryGetProperty("duplicate_window_ms", out var dupWindowEl) && dupWindowEl.TryGetInt32(out var dupWindow))
                config.DuplicateWindowMs = dupWindow;

            if (root.TryGetProperty("sealed", out var sealedEl) && sealedEl.ValueKind is JsonValueKind.True or JsonValueKind.False)
                config.Sealed = sealedEl.GetBoolean();

            if (root.TryGetProperty("deny_delete", out var denyDeleteEl) && denyDeleteEl.ValueKind is JsonValueKind.True or JsonValueKind.False)
                config.DenyDelete = denyDeleteEl.GetBoolean();

            if (root.TryGetProperty("deny_purge", out var denyPurgeEl) && denyPurgeEl.ValueKind is JsonValueKind.True or JsonValueKind.False)
                config.DenyPurge = denyPurgeEl.GetBoolean();

            if (root.TryGetProperty("allow_direct", out var allowDirectEl) && allowDirectEl.ValueKind is JsonValueKind.True or JsonValueKind.False)
                config.AllowDirect = allowDirectEl.GetBoolean();

            if (root.TryGetProperty("discard", out var discardEl))
            {
                var discard = discardEl.GetString();
                if (string.Equals(discard, "new", StringComparison.OrdinalIgnoreCase))
                    config.Discard = DiscardPolicy.New;
                else if (string.Equals(discard, "old", StringComparison.OrdinalIgnoreCase))
                    config.Discard = DiscardPolicy.Old;
            }

            if (root.TryGetProperty("storage", out var storageEl))
            {
                var storage = storageEl.GetString();
                if (string.Equals(storage, "file", StringComparison.OrdinalIgnoreCase))
                    config.Storage = StorageType.File;
                else
                    config.Storage = StorageType.Memory;
            }

            if (root.TryGetProperty("source", out var sourceEl))
                config.Source = sourceEl.GetString();

            if (root.TryGetProperty("sources", out var sourcesEl) && sourcesEl.ValueKind == JsonValueKind.Array)
            {
                foreach (var source in sourcesEl.EnumerateArray())
                {
                    if (source.ValueKind == JsonValueKind.String)
                    {
                        var name = source.GetString();
                        if (!string.IsNullOrWhiteSpace(name))
                            config.Sources.Add(new StreamSourceConfig { Name = name });
                    }
                    else if (source.ValueKind == JsonValueKind.Object &&
                        source.TryGetProperty("name", out var sourceNameEl))
                    {
                        var name = sourceNameEl.GetString();
                        if (!string.IsNullOrWhiteSpace(name))
                        {
                            var sourceConfig = new StreamSourceConfig { Name = name };
                            if (source.TryGetProperty("subject_transform_prefix", out var prefixEl))
                                sourceConfig.SubjectTransformPrefix = prefixEl.GetString();
                            if (source.TryGetProperty("source_account", out var accountEl))
                                sourceConfig.SourceAccount = accountEl.GetString();
                            config.Sources.Add(sourceConfig);
                        }
                    }
                }
            }

            if (root.TryGetProperty("replicas", out var replicasEl) && replicasEl.TryGetInt32(out var replicas))
                config.Replicas = replicas;

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

    private static ulong ParseSequence(ReadOnlySpan<byte> payload)
    {
        if (payload.IsEmpty)
            return 0;

        try
        {
            using var doc = JsonDocument.Parse(payload.ToArray());
            if (doc.RootElement.TryGetProperty("seq", out var seqEl) && seqEl.TryGetUInt64(out var sequence))
                return sequence;
        }
        catch (JsonException)
        {
        }

        return 0;
    }

    private static byte[]? ParseRestorePayload(ReadOnlySpan<byte> payload)
    {
        if (payload.IsEmpty)
            return null;

        var raw = Encoding.UTF8.GetString(payload).Trim();
        if (raw.Length == 0)
            return null;

        try
        {
            return Convert.FromBase64String(raw);
        }
        catch (FormatException)
        {
        }

        try
        {
            using var doc = JsonDocument.Parse(payload.ToArray());
            if (doc.RootElement.TryGetProperty("payload", out var payloadEl))
            {
                var base64 = payloadEl.GetString();
                if (!string.IsNullOrWhiteSpace(base64))
                    return Convert.FromBase64String(base64);
            }
        }
        catch (JsonException)
        {
        }

        return null;
    }
}
