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

namespace NeonSprawl.Server.Game.AbilityInput;

/// <summary>Maps prototype ability cast POST (NEO-31 + NEO-28 target authority).</summary>
/// <remarks>
/// Epic 1 Slice 3 product telemetry names <c>ability_cast_requested</c> and <c>ability_cast_denied</c>
/// (on JSON denies, <see cref="AbilityCastResponse.ReasonCode"/> — wire JSON <c>reasonCode</c>) — hook sites are marked inline below.
/// TODO(E9.M1): emit cataloged events (payload: route player id, slotIndex, abilityId, targetId; on deny include reasonCode).
/// Optional dev stdout: set <c>NEON_SPRAWL_API_LOG=1</c> (see <see cref="Diagnostics.PrototypeApiConsoleLog"/>) for one line per JSON cast response.
/// HTTP 400/404 here are outside the v1 cast deny contract (no JSON <c>AbilityCastResponse</c>).
/// NEO-32: JSON deny <c>on_cooldown</c> when the slot is inside the prototype global cooldown window.
/// </remarks>
public static class AbilityCastApi
{
    public const string ReasonSlotOutOfBounds = "slot_out_of_bounds";
    public const string ReasonSlotUnbound = "slot_unbound";
    public const string ReasonLoadoutMismatch = "loadout_mismatch";
    public const string ReasonUnknownAbility = "unknown_ability";

    /// <summary>Request target missing, unknown to the prototype registry, or not the server lock (NEO-28).</summary>
    public const string ReasonInvalidTarget = TargetValidity.InvalidTarget;

    /// <summary>Locked prototype target is outside horizontal reach of authoritative position (NEO-28).</summary>
    public const string ReasonOutOfRange = TargetValidity.OutOfRange;

    /// <summary>Slot still inside server-authoritative cooldown window (NEO-32).</summary>
    public const string ReasonOnCooldown = "on_cooldown";

    private static IResult JsonAbilityCast(
        string id,
        AbilityCastRequest body,
        AbilityCastResponse response,
        string? abilityIdForLog = null)
    {
        PrototypeApiConsoleLog.WriteAbilityCastLine(
            id,
            body.SlotIndex,
            abilityIdForLog ?? body.AbilityId,
            body.TargetId,
            response.Accepted,
            response.ReasonCode);
        return Results.Json(response);
    }

