defmodule MatchmakingEx.Server do
  @moduledoc "The central matchmaking server, focused on the queue and pairing logic."
  use GenServer

  alias MatchmakingEx.CooldownManager

  @typedoc "Represents a player in the matchmaking queue."
  @type t :: %__MODULE__{
          pid: pid(),
          id: String.t(),
          filters: map(),
          inserted_at: DateTime.t()
        }
  defstruct [:pid, :id, :filters, :inserted_at]

  # --- Public API ---

  @doc "Starts the Server GenServer."
  @spec start_link(any) :: GenServer.on_start()
  def start_link(_opts) do
    GenServer.start_link(__MODULE__, %{}, name: __MODULE__)
  end

  @doc "Adds a player to the queue and attempts to find a match."
  @spec find_match(pid :: pid(), player_id :: String.t(), filters :: map()) :: :ok
  def find_match(pid, player_id, filters \\ %{}) do
    player = %__MODULE__{pid: pid, id: player_id, filters: filters, inserted_at: DateTime.utc_now()}
    GenServer.cast(__MODULE__, {:find_match, player})
  end

  @doc "Removes a player from the queue."
  @spec cancel_search(player_id :: String.t()) :: :ok
  def cancel_search(player_id) do
    GenServer.cast(__MODULE__, {:cancel_search, player_id})
  end

  @doc "Adds a player back into the queue."
  @spec requeue_player(player :: t()) :: :ok
  def requeue_player(player) do
    GenServer.cast(__MODULE__, {:requeue, player})
  end

  # --- GenServer Callbacks ---

  @impl true
  def init(_opts) do
    {:ok, %{queue: %{}}}
  end

  @impl true
  def handle_cast({:find_match, player}, state) do
    case CooldownManager.is_on_cooldown?(player.id) do
      {:ok, true, expiration_time} ->
        send(player.pid, {:search_rejected_cooldown, expiration_time})
        {:noreply, state}

      {:ok, false} ->
        find_opponent_or_queue(player, state)
    end
  end

  @impl true
  def handle_cast({:requeue, player}, state) do
    new_queue = Map.put(state.queue, player.id, player)
    {:noreply, %{state | queue: new_queue}}
  end

  @impl true
  def handle_cast({:cancel_search, player_id}, state) do
    new_queue = Map.delete(state.queue, player_id)
    {:noreply, %{state | queue: new_queue}}
  end

  # --- Private Functions ---

  defp find_opponent_or_queue(player, state) do
    case find_opponent(player, state.queue) do
      {:ok, opponent} ->
        new_queue = Map.delete(state.queue, opponent.id)
        MatchmakingEx.PendingMatch.create(player, opponent)
        {:noreply, %{state | queue: new_queue}}
      :not_found ->
        new_queue = Map.put(state.queue, player.id, player)
        {:noreply, %{state | queue: new_queue}}
    end
  end

  defp find_opponent(player, queue) do
    Enum.find(queue, :not_found, fn {_opponent_id, opponent} ->
      player.id != opponent.id and filters_compatible?(player.filters, opponent.filters)
    end)
    |> case do
      {_opponent_id, opponent} -> {:ok, opponent}
      :not_found -> :not_found
    end
  end

  defp filters_compatible?(filters1, filters2) do
    check_requirements(filters1, filters2) and check_requirements(filters2, filters1)
  end

  defp check_requirements(required_filters, target_filters) do
    Enum.all?(required_filters, fn {key, required_value} ->
      target_value = Map.get(target_filters, key)
      target_value == nil or target_value == required_value
    end)
  end
end
