using System.Security.Claims;
using System.Text.Encodings.Web;
using Femto.Api.Sessions;
using Femto.Common;
using Femto.Modules.Auth.Application;
using Femto.Modules.Auth.Application.Dto;
using Femto.Modules.Auth.Contracts;
using Femto.Modules.Auth.Models;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Options;

namespace Femto.Api.Auth;

internal class SessionAuthenticationHandler(
    IOptionsMonitor<AuthenticationSchemeOptions> options,
    ILoggerFactory logger,
    UrlEncoder encoder,
    IAuthService authService,
    CurrentUserContext currentUserContext
) : AuthenticationHandler<AuthenticationSchemeOptions>(options, logger, encoder)
{
    protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        Logger.LogDebug("{TraceId} Authenticating session", this.Context.TraceIdentifier);

        var user = await this.TryAuthenticateWithSession();

        if (user is null)
            user = await this.TryAuthenticateWithRememberMeToken();

        if (user is null)
            return AuthenticateResult.NoResult();

        var claims = new List<Claim>
        {
            new(ClaimTypes.Name, user.Username),
            new("sub", user.Id.ToString()),
            new("user_id", user.Id.ToString()),
        };

        claims.AddRange(user.Roles.Select(role => new Claim(ClaimTypes.Role, role.ToString())));

        var identity = new ClaimsIdentity(claims, this.Scheme.Name);
        var principal = new ClaimsPrincipal(identity);
        currentUserContext.CurrentUser = new CurrentUser(
            user.Id,
            user.Username,
            user.Roles.Contains(Role.SuperUser)
        );

        return AuthenticateResult.Success(new AuthenticationTicket(principal, this.Scheme.Name));
    }

    private async Task<UserInfo?> TryAuthenticateWithSession()
    {
        var sessionId = this.Context.GetSessionId();

        if (sessionId is null)
        {
            Logger.LogDebug("{TraceId} SessionId was null ", this.Context.TraceIdentifier);
            return null;
        }

        var session = await authService.GetSession(sessionId);

        if (session is null)
        {
            Logger.LogDebug("{TraceId} Loaded session was null ", this.Context.TraceIdentifier);
            return null;
        }

        if (session.IsExpired)
        {
            Logger.LogDebug("{TraceId} Loaded session was expired ", this.Context.TraceIdentifier);
            await authService.DeleteSession(sessionId);
            this.Context.DeleteSession();
            return null;
        }

        var user = await authService.GetUserWithId(session.UserId);

        if (user is null)
        {
            await authService.DeleteSession(sessionId);
            this.Context.DeleteSession();
            return null;
        }

        if (session.ExpiresSoon)
        {
            session = await authService.CreateWeakSession(session.UserId);
            this.Context.SetSession(session, user);
        }

        return user;
    }

    private async Task<UserInfo?> TryAuthenticateWithRememberMeToken()
    {
        /*
         * load remember me from token
         * if it is null, return null
         * if it exists, validate it
         * if it is valid, create a new weak session, return the user
         * if it is almost expired, refresh it
         */

        var rememberMeToken = this.Context.GetRememberMeToken();

        if (rememberMeToken is null)
            return null;

        var (user, newRememberMeToken) = await authService.GetUserWithRememberMeToken(
            rememberMeToken
        );

        if (user is null)
            return null;

        var session = await authService.CreateWeakSession(user.Id);

        this.Context.SetSession(session, user);

        if (newRememberMeToken is not null)
            this.Context.SetRememberMeToken(newRememberMeToken);

        return user;
    }
}
