using DeepDrftData;
using DeepDrftModels.DTOs;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.RateLimiting;

namespace DeepDrftAPI.Controllers;

/// <summary>
/// Anonymous play/share telemetry intake (Phase 16 §2.2 / §4.3). Unauthenticated — same posture as the
/// public reads — but IP rate-limited (the "events" limiter, registered in Program.cs) and payload-
/// validated to make casual inflation annoying (§2.5). Both endpoints return <c>202 Accepted</c>: these
/// are fire-and-forget telemetry, not transactions, and the client (a <c>sendBeacon</c>) never reads the
/// response. The release dimension on a play is resolved server-side from the track key (§2.3 / D4).
/// The controller is a thin HTTP boundary; all write logic lives in <see cref="IEventService"/>.
/// </summary>
[ApiController]
[Route("api/event")]
[EnableRateLimiting("events")]
public class EventController : ControllerBase
{
    // Reject oversized bodies before deserialization — a coarse abuse guard (§2.5). The legitimate
    // payloads are a track key + an enum, well under 1 KB.
    private const int MaxBodyBytes = 1024;

    // The anonId is a client-minted GUID string (~36 chars); the anon_id column is varchar(64). Reject
    // anything longer as malformed rather than silently truncating — an over-long token is either a bug
    // or an inflation attempt, and a truncated id would corrupt the distinct-listener count by colliding
    // distinct listeners onto one prefix. Whitespace-only is treated as absent.
    private const int MaxAnonIdLength = 64;

    private readonly IEventService _eventService;
    private readonly ILogger<EventController> _logger;

    public EventController(IEventService eventService, ILogger<EventController> logger)
    {
        _eventService = eventService;
        _logger = logger;
    }

    // POST api/event/play (unauthenticated, rate-limited)
    [HttpPost("play")]
    [RequestSizeLimit(MaxBodyBytes)]
    public async Task<ActionResult> RecordPlay([FromBody] PlayEventDto payload, CancellationToken ct = default)
    {
        // Reject a missing track key and an out-of-range bucket (§2.5). [ApiController] model binding
        // already 400s a malformed/oversized body and an undefined enum value, but the explicit guards
        // keep the contract obvious and cover the empty-string key the model binder lets through.
        if (string.IsNullOrWhiteSpace(payload.TrackEntryKey))
            return BadRequest("trackEntryKey is required");
        if (!Enum.IsDefined(payload.Bucket))
            return BadRequest("bucket is invalid");
        if (!TryNormalizeAnonId(payload.AnonId, out var anonId))
            return BadRequest("anonId is invalid");

        var result = await _eventService.RecordPlay(payload.TrackEntryKey, payload.Bucket, anonId, ct);
        if (!result.Success)
        {
            // A telemetry failure must never surface to the listener as an error they can act on, but
            // we still log it and answer 5xx so a monitor can see the substrate is unhealthy. The
            // beacon ignores the status either way.
            _logger.LogWarning("RecordPlay failed: {Error}", result.Messages.FirstOrDefault()?.Message);
            return StatusCode(500);
        }

        return Accepted();
    }

    // POST api/event/share (unauthenticated, rate-limited)
    [HttpPost("share")]
    [RequestSizeLimit(MaxBodyBytes)]
    public async Task<ActionResult> RecordShare([FromBody] ShareEventDto payload, CancellationToken ct = default)
    {
        if (string.IsNullOrWhiteSpace(payload.TargetKey))
            return BadRequest("targetKey is required");
        if (!Enum.IsDefined(payload.TargetType))
            return BadRequest("targetType is invalid");
        if (!Enum.IsDefined(payload.Channel))
            return BadRequest("channel is invalid");
        if (!TryNormalizeAnonId(payload.AnonId, out var anonId))
            return BadRequest("anonId is invalid");

        var result = await _eventService.RecordShare(payload.TargetType, payload.TargetKey, payload.Channel, anonId, ct);
        if (!result.Success)
        {
            _logger.LogWarning("RecordShare failed: {Error}", result.Messages.FirstOrDefault()?.Message);
            return StatusCode(500);
        }

        return Accepted();
    }

    // Normalize an incoming anonId (wave 16.3): whitespace-only / empty / null collapses to a null token
    // (the listener didn't send one, or storage was unavailable — a valid, anonId-less event). A token
    // over the column width is rejected (400) rather than truncated, since truncation would collide
    // distinct listeners. Returns false only on the over-long case; null and a valid token both pass.
    private static bool TryNormalizeAnonId(string? raw, out string? anonId)
    {
        if (string.IsNullOrWhiteSpace(raw))
        {
            anonId = null;
            return true;
        }

        var trimmed = raw.Trim();
        if (trimmed.Length > MaxAnonIdLength)
        {
            anonId = null;
            return false;
        }

        anonId = trimmed;
        return true;
    }
}
