using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
using Nexus.Api.Models;

namespace Nexus.Api.Services;

public sealed class OpenClawEventProjectionService(IGatewayConnector connector)
    : IOpenClawEventProjectionService
{
    public OpenClawEventBatch Project(string? lastEventId, int limit = 500)
    {
        var projectedAt = DateTimeOffset.UtcNow;
        var requestedCursor = NormalizeCursor(lastEventId);
        var envelopes = connector
            .GetRecentEvents(Math.Clamp(limit, 1, 500))
            .OrderBy(item => item.ReceivedAt)
            .ThenBy(item => item.Sequence)
            .ThenBy(OpenClawEventIdentity.Create, StringComparer.Ordinal)
            .ToList();

        var allEvents = ProjectEvents(envelopes);
        var oldestId = allEvents.FirstOrDefault()?.Id;
        var latestId = allEvents.LastOrDefault()?.Id;
        var replayBoundaryMissed = false;
        IReadOnlyList<OpenClawStreamEventDto> selected = allEvents;

        if (requestedCursor is not null)
        {
            var cursorIndex = allEvents.FindIndex(item =>
                string.Equals(item.Id, requestedCursor, StringComparison.Ordinal));
            if (cursorIndex >= 0)
            {
                selected = allEvents.Skip(cursorIndex + 1).ToList();
            }
            else
            {
                replayBoundaryMissed = true;
            }
        }

        var cursor = selected.LastOrDefault()?.Id
            ?? (replayBoundaryMissed ? latestId ?? "origin" : requestedCursor);

        return new OpenClawEventBatch(
            selected,
            cursor,
            replayBoundaryMissed,
            oldestId,
            latestId,
            projectedAt);
    }

    public OpenClawStreamEventDto CreateConnectionEvent(string? cursor)
    {
        var occurredAt = DateTimeOffset.UtcNow;
        return new OpenClawStreamEventDto(
            NormalizeCursor(cursor) ?? "origin",
            "openclaw.connection",
            "connection",
            "connection",
            null,
            null,
            null,
            false,
            false,
            null,
            null,
            occurredAt,
            new JsonObject
            {
                ["state"] = connector.ConnectionState.ToString().ToLowerInvariant(),
                ["connected"] = connector.ConnectionState == GatewayConnectionState.Connected,
                ["gatewayVersion"] = connector.GatewayVersion,
                ["protocolVersion"] = connector.ProtocolVersion,
                ["deviceId"] = connector.DeviceId,
                ["deviceTokenConfigured"] = connector.DeviceTokenConfigured,
                ["pairingRequired"] = connector.PairingRequired,
                ["pairingRequestId"] = connector.PairingRequestId,
                ["lastConnectedAt"] = connector.LastConnectedAt,
                ["lastEventAt"] = connector.LastEventAt,
                ["reconnectAttempts"] = connector.ReconnectAttempts,
                ["message"] = connector.StatusMessage
            });
    }

    public OpenClawStreamEventDto CreateHeartbeatEvent(string? cursor)
    {
        var occurredAt = DateTimeOffset.UtcNow;
        return new OpenClawStreamEventDto(
            NormalizeCursor(cursor) ?? "origin",
            "openclaw.heartbeat",
            "heartbeat",
            "heartbeat",
            null,
            null,
            null,
            false,
            false,
            null,
            null,
            occurredAt,
            new JsonObject
            {
                ["connected"] = connector.ConnectionState == GatewayConnectionState.Connected,
                ["lastEventAt"] = connector.LastEventAt,
                ["sentAt"] = occurredAt
            });
    }

    private static List<OpenClawStreamEventDto> ProjectEvents(
        IReadOnlyList<GatewayEventEnvelope> envelopes)
    {
        var result = new List<OpenClawStreamEventDto>(envelopes.Count);
        long? previousSequence = null;

        foreach (var envelope in envelopes)
        {
            var sequenceGap = envelope.Sequence.HasValue
                              && previousSequence.HasValue
                              && envelope.Sequence.Value > previousSequence.Value + 1;
            var sequenceReset = envelope.Sequence.HasValue
                                && previousSequence.HasValue
                                && envelope.Sequence.Value < previousSequence.Value;
            var missingFrom = sequenceGap ? previousSequence + 1 : null;
            var missingTo = sequenceGap ? envelope.Sequence - 1 : null;
            var category = Classify(envelope.Event, envelope.Payload);

            result.Add(new OpenClawStreamEventDto(
                OpenClawEventIdentity.Create(envelope),
                $"openclaw.{category}",
                envelope.Event,
                category,
                envelope.Sequence,
                envelope.StateVersion,
                previousSequence,
                sequenceGap,
                sequenceReset,
                missingFrom,
                missingTo,
                envelope.ReceivedAt,
                OpenClawPayloadSanitizer.Redact(envelope.Payload)));

            if (envelope.Sequence.HasValue)
                previousSequence = envelope.Sequence;
        }

        return result;
    }

    private static string Classify(string eventName, JsonNode? payload)
    {
        var normalized = eventName.ToLowerInvariant();
        if (normalized.Contains("approval", StringComparison.Ordinal))
            return "approval";
        if (normalized.Contains("artifact", StringComparison.Ordinal))
            return "artifact";
        if (normalized.Contains("tool", StringComparison.Ordinal))
            return "tool";
        if (normalized.Contains("session", StringComparison.Ordinal))
            return "session";
        if (normalized is "chat" or "agent"
            || normalized.Contains("run", StringComparison.Ordinal)
            || payload?["runId"] is not null)
        {
            return "run";
        }

        return "gateway";
    }

    private static string? NormalizeCursor(string? cursor)
    {
        var trimmed = cursor?.Trim();
        return string.IsNullOrWhiteSpace(trimmed)
               || string.Equals(trimmed, "origin", StringComparison.Ordinal)
            ? null
            : trimmed;
    }
}

internal static class OpenClawEventIdentity
{
    private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web);

    public static string Create(GatewayEventEnvelope envelope)
    {
        var payload = envelope.Payload?.ToJsonString(JsonOptions) ?? "null";
        var fingerprint = string.Join(
            "\u001f",
            envelope.Event,
            envelope.ReceivedAt.UtcDateTime.Ticks,
            envelope.Sequence?.ToString() ?? string.Empty,
            envelope.StateVersion?.ToString() ?? string.Empty,
            payload);
        var hash = SHA256.HashData(Encoding.UTF8.GetBytes(fingerprint));
        return $"gw-{envelope.ReceivedAt.UtcDateTime.Ticks:x16}-{Convert.ToHexString(hash.AsSpan(0, 8)).ToLowerInvariant()}";
    }
}
