using System.Diagnostics; // Stopwatch
using System.Security.Cryptography; // Aes, Rfc2898DeriveBytes itp.
using System.Security.Principal; // GenericIdentity, GenericPrincipal
using System.Text; // Encoding

using static System.Convert; // ToBase64String, FromBase64String

namespace Packt.Shared;

public static class Protector
{
  // Sól musi składać się z co najmniej 8 bajtów. Tutaj jest to 16 bajtów.
  private static readonly byte[] salt =
    Encoding.Unicode.GetBytes("7BANANAS");


  // Domyślnie klasa Rfc2898DeriveBytes wykonuje 1000 iteracji.
  // Liczba ta powinna być na tyle duża, aby wygenerowanie klucza i wektora inicjującego
  // trwało przynajmniej 100 ms. Na moim komputerze wyposażonym w procesor 11. generacji 
  // Intel Core i7-1165G7 @ 2,80 GHz wykonanie 150 00 iteracji zajmuje 139 ms. 
  private static readonly int iterations = 150_000;

  public static string Encrypt(
    string plainText, string password)
  {
    byte[] encryptedBytes;
    byte[] plainBytes = Encoding.Unicode.GetBytes(plainText);

    using (Aes aes = Aes.Create()) // Metoda fabryczna klasy abstrakcyjnej.
    {
      // Rejestrowanie czasu generowania klucza i wektora.
      Stopwatch timer = Stopwatch.StartNew();

      using (Rfc2898DeriveBytes pbkdf2 = new(
        password, salt, iterations, HashAlgorithmName.SHA256))
      {
        WriteLine("Algorytm skracający: {0}, liczba iteracji: {1:N0}",
          pbkdf2.HashAlgorithm, pbkdf2.IterationCount);

        aes.Key = pbkdf2.GetBytes(32); // Klucz 256-bitowy.
        aes.IV = pbkdf2.GetBytes(16); // Wektor 128-bitowy.
      }

      timer.Stop();

      WriteLine("Czas wygenerowania klucza i wektora inicjującego: {0:N0} ms.",
        arg0: timer.ElapsedMilliseconds);

      WriteLine("Algorytm szyfrujący: {0}-{1}, tryb {2} z uzupełnieniem {3}.",
        "AES", aes.KeySize, aes.Mode, aes.Padding);

      using (MemoryStream ms = new())
      {
        using (ICryptoTransform transformer = aes.CreateEncryptor())
        {
          using (CryptoStream cs = new(
            ms, transformer, CryptoStreamMode.Write))
          {
            cs.Write(plainBytes, 0, plainBytes.Length);

            if (!cs.HasFlushedFinalBlock)
            {
              cs.FlushFinalBlock();
            }
          }
        }
        encryptedBytes = ms.ToArray();
      }
    }

    return ToBase64String(encryptedBytes);
  }

  public static string Decrypt(
    string cipherText, string password)
  {
    byte[] plainBytes;
    byte[] cryptoBytes = FromBase64String(cipherText);

    using (Aes aes = Aes.Create())
    {
      using (Rfc2898DeriveBytes pbkdf2 = new(
        password, salt, iterations, HashAlgorithmName.SHA256))
      {
        aes.Key = pbkdf2.GetBytes(32);
        aes.IV = pbkdf2.GetBytes(16);
      }

      using (MemoryStream ms = new())
      {
        using (ICryptoTransform transformer = aes.CreateDecryptor())
        {
          using (CryptoStream cs = new(
            ms, aes.CreateDecryptor(), CryptoStreamMode.Write))
          {
            cs.Write(cryptoBytes, 0, cryptoBytes.Length);

            if (!cs.HasFlushedFinalBlock)
            {
              cs.FlushFinalBlock();
            }
          }
        }
        plainBytes = ms.ToArray();
      }
    }

    return Encoding.Unicode.GetString(plainBytes);
  }

  private static Dictionary<string, User> Users = new();

  public static User Register(string username,
    string password, string[]? roles = null)
  {
    // Wygenerowanie losowej soli.
    RandomNumberGenerator rng = RandomNumberGenerator.Create();
    byte[] saltBytes = new byte[16];
    rng.GetBytes(saltBytes);
    string saltText = ToBase64String(saltBytes);

    // Wygenerowanie skrótu soli i hasła.
    string saltedhashedPassword = SaltAndHashPassword(password, saltText);

    User user = new(username, saltText,
      saltedhashedPassword, roles);

    Users.Add(user.Name, user);

    return user;
  }

  public static void LogIn(string username, string password)
  {
    if (CheckPassword(username, password))
    {
      GenericIdentity gi = new(
        name: username, type: "PacktAuth");

      GenericPrincipal gp = new(
        identity: gi, roles: Users[username].Roles);

      // Ustawienie podmiotu zabezpieczeń bieżącego wątku tak,
      // aby był domyślnie wykorzystywany do autoryzacji.
      Thread.CurrentPrincipal = gp;
    }
  }

  // Sprawdzenie hasła zapisanego
  // w prywatnym, statycznym słowniku Users.
  public static bool CheckPassword(string username, string password)
  {
    if (!Users.ContainsKey(username))
    {
      return false;
    }

    User u = Users[username];

    return CheckPassword(password,
      u.Salt, u.SaltedHashedPassword);
  }

  // Sprawdzenie hasła z użyciem skrótu.
  public static bool CheckPassword(string password,
    string salt, string hashedPassword)
  {
    // Powtórne wygenerowanie skrótu soli i hasła.
    string saltedhashedPassword = SaltAndHashPassword(
      password, salt);

    return (saltedhashedPassword == hashedPassword);
  }

  private static string SaltAndHashPassword(string password, string salt)
  {
    using (SHA256 sha = SHA256.Create())
    {
      string saltedPassword = password + salt;
      return ToBase64String(sha.ComputeHash(
        Encoding.Unicode.GetBytes(saltedPassword)));
    }
  }

  public static string? PublicKey;

  public static string GenerateSignature(string data)
  {
    byte[] dataBytes = Encoding.Unicode.GetBytes(data);
    SHA256 sha = SHA256.Create();
    byte[] hashedData = sha.ComputeHash(dataBytes);
    RSA rsa = RSA.Create();

    PublicKey = rsa.ToXmlString(false); // Wykluczenie klucza prywatnego.

    return ToBase64String(rsa.SignHash(hashedData,
      HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1));
  }

  public static bool ValidateSignature(
    string data, string signature)
  {
    if (PublicKey is null) return false;

    byte[] dataBytes = Encoding.Unicode.GetBytes(data);
    SHA256 sha = SHA256.Create();

    byte[] hashedData = sha.ComputeHash(dataBytes);
    byte[] signatureBytes = FromBase64String(signature);

    RSA rsa = RSA.Create();
    rsa.FromXmlString(PublicKey);

    return rsa.VerifyHash(hashedData, signatureBytes,
      HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
  }

  public static byte[] GetRandomKeyOrIV(int size)
  {
    RandomNumberGenerator r = RandomNumberGenerator.Create();

    byte[] data = new byte[size];
    r.GetBytes(data);

    // Zmienna data jest tablicą silnych kryptograficznie
    // liczb losowych typu byte.
    return data;
  }

}