// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;

namespace Microsoft.AspNetCore.Authentication
{
    /// <summary>
    /// Extension methods for storing authentication tokens in <see cref="AuthenticationProperties"/>.
    /// </summary>
    public static class AuthenticationTokenExtensions
    {
        private const string TokenNamesKey = ".TokenNames";
        private const string TokenKeyPrefix = ".Token.";

        /// <summary>
        /// Stores a set of authentication tokens, after removing any old tokens.
        /// </summary>
        /// <param name="properties">The <see cref="AuthenticationProperties"/> properties.</param>
        /// <param name="tokens">The tokens to store.</param>
        public static void StoreTokens(this AuthenticationProperties properties, IEnumerable<AuthenticationToken> tokens)
        {
            if (properties == null)
            {
                throw new ArgumentNullException(nameof(properties));
            }
            if (tokens == null)
            {
                throw new ArgumentNullException(nameof(tokens));
            }

            // Clear old tokens first
            var oldTokens = properties.GetTokens();
            foreach (var t in oldTokens)
            {
                properties.Items.Remove(TokenKeyPrefix + t.Name);
            }
            properties.Items.Remove(TokenNamesKey);

            var tokenNames = new List<string>();
            foreach (var token in tokens)
            {
                if (token.Name is null)
                {
                    throw new ArgumentNullException(nameof(tokens), "Token name cannot be null.");
                }

                // REVIEW: should probably check that there are no ; in the token name and throw or encode
                tokenNames.Add(token.Name);
                properties.Items[TokenKeyPrefix + token.Name] = token.Value;
            }
            if (tokenNames.Count > 0)
            {
                properties.Items[TokenNamesKey] = string.Join(";", tokenNames.ToArray());
            }
        }

        /// <summary>
        /// Returns the value of a token.
        /// </summary>
        /// <param name="properties">The <see cref="AuthenticationProperties"/> properties.</param>
        /// <param name="tokenName">The token name.</param>
        /// <returns>The token value.</returns>
        public static string? GetTokenValue(this AuthenticationProperties properties, string tokenName)
        {
            if (properties == null)
            {
                throw new ArgumentNullException(nameof(properties));
            }
            if (tokenName == null)
            {
                throw new ArgumentNullException(nameof(tokenName));
            }

            var tokenKey = TokenKeyPrefix + tokenName;

            return properties.Items.TryGetValue(tokenKey, out var value) ? value : null;
        }

        /// <summary>
        /// Updates the value of a token if already present.
        /// </summary>
        /// <param name="properties">The <see cref="AuthenticationProperties"/> to update.</param>
        /// <param name="tokenName">The token name.</param>
        /// <param name="tokenValue">The token value.</param>
        /// <returns><see langword="true"/> if the token was updated, otherwise <see langword="false"/>.</returns>
        public static bool UpdateTokenValue(this AuthenticationProperties properties, string tokenName, string tokenValue)
        {
            if (properties == null)
            {
                throw new ArgumentNullException(nameof(properties));
            }
            if (tokenName == null)
            {
                throw new ArgumentNullException(nameof(tokenName));
            }

            var tokenKey = TokenKeyPrefix + tokenName;
            if (!properties.Items.ContainsKey(tokenKey))
            {
                return false;
            }
            properties.Items[tokenKey] = tokenValue;
            return true;
        }

        /// <summary>
        /// Returns all of the <see cref="AuthenticationToken"/> instances contained in the properties.
        /// </summary>
        /// <param name="properties">The <see cref="AuthenticationProperties"/> properties.</param>
        /// <returns>The authentication tokens.</returns>
        public static IEnumerable<AuthenticationToken> GetTokens(this AuthenticationProperties properties)
        {
            if (properties == null)
            {
                throw new ArgumentNullException(nameof(properties));
            }

            var tokens = new List<AuthenticationToken>();
            if (properties.Items.TryGetValue(TokenNamesKey, out var value) && !string.IsNullOrEmpty(value))
            {
                var tokenNames = value.Split(';');
                foreach (var name in tokenNames)
                {
                    var token = properties.GetTokenValue(name);
                    if (token != null)
                    {
                        tokens.Add(new AuthenticationToken { Name = name, Value = token });
                    }
                }
            }

            return tokens;
        }

        /// <summary>
        /// Authenticates the request using the specified authentication scheme and returns the value for the token.
        /// </summary>
        /// <param name="auth">The <see cref="IAuthenticationService"/>.</param>
        /// <param name="context">The <see cref="HttpContext"/> context.</param>
        /// <param name="tokenName">The name of the token.</param>
        /// <returns>The value of the token if present.</returns>
        public static Task<string?> GetTokenAsync(this IAuthenticationService auth, HttpContext context, string tokenName)
            => auth.GetTokenAsync(context, scheme: null, tokenName: tokenName);

        /// <summary>
        /// Authenticates the request using the specified authentication scheme and returns the value for the token.
        /// </summary>
        /// <param name="auth">The <see cref="IAuthenticationService"/>.</param>
        /// <param name="context">The <see cref="HttpContext"/> context.</param>
        /// <param name="scheme">The name of the authentication scheme.</param>
        /// <param name="tokenName">The name of the token.</param>
        /// <returns>The value of the token if present.</returns>
        public static async Task<string?> GetTokenAsync(this IAuthenticationService auth, HttpContext context, string? scheme, string tokenName)
        {
            if (auth == null)
            {
                throw new ArgumentNullException(nameof(auth));
            }
            if (tokenName == null)
            {
                throw new ArgumentNullException(nameof(tokenName));
            }

            var result = await auth.AuthenticateAsync(context, scheme);
            return result?.Properties?.GetTokenValue(tokenName);
        }
    }
}
