using NeonSprawl.Server.Game.PositionState;
using NeonSprawl.Server.Game.World;

namespace NeonSprawl.Server.Game.Targeting;

/// <summary>Maps <c>GET/POST …/target</c> selection APIs (NEO-23).</summary>
public static class TargetingApi
{
    // NEO-27 note: these are target-selection deny reasons, not cast telemetry events.
    // Future ability flow (E1.M4 + E5.M1) will hook `ability_cast_denied` on cast endpoints.
    // TODO(E9.M1): map deny reason telemetry to cataloged event schema.
    public const string ReasonUnknownTarget = "unknown_target";
    public const string ReasonOutOfRange = "out_of_range";

    public static WebApplication MapTargetingApi(this WebApplication app)
    {
        app.MapGet(
            "/game/players/{id}/target",
            (string id, IPositionStateStore positions, IPlayerTargetLockStore locks) =>
            {
                if (!positions.TryGetPosition(id, out var snap))
                {
                    return Results.NotFound();
                }

                var (lockId, seq) = locks.GetLockState(id);
                var body = PlayerTargetStateReader.Build(id, lockId, seq, in snap);
                return Results.Json(body);
            });

        app.MapPost(
            "/game/players/{id}/target/select",
            (string id, TargetSelectRequest? body, IPositionStateStore positions, IPlayerTargetLockStore locks) =>
            {
                if (body is null || body.SchemaVersion != TargetSelectRequest.CurrentSchemaVersion)
                {
                    return Results.BadRequest();
                }

                if (!positions.TryGetPosition(id, out var snap))
                {
                    return Results.NotFound();
                }

                // Clear intent: property omitted or JSON null.
                if (body.TargetId is null)
                {
                    var cleared = locks.ApplyClear(id);
                    var stateAfter = PlayerTargetStateReader.Build(id, cleared.LockIdLowercase, cleared.Sequence, in snap);
                    return Results.Json(
                        new TargetSelectResponse
                        {
                            SchemaVersion = TargetSelectResponse.CurrentSchemaVersion,
                            SelectionApplied = true,
                            TargetState = stateAfter,
                        });
                }

                var raw = body.TargetId;
                var trimmed = raw.Trim();
                if (trimmed.Length == 0)
                {
                    return Results.BadRequest();
                }

                var lookupKey = trimmed.ToLowerInvariant();
                if (!PrototypeTargetRegistry.TryGet(lookupKey, out var entry))
                {
                    var (lockUnknown, seqUnknown) = locks.GetLockState(id);
                    var stateUnknown = PlayerTargetStateReader.Build(id, lockUnknown, seqUnknown, in snap);
                    return Results.Json(
                        new TargetSelectResponse
                        {
                            SchemaVersion = TargetSelectResponse.CurrentSchemaVersion,
                            SelectionApplied = false,
                            TargetState = stateUnknown,
                            ReasonCode = ReasonUnknownTarget,
                        });
                }

                // Prefer the client-reported `positionHint` when present: the stored `snap` can
                // trail the visible capsule by several seconds when `move-stream` has been idle,
                // which caused false `out_of_range` denials (NEO-24 follow-up #5). The hint is
                // advisory — position writes stay the sole responsibility of `move-stream`. Real
                // combat (E5.M1) will bound the hint vs. stored snap before trusting it.
                // The hinted snap is used for the radius check *and* for building the response's
                // `validity` so a hint-accepted select does not echo a stale `out_of_range`.
                var effective = body.PositionHint is { } hint
                    ? new PositionSnapshot(hint.X, hint.Y, hint.Z, snap.Sequence)
                    : snap;
                if (!HorizontalReach.IsWithinHorizontalRadius(effective.X, effective.Z, entry.X, entry.Z, entry.LockRadius))
                {
                    var (lockFar, seqFar) = locks.GetLockState(id);
                    var stateFar = PlayerTargetStateReader.Build(id, lockFar, seqFar, in effective);
                    return Results.Json(
                        new TargetSelectResponse
                        {
                            SchemaVersion = TargetSelectResponse.CurrentSchemaVersion,
                            SelectionApplied = false,
                            TargetState = stateFar,
                            ReasonCode = ReasonOutOfRange,
                        });
                }

                var applied = locks.ApplySet(id, lookupKey);
                var stateOk = PlayerTargetStateReader.Build(id, applied.LockIdLowercase, applied.Sequence, in effective);
                return Results.Json(
                    new TargetSelectResponse
                    {
                        SchemaVersion = TargetSelectResponse.CurrentSchemaVersion,
                        SelectionApplied = true,
                        TargetState = stateOk,
                    });
            });

        return app;
    }
}
