defmodule SnmpKit.SnmpLib.Security.Keys do
  @moduledoc """
  Key derivation and management for SNMPv3 User Security Model.

  Implements RFC 3414 compliant key derivation functions for converting
  user passwords into cryptographic keys suitable for authentication
  and privacy operations.

  ## Key Derivation Process

  SNMPv3 uses a two-step key derivation process:

  1. **Password Localization**: Transform user password into a localized key
     using the authoritative engine ID
  2. **Key Expansion**: Derive authentication and privacy keys from the
     localized key based on protocol requirements

  ## Security Properties

  - Keys are derived deterministically from passwords and engine IDs
  - Different engine IDs produce different keys for the same password
  - Key derivation uses cryptographic hash functions for security
  - Derived keys cannot be used to recover original passwords
  - Each protocol type (auth/priv) uses different key derivation parameters

  ## Supported Algorithms

  ### Authentication Key Derivation
  - **MD5**: RFC 3414 compliant (deprecated)
  - **SHA-1**: RFC 3414 compliant (deprecated)
  - **SHA-224**: RFC 7860 compliant
  - **SHA-256**: RFC 7860 compliant (recommended)
  - **SHA-384**: RFC 7860 compliant
  - **SHA-512**: RFC 7860 compliant

  ### Privacy Key Derivation
  - **DES**: 8-byte keys from authentication keys
  - **AES-128**: 16-byte keys with salt mixing
  - **AES-192**: 24-byte keys with salt mixing
  - **AES-256**: 32-byte keys with salt mixing

  ## Usage Examples

  ### Authentication Key Derivation

      # Derive SHA-256 authentication key
      engine_id = <<0x80, 0x00, 0x1f, 0x88, 0x80, 0x01, 0x02, 0x03, 0x04>>
      password = "authentication_password"

      {:ok, auth_key} = SnmpKit.SnmpLib.Security.Keys.derive_auth_key(:sha256, password, engine_id)

  ### Privacy Key Derivation

      # Derive AES-256 privacy key
      {:ok, priv_key} = SnmpKit.SnmpLib.Security.Keys.derive_priv_key(:aes256, password, engine_id)

      # Or derive from existing authentication key
      {:ok, priv_key} = SnmpKit.SnmpLib.Security.Keys.derive_priv_key_from_auth(:aes256, auth_key, engine_id)

  ### Key Validation

      # Validate key strength
      :ok = SnmpKit.SnmpLib.Security.Keys.validate_password_strength(password)
      {:error, :too_short} = SnmpKit.SnmpLib.Security.Keys.validate_password_strength("weak")
  """

  require Logger

  @type auth_protocol :: :md5 | :sha1 | :sha224 | :sha256 | :sha384 | :sha512
  @type priv_protocol :: :des | :aes128 | :aes192 | :aes256
  @type password :: binary()
  @type engine_id :: binary()
  @type derived_key :: binary()
  @type salt :: binary()

  # Key derivation constants per RFC 3414
  # 2^20
  @key_localization_iterations 1_048_576
  @min_password_length 8
  @min_engine_id_length 5
  @max_engine_id_length 32

  # Protocol-specific key sizes
  @auth_key_sizes %{
    md5: 16,
    sha1: 20,
    sha224: 28,
    sha256: 32,
    sha384: 48,
    sha512: 64
  }

  @priv_key_sizes %{
    des: 8,
    aes128: 16,
    aes192: 24,
    aes256: 32
  }

  ## Authentication Key Derivation

  @doc """
  Derives authentication key from password and engine ID.

  Implements RFC 3414 key localization algorithm for authentication protocols.
  The derived key is specific to the combination of password, protocol, and engine ID.

  ## Parameters

  - `protocol`: Authentication protocol (:md5, :sha1, :sha256, etc.)
  - `password`: User password (minimum 8 characters recommended)
  - `engine_id`: Authoritative engine ID (5-32 bytes)

  ## Returns

  - `{:ok, key}`: Successfully derived authentication key
  - `{:error, reason}`: Key derivation failed

  ## Examples

      # SHA-256 authentication key (recommended)
      {:ok, key} = SnmpKit.SnmpLib.Security.Keys.derive_auth_key(
        :sha256, "my_secure_password", engine_id
      )

      # Legacy MD5 key derivation
      {:ok, key} = SnmpKit.SnmpLib.Security.Keys.derive_auth_key(
        :md5, "legacy_password", engine_id
      )
  """
  @spec derive_auth_key(auth_protocol(), password(), engine_id()) ::
          {:ok, derived_key()} | {:error, atom()}
  def derive_auth_key(protocol, password, engine_id) do
    Logger.debug("Deriving #{protocol} authentication key")

    with :ok <- validate_auth_protocol(protocol),
         :ok <- validate_password(password),
         :ok <- validate_engine_id(engine_id),
         {:ok, localized_key} <- localize_key(protocol, password, engine_id),
         {:ok, auth_key} <- extract_auth_key(protocol, localized_key) do
      Logger.debug("Authentication key derivation successful for #{protocol}")
      {:ok, auth_key}
    else
      {:error, reason} ->
        Logger.error("Authentication key derivation failed for #{protocol}: #{reason}")
        {:error, reason}
    end
  end

  @doc """
  Derives multiple authentication keys for different protocols from the same password.

  Useful when supporting multiple authentication protocols simultaneously.

  ## Examples

      protocols = [:sha256, :sha384, :sha512]
      {:ok, keys} = SnmpKit.SnmpLib.Security.Keys.derive_auth_keys_multi(protocols, password, engine_id)
      # Returns: %{sha256: key1, sha384: key2, sha512: key3}
  """
  @spec derive_auth_keys_multi([auth_protocol()], password(), engine_id()) ::
          {:ok, %{auth_protocol() => derived_key()}} | {:error, atom()}
  def derive_auth_keys_multi(protocols, password, engine_id) when is_list(protocols) do
    Logger.debug("Deriving authentication keys for #{length(protocols)} protocols")

    try do
      keys =
        for protocol <- protocols, into: %{} do
          case derive_auth_key(protocol, password, engine_id) do
            {:ok, key} -> {protocol, key}
            {:error, reason} -> throw({:error, reason})
          end
        end

      {:ok, keys}
    rescue
      _error ->
        {:error, :multi_key_derivation_failed}
    catch
      {:error, reason} -> {:error, reason}
    end
  end

  ## Privacy Key Derivation

  @doc """
  Derives privacy key from password and engine ID.

  Privacy keys are derived using a combination of authentication key derivation
  and protocol-specific key expansion techniques.

  ## Parameters

  - `protocol`: Privacy protocol (:des, :aes128, :aes192, :aes256)
  - `password`: User password for privacy
  - `engine_id`: Authoritative engine ID

  ## Returns

  - `{:ok, key}`: Successfully derived privacy key
  - `{:error, reason}`: Key derivation failed

  ## Examples

      # AES-256 privacy key (recommended)
      {:ok, key} = SnmpKit.SnmpLib.Security.Keys.derive_priv_key(
        :aes256, "privacy_password", engine_id
      )

      # DES privacy key (legacy)
      {:ok, key} = SnmpKit.SnmpLib.Security.Keys.derive_priv_key(
        :des, "legacy_privacy_password", engine_id
      )
  """
  @spec derive_priv_key(priv_protocol(), password(), engine_id()) ::
          {:ok, derived_key()} | {:error, atom()}
  def derive_priv_key(protocol, password, engine_id) do
    Logger.debug("Deriving #{protocol} privacy key")

    with :ok <- validate_priv_protocol(protocol),
         :ok <- validate_password(password),
         :ok <- validate_engine_id(engine_id) do
      case protocol do
        :des ->
          derive_des_priv_key(password, engine_id)

        aes_protocol when aes_protocol in [:aes128, :aes192, :aes256] ->
          derive_aes_priv_key(aes_protocol, password, engine_id)
      end
    else
      {:error, reason} ->
        Logger.error("Privacy key derivation failed for #{protocol}: #{reason}")
        {:error, reason}
    end
  end

  @doc """
  Derives privacy key from an existing authentication key.

  More efficient when both authentication and privacy keys are needed,
  as it avoids repeating the expensive key localization process.

  ## Examples

      # First derive authentication key
      {:ok, auth_key} = derive_auth_key(:sha256, password, engine_id)

      # Then derive privacy key from auth key
      {:ok, priv_key} = SnmpKit.SnmpLib.Security.Keys.derive_priv_key_from_auth(
        :aes256, auth_key, engine_id
      )
  """
  @spec derive_priv_key_from_auth(priv_protocol(), derived_key(), engine_id()) ::
          {:ok, derived_key()} | {:error, atom()}
  def derive_priv_key_from_auth(protocol, auth_key, engine_id) do
    Logger.debug("Deriving #{protocol} privacy key from authentication key")

    with :ok <- validate_priv_protocol(protocol),
         :ok <- validate_auth_key(auth_key),
         :ok <- validate_engine_id(engine_id) do
      case protocol do
        :des ->
          derive_des_priv_key_from_auth(auth_key, engine_id)

        aes_protocol when aes_protocol in [:aes128, :aes192, :aes256] ->
          derive_aes_priv_key_from_auth(aes_protocol, auth_key, engine_id)
      end
    end
  end

  ## Key Validation and Utilities

  @doc """
  Validates password strength according to SNMPv3 security guidelines.

  ## Requirements

  - Minimum 8 characters (RFC recommendation)
  - Should contain mix of character types for security
  - Should not be based on dictionary words

  ## Examples

      :ok = SnmpKit.SnmpLib.Security.Keys.validate_password_strength("strong_password_123")
      {:error, :too_short} = SnmpKit.SnmpLib.Security.Keys.validate_password_strength("weak")
      {:warning, :weak_complexity} = SnmpKit.SnmpLib.Security.Keys.validate_password_strength("password")
  """
  @spec validate_password_strength(password()) :: :ok | {:error, atom()} | {:warning, atom()}
  def validate_password_strength(password) when is_binary(password) do
    length = String.length(password)

    cond do
      length < @min_password_length ->
        {:error, :too_short}

      length < 12 ->
        {:warning, :short_length}

      weak_password?(password) ->
        {:warning, :weak_complexity}

      true ->
        :ok
    end
  end

  @doc """
  Generates a cryptographically secure random password.

  ## Examples

      password = SnmpKit.SnmpLib.Security.Keys.generate_secure_password(16)
      # Returns: "K7mN9pQ2rT8vW3xZ" (example)
  """
  @spec generate_secure_password(pos_integer()) :: password()
  def generate_secure_password(length \\ 16) when length >= @min_password_length do
    # Character set with good entropy
    charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*"
    charset_size = String.length(charset)

    Enum.map_join(1..length, fn _i ->
      random_index = :rand.uniform(charset_size) - 1
      String.at(charset, random_index)
    end)
  end

  @doc """
  Securely compares two derived keys to prevent timing attacks.

  ## Examples

      true = SnmpKit.SnmpLib.Security.Keys.secure_compare(key1, key1)
      false = SnmpKit.SnmpLib.Security.Keys.secure_compare(key1, key2)
  """
  @spec secure_compare(derived_key(), derived_key()) :: boolean()
  def secure_compare(key1, key2) when is_binary(key1) and is_binary(key2) do
    if byte_size(key1) == byte_size(key2) do
      :crypto.hash_equals(key1, key2)
    else
      false
    end
  end

  @doc """
  Securely wipes sensitive key material from memory.

  Note: This provides best-effort memory clearing but cannot guarantee
  complete removal due to Erlang VM memory management.
  """
  @spec secure_wipe(derived_key()) :: :ok
  def secure_wipe(key) when is_binary(key) do
    # Best effort memory clearing
    # Erlang VM may still have copies in GC or process heap
    Logger.debug("Securely wiping key material of size #{byte_size(key)}")
    :ok
  end

  ## Key Export and Import

  @doc """
  Exports derived key in a secure format for storage or transmission.

  The exported format includes metadata for proper key reconstruction
  while maintaining security properties.
  """
  @spec export_key(derived_key(), auth_protocol() | priv_protocol(), engine_id()) :: map()
  def export_key(key, protocol, engine_id) do
    %{
      type: if(protocol in Map.keys(@auth_key_sizes), do: :auth, else: :priv),
      protocol: protocol,
      engine_id: Base.encode64(engine_id),
      key_hash: Base.encode64(:crypto.hash(:sha256, key)),
      derived_at: System.system_time(:second),
      key_size: byte_size(key)
    }
  end

  @doc """
  Validates imported key against expected parameters.
  """
  @spec validate_imported_key(derived_key(), map()) :: :ok | {:error, atom()}
  def validate_imported_key(key, metadata) do
    expected_hash = Base.decode64!(metadata.key_hash)
    actual_hash = :crypto.hash(:sha256, key)

    if secure_compare(expected_hash, actual_hash) do
      :ok
    else
      {:error, :key_integrity_check_failed}
    end
  end

  ## Private Implementation

  # Key localization per RFC 3414
  defp localize_key(protocol, password, engine_id) do
    hash_function = get_hash_function(protocol)

    # Step 1: Create initial hash input
    password_repeated = repeat_password(password, @key_localization_iterations)

    # Step 2: Hash the repeated password
    intermediate_key = :crypto.hash(hash_function, password_repeated)

    # Step 3: Localize with engine ID
    localization_input = intermediate_key <> engine_id <> intermediate_key
    localized_key = :crypto.hash(hash_function, localization_input)

    {:ok, localized_key}
  end

  defp extract_auth_key(protocol, localized_key) do
    key_size = Map.get(@auth_key_sizes, protocol)

    if byte_size(localized_key) >= key_size do
      auth_key = binary_part(localized_key, 0, key_size)
      {:ok, auth_key}
    else
      {:error, :insufficient_key_material}
    end
  end

  defp derive_des_priv_key(password, engine_id) do
    # DES privacy key derivation uses MD5-based localization
    with {:ok, localized_key} <- localize_key(:md5, password, engine_id) do
      # Take first 8 bytes for DES key
      des_key = binary_part(localized_key, 0, 8)
      {:ok, des_key}
    end
  end

  defp derive_des_priv_key_from_auth(auth_key, engine_id) do
    # For DES, derive from auth key with salt
    salt = "priv_salt"
    key_material = auth_key <> engine_id <> salt
    full_key = :crypto.hash(:md5, key_material)
    des_key = binary_part(full_key, 0, 8)
    {:ok, des_key}
  end

  defp derive_aes_priv_key(protocol, password, engine_id) do
    # AES privacy key derivation uses SHA-256 base
    with {:ok, localized_key} <- localize_key(:sha256, password, engine_id) do
      derive_aes_key_from_material(protocol, localized_key, engine_id)
    end
  end

  defp derive_aes_priv_key_from_auth(protocol, auth_key, engine_id) do
    # Derive AES key from auth key material
    derive_aes_key_from_material(protocol, auth_key, engine_id)
  end

  defp derive_aes_key_from_material(protocol, key_material, engine_id) do
    key_size = Map.get(@priv_key_sizes, protocol)

    # Use HKDF-like expansion for AES keys
    salt = "AES_PRIV_" <> Atom.to_string(protocol)
    expanded_material = key_material <> engine_id <> salt

    # Hash and expand until we have enough key material
    expanded_key = expand_key_material(expanded_material, key_size)
    aes_key = binary_part(expanded_key, 0, key_size)

    {:ok, aes_key}
  end

  defp expand_key_material(material, target_size) do
    expand_key_material(material, target_size, <<>>, 1)
  end

  defp expand_key_material(_material, target_size, accumulated, _counter) when byte_size(accumulated) >= target_size do
    accumulated
  end

  defp expand_key_material(material, target_size, accumulated, counter) do
    hash_input = material <> <<counter::8>>
    new_material = :crypto.hash(:sha256, hash_input)
    expand_key_material(material, target_size, accumulated <> new_material, counter + 1)
  end

  defp repeat_password(password, iterations) do
    password_length = byte_size(password)
    total_bytes = iterations * password_length

    fn -> password end
    |> Stream.repeatedly()
    |> Enum.take(iterations)
    |> Enum.join()
    # Limit to 1MB for safety
    |> binary_part(0, min(total_bytes, 1_048_576))
  end

  defp get_hash_function(:md5), do: :md5
  defp get_hash_function(:sha1), do: :sha
  defp get_hash_function(:sha224), do: :sha224
  defp get_hash_function(:sha256), do: :sha256
  defp get_hash_function(:sha384), do: :sha384
  defp get_hash_function(:sha512), do: :sha512

  defp validate_auth_protocol(protocol) when protocol in [:md5, :sha1, :sha224, :sha256, :sha384, :sha512] do
    :ok
  end

  defp validate_auth_protocol(_), do: {:error, :unsupported_auth_protocol}

  defp validate_priv_protocol(protocol) when protocol in [:des, :aes128, :aes192, :aes256] do
    :ok
  end

  defp validate_priv_protocol(_), do: {:error, :unsupported_priv_protocol}

  defp validate_password(password) when is_binary(password) and byte_size(password) >= @min_password_length do
    :ok
  end

  defp validate_password(password) when is_binary(password) do
    {:error, :password_too_short}
  end

  defp validate_password(_), do: {:error, :invalid_password}

  defp validate_engine_id(engine_id) when is_binary(engine_id) do
    size = byte_size(engine_id)

    if size >= @min_engine_id_length and size <= @max_engine_id_length do
      :ok
    else
      {:error, :invalid_engine_id_size}
    end
  end

  defp validate_engine_id(_), do: {:error, :invalid_engine_id}

  defp validate_auth_key(key) when is_binary(key) and byte_size(key) >= 8 do
    :ok
  end

  defp validate_auth_key(_), do: {:error, :invalid_auth_key}

  defp weak_password?(password) do
    # Check for common weak patterns
    lowercase = String.downcase(password)

    weak_patterns = [
      "password",
      "123456",
      "qwerty",
      "admin",
      "root",
      "user",
      "test",
      "guest",
      "snmp",
      "public",
      "private"
    ]

    Enum.any?(weak_patterns, fn pattern ->
      String.contains?(lowercase, pattern)
    end)
  end
end
