using Knoks.Api.Authentication;
using Knoks.Api.Controllers.Base;
using Knoks.Api.Entities;
using Knoks.Core.Logic.Interfaces;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using System;
using System.Linq;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Threading.Tasks;
using static Knoks.Api.Authentication.IdentityConsts;

namespace Knoks.Api.Controllers
{
    [Route("v1/[controller]")]
    public class TokenController : ApiConsumerController
    {
        private readonly ILogger<TokenController> _logger;
        private readonly IUserManager _userManager;
        private readonly IOperatorManager _operatorManager;
        private readonly JwtIssuerOptions _jwtOptions;

        public TokenController(
            ILogger<TokenController> logger,
            IUserManager userManager,
            IOperatorManager operatorManager,
            JwtIssuerOptions jwtOptions)
        {
            _logger = logger;
            _userManager = userManager;
            _operatorManager = operatorManager;
            _jwtOptions = jwtOptions;

            ValidateOptions(_jwtOptions);
        }

        #region class methods

        private static void ValidateOptions(JwtIssuerOptions options)
        {
            if (options == null)
                throw new ArgumentNullException(nameof(options));

            if (options.ValidFor <= TimeSpan.Zero)
                throw new ArgumentException("Must be a non-zero TimeSpan.", nameof(JwtIssuerOptions.ValidFor));

            if (options.SigningCredentials == null)
                throw new ArgumentNullException(nameof(JwtIssuerOptions.SigningCredentials));

            if (options.JtiGenerator == null)
                throw new ArgumentNullException(nameof(JwtIssuerOptions.JtiGenerator));
        }

        private async Task<string> CreateToken(long? userId = null, int? operatorId = null)
        {
            return await CreateToken(
                new Claim(ApiIdentifierClaim, ApiConsumer.ApiIdentifier.ToString("N")),
                userId.HasValue ? new Claim(UserIdClaim, userId.ToString(), ClaimValueTypes.Integer64) : null,
                operatorId.HasValue ? new Claim(OperatorIdClaim, operatorId.ToString(), ClaimValueTypes.Integer32) : null);
        }

        private async Task<string> RefreshToken()
        {
            return await CreateToken(
                User.FindFirst(ApiIdentifierClaim),
                User.FindFirst(UserIdClaim),
                User.FindFirst(OperatorIdClaim));
        }

        private async Task<string> CreateToken(params Claim[] claims)
        {
            var claimList = new List<Claim>
            {
                new Claim(JwtRegisteredClaimNames.Jti, await _jwtOptions.JtiGenerator()),
                new Claim(JwtRegisteredClaimNames.Iat, _jwtOptions.IssuedAt.ToUnixEpochDate().ToString(), ClaimValueTypes.Integer64),
                new Claim(JwtRegisteredClaimNames.Sub, Guid.NewGuid().ToString("N"))
            };

            claimList.AddRange(claims.Where(claim => claim != null));

            return new JwtSecurityTokenHandler().WriteToken(new JwtSecurityToken(
                issuer: _jwtOptions.Issuer,
                audience: _jwtOptions.Audience,
                notBefore: _jwtOptions.NotBefore,
                expires: _jwtOptions.Expiration,
                signingCredentials: _jwtOptions.SigningCredentials,
                claims: claimList));
        }

        private async Task<TokenResponse> GetTokenResult(UserCredentials userCredentials = null, OperatorCredentials operatorCredentials = null)
        {
            long? userId = null;
            if (userCredentials != null)
            {
                userId = (await _userManager.AuthenticateUser(ApiConsumer.Id, userCredentials.Email, userCredentials.Password))?.UserId;
                if (userId == null)
                {
                    _logger.LogInformation($"Invalid username or password. Email = '{userCredentials.Email}'");
                    throw new ApiResponseException(ApiErrorType.InvalidUsernameOrPassword, $"Invalid username or password. Email = '{userCredentials.Email}'");
                }
            }

            int? operatorId = null;
            if (operatorCredentials != null)
            {
                operatorId = (await _operatorManager.AuthenticateOperator(ApiConsumer.Id, operatorCredentials.OperatorName, operatorCredentials.OperatorPassword))?.OperatorId;
                if (operatorId == null)
                {
                    _logger.LogInformation($"Invalid username or password. Username = '{operatorCredentials.OperatorName}'");
                    throw new ApiResponseException(ApiErrorType.InvalidUsernameOrPassword, $"Invalid username or password. Username = '{operatorCredentials.OperatorName}'");
                }
            }

            return new TokenResponse
            {
                Token = await CreateToken(userId, operatorId),
                Expiry = (int)_jwtOptions.ValidFor.TotalSeconds,
                IsUser = userId != null,
                IsOperator = operatorId != null
            };
        }

        #endregion

        [AllowAnonymous]
        [HttpGet("{apiIdentifier}")]
        public async Task<TokenResponse> GenerateToken()
        {
            return await GetTokenResult();
        }

        [AllowAnonymous]
        [HttpPost("{apiIdentifier}")]
        public async Task<TokenResponse> GenerateToken([FromBody]UserCredentials credentials)
        {
            return await GetTokenResult(userCredentials: credentials);
        }

        [AllowAnonymous]
        [HttpPost("{apiIdentifier}/Operator")]
        public async Task<TokenResponse> GenerateOperatorToken([FromBody]OperatorCredentials credentials)
        {
            return await GetTokenResult(operatorCredentials: credentials);
        }

        [HttpGet("refresh")]
        public async Task<TokenResponse> Refresh()
        {
            return new TokenResponse
            {
                Token = await RefreshToken(),
                Expiry = (int)_jwtOptions.ValidFor.TotalSeconds,
                IsUser = User.FindFirst(UserIdClaim) != null,
                IsOperator = User.FindFirst(OperatorIdClaim) != null
            };
        }

        [HttpGet("heartbit")]
        public ApiObject<bool> Heartbit()
        {
            return ApiObject(true);
        }
    }
}
