using NeonSprawl.Server.Diagnostics;
using NeonSprawl.Server.Game.Combat;
using NeonSprawl.Server.Game.Gigs;
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 server cooldown window.
/// NEO-82: after E1 gates pass, invokes <see cref="CombatOperations.TryResolve"/>; per-ability catalog cooldown on successful resolve.
/// NEO-44: on accept when <see cref="CombatResult.TargetDefeated"/> is true, best-effort gig XP grant via <see cref="CombatDefeatGigXpGrant"/> (not E2.M2 skill XP).
/// </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,
                IAbilityDefinitionRegistry abilities,
                ICombatEntityHealthStore healthStore,
                IPlayerGigProgressionStore gigStore,
                ILoggerFactory loggerFactory,
                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) ||
                    !abilities.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 });
                }

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

                var combatResult = CombatOperations.TryResolve(
                    normalizedRequest,
                    lookupKey,
                    abilities,
                    healthStore);

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

                cooldowns.StartCooldown(
                    id,
                    body.SlotIndex,
                    now,
                    TimeSpan.FromSeconds(definition.CooldownSeconds));

                if (combatResult.TargetDefeated)
                {
                    CombatDefeatGigXpGrant.GrantOnCombatDefeat(
                        id.Trim(),
                        gigStore,
                        loggerFactory.CreateLogger(nameof(CombatDefeatGigXpGrant)));
                }

                // 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,
                    MapAcceptResponse(normalizedRequest, lookupKey, combatResult),
                    normalizedRequest);
            });

        return app;
    }

    internal static AbilityCastResponse MapAcceptResponse(
        string normalizedAbilityId,
        string normalizedTargetId,
        CombatResult result)
    {
        return new AbilityCastResponse
        {
            Accepted = true,
            CombatResolution = new CombatResolutionJson
            {
                AbilityId = normalizedAbilityId,
                TargetId = normalizedTargetId,
                DamageDealt = result.DamageDealt,
                TargetRemainingHp = result.TargetRemainingHp!.Value,
                TargetDefeated = result.TargetDefeated,
            },
        };
    }
}
