defmodule Towerops.Accounts.WebAuthn do
  @moduledoc """
  WebAuthn verification for passkey authentication.

  This module handles registration (attestation) and authentication (assertion)
  verification for WebAuthn/FIDO2 credentials using CBOR decoding and
  standard Erlang crypto libraries.
  """

  import Bitwise

  require Logger

  @doc """
  Verifies a WebAuthn registration (attestation) response.

  Returns `{:ok, credential_data}` with the parsed credential information,
  or `{:error, reason}` if verification fails.
  """
  def verify_attestation(attestation, challenge, origin, rp_id) do
    with {:ok, client_data} <- decode_client_data(attestation["clientDataJSON"]),
         :ok <- verify_client_data_type(client_data, "webauthn.create"),
         :ok <- verify_challenge(client_data, challenge),
         :ok <- verify_origin(client_data, origin),
         {:ok, attestation_object} <- decode_attestation_object(attestation["attestationObject"]),
         {:ok, auth_data} <- parse_authenticator_data(attestation_object["authData"]),
         :ok <- verify_rp_id_hash(auth_data.rp_id_hash, rp_id),
         :ok <- verify_user_present(auth_data.flags),
         {:ok, credential_data} <- extract_credential_data(auth_data) do
      {:ok,
       %{
         credential_id: credential_data.credential_id,
         public_key: credential_data.public_key,
         aaguid: auth_data.aaguid,
         sign_count: auth_data.sign_count,
         attestation_format: attestation_object["fmt"]
       }}
    else
      {:error, reason} = error ->
        Logger.error("WebAuthn attestation verification failed: #{inspect(reason)}")
        error
    end
  end

  @doc """
  Verifies a WebAuthn authentication (assertion) response.

  Returns `{:ok, sign_count}` if verification succeeds,
  or `{:error, reason}` if verification fails.
  """
  def verify_assertion(assertion, challenge, origin, rp_id, public_key, stored_sign_count) do
    with {:ok, client_data} <- decode_client_data(assertion["clientDataJSON"]),
         :ok <- verify_client_data_type(client_data, "webauthn.get"),
         :ok <- verify_challenge(client_data, challenge),
         :ok <- verify_origin(client_data, origin),
         {:ok, auth_data_bytes} <- decode_base64url(assertion["authenticatorData"]),
         {:ok, auth_data} <- parse_authenticator_data(auth_data_bytes),
         :ok <- verify_rp_id_hash(auth_data.rp_id_hash, rp_id),
         :ok <- verify_user_present(auth_data.flags),
         :ok <- verify_sign_count(auth_data.sign_count, stored_sign_count),
         {:ok, signature} <- decode_base64url(assertion["signature"]),
         {:ok, client_data_json} <- decode_base64url(assertion["clientDataJSON"]),
         :ok <- verify_signature(auth_data_bytes, client_data_json, signature, public_key) do
      {:ok, auth_data.sign_count}
    else
      {:error, reason} = error ->
        Logger.error("WebAuthn assertion verification failed: #{inspect(reason)}")
        error
    end
  end

  # Private functions

  defp decode_client_data(base64_client_data) do
    with {:ok, json_bytes} <- decode_base64url(base64_client_data),
         {:ok, client_data} <- Jason.decode(json_bytes) do
      {:ok, client_data}
    else
      {:error, reason} = error ->
        Logger.error("Failed to decode client data: #{inspect(reason)}")
        error
    end
  end

  defp decode_attestation_object(base64_attestation) do
    with {:ok, cbor_bytes} <- decode_base64url(base64_attestation),
         {:ok, attestation_object, _} <- CBOR.decode(cbor_bytes) do
      {:ok, attestation_object}
    else
      _ -> {:error, :invalid_attestation_object}
    end
  end

  defp decode_base64url(base64_string) do
    decoded = Base.url_decode64!(base64_string, padding: false)
    {:ok, decoded}
  rescue
    _ -> {:error, :invalid_base64}
  end

  defp verify_client_data_type(client_data, expected_type) do
    received_type = client_data["type"]

    if received_type == expected_type do
      :ok
    else
      Logger.error("Client data type mismatch. Expected: #{inspect(expected_type)}, Received: #{inspect(received_type)}")

      {:error, :invalid_client_data_type}
    end
  end

  defp verify_challenge(client_data, expected_challenge) do
    received_challenge = client_data["challenge"]

    if received_challenge == expected_challenge do
      :ok
    else
      Logger.error(
        "Challenge mismatch. Expected: #{inspect(expected_challenge)}, Received: #{inspect(received_challenge)}"
      )

      {:error, :challenge_mismatch}
    end
  end

  defp verify_origin(client_data, expected_origin) do
    received_origin = client_data["origin"]

    if received_origin == expected_origin do
      :ok
    else
      Logger.error("Origin mismatch. Expected: #{inspect(expected_origin)}, Received: #{inspect(received_origin)}")

      {:error, :origin_mismatch}
    end
  end

  defp verify_rp_id_hash(rp_id_hash, rp_id) do
    expected_hash = :crypto.hash(:sha256, rp_id)

    if rp_id_hash == expected_hash do
      :ok
    else
      {:error, :rp_id_mismatch}
    end
  end

  defp verify_user_present(flags) do
    # Bit 0: User Present (UP)
    user_present = (flags &&& 0x01) != 0

    if user_present do
      :ok
    else
      {:error, :user_not_present}
    end
  end

  defp verify_sign_count(new_count, stored_count) do
    cond do
      new_count == 0 and stored_count == 0 ->
        # Both zero is acceptable (authenticator doesn't support counters)
        :ok

      new_count > stored_count ->
        # Counter increased as expected
        :ok

      true ->
        # Counter decreased or didn't increase - possible cloned authenticator
        {:error, :invalid_sign_count}
    end
  end

  defp verify_signature(auth_data_bytes, client_data_json, signature, public_key) do
    # Construct the signed data: authenticator data || SHA-256(client data JSON)
    client_data_hash = :crypto.hash(:sha256, client_data_json)
    signed_data = auth_data_bytes <> client_data_hash

    # Parse the public key and verify signature
    case parse_public_key(public_key) do
      {:ok, parsed_key} ->
        if :public_key.verify(signed_data, :sha256, signature, parsed_key) do
          :ok
        else
          {:error, :signature_verification_failed}
        end

      {:error, _} = error ->
        error
    end
  end

  # Handle CBOR.Tag wrapped authenticator data
  defp parse_authenticator_data(%CBOR.Tag{tag: :bytes, value: binary_data}) do
    parse_authenticator_data(binary_data)
  end

  defp parse_authenticator_data(auth_data) when is_binary(auth_data) do
    # Authenticator data structure:
    # - 32 bytes: RP ID hash
    # - 1 byte: Flags
    # - 4 bytes: Sign count (big-endian)
    # - Variable: Attested credential data (if present)
    # - Variable: Extensions (if present)

    case auth_data do
      <<rp_id_hash::binary-size(32), flags::8, sign_count::32, rest::binary>> ->
        # Check if attested credential data is present (bit 6 of flags)
        attested_credential_present = (flags &&& 0x40) != 0

        if attested_credential_present do
          parse_attested_credential_data(rest, rp_id_hash, flags, sign_count)
        else
          {:ok,
           %{
             rp_id_hash: rp_id_hash,
             flags: flags,
             sign_count: sign_count,
             aaguid: nil,
             credential_data: nil
           }}
        end

      _ ->
        {:error, :invalid_authenticator_data}
    end
  end

  defp parse_attested_credential_data(data, rp_id_hash, flags, sign_count) do
    # Attested credential data structure:
    # - 16 bytes: AAGUID
    # - 2 bytes: Credential ID length (big-endian)
    # - Variable: Credential ID
    # - Variable: Credential public key (CBOR-encoded COSE_Key)

    with <<aaguid::binary-size(16), cred_id_length::16, rest::binary>> <- data,
         true <- byte_size(rest) >= cred_id_length,
         <<credential_id::binary-size(^cred_id_length), public_key_bytes::binary>> <- rest,
         {:ok, cose_key, remaining} <- CBOR.decode(public_key_bytes),
         # Calculate the actual length of the COSE key CBOR data
         public_key_cbor_length = byte_size(public_key_bytes) - byte_size(remaining),
         <<public_key_cbor::binary-size(^public_key_cbor_length), _::binary>> <-
           public_key_bytes do
      {:ok,
       %{
         rp_id_hash: rp_id_hash,
         flags: flags,
         sign_count: sign_count,
         aaguid: aaguid,
         credential_data: %{
           credential_id: credential_id,
           public_key: public_key_cbor,
           cose_key: cose_key
         }
       }}
    else
      false -> {:error, :invalid_credential_id_length}
      {:error, _} -> {:error, :invalid_public_key}
      _ -> {:error, :invalid_attested_credential_data}
    end
  end

  defp extract_credential_data(%{credential_data: nil}) do
    {:error, :no_credential_data}
  end

  defp extract_credential_data(%{credential_data: credential_data}) do
    # The public_key is already CBOR-encoded, ready for storage
    {:ok,
     %{
       credential_id: credential_data.credential_id,
       public_key: credential_data.public_key
     }}
  end

  defp parse_public_key(public_key_binary) do
    # Decode the stored COSE key
    case CBOR.decode(public_key_binary) do
      {:ok, cose_key, _} ->
        cose_key_to_erlang_key(cose_key)

      _ ->
        {:error, :invalid_stored_public_key}
    end
  end

  defp cose_key_to_erlang_key(cose_key) do
    # COSE key format (CBOR map):
    # 1 (kty): Key type (2 = EC2, 3 = RSA)
    # 3 (alg): Algorithm (-7 = ES256, -257 = RS256)
    # -1 (crv): EC2 curve (1 = P-256)
    # -2 (x): EC2 x-coordinate
    # -3 (y): EC2 y-coordinate
    # For RSA: -1 (n): modulus, -2 (e): exponent

    kty = Map.get(cose_key, 1)
    alg = Map.get(cose_key, 3)

    case {kty, alg} do
      # EC2 with ES256 (P-256)
      {2, -7} ->
        x = Map.get(cose_key, -2)
        y = Map.get(cose_key, -3)

        if x && y do
          # Convert to Erlang public key format
          # EC point in uncompressed format: 0x04 || x || y
          point = <<0x04, x::binary, y::binary>>

          ec_params = {:namedCurve, {1, 2, 840, 10_045, 3, 1, 7}}
          {:ok, {:ECPoint, point, ec_params}}
        else
          {:error, :invalid_ec_key}
        end

      # RSA with RS256
      {3, -257} ->
        n = Map.get(cose_key, -1)
        e = Map.get(cose_key, -2)

        if n && e do
          # Convert binary to integer
          n_int = :binary.decode_unsigned(n)
          e_int = :binary.decode_unsigned(e)

          {:ok, {:RSAPublicKey, n_int, e_int}}
        else
          {:error, :invalid_rsa_key}
        end

      _ ->
        {:error, :unsupported_key_type}
    end
  end
end
