// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Data;
using System.Security.Cryptography;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.WebUtilities;

namespace Microsoft.AspNetCore.Components.Server.Circuits
{
    // This is a singleton instance
    // Generates strong cryptographic ids for circuits that are protected with authenticated encryption.
    internal class CircuitIdFactory
    {
        private const string CircuitIdProtectorPurpose = "Microsoft.AspNetCore.Components.Server.CircuitIdFactory,V1";

        // We use 64 bytes, where the last 32 are the public version of the id.
        // This way we can always recover the public id from the secret form.
        private const int SecretLength = 64;
        private const int IdLength = 32;

        private readonly RandomNumberGenerator _generator = RandomNumberGenerator.Create();
        private readonly IDataProtector _protector;

        public CircuitIdFactory(IDataProtectionProvider provider)
        {
            _protector = provider.CreateProtector(CircuitIdProtectorPurpose);
        }

        // Generates a circuit id that is produced from a strong cryptographic random number generator
        // we don't care about the underlying payload, other than its uniqueness and the fact that we
        // authenticate encrypt it using data protection.
        // For validation, the fact that we can unprotect the payload is guarantee enough.
        public CircuitId CreateCircuitId()
        {
            var buffer = new byte[SecretLength];
            _generator.GetBytes(buffer);

            var id = new byte[IdLength];
            Array.Copy(
                sourceArray: buffer,
                sourceIndex: SecretLength - IdLength,
                destinationArray: id,
                destinationIndex: 0,
                length: IdLength);

            var secret = _protector.Protect(buffer);
            return new CircuitId(Base64UrlTextEncoder.Encode(secret), Base64UrlTextEncoder.Encode(id));
        }

        public bool TryParseCircuitId(string text, out CircuitId circuitId)
        {
            if (text is null)
            {
                circuitId = default;
                return false;
            }

            try
            {
                var protectedBytes = Base64UrlTextEncoder.Decode(text);
                var unprotectedBytes = _protector.Unprotect(protectedBytes);

                if (unprotectedBytes.Length != SecretLength)
                {
                    // Wrong length
                    circuitId = default;
                    return false;
                }

                var id = new byte[IdLength];
                Array.Copy(
                    sourceArray: unprotectedBytes,
                    sourceIndex: SecretLength - IdLength,
                    destinationArray: id,
                    destinationIndex: 0,
                    length: IdLength);

                circuitId = new CircuitId(text, Base64UrlTextEncoder.Encode(id));
                return true;
            }
            catch (Exception)
            {
                // The payload format is not correct (either not base64urlencoded or not data protected)
                circuitId = default;
                return false;
            }
        }
    }
}
