defmodule ConduitMcp.Session.EtsStore do
  @moduledoc """
  Default in-memory session store backed by a public, named ETS table.

  Sessions are keyed by `session_id` (a random URL-safe base64 string
  generated by `ConduitMcp.Session.generate_id/0` on first `initialize`)
  and store an arbitrary metadata map plus `"created_at"` (millisecond
  Unix timestamp). The table is lazily created on first use via
  `ensure_table/0` so the store works without explicit application
  start-up wiring.

  ## Concurrency

  The table is `:public, :set, read_concurrency: true`. Reads do not
  block writes; writers serialize per key. Suitable for production
  single-node workloads. For multi-node deployments, implement a
  `ConduitMcp.Session.Store` against your distributed backend — see
  `guides/multi_node_sessions.md`.

  ## TTL & cleanup

  This store does **not** evict sessions on its own. `cleanup/1` is
  provided so a janitor can prune expired rows; without it, the table
  grows unbounded as `initialize` requests accumulate. Wire
  `ConduitMcp.Session.Janitor` into your supervision tree:

      children = [
        {ConduitMcp.Session.Janitor,
         store: ConduitMcp.Session.EtsStore,
         ttl: :timer.minutes(30),
         interval: :timer.minutes(1)}
      ]

  ## Configuration

  Reference the store from a transport's `:session` option:

      {ConduitMcp.Transport.StreamableHTTP,
       server_module: MyServer,
       session: [store: ConduitMcp.Session.EtsStore, ttl: :timer.minutes(60)]}
  """

  @behaviour ConduitMcp.Session.Store

  @table :conduit_mcp_sessions

  @doc """
  Ensures the ETS table exists. Called automatically on first use.
  """
  def ensure_table do
    if :ets.whereis(@table) == :undefined do
      :ets.new(@table, [:named_table, :public, :set, read_concurrency: true])
    end

    :ok
  end

  @impl true
  def create(session_id, metadata) do
    ensure_table()

    entry =
      Map.merge(metadata, %{
        "created_at" => System.system_time(:millisecond)
      })

    :ets.insert(@table, {session_id, entry})
    :ok
  end

  @impl true
  def get(session_id) do
    ensure_table()

    case :ets.lookup(@table, session_id) do
      [{^session_id, metadata}] -> {:ok, metadata}
      [] -> {:error, :not_found}
    end
  end

  @impl true
  def delete(session_id) do
    ensure_table()
    :ets.delete(@table, session_id)
    :ok
  end

  @impl true
  def update(session_id, new_metadata) do
    ensure_table()

    case :ets.lookup(@table, session_id) do
      [{^session_id, existing}] ->
        :ets.insert(@table, {session_id, Map.merge(existing, new_metadata)})
        :ok

      [] ->
        {:error, :not_found}
    end
  end

  @doc """
  Removes sessions older than `ttl_ms` milliseconds.
  """
  @impl true
  def cleanup(ttl_ms) do
    ensure_table()
    now = System.system_time(:millisecond)

    :ets.foldl(
      fn {session_id, metadata}, acc ->
        created_at = Map.get(metadata, "created_at", 0)

        if now - created_at > ttl_ms do
          :ets.delete(@table, session_id)
          acc + 1
        else
          acc
        end
      end,
      0,
      @table
    )
  end
end
