using System.Text.Json;
using System.Text.Json.Nodes;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Nexus.Api.Models;
using Nexus.Api.Services;

namespace Nexus.Api.Controllers;

/// <summary>
/// Authenticated browser-safe Server-Sent Events projection over the bounded
/// Gateway event buffer. The browser never receives Gateway credentials.
/// </summary>
[Authorize]
[ApiController]
[Route("api/v1/openclaw/events")]
public sealed class OpenClawEventsController(IOpenClawEventProjectionService events) : ControllerBase
{
    private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web);
    private static readonly TimeSpan PollInterval = TimeSpan.FromMilliseconds(750);
    private static readonly TimeSpan HeartbeatInterval = TimeSpan.FromSeconds(15);

    [HttpGet]
    public async Task Stream(
        [FromQuery] string? lastEventId = null,
        [FromQuery] bool follow = true,
        CancellationToken cancellationToken = default)
    {
        Response.StatusCode = StatusCodes.Status200OK;
        Response.ContentType = "text/event-stream";
        Response.Headers.CacheControl = "no-cache, no-store";
        Response.Headers.Connection = "keep-alive";
        Response.Headers["X-Accel-Buffering"] = "no";

        var headerCursor = Request.Headers["Last-Event-ID"].FirstOrDefault();
        var cursor = string.IsNullOrWhiteSpace(headerCursor) ? lastEventId : headerCursor;
        var lastHeartbeatAt = DateTimeOffset.UtcNow;

        var connectionEvent = events.CreateConnectionEvent(cursor);
        var connectionSignature = GetConnectionSignature(connectionEvent);
        await Response.WriteAsync("retry: 2000\n\n", cancellationToken);
        await WriteEventAsync(connectionEvent, cancellationToken);
        await Response.Body.FlushAsync(cancellationToken);

        try
        {
            do
            {
                connectionEvent = events.CreateConnectionEvent(cursor);
                var currentConnectionSignature = GetConnectionSignature(connectionEvent);
                if (!string.Equals(
                        connectionSignature,
                        currentConnectionSignature,
                        StringComparison.Ordinal))
                {
                    await WriteEventAsync(connectionEvent, cancellationToken);
                    connectionSignature = currentConnectionSignature;
                }

                var batch = events.Project(cursor);
                if (batch.ReplayBoundaryMissed)
                {
                    await WriteEventAsync(CreateGapEvent(cursor, batch), cancellationToken);
                }

                foreach (var item in batch.Events)
                {
                    await WriteEventAsync(item, cancellationToken);
                }

                cursor = batch.Cursor ?? cursor;
                var now = DateTimeOffset.UtcNow;
                if (!follow || now - lastHeartbeatAt >= HeartbeatInterval)
                {
                    await WriteEventAsync(events.CreateHeartbeatEvent(cursor), cancellationToken);
                    lastHeartbeatAt = now;
                }

                await Response.Body.FlushAsync(cancellationToken);
                if (!follow)
                    break;

                await Task.Delay(PollInterval, cancellationToken);
            } while (!cancellationToken.IsCancellationRequested);
        }
        catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
        {
            // Expected when EventSource disconnects or the request is aborted.
        }
    }

    private async Task WriteEventAsync(
        OpenClawStreamEventDto item,
        CancellationToken cancellationToken)
    {
        var id = StripSseControlCharacters(item.Id);
        var eventType = StripSseControlCharacters(item.Type);
        var data = JsonSerializer.Serialize(item, JsonOptions);
        await Response.WriteAsync(
            $"id: {id}\nevent: {eventType}\ndata: {data}\n\n",
            cancellationToken);
    }

    private static OpenClawStreamEventDto CreateGapEvent(
        string? requestedCursor,
        OpenClawEventBatch batch)
    {
        var occurredAt = DateTimeOffset.UtcNow;
        var gapCursor = batch.Events.Count == 0
            ? batch.Cursor ?? "origin"
            : string.IsNullOrWhiteSpace(requestedCursor) ? "origin" : requestedCursor;
        return new OpenClawStreamEventDto(
            gapCursor,
            "openclaw.gap",
            "gap",
            "gateway",
            null,
            null,
            null,
            true,
            false,
            null,
            null,
            occurredAt,
            new JsonObject
            {
                ["reason"] = "last-event-id-outside-buffer",
                ["requestedLastEventId"] = requestedCursor,
                ["oldestAvailableId"] = batch.OldestAvailableId,
                ["latestAvailableId"] = batch.LatestAvailableId,
                ["requiresAuthoritativeRefresh"] = true
            });
    }

    private static string StripSseControlCharacters(string value)
        => value.Replace("\r", string.Empty, StringComparison.Ordinal)
            .Replace("\n", string.Empty, StringComparison.Ordinal);

    private static string GetConnectionSignature(OpenClawStreamEventDto item)
        => string.Join(
            "\u001f",
            item.Payload?["state"]?.ToJsonString() ?? string.Empty,
            item.Payload?["connected"]?.ToJsonString() ?? string.Empty,
            item.Payload?["gatewayVersion"]?.ToJsonString() ?? string.Empty,
            item.Payload?["protocolVersion"]?.ToJsonString() ?? string.Empty,
            item.Payload?["deviceId"]?.ToJsonString() ?? string.Empty,
            item.Payload?["deviceTokenConfigured"]?.ToJsonString() ?? string.Empty,
            item.Payload?["pairingRequired"]?.ToJsonString() ?? string.Empty,
            item.Payload?["pairingRequestId"]?.ToJsonString() ?? string.Empty,
            item.Payload?["lastConnectedAt"]?.ToJsonString() ?? string.Empty,
            item.Payload?["reconnectAttempts"]?.ToJsonString() ?? string.Empty,
            item.Payload?["message"]?.ToJsonString() ?? string.Empty);
}
