defmodule Parrhesia.API.Events do
  @moduledoc """
  Canonical event publish, query, and count API.

  This is the main in-process API for working with Nostr events. It applies the same core
  validation and policy checks used by the relay edge, but without going through a socket or
  HTTP transport.

  All public functions expect `opts[:context]` to contain a `Parrhesia.API.RequestContext`.
  That context drives authorization, caller attribution, and downstream policy behavior.

  `publish/2` intentionally returns `{:ok, %PublishResult{accepted: false}}` for policy and
  storage rejections so callers can mirror relay `OK` semantics without treating a rejected
  event as a process error.
  """

  alias Parrhesia.API.Events.PublishResult
  alias Parrhesia.API.RequestContext
  alias Parrhesia.Fanout.Dispatcher
  alias Parrhesia.Fanout.MultiNode
  alias Parrhesia.NIP43
  alias Parrhesia.Policy.EventPolicy
  alias Parrhesia.Protocol
  alias Parrhesia.Protocol.Filter
  alias Parrhesia.Storage
  alias Parrhesia.Telemetry

  @default_max_event_bytes 262_144

  @marmot_kinds MapSet.new([
                  443,
                  444,
                  445,
                  1059,
                  10_050,
                  10_051,
                  446,
                  447,
                  448,
                  449
                ])

  @doc """
  Validates, authorizes, persists, and fans out an event.

  Required options:

  - `:context` - a `Parrhesia.API.RequestContext`

  Supported options:

  - `:max_event_bytes` - overrides the configured max encoded event size
  - `:path`, `:private_key`, `:configured_private_key` - forwarded to the NIP-43 helper flow

  Return semantics:

  - `{:ok, %PublishResult{accepted: true}}` for accepted events
  - `{:ok, %PublishResult{accepted: false}}` for rejected or duplicate events
  - `{:error, :invalid_context}` only when the call itself is malformed
  """
  @spec publish(map(), keyword()) :: {:ok, PublishResult.t()} | {:error, term()}
  def publish(event, opts \\ [])

  def publish(event, opts) when is_map(event) and is_list(opts) do
    started_at = System.monotonic_time()
    event_id = Map.get(event, "id", "")
    telemetry_metadata = telemetry_metadata_for_event(event)

    with {:ok, context} <- fetch_context(opts),
         :ok <- validate_event_payload_size(event, max_event_bytes(opts)),
         :ok <- Protocol.validate_event(event),
         :ok <- EventPolicy.authorize_write(event, context.authenticated_pubkeys, context),
         {:ok, publish_state} <- NIP43.prepare_publish(event, nip43_opts(opts, context)),
         {:ok, _stored, message} <- persist_event(event) do
      Telemetry.emit(
        [:parrhesia, :ingest, :stop],
        %{duration: System.monotonic_time() - started_at},
        telemetry_metadata
      )

      emit_ingest_result(telemetry_metadata, :accepted, :accepted)

      message =
        case NIP43.finalize_publish(event, publish_state, nip43_opts(opts, context)) do
          {:ok, override} when is_binary(override) -> override
          :ok -> message
        end

      Dispatcher.dispatch(event)
      maybe_publish_multi_node(event, context)

      {:ok,
       %PublishResult{
         event_id: event_id,
         accepted: true,
         message: message,
         reason: nil
       }}
    else
      {:error, :invalid_context} = error ->
        emit_ingest_result(telemetry_metadata, :rejected, :invalid_context)
        error

      {:error, reason} ->
        emit_ingest_result(telemetry_metadata, :rejected, reason)

        {:ok,
         %PublishResult{
           event_id: event_id,
           accepted: false,
           message: error_message_for_publish_failure(reason),
           reason: reason
         }}
    end
  end

  def publish(_event, _opts), do: {:error, :invalid_event}

  @doc """
  Queries stored events plus any dynamic NIP-43 events visible to the caller.

  Required options:

  - `:context` - a `Parrhesia.API.RequestContext`

  Supported options:

  - `:max_filter_limit` - overrides the configured per-filter limit
  - `:validate_filters?` - skips filter validation when `false`
  - `:authorize_read?` - skips read policy checks when `false`

  The skip flags are primarily for internal composition, such as `Parrhesia.API.Stream`.
  External callers should normally leave them enabled.
  """
  @spec query([map()], keyword()) :: {:ok, [map()]} | {:error, term()}
  def query(filters, opts \\ [])

  def query(filters, opts) when is_list(filters) and is_list(opts) do
    started_at = System.monotonic_time()
    telemetry_metadata = telemetry_metadata_for_filters(filters, :query)

    with {:ok, context} <- fetch_context(opts),
         :ok <- maybe_validate_filters(filters, opts),
         :ok <- maybe_authorize_read(filters, context, opts),
         {:ok, events} <- Storage.events().query(%{}, filters, storage_query_opts(context, opts)) do
      events = NIP43.dynamic_events(filters, nip43_opts(opts, context)) ++ events

      Telemetry.emit(
        [:parrhesia, :query, :stop],
        %{duration: System.monotonic_time() - started_at, result_count: length(events)},
        telemetry_metadata
      )

      emit_query_result(telemetry_metadata, :ok)

      {:ok, events}
    else
      {:error, reason} = error ->
        emit_query_result(telemetry_metadata, :error, reason)
        error
    end
  end

  def query(_filters, _opts), do: {:error, :invalid_filters}

  @doc """
  Counts events matching the given filters.

  Required options:

  - `:context` - a `Parrhesia.API.RequestContext`

  Supported options:

  - `:validate_filters?` - skips filter validation when `false`
  - `:authorize_read?` - skips read policy checks when `false`
  - `:options` - when set to a map, returns a NIP-45-style payload instead of a bare integer

  When `opts[:options]` is a map, the result shape is `%{"count" => count, "approximate" => false}`.
  If `opts[:options]["hll"]` is `true` and the feature is enabled, an `"hll"` field is included.
  """
  @spec count([map()], keyword()) :: {:ok, non_neg_integer() | map()} | {:error, term()}
  def count(filters, opts \\ [])

  def count(filters, opts) when is_list(filters) and is_list(opts) do
    started_at = System.monotonic_time()
    telemetry_metadata = telemetry_metadata_for_filters(filters, :count)

    with {:ok, context} <- fetch_context(opts),
         :ok <- maybe_validate_filters(filters, opts),
         :ok <- maybe_authorize_read(filters, context, opts),
         {:ok, count} <-
           Storage.events().count(%{}, filters, requester_pubkeys: requester_pubkeys(context)),
         count <- count + NIP43.dynamic_count(filters, nip43_opts(opts, context)),
         {:ok, result} <- maybe_build_count_result(filters, count, Keyword.get(opts, :options)) do
      Telemetry.emit(
        [:parrhesia, :query, :stop],
        %{duration: System.monotonic_time() - started_at, result_count: count},
        telemetry_metadata
      )

      emit_query_result(telemetry_metadata, :ok)

      {:ok, result}
    else
      {:error, reason} = error ->
        emit_query_result(telemetry_metadata, :error, reason)
        error
    end
  end

  def count(_filters, _opts), do: {:error, :invalid_filters}

  defp maybe_validate_filters(filters, opts) do
    if Keyword.get(opts, :validate_filters?, true) do
      Filter.validate_filters(filters)
    else
      :ok
    end
  end

  defp maybe_authorize_read(filters, context, opts) do
    if Keyword.get(opts, :authorize_read?, true) do
      EventPolicy.authorize_read(filters, context.authenticated_pubkeys, context)
    else
      :ok
    end
  end

  defp storage_query_opts(context, opts) do
    [
      max_filter_limit:
        Keyword.get(opts, :max_filter_limit, Parrhesia.Config.get([:limits, :max_filter_limit])),
      requester_pubkeys: requester_pubkeys(context)
    ]
  end

  defp requester_pubkeys(%RequestContext{} = context),
    do: MapSet.to_list(context.authenticated_pubkeys)

  defp maybe_build_count_result(_filters, count, nil) when is_integer(count), do: {:ok, count}

  defp maybe_build_count_result(filters, count, options)
       when is_integer(count) and is_map(options) do
    build_count_payload(filters, count, options)
  end

  defp maybe_build_count_result(_filters, count, _options) when is_integer(count),
    do: {:ok, count}

  defp maybe_build_count_result(_filters, count, _options), do: {:ok, count}

  defp build_count_payload(filters, count, options) do
    include_hll? =
      Map.get(options, "hll", false) and Parrhesia.Config.get([:features, :nip_45_count], true)

    payload = %{"count" => count, "approximate" => false}

    payload =
      if include_hll? do
        Map.put(payload, "hll", generate_hll_payload(filters, count))
      else
        payload
      end

    {:ok, payload}
  end

  defp generate_hll_payload(filters, count) do
    filters
    |> JSON.encode!()
    |> then(&"#{&1}:#{count}")
    |> then(&:crypto.hash(:sha256, &1))
    |> Base.encode64()
  end

  defp persist_event(event) do
    kind = Map.get(event, "kind")

    cond do
      kind in [5, 62] -> persist_control_event(kind, event)
      ephemeral_kind?(kind) -> persist_ephemeral_event()
      true -> persist_regular_event(event)
    end
  end

  defp persist_control_event(5, event) do
    with {:ok, deleted_count} <- Storage.events().delete_by_request(%{}, event) do
      {:ok, deleted_count, "ok: deletion request processed"}
    end
  end

  defp persist_control_event(62, event) do
    with {:ok, deleted_count} <- Storage.events().vanish(%{}, event) do
      {:ok, deleted_count, "ok: vanish request processed"}
    end
  end

  defp persist_ephemeral_event do
    if accept_ephemeral_events?() do
      {:ok, :ephemeral, "ok: ephemeral event accepted"}
    else
      {:error, :ephemeral_events_disabled}
    end
  end

  defp persist_regular_event(event) do
    case Storage.events().put_event(%{}, event) do
      {:ok, persisted_event} -> {:ok, persisted_event, "ok: event stored"}
      {:error, :duplicate_event} -> {:error, :duplicate_event}
      {:error, reason} -> {:error, reason}
    end
  end

  defp maybe_publish_multi_node(event, %RequestContext{} = context) do
    relay_guard? = Parrhesia.Config.get([:sync, :relay_guard], false)

    if relay_guard? and context.caller == :sync do
      :ok
    else
      MultiNode.publish(event)
      :ok
    end
  catch
    :exit, _reason -> :ok
  end

  defp telemetry_metadata_for_event(event) do
    %{traffic_class: traffic_class_for_event(event)}
  end

  defp telemetry_metadata_for_filters(filters, operation) do
    %{traffic_class: traffic_class_for_filters(filters), operation: operation}
  end

  defp traffic_class_for_filters(filters) do
    if Enum.any?(filters, &marmot_filter?/1) do
      :marmot
    else
      :generic
    end
  end

  defp marmot_filter?(filter) when is_map(filter) do
    has_marmot_kind? =
      case Map.get(filter, "kinds") do
        kinds when is_list(kinds) -> Enum.any?(kinds, &MapSet.member?(@marmot_kinds, &1))
        _other -> false
      end

    has_marmot_kind? or Map.has_key?(filter, "#h") or Map.has_key?(filter, "#i")
  end

  defp marmot_filter?(_filter), do: false

  defp traffic_class_for_event(event) when is_map(event) do
    if MapSet.member?(@marmot_kinds, Map.get(event, "kind")) do
      :marmot
    else
      :generic
    end
  end

  defp traffic_class_for_event(_event), do: :generic

  defp emit_ingest_result(metadata, outcome, reason) do
    Telemetry.emit(
      [:parrhesia, :ingest, :result],
      %{count: 1},
      Map.merge(metadata, %{outcome: outcome, reason: normalize_reason(reason)})
    )
  end

  defp emit_query_result(metadata, outcome, reason \\ nil) do
    Telemetry.emit(
      [:parrhesia, :query, :result],
      %{count: 1},
      Map.merge(
        metadata,
        %{outcome: outcome, reason: normalize_reason(reason || outcome)}
      )
    )
  end

  defp normalize_reason(reason) when is_atom(reason), do: reason
  defp normalize_reason(reason) when is_binary(reason), do: reason
  defp normalize_reason(nil), do: :none
  defp normalize_reason(_reason), do: :unknown

  defp fetch_context(opts) do
    case Keyword.get(opts, :context) do
      %RequestContext{} = context -> {:ok, context}
      _other -> {:error, :invalid_context}
    end
  end

  defp nip43_opts(opts, %RequestContext{} = context) do
    [context: context, relay_url: Application.get_env(:parrhesia, :relay_url)]
    |> Kernel.++(Keyword.take(opts, [:path, :private_key, :configured_private_key]))
  end

  defp error_message_for_publish_failure(:duplicate_event),
    do: "duplicate: event already stored"

  defp error_message_for_publish_failure(:event_too_large),
    do: "invalid: event exceeds max event size"

  defp error_message_for_publish_failure(:ephemeral_events_disabled),
    do: "blocked: ephemeral events are disabled"

  defp error_message_for_publish_failure(reason)
       when reason in [
              :auth_required,
              :pubkey_not_allowed,
              :restricted_giftwrap,
              :sync_write_not_allowed,
              :protected_event_requires_auth,
              :protected_event_pubkey_mismatch,
              :pow_below_minimum,
              :pubkey_banned,
              :event_banned,
              :media_metadata_tags_exceeded,
              :media_metadata_tag_value_too_large,
              :media_metadata_url_too_long,
              :media_metadata_invalid_url,
              :media_metadata_invalid_hash,
              :media_metadata_invalid_mime,
              :media_metadata_mime_not_allowed,
              :media_metadata_unsupported_version,
              :push_notification_relay_tags_exceeded,
              :push_notification_payload_too_large,
              :push_notification_replay_window_exceeded,
              :push_notification_missing_expiration,
              :push_notification_expiration_too_far,
              :push_notification_server_recipients_exceeded
            ],
       do: EventPolicy.error_message(reason)

  defp error_message_for_publish_failure(reason) when is_binary(reason), do: reason
  defp error_message_for_publish_failure(reason), do: "error: #{inspect(reason)}"

  defp validate_event_payload_size(event, max_event_bytes)
       when is_map(event) and is_integer(max_event_bytes) and max_event_bytes > 0 do
    if byte_size(JSON.encode!(event)) <= max_event_bytes do
      :ok
    else
      {:error, :event_too_large}
    end
  end

  defp validate_event_payload_size(_event, _max_event_bytes), do: :ok

  defp max_event_bytes(opts) do
    opts
    |> Keyword.get(:max_event_bytes, configured_max_event_bytes())
    |> normalize_max_event_bytes()
  end

  defp normalize_max_event_bytes(value) when is_integer(value) and value > 0, do: value
  defp normalize_max_event_bytes(_value), do: configured_max_event_bytes()

  defp configured_max_event_bytes do
    :parrhesia
    |> Application.get_env(:limits, [])
    |> Keyword.get(:max_event_bytes, @default_max_event_bytes)
  end

  defp ephemeral_kind?(kind) when is_integer(kind), do: kind >= 20_000 and kind < 30_000
  defp ephemeral_kind?(_kind), do: false

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