using Microsoft.IdentityModel.Tokens;
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Security.Claims;
using System.Text;

namespace HRD.LDAPService.JWT
{
    public class JwtManager

    {
        private const string GlbExtendedAttributes = "ExtendedAttributes_";

        private readonly LdapAuthenticationService _ldapAuthService;

        public JwtManager(LdapAuthenticationService ldapAuthService)
        {
            _ldapAuthService = ldapAuthService;
        }

        public static LdapUser DecryptTokenAsLdapUser(string token)
        {
            if (string.IsNullOrEmpty(token)) { return default; }
            //Check token with "Bearer" prefix
            if (token.StartsWith("Bearer", StringComparison.InvariantCultureIgnoreCase))
            {
                token = token.Split(" ").Last();
            }
            if (string.IsNullOrEmpty(token)) { return default; }

            JwtSecurityToken jwtSecurityToken = DecryptToken(token);
            if (jwtSecurityToken == null) { return default; }

            LdapUser ldapUser = ClaimsIdentityToLdapUser(jwtSecurityToken.Claims.ToList());
            if (ldapUser == null)
            {
                return default;
            }

            ldapUser.Token = token;

            return ldapUser;
        }

        public static JwtSecurityToken DecryptToken(string token)
        {
            var tokenHandler = new JwtSecurityTokenHandler();
            var key = Encoding.ASCII.GetBytes(JwtTokenConfig.Secret);
            try
            {
                tokenHandler.ValidateToken(token, new TokenValidationParameters
                {
                    ValidateIssuerSigningKey = true,
                    IssuerSigningKey = new SymmetricSecurityKey(key),
                    ValidateIssuer = false,
                    ValidateAudience = false,
                    ClockSkew = TimeSpan.FromSeconds(60)  // set clockskew to zero so tokens expire exactly at token expiration time
                }, out SecurityToken validatedToken);

                var jwtToken = (JwtSecurityToken)validatedToken;
                return jwtToken;
            }
            //IDX10223: Lifetime validation failed. The token is expired. ValidTo: 'System.DateTime', Current time: 'System.DateTime'.
            catch (SecurityTokenExpiredException ex)
            {
                throw ex;
            }
            catch (Exception ex)
            {
                // return null if validation fails
                throw ex;
            }
        }

        public LdapUser RenewLdapUserWithJwtToken(string token)
        {
            LdapUser renewLdapUser = null;
            try
            {
                if (string.IsNullOrEmpty(token))
                {
                    throw new ArgumentNullException($"Token is missing!");
                }

                renewLdapUser = _ldapAuthService.RenewIdentity(token);
                if (renewLdapUser is null)
                {
                    throw new Exception($"Can't renew from token!");
                }

                if (!renewLdapUser.IsValidatCredentials)
                {
                    throw new Exception($"Invalid credentials!");
                }

                if (!renewLdapUser.Enabled)
                {
                    throw new Exception($"Ldap-User is disabled!");
                }

                (string newtoken, DateTime newExpiredOn) = CreateToken(renewLdapUser);
                renewLdapUser.Token = newtoken;
                renewLdapUser.JwtExpiredOn = newExpiredOn;

                if (!renewLdapUser.IsValid())
                {
                    throw new Exception($"Ldapuser is not valid!");
                }

                return renewLdapUser;
            }
            catch (Exception ex)
            {
                throw ex;
            }
        }

        public LdapUser RenewLdapUserWithJwtToken(LdapUser ldapUser)
        {
            LdapUser renewLdapUser = null;
            try
            {
                if (string.IsNullOrEmpty(ldapUser?.Token))
                {
                    throw new Exception($"Token is missing (Login:{ldapUser.LoginName})");
                }

                renewLdapUser = _ldapAuthService.RenewIdentity(ldapUser);
                if (renewLdapUser is null)
                {
                    return default;
                }

                //if (!ldapUser.IsValidatCredentials)
                //{
                //    ldapUser.Token = string.Empty;
                //    return false;
                //}

                if (!renewLdapUser.Enabled)
                {
                    renewLdapUser.Token = string.Empty;
                    return renewLdapUser;
                }

                var tokenHandler = new JwtSecurityTokenHandler();
                var key = Encoding.ASCII.GetBytes(JwtTokenConfig.Secret);
                var claims = CreateClaimsIdentity(renewLdapUser);

                var tokenDescriptor = new SecurityTokenDescriptor
                {
                    Subject = claims,
                    Expires = DateTime.UtcNow.AddMinutes(JwtTokenConfig.ExpirationInMin),
                    SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
                };
                var token = tokenHandler.CreateToken(tokenDescriptor);
                ldapUser.Token = tokenHandler.WriteToken(token);
                ldapUser.JwtExpiredOn = token.ValidTo;
                if (renewLdapUser.IsValid())
                {
                    return renewLdapUser;
                }

                return default;
            }
            catch (Exception ex)
            {
                throw ex;
            }
        }

        public bool GenerateLdapUserWithJwtToken(LdapUser ldapUser)
        {
            try
            {
                if (!_ldapAuthService.CheckAndUpdateIdentityWithPassword(ldapUser))
                {
                    return false;
                }

                if (!ldapUser.IsValidatCredentials)
                {
                    ldapUser.Token = string.Empty;
                    return false;
                }

                if (!ldapUser.Enabled)
                {
                    ldapUser.Token = string.Empty;
                    return false;
                }

                (string token, DateTime jwtExpiredOn) = CreateToken(ldapUser);
                ldapUser.Token = token;
                ldapUser.JwtExpiredOn = jwtExpiredOn;
                return ldapUser.IsValid();
            }
            catch (Exception ex)
            {
                throw ex;
            }
        }

