defmodule MatchmakingEx.PendingMatch do
  @moduledoc """
  A GenServer representing a temporary, pending match between two players.

  This process is created when two compatible players are found. It is responsible
  for managing the confirmation phase where both players must accept the match
  within a given timeframe. It is dynamically supervised and should not be restarted.
  """
  use GenServer

  alias MatchmakingEx.Server
  alias MatchmakingEx.CooldownManager

  @default_timeout 15_000

  # --- Public API ---

  @doc "Starts a `PendingMatch` process."
  @spec start_link(init_arg :: term()) :: GenServer.on_start()
  def start_link(init_arg) do
    GenServer.start_link(__MODULE__, init_arg, [])
  end

  @doc "Creates and starts a `PendingMatch` process under the `PendingMatchSupervisor`."
  @spec create(
          player1 :: MatchmakingEx.Server.t(),
          player2 :: MatchmakingEx.Server.t(),
          timeout :: timeout()
        ) :: {:ok, pid} | {:error, any}
  def create(player1, player2, timeout \\ @default_timeout) do
    seed = {player1.id, player2.id, :erlang.system_time()}
    match_id = :erlang.phash2(seed) |> to_string()

    # Children of a DynamicSupervisor for one-off tasks should be temporary.
    spec = %{
      id: match_id,
      start: {__MODULE__, :start_link, [{player1, player2, match_id, timeout}]},
      restart: :temporary
    }

    DynamicSupervisor.start_child(MatchmakingEx.PendingMatchSupervisor, spec)
  end

  @doc "Casts a message for a player to accept the match."
  @spec accept(match_id :: String.t(), player_id :: String.t()) :: :ok
  def accept(match_id, player_id) do
    case Registry.lookup(MatchmakingEx.Registry, match_id) do
      [{pid, _}] -> GenServer.cast(pid, {:accept, player_id})
      [] -> :ok
    end
  end

  @doc "Synchronously informs the process that a player has declined the match."
  @spec decline(match_id :: String.t(), player_id :: String.t()) :: :ok
  def decline(match_id, player_id) do
    case Registry.lookup(MatchmakingEx.Registry, match_id) do
      [{pid, _}] -> GenServer.call(pid, {:decline, player_id}, 30_000)
      [] -> :ok
    end
  rescue
    # A call to a dead process raises an EXIT signal. We treat this as a success.
    _ -> :ok
  end

  # --- GenServer Callbacks ---

  @impl true
  def init({player1, player2, match_id, timeout}) do
    case Registry.register(MatchmakingEx.Registry, match_id, self()) do
      {:ok, _pid} ->
        send(player1.pid, {:match_ready_for_confirmation, match_id, [player1.id, player2.id]})
        send(player2.pid, {:match_ready_for_confirmation, match_id, [player1.id, player2.id]})
        timer = Process.send_after(self(), :timeout, timeout)
        state = %{match_id: match_id, players: %{player1.id => player1, player2.id => player2}, accepted: MapSet.new(), timer: timer}
        {:ok, state}
      {:error, reason} ->
        {:stop, {:registry_failed, reason}}
    end
  end

  @impl true
  def handle_cast({:accept, player_id}, state) do
    new_accepted = MapSet.put(state.accepted, player_id)
    new_state = %{state | accepted: new_accepted}

    if MapSet.size(new_accepted) == map_size(state.players) do
      Process.cancel_timer(state.timer)
      Enum.each(state.players, fn {_id, player} -> send(player.pid, {:match_confirmed, state.match_id}) end)
      {:stop, :normal, new_state}
    else
      {:noreply, new_state}
    end
  end

  @impl true
  def handle_call({:decline, declining_player_id}, _from, state) do
    Process.cancel_timer(state.timer)
    Enum.each(state.players, fn {_id, player} -> send(player.pid, {:match_cancelled, "Opponent has declined."}) end)
    handle_decline_consequences(state.players, declining_player_id)
    {:stop, :normal, :ok, state}
  end

  @impl true
  def handle_info(:timeout, state) do
    Process.cancel_timer(state.timer)
    Enum.each(state.players, fn {_id, player} -> send(player.pid, {:match_cancelled, "Match timed out."}) end)
    requeue_accepted_players(state.players, state.accepted)
    {:stop, :normal, state}
  end

  @impl true
  def terminate(_reason, state) do
    # Ensure the process is unregistered when it terminates for any reason.
    Registry.unregister(MatchmakingEx.Registry, state.match_id)
    :ok
  end

  # --- Private Functions ---

  defp requeue_accepted_players(players, accepted_players) do
    players
    |> Enum.filter(fn {player_id, _player} -> MapSet.member?(accepted_players, player_id) end)
    |> Enum.each(fn {_id, player_to_requeue} -> Server.requeue_player(player_to_requeue) end)
  end

  defp handle_decline_consequences(players, declining_player_id) do
    CooldownManager.start_cooldown(declining_player_id)
    players
    |> Map.delete(declining_player_id)
    |> Enum.each(fn {_id, player_to_requeue} -> Server.requeue_player(player_to_requeue) end)
  end
end