    public static WebApplication MapAbilityCastApi(this WebApplication app)
    {
        app.MapPost(
            "/game/players/{id}/ability-cast",
            (
                string id,
                AbilityCastRequest? body,
                IPositionStateStore positions,
                IPlayerHotbarLoadoutStore store,
                IPlayerTargetLockStore locks,
                IPlayerAbilityCooldownStore cooldowns,
                TimeProvider clock) =>
            {
                // NEO-30: not a Slice 3 `ability_cast_denied` site — no AbilityCastResponse body.
                if (body is null || body.SchemaVersion != AbilityCastRequest.CurrentSchemaVersion)
                {
                    return Results.BadRequest();
                }

                // NEO-30: not a Slice 3 `ability_cast_denied` site — unknown player for cast prototype.
                if (!positions.TryGetPosition(id, out var snap))
                {
                    return Results.NotFound();
                }

                // NEO-30: not a Slice 3 `ability_cast_denied` site — no persisted loadout row.
                if (!store.TryGetBindings(id, out var bindings))
                {
                    return Results.NotFound();
                }

                if (body.SlotIndex < 0 || body.SlotIndex >= HotbarLoadoutResponse.SlotCountV1)
                {
                    // NEO-30 telemetry hook site: `ability_cast_denied` + reasonCode (TODO(E9.M1): catalog emit).
                    return JsonAbilityCast(
                        id,
                        body,
                        new AbilityCastResponse
                        {
                            Accepted = false,
                            ReasonCode = ReasonSlotOutOfBounds,
                        });
                }

                if (!bindings.TryGetValue(body.SlotIndex, out var boundAbility) ||
                    string.IsNullOrWhiteSpace(boundAbility))
                {
                    // NEO-30 telemetry hook site: `ability_cast_denied` + reasonCode (TODO(E9.M1): catalog emit).
                    return JsonAbilityCast(
                        id,
                        body,
                        new AbilityCastResponse
                        {
                            Accepted = false,
                            ReasonCode = ReasonSlotUnbound,
                        });
                }

                if (string.IsNullOrWhiteSpace(body.AbilityId) ||
                    !PrototypeAbilityRegistry.TryNormalizeKnown(body.AbilityId, out var normalizedRequest))
                {
                    // NEO-30 telemetry hook site: `ability_cast_denied` + reasonCode (TODO(E9.M1): catalog emit).
                    return JsonAbilityCast(
                        id,
                        body,
                        new AbilityCastResponse
                        {
                            Accepted = false,
                            ReasonCode = ReasonUnknownAbility,
                        });
                }

                if (!string.Equals(normalizedRequest, boundAbility, StringComparison.Ordinal))
                {
                    // NEO-30 telemetry hook site: `ability_cast_denied` + reasonCode (TODO(E9.M1): catalog emit).
                    return JsonAbilityCast(
                        id,
                        body,
                        new AbilityCastResponse
                        {
                            Accepted = false,
                            ReasonCode = ReasonLoadoutMismatch,
                        });
                }

                if (string.IsNullOrWhiteSpace(body.TargetId))
                {
                    // NEO-30 telemetry hook site: `ability_cast_denied` + reasonCode (TODO(E9.M1): catalog emit).
                    return JsonAbilityCast(
                        id,
                        body,
                        new AbilityCastResponse { Accepted = false, ReasonCode = ReasonInvalidTarget });
                }

                var lookupKey = body.TargetId.Trim().ToLowerInvariant();
                if (!PrototypeTargetRegistry.TryGet(lookupKey, out var entry))
                {
                    // NEO-30 telemetry hook site: `ability_cast_denied` + reasonCode (TODO(E9.M1): catalog emit).
                    return JsonAbilityCast(
                        id,
                        body,
                        new AbilityCastResponse { Accepted = false, ReasonCode = ReasonInvalidTarget });
                }

                var (lockIdLowercase, _) = locks.GetLockState(id);
                if (string.IsNullOrEmpty(lockIdLowercase) ||
                    !string.Equals(lookupKey, lockIdLowercase, StringComparison.Ordinal))
                {
                    // NEO-30 telemetry hook site: `ability_cast_denied` + reasonCode (TODO(E9.M1): catalog emit).
                    return JsonAbilityCast(
                        id,
                        body,
                        new AbilityCastResponse { Accepted = false, ReasonCode = ReasonInvalidTarget });
                }

                if (!HorizontalReach.IsWithinHorizontalRadius(snap.X, snap.Z, entry.X, entry.Z, entry.LockRadius))
                {
                    // NEO-30 telemetry hook site: `ability_cast_denied` + reasonCode (TODO(E9.M1): catalog emit).
                    return JsonAbilityCast(
                        id,
                        body,
                        new AbilityCastResponse { Accepted = false, ReasonCode = ReasonOutOfRange });
                }

                var now = clock.GetUtcNow();
                if (cooldowns.IsOnCooldown(id, body.SlotIndex, now))
                {
                    // NEO-30 telemetry hook site: `ability_cast_denied` + reasonCode (TODO(E9.M1): catalog emit).
                    return JsonAbilityCast(
                        id,
                        body,
                        new AbilityCastResponse { Accepted = false, ReasonCode = ReasonOnCooldown });
                }

                cooldowns.StartCooldown(id, body.SlotIndex, now, AbilityPrototypeCooldown.GlobalDuration);

                // NEO-30 telemetry hook site: `ability_cast_requested` at authoritative accept (TODO(E9.M1): catalog emit).
                // Payload notes for E9.M1: player id = route `id`, body.SlotIndex, normalized ability id, body.TargetId (lock-aligned).
                return JsonAbilityCast(
                    id,
                    body,
                    new AbilityCastResponse { Accepted = true },
                    normalizedRequest);
            });

        return app;
    }
}