        public static string GenerateHash(string password)
        {
            return JWTCrypt.SHA512(password);
        }

        private static (string, DateTime) CreateToken(LdapUser ldapUser)
        {
            var tokenHandler = new JwtSecurityTokenHandler();
            var key = Encoding.ASCII.GetBytes(JwtTokenConfig.Secret);
            var claims = CreateClaimsIdentity(ldapUser);

            var tokenDescriptor = new SecurityTokenDescriptor
            {
                Subject = claims,
                Expires = DateTime.UtcNow.AddMinutes(JwtTokenConfig.ExpirationInMin),
                SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
            };
            var token = tokenHandler.CreateToken(tokenDescriptor);
            return (tokenHandler.WriteToken(token), token.ValidTo);
        }

        public static bool IsValidatJwtTokenSubject(string token)
        {
            if (string.IsNullOrEmpty(token))
            {
                return false;
            }

            token = token.Trim();

            if (token.IndexOf(" ", StringComparison.InvariantCultureIgnoreCase) > 0)
            {
                if (token.StartsWith("Bearer", StringComparison.InvariantCultureIgnoreCase)) //token with "Bearer" prefix
                {
                    token = token.Split(" ").Last();
                }
                else
                {
                    token = token.Split(" ").First();
                }
            }

            try
            {
                var jwtToken = DecryptToken(token);
                return !String.IsNullOrEmpty(jwtToken?.Subject); //Loginname
            }
            catch (Exception ex)
            {
                throw ex;
            }
        }

        private static LdapUser ClaimsIdentityToLdapUser(List<Claim> claims)
        {
            LdapUser user = new LdapUser("");
            foreach (var claim in claims)
            {
                switch (claim.Type)
                {
                    case JwtRegisteredClaimNames.Sub:
                        user.LoginName = claim.Value;
                        break;

                    case JwtRegisteredClaimNames.Email:
                        user.Email = claim.Value;
                        break;

                    case JwtRegisteredClaimNames.NameId:
                        {
                            if (int.TryParse(claim.Value, out int id))
                            {
                                user.UserId = id;
                            }
                        }
                        break;

                    case JwtRegisteredClaimNames.Jti:
                        {
                            if (Guid.TryParse(claim.Value, out Guid g))
                            {
                                user.LdapGuid = g;
                            }
                        }
                        break;

                    case JwtGlobals.CLAIM_DEPARTNENTID:
                        user.DepartmentId = int.Parse(claim.Value);
                        break;

                    case JwtGlobals.CLAIM_EXTENDETDEPARTNENTIDLIST:
                        user.ExtendedDepartmentIdList = claim.Value;
                        break;

                    case JwtGlobals.CLAIM_ROLE:
                        user.AddRole(claim.Value);
                        break;

                    case JwtRegisteredClaimNames.Exp:
                        {
                            //#pragma warning disable CA1305 // Specify IFormatProvider
                            var expValue = Convert.ToInt32(claim.Value);
                            //#pragma warning restore CA1305 // Specify IFormatProvider
                            DateTimeOffset dateTimeOffset = DateTimeOffset.FromUnixTimeSeconds(expValue);
                            user.JwtExpiredOn = dateTimeOffset.UtcDateTime;
                        }
                        break;

                    case var s when claim.Type.StartsWith(GlbExtendedAttributes):
                        var strKey = claim.Type.Substring(GlbExtendedAttributes.Length, claim.Type.Length - GlbExtendedAttributes.Length);
                        user.ExtendedAttributesList.Add(new KeyValuePair<string, string>(strKey, claim.Value));
                        break;

                    default:
                        break;
                }
            }

            return user;
        }

        private static ClaimsIdentity CreateClaimsIdentity(LdapUser user)
        {
            ClaimsIdentity claimsIdentity = new ClaimsIdentity();

            List<Claim> claims = new List<Claim>
            {
                CreateClaim(JwtRegisteredClaimNames.Sub, user.LoginName),
                CreateClaim(JwtRegisteredClaimNames.NameId, user.UserId),
                CreateClaim(JwtRegisteredClaimNames.Email, user.Email),
                CreateClaim(JwtGlobals.CLAIM_DEPARTNENTID, user.DepartmentId),
                CreateClaim(JwtGlobals.CLAIM_EXTENDETDEPARTNENTIDLIST, user.ExtendedDepartmentIdList)
            };

            user.RoleList.ForEach(role => claims.Add(
                CreateClaim(ClaimTypes.Role, role.Role)
                ));

            user.ExtendedAttributesList.ForEach(item => claims.Add(
                CreateClaim($"{GlbExtendedAttributes}{item.Key}", item.Value)
                ));

            claimsIdentity.AddClaims(claims);
            return claimsIdentity;
        }

        private static Claim CreateClaim(string claimName, int claimValue)
        {
            return new Claim(claimName, string.IsNullOrEmpty($"{claimValue}") ? string.Empty : $"{claimValue}");
        }

        private static Claim CreateClaim(string claimName, string claimValue)
        {
            return new Claim(claimName, string.IsNullOrEmpty(claimValue) ? string.Empty : claimValue);
        }
    }
}