using System.Security.Claims;
using System.Text;
using System.Text.Encodings.Web;
using System.Text.Json;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Options;
using SVSim.Database.Enums;
using SVSim.Database.Models;
using SVSim.Database.Repositories.Viewer;
using SVSim.EmulatedEntrypoint.Constants;
using SVSim.EmulatedEntrypoint.Extensions;
using SVSim.EmulatedEntrypoint.Models.Dtos.Requests;
using SVSim.EmulatedEntrypoint.Services;

namespace SVSim.EmulatedEntrypoint.Security.SteamSessionAuthentication;

public class SteamSessionAuthenticationHandler : AuthenticationHandler<SteamAuthenticationHandlerOptions>
{
    // Must mirror the controller-side JSON options — the translation middleware rewrites the
    // request body in snake_case, and we have to read it back the same way or every property
    // binds to null and we NRE downstream against the Steam ticket.
    private static readonly JsonSerializerOptions RequestJsonOptions = new()
    {
        PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
    };

    private readonly SteamSessionService _sessionService;
    private readonly IViewerRepository _viewerRepository;
    public SteamSessionAuthenticationHandler(IOptionsMonitor<SteamAuthenticationHandlerOptions> options, ILoggerFactory logger, UrlEncoder encoder, SteamSessionService sessionService, IViewerRepository viewerRepository) : base(options, logger, encoder)
    {
        _sessionService = sessionService;
        _viewerRepository = viewerRepository;
    }

    protected async override Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        string path = Request.Path;
        byte[] requestBytes;
        try
        {
            using (var requestBytesStream = new MemoryStream())
            {
                // Reset request stream
                Request.Body.Seek(0, SeekOrigin.Begin);

                await Request.Body.CopyToAsync(requestBytesStream);
                requestBytes = requestBytesStream.ToArray();

                // Reset request stream
                Request.Body.Seek(0, SeekOrigin.Begin);
            }
        }
        catch (Exception e)
        {
            Logger.LogWarning(e, "Auth: failed to read request body on {Path}.", path);
            return AuthenticateResult.Fail("Failed to read request body.");
        }

        // Convert bytes to json
        string requestString = Encoding.UTF8.GetString(requestBytes);
        BaseRequest? requestJson;
        try
        {
            requestJson = JsonSerializer.Deserialize<BaseRequest>(requestString, RequestJsonOptions);
        }
        catch (JsonException ex)
        {
            Logger.LogWarning(ex,
                "Auth: failed to JSON-parse request body on {Path} (bodyLen={BodyLen}). " +
                "Translation middleware should have rewritten this to JSON — if it didn't, the request bypassed translation (non-Unity UA?).",
                path, requestBytes.Length);
            return AuthenticateResult.Fail("Invalid request body.");
        }

        if (requestJson is null || string.IsNullOrEmpty(requestJson.SteamSessionTicket))
        {
            Logger.LogWarning(
                "Auth: request body missing steam_session_ticket on {Path} (bodyLen={BodyLen}, hasViewerId={HasViewerId}, steamId={SteamId}).",
                path, requestBytes.Length,
                !string.IsNullOrEmpty(requestJson?.ViewerId), requestJson?.SteamId ?? 0);
            return AuthenticateResult.Fail("Invalid request body.");
        }

        // Check steam session validity
        bool sessionIsValid = _sessionService.IsTicketValidForUser(requestJson.SteamSessionTicket, requestJson.SteamId);
        if (!sessionIsValid)
        {
            Logger.LogWarning(
                "Auth: Steam ticket rejected on {Path} for steamId={SteamId} (ticketLen={TicketLen}). " +
                "See SteamSessionService logs above for the underlying Steam reason (BeginAuthSession failure, duplicate, etc.).",
                path, requestJson.SteamId, requestJson.SteamSessionTicket.Length);
            return AuthenticateResult.Fail("Invalid ticket.");
        }

        Viewer? viewer =
            await _viewerRepository.GetViewerBySocialConnection(SocialAccountType.Steam, requestJson.SteamId);

        if (viewer is null)
        {
            // Find-or-link: first authenticated request after /tool/signup. The client signed up
            // anonymously and has no Steam social row yet; if the UDID resolves to a viewer, attach
            // Steam to it now so subsequent requests hit the fast SteamId path. The unique index
            // on SocialAccountConnection (AccountType, AccountId) — declared in OnModelCreating —
            // is the second-layer dedup backstop: if two concurrent first-touches both pass the
            // .Any(...) check in LinkSteamToViewer, the second SaveChanges throws cleanly instead
            // of silently duplicating connections.
            Guid? udid = Context.GetUdid();
            if (udid is Guid u && u != Guid.Empty)
            {
                viewer = await _viewerRepository.GetViewerByUdid(u);
                if (viewer is not null)
                {
                    await _viewerRepository.LinkSteamToViewer(viewer.Id, requestJson.SteamId);
                    // Re-read with socials so transition_account_data downstream sees the new link.
                    viewer = await _viewerRepository.GetViewerWithSocials(viewer.Id) ?? viewer;
                    Logger.LogInformation(
                        "Auth: linked steamId={SteamId} to UDID-keyed viewer_id={ViewerId} on {Path} (first-Steam-touch).",
                        requestJson.SteamId, viewer.Id, path);
                }
            }

            if (viewer is null)
            {
                Logger.LogWarning(
                    "Auth: no viewer linked to steamId={SteamId} on {Path}, and no UDID-keyed viewer to link to. " +
                    "Client must call /tool/signup before authenticated endpoints.",
                    requestJson.SteamId, path);
                return AuthenticateResult.Fail("User not found.");
            }
        }
        
        // Add viewer to context
        Context.SetViewer(viewer);

        // Build identity
        ClaimsIdentity identity = new ClaimsIdentity(SteamAuthenticationConstants.SchemeName);
        identity.AddClaim(new Claim(ClaimTypes.Name, viewer.DisplayName));
        identity.AddClaim(new Claim(ShadowverseClaimTypes.ShortUdidClaim, viewer.ShortUdid.ToString()));
        identity.AddClaim(new Claim(ShadowverseClaimTypes.ViewerIdClaim, viewer.Id.ToString()));
        identity.AddClaim(new Claim(SteamAuthenticationConstants.SteamIdClaim, requestJson.SteamId.ToString()));
        
        // Build and return final ticket
        AuthenticationTicket ticket =
            new AuthenticationTicket(new ClaimsPrincipal(identity), SteamAuthenticationConstants.SchemeName);
        return AuthenticateResult.Success(ticket);
    }
}