using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.Net.Http.Headers;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Security.Claims;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Threading.Tasks;
using Timeline.Models;
using Timeline.Models.Http;
using Timeline.Services.Token;
using Timeline.Services.User;
namespace Timeline.Auth
{
    public static class AuthenticationConstants
    {
        public const string Scheme = "Bearer";
        public const string DisplayName = "My Jwt Auth Scheme";
        public const string PermissionClaimName = "Permission";
    }
    public class MyAuthenticationOptions : AuthenticationSchemeOptions
    {
        /// 
        /// The query param key to search for token. If null then query params are not searched for token. Default to "token".
        /// 
        public string TokenQueryParamKey { get; set; } = "token";
    }
    public class MyAuthenticationHandler : AuthenticationHandler
    {
        private const string TokenErrorCodeKey = "TokenErrorCode";
        private static int GetErrorCodeForUserTokenException(UserTokenException e)
        {
            return e switch
            {
                UserTokenTimeExpiredException => ErrorCodes.Common.Token.TimeExpired,
                UserTokenVersionExpiredException => ErrorCodes.Common.Token.VersionExpired,
                UserTokenBadFormatException => ErrorCodes.Common.Token.BadFormat,
                UserTokenUserNotExistException => ErrorCodes.Common.Token.UserNotExist,
                _ => ErrorCodes.Common.Token.Unknown
            };
        }
        private static string GetTokenErrorMessageFromErrorCode(int errorCode)
        {
            return errorCode switch
            {
                ErrorCodes.Common.Token.TimeExpired => Resource.MessageTokenTimeExpired,
                ErrorCodes.Common.Token.VersionExpired => Resource.MessageTokenVersionExpired,
                ErrorCodes.Common.Token.BadFormat => Resource.MessageTokenBadFormat,
                ErrorCodes.Common.Token.UserNotExist => Resource.MessageTokenUserNotExist,
                _ => Resource.MessageTokenUnknownError
            };
        }
        private readonly ILogger _logger;
        private readonly IUserTokenManager _userTokenManager;
        private readonly IUserPermissionService _userPermissionService;
        private readonly IOptionsMonitor _jsonOptions;
        public MyAuthenticationHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock, IUserTokenManager userTokenManager, IUserPermissionService userPermissionService, IOptionsMonitor jsonOptions)
            : base(options, logger, encoder, clock)
        {
            _logger = logger.CreateLogger();
            _userTokenManager = userTokenManager;
            _userPermissionService = userPermissionService;
            _jsonOptions = jsonOptions;
        }
        // return null if no token is found
        private string? ExtractToken()
        {
            // check the authorization header
            string header = Request.Headers[HeaderNames.Authorization];
            if (!string.IsNullOrEmpty(header) && header.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
            {
                var token = header["Bearer ".Length..].Trim();
                _logger.LogInformation(Resource.LogTokenFoundInHeader, token);
                return token;
            }
            // check the query params
            var paramQueryKey = Options.TokenQueryParamKey;
            if (!string.IsNullOrEmpty(paramQueryKey))
            {
                string token = Request.Query[paramQueryKey];
                if (!string.IsNullOrEmpty(token))
                {
                    _logger.LogInformation(Resource.LogTokenFoundInQuery, paramQueryKey, token);
                    return token;
                }
            }
            {
                var token = Request.Query["access_token"];
                var path = Context.Request.Path;
                if (!string.IsNullOrEmpty(token) && path.StartsWithSegments("/api/hub"))
                {
                    _logger.LogInformation(Resource.LogTokenFoundInQuery, "access_token", token);
                    return token;
                }
            }
            // not found anywhere then return null
            return null;
        }
        protected override async Task HandleAuthenticateAsync()
        {
            var token = ExtractToken();
            if (string.IsNullOrEmpty(token))
            {
                _logger.LogInformation(Resource.LogTokenNotFound);
                return AuthenticateResult.NoResult();
            }
            try
            {
                var user = await _userTokenManager.VerifyTokenAsync(token);
                var identity = new ClaimsIdentity(AuthenticationConstants.Scheme);
                identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, user.Id.ToString(CultureInfo.InvariantCulture), ClaimValueTypes.Integer64));
                identity.AddClaim(new Claim(identity.NameClaimType, user.Username, ClaimValueTypes.String));
                var permissions = await _userPermissionService.GetPermissionsOfUserAsync(user.Id);
                identity.AddClaims(permissions.Select(permission => new Claim(AuthenticationConstants.PermissionClaimName, permission.ToString(), ClaimValueTypes.String)));
                var principal = new ClaimsPrincipal();
                principal.AddIdentity(identity);
                return AuthenticateResult.Success(new AuthenticationTicket(principal, AuthenticationConstants.Scheme));
            }
            catch (UserTokenException e)
            {
                var errorCode = GetErrorCodeForUserTokenException(e);
                _logger.LogInformation(e, Resource.LogTokenValidationFail, GetTokenErrorMessageFromErrorCode(errorCode));
                return AuthenticateResult.Fail(e, new AuthenticationProperties(new Dictionary()
                {
                    [TokenErrorCodeKey] = errorCode.ToString(CultureInfo.InvariantCulture)
                }));
            }
        }
        protected override async Task HandleChallengeAsync(AuthenticationProperties properties)
        {
            Response.StatusCode = 401;
            CommonResponse body;
            if (properties.Items.TryGetValue(TokenErrorCodeKey, out var tokenErrorCode))
            {
                if (!int.TryParse(tokenErrorCode, out var errorCode))
                    errorCode = ErrorCodes.Common.Token.Unknown;
                body = new CommonResponse(errorCode, GetTokenErrorMessageFromErrorCode(errorCode));
            }
            else
            {
                body = new CommonResponse(ErrorCodes.Common.Unauthorized, Resource.MessageNoToken);
            }
            var bodyData = JsonSerializer.SerializeToUtf8Bytes(body, typeof(CommonResponse), _jsonOptions.CurrentValue.JsonSerializerOptions);
            Response.ContentType = MimeTypes.ApplicationJson;
            Response.ContentLength = bodyData.Length;
            await Response.Body.WriteAsync(bodyData);
        }
    }
}