using Insight.Domain.Models;
using Insight.Infrastructure.Entities;
using Insight.Infrastructure.Models;
using Microsoft.IdentityModel.Tokens;
using MongoDB.Driver;
using System.IdentityModel.Tokens.Jwt;
using System.Net;
using System.Security.Cryptography;
using System.Text;

namespace Insight.Infrastructure.Services
{
    public class TokenService
    {
        private readonly TokenOptions _options;
        private readonly IdentityService _identityService;
        private readonly IMongoDatabase _database;

        public TokenService(TokenOptions options, IdentityService identityService, IMongoDatabase database)
        {
            _options = options;
            _identityService = identityService;
            _database = database;
        }

        public async Task<TokenResponse> GetAsync(string email, string password, string? code = null, IPAddress? ipa = null)
        {
            var user = await _identityService.LoginAsync(email, password, code).ConfigureAwait(false);

            var accessToken = await CreateAccessTokenAsync(user, ipa).ConfigureAwait(false);
            var refreshToken = await CreateRefreshTokenAsync(user, ipa).ConfigureAwait(false);

            return new TokenResponse
            {
                AccessToken = accessToken.Item1,
                ExpireInSeconds = accessToken.Item2,
                RefreshToken = refreshToken.Item1
            };
        }

        public async Task<TokenResponse> RefreshAsync(string refreshToken, IPAddress? ipa = null)
        {
            if (string.IsNullOrWhiteSpace(refreshToken)) throw new ArgumentNullException(nameof(refreshToken));

            var user = await _database.User().Find(p => p.RefreshTokens.Any(t => t.Token == refreshToken)).FirstOrDefaultAsync();
            if (user is null || user.RefreshTokens is null) throw new InvalidDataException("Invalid Refresh Token");

            var token = user.RefreshTokens.First(p => p.Token == refreshToken);

            if (token.IsRevoked)
            {
                // todo: revoke all descendant tokens in case this token has been compromised
                throw new InvalidDataException("Invalid Refresh Token");
            }

            if (token.IsActive is false)
            {
                throw new InvalidDataException("Invalid Refresh Token");
            }

            // remove actual refresh token
            user.RefreshTokens.Remove(token);

            // remove old refresh tokens from user
            user.RefreshTokens.RemoveAll(p => p.IsExpired && p.IsRevoked is false);

            // update users refreshTokens
            await _database.User().UpdateOneAsync(Builders<InsightUser>
                .Filter.Eq(p => p.UserName, user.UserName), Builders<InsightUser>
                .Update.Set(p => p.RefreshTokens, user.RefreshTokens));

            // create new refresh token
            var newRefreshToken = await CreateRefreshTokenAsync(user, ipa).ConfigureAwait(false);

            // create access token
            var accessToken = await CreateAccessTokenAsync(user, ipa).ConfigureAwait(false);

            return new TokenResponse
            {
                AccessToken = accessToken.Item1,
                ExpireInSeconds = accessToken.Item2,
                RefreshToken = newRefreshToken.Item1,
            };
        }

        public async Task RevokeAsync(string refreshToken, string reason, IPAddress? ipa = null)
        {
            if (string.IsNullOrWhiteSpace(refreshToken)) throw new ArgumentNullException(nameof(refreshToken));

            var user = await _database.User().Find(p => p.RefreshTokens.Any(t => t.Token == refreshToken)).FirstOrDefaultAsync();
            if (user is null || user.RefreshTokens is null) throw new InvalidDataException("Invalid Refresh Token");

            var token = user.RefreshTokens.First(p => p.Token == refreshToken);

            if (token.IsActive is false)
            {
                throw new InvalidDataException("Invalid Refresh Token");
            }

            token.Revoked = DateTime.Now;
            token.RevokedByIp = ipa?.ToString();
            token.ReasonRevoked = reason;
        }

        private async Task<(string, int)> CreateAccessTokenAsync(InsightUser user, IPAddress? ipa = null)
        {
            var claims = await _identityService.GetClaimsAsync(user).ConfigureAwait(false);

            var key = Encoding.UTF8.GetBytes(_options.Key);
            var secret = new SymmetricSecurityKey(key);
            var signing = new SigningCredentials(secret, SecurityAlgorithms.HmacSha256);

            var securityToken = new JwtSecurityToken(
                _options.Issuer?.ToString(),
                _options.Audience?.ToString(),
                claims,
                DateTime.Now,
                DateTime.Now.AddSeconds(Convert.ToDouble(_options.Expires)),
                signing);

            var token = new JwtSecurityTokenHandler().WriteToken(securityToken);

            return (token, (int)TimeSpan.FromMinutes(Convert.ToDouble(_options.Expires)).TotalSeconds);
        }

        private async Task<(string, int)> CreateRefreshTokenAsync(InsightUser user, IPAddress? ipa = null)
        {
            var randomNumber = new byte[32];

            using var rng = RandomNumberGenerator.Create();
            rng.GetBytes(randomNumber);

            var refreshToken = Convert.ToBase64String(randomNumber);

            await _database.User()
                .UpdateOneAsync(Builders<InsightUser>
                .Filter.Eq(p => p.UserName, user.UserName), Builders<InsightUser>
                .Update.AddToSet(p => p.RefreshTokens, new RefreshToken
                {
                    Token = refreshToken,
                    Created = DateTime.Now,
                    Expires = DateTime.Now.AddMinutes(30), // change offset to config based
                    CreatedByIp = ipa?.ToString()
                }));

            return (refreshToken, (int)TimeSpan.FromMinutes(30).TotalSeconds);
        }
    }
}