defmodule Parrhesia.Protocol.EventValidator do
  @moduledoc """
  Strict NIP-01 event validation helpers.
  """

  @required_fields ~w[id pubkey created_at kind tags content sig]
  @max_kind 65_535
  @default_max_event_future_skew_seconds 900
  @default_max_tags_per_event 256
  @default_nip43_request_max_age_seconds 300
  @verify_event_signatures_locked Application.compile_env(
                                    :parrhesia,
                                    [:features, :verify_event_signatures_locked?],
                                    false
                                  )

  @supported_mls_ciphersuites MapSet.new(~w[0x0001 0x0002 0x0003 0x0004 0x0005 0x0006 0x0007])
  @required_mls_extensions MapSet.new(["0xf2ee", "0x000a"])
  @supported_keypackage_ref_sizes [32, 48, 64]

  @type error_reason ::
          :invalid_shape
          | :invalid_id
          | :invalid_pubkey
          | :invalid_created_at
          | :created_at_too_far_in_future
          | :invalid_kind
          | :too_many_tags
          | :invalid_tags
          | :invalid_content
          | :invalid_sig
          | :invalid_id_hash
          | :invalid_signature
          | :invalid_marmot_keypackage_content
          | :missing_marmot_encoding_tag
          | :invalid_marmot_encoding_tag
          | :missing_marmot_protocol_version_tag
          | :invalid_marmot_protocol_version_tag
          | :missing_marmot_ciphersuite_tag
          | :invalid_marmot_ciphersuite_tag
          | :missing_marmot_extensions_tag
          | :invalid_marmot_extensions_tag
          | :missing_marmot_relays_tag
          | :invalid_marmot_relays_tag
          | :missing_marmot_keypackage_ref_tag
          | :invalid_marmot_keypackage_ref_tag
          | :missing_marmot_relay_tag
          | :invalid_marmot_relay_tag
          | :invalid_marmot_direct_welcome_event
          | :invalid_giftwrap_content
          | :missing_giftwrap_recipient_tag
          | :invalid_giftwrap_recipient_tag
          | :missing_marmot_group_tag
          | :invalid_marmot_group_tag
          | :invalid_marmot_group_content
          | :missing_nip66_d_tag
          | :invalid_nip66_d_tag
          | :invalid_nip66_discovery_tag
          | :missing_nip66_frequency_tag
          | :invalid_nip66_frequency_tag
          | :invalid_nip66_timeout_tag
          | :invalid_nip66_check_tag
          | :missing_nip43_protected_tag
          | :missing_nip43_claim_tag
          | :invalid_nip43_claim_tag
          | :missing_nip43_member_tag
          | :invalid_nip43_member_tag
          | :missing_nip43_pubkey_tag
          | :invalid_nip43_pubkey_tag
          | :stale_nip43_join_request
          | :stale_nip43_leave_request

  @spec validate(map()) :: :ok | {:error, error_reason()}
  def validate(event) when is_map(event) do
    with :ok <- validate_required_fields(event),
         :ok <- validate_id_shape(event["id"]),
         :ok <- validate_pubkey(event["pubkey"]),
         :ok <- validate_created_at(event["created_at"]),
         :ok <- validate_kind(event["kind"]),
         :ok <- validate_tags(event["tags"]),
         :ok <- validate_content(event["content"]),
         :ok <- validate_sig(event["sig"]),
         :ok <- validate_id_hash(event),
         :ok <- validate_signature(event) do
      validate_kind_specific(event)
    end
  end

  def validate(_event), do: {:error, :invalid_shape}

  @spec compute_id(map()) :: String.t()
  def compute_id(event) do
    [
      0,
      event["pubkey"],
      event["created_at"],
      event["kind"],
      event["tags"],
      event["content"]
    ]
    |> JSON.encode!()
    |> then(&:crypto.hash(:sha256, &1))
    |> Base.encode16(case: :lower)
  end

  @error_messages %{
    invalid_shape:
      "invalid: event must include id, pubkey, created_at, kind, tags, content, and sig",
    invalid_id: "invalid: id must be 32-byte lowercase hex",
    invalid_pubkey: "invalid: pubkey must be 32-byte lowercase hex",
    invalid_created_at: "invalid: created_at must be a non-negative integer unix timestamp",
    created_at_too_far_in_future:
      "invalid: event creation date is too far off from the current time",
    invalid_kind: "invalid: kind must be an integer between 0 and 65535",
    too_many_tags: "invalid: event tags exceed configured limit",
    invalid_tags: "invalid: tags must be an array of non-empty string arrays",
    invalid_content: "invalid: content must be a string",
    invalid_sig: "invalid: sig must be 64-byte lowercase hex",
    invalid_id_hash: "invalid: event id does not match serialized event",
    invalid_signature: "invalid: event signature is invalid",
    invalid_marmot_keypackage_content: "invalid: kind 443 content must be non-empty base64",
    missing_marmot_encoding_tag: "invalid: kind 443 must include [\"encoding\", \"base64\"]",
    invalid_marmot_encoding_tag: "invalid: kind 443 must include [\"encoding\", \"base64\"]",
    missing_marmot_protocol_version_tag:
      "invalid: kind 443 must include [\"mls_protocol_version\", \"1.0\"]",
    invalid_marmot_protocol_version_tag:
      "invalid: kind 443 must include [\"mls_protocol_version\", \"1.0\"]",
    missing_marmot_ciphersuite_tag:
      "invalid: kind 443 must include a supported [\"mls_ciphersuite\", \"0x....\"]",
    invalid_marmot_ciphersuite_tag:
      "invalid: kind 443 must include a supported [\"mls_ciphersuite\", \"0x....\"]",
    missing_marmot_extensions_tag:
      "invalid: kind 443 must include [\"mls_extensions\", ...] with 0xf2ee and 0x000a",
    invalid_marmot_extensions_tag:
      "invalid: kind 443 must include [\"mls_extensions\", ...] with 0xf2ee and 0x000a",
    missing_marmot_relays_tag: "invalid: kind 443 must include [\"relays\", <ws/wss url>, ...]",
    invalid_marmot_relays_tag: "invalid: kind 443 must include [\"relays\", <ws/wss url>, ...]",
    missing_marmot_keypackage_ref_tag:
      "invalid: kind 443 must include [\"i\", <hex keypackage ref>]",
    invalid_marmot_keypackage_ref_tag:
      "invalid: kind 443 must include [\"i\", <hex keypackage ref>]",
    missing_marmot_relay_tag:
      "invalid: kind 10051 must include at least one [\"relay\", <ws/wss url>] tag",
    invalid_marmot_relay_tag:
      "invalid: kind 10051 must include at least one [\"relay\", <ws/wss url>] tag",
    invalid_marmot_direct_welcome_event:
      "invalid: kind 444 welcome events must be wrapped in kind 1059 and remain unsigned",
    invalid_giftwrap_content: "invalid: kind 1059 content must be a non-empty encrypted payload",
    missing_giftwrap_recipient_tag:
      "invalid: kind 1059 must include at least one recipient p tag",
    invalid_giftwrap_recipient_tag:
      "invalid: kind 1059 recipient p tags must contain lowercase hex pubkeys",
    missing_marmot_group_tag: "invalid: kind 445 must include at least one h tag with a group id",
    invalid_marmot_group_tag:
      "invalid: kind 445 h tags must contain 32-byte lowercase hex group ids",
    invalid_marmot_group_content: "invalid: kind 445 content must be non-empty base64",
    missing_nip66_d_tag:
      "invalid: kind 30166 must include a single [\"d\", <normalized ws/wss url or relay pubkey>] tag",
    invalid_nip66_d_tag:
      "invalid: kind 30166 must include a single [\"d\", <normalized ws/wss url or relay pubkey>] tag",
    invalid_nip66_discovery_tag: "invalid: kind 30166 includes malformed NIP-66 discovery tags",
    missing_nip66_frequency_tag:
      "invalid: kind 10166 must include a single [\"frequency\", <seconds>] tag",
    invalid_nip66_frequency_tag:
      "invalid: kind 10166 must include a single [\"frequency\", <seconds>] tag",
    invalid_nip66_timeout_tag:
      "invalid: kind 10166 timeout tags must be [\"timeout\", <check>, <ms>]",
    invalid_nip66_check_tag: "invalid: kind 10166 c tags must contain lowercase check names",
    missing_nip43_protected_tag:
      "invalid: NIP-43 events must include a NIP-70 protected [\"-\"] tag",
    missing_nip43_claim_tag:
      "invalid: kinds 28934 and 28935 must include a single [\"claim\", <invite code>] tag",
    invalid_nip43_claim_tag:
      "invalid: kinds 28934 and 28935 must include a single [\"claim\", <invite code>] tag",
    missing_nip43_member_tag:
      "invalid: kind 13534 must include at least one [\"member\", <hex pubkey>] tag",
    invalid_nip43_member_tag:
      "invalid: kind 13534 member tags must contain lowercase hex pubkeys",
    missing_nip43_pubkey_tag:
      "invalid: kinds 8000 and 8001 must include a single [\"p\", <hex pubkey>] tag",
    invalid_nip43_pubkey_tag:
      "invalid: kinds 8000 and 8001 must include a single [\"p\", <hex pubkey>] tag",
    stale_nip43_join_request: "invalid: kind 28934 created_at must be recent",
    stale_nip43_leave_request: "invalid: kind 28936 created_at must be recent"
  }

  @spec error_message(error_reason()) :: String.t()
  def error_message(reason), do: Map.fetch!(@error_messages, reason)

  defp validate_required_fields(event) do
    if Enum.all?(@required_fields, &Map.has_key?(event, &1)) do
      :ok
    else
      {:error, :invalid_shape}
    end
  end

  defp validate_id_shape(id) when is_binary(id) do
    if lowercase_hex?(id, 32), do: :ok, else: {:error, :invalid_id}
  end

  defp validate_id_shape(_id), do: {:error, :invalid_id}

  defp validate_pubkey(pubkey) when is_binary(pubkey) do
    if lowercase_hex?(pubkey, 32), do: :ok, else: {:error, :invalid_pubkey}
  end

  defp validate_pubkey(_pubkey), do: {:error, :invalid_pubkey}

  defp validate_created_at(created_at) when is_integer(created_at) and created_at >= 0 do
    now = System.system_time(:second)
    max_future_skew = max_event_future_skew_seconds()

    if created_at <= now + max_future_skew do
      :ok
    else
      {:error, :created_at_too_far_in_future}
    end
  end

  defp validate_created_at(_created_at), do: {:error, :invalid_created_at}

  defp validate_kind(kind) when is_integer(kind) and kind >= 0 and kind <= @max_kind, do: :ok
  defp validate_kind(_kind), do: {:error, :invalid_kind}

  defp validate_tags(tags) when is_list(tags), do: validate_tags(tags, max_tags_per_event(), 0)

  defp validate_tags(_tags), do: {:error, :invalid_tags}

  defp validate_tags([], _max_tags, _count), do: :ok

  defp validate_tags([tag | rest], max_tags, count) do
    cond do
      count + 1 > max_tags ->
        {:error, :too_many_tags}

      valid_tag?(tag) ->
        validate_tags(rest, max_tags, count + 1)

      true ->
        {:error, :invalid_tags}
    end
  end

  defp validate_content(content) when is_binary(content), do: :ok
  defp validate_content(_content), do: {:error, :invalid_content}

  defp validate_sig(sig) when is_binary(sig) do
    if lowercase_hex?(sig, 64), do: :ok, else: {:error, :invalid_sig}
  end

  defp validate_sig(_sig), do: {:error, :invalid_sig}

  defp validate_id_hash(event) do
    if event["id"] == compute_id(event) do
      :ok
    else
      {:error, :invalid_id_hash}
    end
  end

  defp validate_signature(event) do
    if @verify_event_signatures_locked or verify_event_signatures?() do
      verify_signature(event)
    else
      :ok
    end
  end

  defp verify_signature(%{"id" => id, "pubkey" => pubkey, "sig" => sig}) do
    with {:ok, id_bin} <- Base.decode16(id, case: :lower),
         {:ok, pubkey_bin} <- Base.decode16(pubkey, case: :lower),
         {:ok, sig_bin} <- Base.decode16(sig, case: :lower),
         true <- Secp256k1.schnorr_valid?(sig_bin, id_bin, pubkey_bin) do
      :ok
    else
      _other -> {:error, :invalid_signature}
    end
  rescue
    _error -> {:error, :invalid_signature}
  end

  defp verify_signature(_event), do: {:error, :invalid_signature}

  defp valid_tag?(tag) when is_list(tag) do
    tag != [] and Enum.all?(tag, &is_binary/1)
  end

  defp valid_tag?(_tag), do: false

  defp validate_kind_specific(%{"kind" => 443} = event),
    do: validate_marmot_keypackage_event(event)

  defp validate_kind_specific(%{"kind" => 10_051} = event),
    do: validate_marmot_keypackage_relay_list(event)

  defp validate_kind_specific(%{"kind" => 445} = event),
    do: validate_marmot_group_event(event)

  defp validate_kind_specific(%{"kind" => 444}),
    do: {:error, :invalid_marmot_direct_welcome_event}

  defp validate_kind_specific(%{"kind" => 1059} = event),
    do: validate_giftwrap_event(event)

  defp validate_kind_specific(%{"kind" => 30_166} = event),
    do: validate_nip66_discovery_event(event)

  defp validate_kind_specific(%{"kind" => 10_166} = event),
    do: validate_nip66_monitor_announcement(event)

  defp validate_kind_specific(%{"kind" => 13_534} = event),
    do: validate_nip43_membership_list(event)

  defp validate_kind_specific(%{"kind" => kind} = event) when kind in [8_000, 8_001],
    do: validate_nip43_membership_delta(event)

  defp validate_kind_specific(%{"kind" => 28_934} = event),
    do: validate_nip43_join_request(event)

  defp validate_kind_specific(%{"kind" => 28_935} = event),
    do: validate_nip43_invite_response(event)

  defp validate_kind_specific(%{"kind" => 28_936} = event),
    do: validate_nip43_leave_request(event)

  defp validate_kind_specific(_event), do: :ok

  defp validate_marmot_keypackage_event(event) do
    tags = Map.get(event, "tags", [])

    with :ok <- validate_non_empty_base64_content(event),
         {:ok, ["encoding", "base64"]} <-
           fetch_single_tag(tags, "encoding", :missing_marmot_encoding_tag),
         :ok <-
           validate_single_string_tag(
             tags,
             "mls_protocol_version",
             "1.0",
             :missing_marmot_protocol_version_tag,
             :invalid_marmot_protocol_version_tag
           ),
         :ok <-
           validate_single_string_tag_with_predicate(
             tags,
             "mls_ciphersuite",
             :missing_marmot_ciphersuite_tag,
             :invalid_marmot_ciphersuite_tag,
             &supported_mls_ciphersuite?/1
           ),
         :ok <- validate_mls_extensions_tag(tags),
         :ok <-
           validate_relays_tag(
             tags,
             "relays",
             :missing_marmot_relays_tag,
             :invalid_marmot_relays_tag
           ),
         :ok <- validate_keypackage_ref_tag(tags) do
      :ok
    else
      {:ok, _other_tag} -> {:error, :invalid_marmot_encoding_tag}
      {:error, _reason} = error -> error
    end
  end

  defp validate_marmot_keypackage_relay_list(event) do
    tags = Map.get(event, "tags", [])

    relay_tags = Enum.filter(tags, &match_tag_name?(&1, "relay"))

    cond do
      relay_tags == [] ->
        {:error, :missing_marmot_relay_tag}

      Enum.all?(relay_tags, &valid_single_relay_tag?/1) ->
        :ok

      true ->
        {:error, :invalid_marmot_relay_tag}
    end
  end

  defp validate_marmot_group_event(event) do
    tags = Map.get(event, "tags", [])

    with :ok <- validate_non_empty_base64_content(event, :invalid_marmot_group_content) do
      validate_marmot_group_tags(tags)
    end
  end

  defp validate_giftwrap_event(event) do
    tags = Map.get(event, "tags", [])

    with :ok <- validate_non_empty_content(event, :invalid_giftwrap_content) do
      validate_giftwrap_recipient_tags(tags)
    end
  end

  defp validate_nip66_discovery_event(event) do
    tags = Map.get(event, "tags", [])

    with :ok <- validate_nip66_d_tag(tags),
         :ok <-
           validate_optional_single_string_tag_with_predicate(
             tags,
             "n",
             :invalid_nip66_discovery_tag,
             &(&1 in ["clearnet", "tor", "i2p", "loki"])
           ),
         :ok <-
           validate_optional_single_string_tag_with_predicate(
             tags,
             "T",
             :invalid_nip66_discovery_tag,
             &valid_pascal_case?/1
           ),
         :ok <-
           validate_optional_single_string_tag_with_predicate(
             tags,
             "g",
             :invalid_nip66_discovery_tag,
             &non_empty_string?/1
           ),
         :ok <-
           validate_optional_repeated_tag(
             tags,
             "N",
             &positive_integer_string?/1,
             :invalid_nip66_discovery_tag
           ),
         :ok <-
           validate_optional_repeated_tag(
             tags,
             "R",
             &valid_nip66_requirement_value?/1,
             :invalid_nip66_discovery_tag
           ),
         :ok <-
           validate_optional_repeated_tag(
             tags,
             "k",
             &valid_nip66_kind_value?/1,
             :invalid_nip66_discovery_tag
           ),
         :ok <-
           validate_optional_repeated_tag(
             tags,
             "t",
             &non_empty_string?/1,
             :invalid_nip66_discovery_tag
           ),
         :ok <-
           validate_optional_single_string_tag_with_predicate(
             tags,
             "rtt-open",
             :invalid_nip66_discovery_tag,
             &positive_integer_string?/1
           ),
         :ok <-
           validate_optional_single_string_tag_with_predicate(
             tags,
             "rtt-read",
             :invalid_nip66_discovery_tag,
             &positive_integer_string?/1
           ) do
      validate_optional_single_string_tag_with_predicate(
        tags,
        "rtt-write",
        :invalid_nip66_discovery_tag,
        &positive_integer_string?/1
      )
    end
  end

  defp validate_nip66_monitor_announcement(event) do
    tags = Map.get(event, "tags", [])

    with :ok <-
           validate_single_string_tag_with_predicate(
             tags,
             "frequency",
             :missing_nip66_frequency_tag,
             :invalid_nip66_frequency_tag,
             &positive_integer_string?/1
           ),
         :ok <- validate_optional_repeated_timeout_tags(tags),
         :ok <-
           validate_optional_repeated_tag(
             tags,
             "c",
             &valid_nip66_check_name?/1,
             :invalid_nip66_check_tag
           ) do
      validate_optional_single_string_tag_with_predicate(
        tags,
        "g",
        :invalid_nip66_discovery_tag,
        &non_empty_string?/1
      )
    end
  end

  defp validate_nip43_membership_list(event) do
    tags = Map.get(event, "tags", [])

    case validate_protected_tag(tags) do
      :ok -> validate_optional_repeated_pubkey_tag(tags, "member", :invalid_nip43_member_tag)
      {:error, _reason} = error -> error
    end
  end

  defp validate_nip43_membership_delta(event) do
    tags = Map.get(event, "tags", [])

    case validate_protected_tag(tags) do
      :ok ->
        validate_single_pubkey_tag(
          tags,
          "p",
          :missing_nip43_pubkey_tag,
          :invalid_nip43_pubkey_tag
        )

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

  defp validate_nip43_join_request(event) do
    tags = Map.get(event, "tags", [])

    case validate_protected_tag(tags) do
      :ok ->
        with :ok <-
               validate_single_string_tag_with_predicate(
                 tags,
                 "claim",
                 :missing_nip43_claim_tag,
                 :invalid_nip43_claim_tag,
                 &non_empty_string?/1
               ) do
          validate_recent_created_at(event, :stale_nip43_join_request)
        end

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

  defp validate_nip43_invite_response(event) do
    tags = Map.get(event, "tags", [])

    case validate_protected_tag(tags) do
      :ok ->
        validate_single_string_tag_with_predicate(
          tags,
          "claim",
          :missing_nip43_claim_tag,
          :invalid_nip43_claim_tag,
          &non_empty_string?/1
        )

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

  defp validate_nip43_leave_request(event) do
    tags = Map.get(event, "tags", [])

    case validate_protected_tag(tags) do
      :ok -> validate_recent_created_at(event, :stale_nip43_leave_request)
      {:error, _reason} = error -> error
    end
  end

  defp validate_non_empty_base64_content(event),
    do: validate_non_empty_base64_content(event, :invalid_marmot_keypackage_content)

  defp validate_non_empty_base64_content(event, error_reason) do
    case Base.decode64(Map.get(event, "content", "")) do
      {:ok, decoded} when byte_size(decoded) > 0 -> :ok
      _other -> {:error, error_reason}
    end
  end

  defp validate_non_empty_content(event, error_reason) do
    case Map.get(event, "content", "") do
      content when is_binary(content) and content != "" -> :ok
      _other -> {:error, error_reason}
    end
  end

  defp validate_marmot_group_tags(tags) do
    group_tags = Enum.filter(tags, &match_tag_name?(&1, "h"))

    cond do
      group_tags == [] ->
        {:error, :missing_marmot_group_tag}

      Enum.all?(group_tags, &valid_marmot_group_tag?/1) ->
        :ok

      true ->
        {:error, :invalid_marmot_group_tag}
    end
  end

  defp validate_giftwrap_recipient_tags(tags) do
    recipient_tags = Enum.filter(tags, &match_tag_name?(&1, "p"))

    cond do
      recipient_tags == [] ->
        {:error, :missing_giftwrap_recipient_tag}

      Enum.all?(recipient_tags, &valid_giftwrap_recipient_tag?/1) ->
        :ok

      true ->
        {:error, :invalid_giftwrap_recipient_tag}
    end
  end

  defp validate_single_string_tag(tags, tag_name, expected_value, missing_error, invalid_error) do
    validate_single_string_tag_with_predicate(
      tags,
      tag_name,
      missing_error,
      invalid_error,
      &(&1 == expected_value)
    )
  end

  defp validate_single_string_tag_with_predicate(
         tags,
         tag_name,
         missing_error,
         invalid_error,
         predicate
       )
       when is_function(predicate, 1) do
    case fetch_single_tag(tags, tag_name, missing_error) do
      {:ok, [^tag_name, value]} ->
        if predicate.(value) do
          :ok
        else
          {:error, invalid_error}
        end

      {:ok, _invalid_tag_shape} ->
        {:error, invalid_error}

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

  defp validate_optional_single_string_tag_with_predicate(
         tags,
         tag_name,
         invalid_error,
         predicate
       )
       when is_function(predicate, 1) do
    case Enum.filter(tags, &match_tag_name?(&1, tag_name)) do
      [] ->
        :ok

      [[^tag_name, value]] ->
        if predicate.(value), do: :ok, else: {:error, invalid_error}

      _other ->
        {:error, invalid_error}
    end
  end

  defp validate_mls_extensions_tag(tags) do
    with {:ok, ["mls_extensions" | extensions]} <-
           fetch_single_tag(tags, "mls_extensions", :missing_marmot_extensions_tag),
         true <- extensions != [],
         true <- Enum.all?(extensions, &valid_mls_extension_id?/1),
         true <- required_mls_extensions_present?(extensions) do
      :ok
    else
      {:ok, _invalid_tag_shape} -> {:error, :invalid_marmot_extensions_tag}
      false -> {:error, :invalid_marmot_extensions_tag}
      {:error, _reason} = error -> error
    end
  end

  defp validate_relays_tag(tags, tag_name, missing_error, invalid_error) do
    with {:ok, [^tag_name | relay_urls]} <- fetch_single_tag(tags, tag_name, missing_error),
         true <- relay_urls != [],
         true <- Enum.all?(relay_urls, &valid_websocket_url?/1) do
      :ok
    else
      {:ok, _invalid_tag_shape} -> {:error, invalid_error}
      false -> {:error, invalid_error}
      {:error, _reason} = error -> error
    end
  end

  defp validate_keypackage_ref_tag(tags) do
    with {:ok, ["i", keypackage_ref]} <-
           fetch_single_tag(tags, "i", :missing_marmot_keypackage_ref_tag),
         true <- valid_keypackage_ref?(keypackage_ref) do
      :ok
    else
      {:ok, _invalid_tag_shape} -> {:error, :invalid_marmot_keypackage_ref_tag}
      false -> {:error, :invalid_marmot_keypackage_ref_tag}
      {:error, _reason} = error -> error
    end
  end

  defp validate_nip66_d_tag(tags) do
    with {:ok, ["d", value]} <- fetch_single_tag(tags, "d", :missing_nip66_d_tag),
         true <- valid_websocket_url?(value) or lowercase_hex?(value, 32) do
      :ok
    else
      {:ok, _invalid_tag_shape} -> {:error, :invalid_nip66_d_tag}
      false -> {:error, :invalid_nip66_d_tag}
      {:error, _reason} = error -> error
    end
  end

  defp validate_optional_repeated_timeout_tags(tags) do
    timeout_tags = Enum.filter(tags, &match_tag_name?(&1, "timeout"))

    if Enum.all?(timeout_tags, &valid_nip66_timeout_tag?/1) do
      :ok
    else
      {:error, :invalid_nip66_timeout_tag}
    end
  end

  defp validate_optional_repeated_tag(tags, tag_name, predicate, invalid_error)
       when is_function(predicate, 1) do
    tags
    |> Enum.filter(&match_tag_name?(&1, tag_name))
    |> Enum.reduce_while(:ok, fn
      [^tag_name, value], :ok ->
        if predicate.(value), do: {:cont, :ok}, else: {:halt, {:error, invalid_error}}

      _other, :ok ->
        {:halt, {:error, invalid_error}}
    end)
  end

  defp validate_protected_tag(tags) do
    if Enum.any?(tags, &match?(["-"], &1)) do
      :ok
    else
      {:error, :missing_nip43_protected_tag}
    end
  end

  defp validate_single_pubkey_tag(tags, tag_name, missing_error, invalid_error) do
    case fetch_single_tag(tags, tag_name, missing_error) do
      {:ok, [^tag_name, value]} ->
        if lowercase_hex?(value, 32) do
          :ok
        else
          {:error, invalid_error}
        end

      {:ok, _invalid_tag_shape} ->
        {:error, invalid_error}

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

  defp validate_optional_repeated_pubkey_tag(tags, tag_name, invalid_error) do
    matching_tags = Enum.filter(tags, &match_tag_name?(&1, tag_name))

    if Enum.all?(matching_tags, fn
         [^tag_name, pubkey | _rest] -> lowercase_hex?(pubkey, 32)
         _other -> false
       end) do
      :ok
    else
      {:error, invalid_error}
    end
  end

  defp validate_recent_created_at(%{"created_at" => created_at}, error_reason)
       when is_integer(created_at) do
    if created_at >= System.system_time(:second) - nip43_request_max_age_seconds() do
      :ok
    else
      {:error, error_reason}
    end
  end

  defp validate_recent_created_at(_event, error_reason), do: {:error, error_reason}

  defp fetch_single_tag(tags, tag_name, missing_error) do
    case Enum.filter(tags, &match_tag_name?(&1, tag_name)) do
      [tag] -> {:ok, tag}
      [] -> {:error, missing_error}
      _duplicates -> {:error, missing_error}
    end
  end

  defp match_tag_name?([tag_name | _rest], expected_tag_name) when is_binary(tag_name),
    do: tag_name == expected_tag_name

  defp match_tag_name?(_tag, _expected_tag_name), do: false

  defp supported_mls_ciphersuite?(value) when is_binary(value) do
    value
    |> String.downcase()
    |> then(&MapSet.member?(@supported_mls_ciphersuites, &1))
  end

  defp supported_mls_ciphersuite?(_value), do: false

  defp valid_mls_extension_id?(value) when is_binary(value) do
    String.match?(value, ~r/^0x[0-9a-fA-F]{4}$/)
  end

  defp valid_mls_extension_id?(_value), do: false

  defp required_mls_extensions_present?(extensions) do
    normalized = MapSet.new(Enum.map(extensions, &String.downcase/1))
    MapSet.subset?(@required_mls_extensions, normalized)
  end

  defp valid_single_relay_tag?(["relay", relay_url]), do: valid_websocket_url?(relay_url)
  defp valid_single_relay_tag?(_tag), do: false

  defp valid_marmot_group_tag?(["h", group_id | _rest]), do: lowercase_hex?(group_id, 32)
  defp valid_marmot_group_tag?(_tag), do: false

  defp valid_giftwrap_recipient_tag?(["p", recipient_pubkey | _rest]),
    do: lowercase_hex?(recipient_pubkey, 32)

  defp valid_giftwrap_recipient_tag?(_tag), do: false

  defp valid_websocket_url?(url) when is_binary(url) do
    case URI.parse(url) do
      %URI{scheme: scheme, host: host}
      when scheme in ["ws", "wss"] and is_binary(host) and host != "" ->
        true

      _other ->
        false
    end
  end

  defp valid_websocket_url?(_url), do: false

  defp valid_nip66_timeout_tag?(["timeout", milliseconds]),
    do: positive_integer_string?(milliseconds)

  defp valid_nip66_timeout_tag?(["timeout", check, milliseconds]) do
    valid_nip66_check_name?(check) and positive_integer_string?(milliseconds)
  end

  defp valid_nip66_timeout_tag?(_tag), do: false

  defp valid_nip66_requirement_value?(value) when is_binary(value) do
    normalized = String.trim_leading(value, "!")
    normalized in ["auth", "writes", "pow", "payment"]
  end

  defp valid_nip66_requirement_value?(_value), do: false

  defp valid_nip66_kind_value?(<<"!", rest::binary>>), do: positive_integer_string?(rest)
  defp valid_nip66_kind_value?(value), do: positive_integer_string?(value)

  defp valid_nip66_check_name?(value) when is_binary(value) do
    String.match?(value, ~r/^[a-z0-9-]+$/)
  end

  defp valid_nip66_check_name?(_value), do: false

  defp valid_pascal_case?(value) when is_binary(value) do
    String.match?(value, ~r/^[A-Z][A-Za-z0-9]*$/)
  end

  defp valid_pascal_case?(_value), do: false

  defp positive_integer_string?(value) when is_binary(value) do
    case Integer.parse(value) do
      {integer, ""} when integer >= 0 -> true
      _other -> false
    end
  end

  defp positive_integer_string?(_value), do: false

  defp non_empty_string?(value) when is_binary(value), do: value != ""
  defp non_empty_string?(_value), do: false

  defp valid_keypackage_ref?(value) when is_binary(value) do
    Enum.any?(@supported_keypackage_ref_sizes, &lowercase_hex?(value, &1))
  end

  defp valid_keypackage_ref?(_value), do: false

  defp lowercase_hex?(value, bytes) do
    byte_size(value) == bytes * 2 and
      match?({:ok, _decoded}, Base.decode16(value, case: :lower))
  end

  defp verify_event_signatures? do
    :parrhesia
    |> Application.get_env(:features, [])
    |> Keyword.get(:verify_event_signatures, true)
  end

  defp max_event_future_skew_seconds do
    :parrhesia
    |> Application.get_env(:limits, [])
    |> Keyword.get(:max_event_future_skew_seconds, @default_max_event_future_skew_seconds)
  end

  defp max_tags_per_event do
    case Application.get_env(:parrhesia, :limits, []) |> Keyword.get(:max_tags_per_event) do
      value when is_integer(value) and value > 0 -> value
      _other -> @default_max_tags_per_event
    end
  end

  defp nip43_request_max_age_seconds do
    :parrhesia
    |> Application.get_env(:nip43, [])
    |> Keyword.get(:request_max_age_seconds, @default_nip43_request_max_age_seconds)
  end
end
