using System.Diagnostics;
using System.Net;
using System.Security.Cryptography;
using System.Text;

namespace zero.Persistence;

/// <summary>
/// Replicates the Rfc6238AuthenticationService from ASP.NET Core internals as it isn't part of the public API
/// (see: https://github.com/dotnet/aspnetcore/blob/release/5.0/src/Identity/Extensions.Core/src/Rfc6238AuthenticationService.cs)
/// </summary>
public static class Rfc6238AuthenticationService
{
  private static readonly TimeSpan _timestep = TimeSpan.FromMinutes(3);
  private static readonly Encoding _encoding = new UTF8Encoding(false, true);

  // Generates a new 80-bit security token
  public static byte[] GenerateRandomKey()
  {
    byte[] bytes = new byte[20];
    RandomNumberGenerator.Fill(bytes);
    return bytes;
  }

  internal static int ComputeTotp(HashAlgorithm hashAlgorithm, ulong timestepNumber, string modifier)
  {
    // # of 0's = length of pin
    const int Mod = 1000000;

    // See https://tools.ietf.org/html/rfc4226
    // We can add an optional modifier
    var timestepAsBytes = BitConverter.GetBytes(IPAddress.HostToNetworkOrder((long)timestepNumber));
    var hash = hashAlgorithm.ComputeHash(ApplyModifier(timestepAsBytes, modifier));

    // Generate DT string
    var offset = hash[^1] & 0xf;
    Debug.Assert(offset + 4 < hash.Length);
    var binaryCode = (hash[offset] & 0x7f) << 24
                      | (hash[offset + 1] & 0xff) << 16
                      | (hash[offset + 2] & 0xff) << 8
                      | (hash[offset + 3] & 0xff);

    return binaryCode % Mod;
  }

  private static byte[] ApplyModifier(byte[] input, string modifier)
  {
    if (String.IsNullOrEmpty(modifier))
    {
      return input;
    }

    var modifierBytes = _encoding.GetBytes(modifier);
    var combined = new byte[checked(input.Length + modifierBytes.Length)];
    Buffer.BlockCopy(input, 0, combined, 0, input.Length);
    Buffer.BlockCopy(modifierBytes, 0, combined, input.Length, modifierBytes.Length);
    return combined;
  }

  // More info: https://tools.ietf.org/html/rfc6238#section-4
  private static ulong GetCurrentTimeStepNumber()
  {
    var delta = DateTimeOffset.UtcNow - DateTimeOffset.UnixEpoch;
    return (ulong)(delta.Ticks / _timestep.Ticks);
  }

  public static int GenerateCode(byte[] securityToken, string modifier = null)
  {
    if (securityToken == null)
    {
      throw new ArgumentNullException(nameof(securityToken));
    }

    // Allow a variance of no greater than 9 minutes in either direction
    var currentTimeStep = GetCurrentTimeStepNumber();
    using var hashAlgorithm = new HMACSHA1(securityToken);
    return ComputeTotp(hashAlgorithm, currentTimeStep, modifier);
  }

  public static bool ValidateCode(byte[] securityToken, int code, string modifier = null)
  {
    if (securityToken == null)
    {
      throw new ArgumentNullException(nameof(securityToken));
    }

    // Allow a variance of no greater than 9 minutes in either direction
    var currentTimeStep = GetCurrentTimeStepNumber();
    using (var hashAlgorithm = new HMACSHA1(securityToken))
    {
      for (var i = -2; i <= 2; i++)
      {
        var computedTotp = ComputeTotp(hashAlgorithm, (ulong)((long)currentTimeStep + i), modifier);
        if (computedTotp == code)
        {
          return true;
        }
      }
    }

    // No match
    return false;
  }
}