defmodule PhoenixLiveState.StateServer.ServerImpl do
  @moduledoc """
  Server-side implementation for PhoenixLiveState.StateServer.
  """
  @moduledoc since: "0.1.0"

  use GenServer

  alias PhoenixLiveState.StateServer.InternalState
  alias PhoenixLiveState.StateServer.Policy
  alias PhoenixLiveState.StateServer.Settings
  alias PhoenixLiveState.StateSpec

  @default_settings %Settings{ttl: 60_000, acl: Policy.default_acl()}

  require Logger

  @impl true
  def init(opts) do
    Process.flag(:trap_exit, true)

    internal_state = %InternalState{
      key: opts[:key],
      state_spec: Keyword.get(opts, :state_spec),
      shared_state: opts |> Keyword.get(:initial_state, %{}) |> Map.new(),
      settings: Keyword.get(opts, :settings, @default_settings),
      clients: MapSet.new([])
    }

    {:ok, internal_state}
  end

  @impl true
  def handle_call(message, pid, state)

  def handle_call(:attach, request_pid, %InternalState{clients: clients} = state) do
    client_pid = resolve_pid(request_pid)

    if MapSet.member?(clients, client_pid) do
      {:reply, {:ok, state.shared_state}, state}
    else
      Logger.info("Attaching client #{inspect(client_pid)}")

      updated_clients = MapSet.put(clients, client_pid)
      Process.monitor(client_pid)

      {:reply, {:ok, state.shared_state}, %{state | clients: updated_clients}}
    end
  end

  def handle_call({:get, property}, request_pid, %{shared_state: shared_state} = state) do
    client_pid = resolve_pid(request_pid)
    Logger.info("reead request from #{inspect(client_pid)}")

    if Policy.allowed?(client_pid, :read, state) do
      value =
        cond do
          is_nil(property) ->
            {:ok, shared_state}

          Map.has_key?(shared_state, property) ->
            {:ok, Map.get(shared_state, property)}

          true ->
            {:error, :property_not_found}
        end

      {:reply, value, state}
    else
      {:reply, {:error, :not_allowed}, state}
    end
  end

  def handle_call(
        {:put, property_name, property_value},
        request_pid,
        %{shared_state: shared_state} = server_state
      ) do
    client_pid = resolve_pid(request_pid)

    if Policy.allowed?(client_pid, :write, server_state) do
      existing_property? = Map.has_key?(shared_state, property_name)
      updated_state = Map.put(shared_state, property_name, property_value)

      if existing_property? do
        broadcast(server_state, {:updated, property_name, property_value})
      else
        broadcast(server_state, {:new, property_name, property_value})
      end

      {:reply, :ok, %{server_state | shared_state: updated_state}}
    else
      {:reply, {:error, :not_allowed}, server_state}
    end
  end

  def handle_call({:delete, property_name}, request_pid, %{shared_state: shared_state} = state) do
    client_pid = resolve_pid(request_pid)

    if Policy.allowed?(client_pid, :delete, state) do
      updated_state = Map.delete(shared_state, property_name)
      broadcast(state, {:deleted, property_name})
      {:reply, :ok, %{state | shared_state: updated_state}}
    else
      {:reply, {:error, :not_allowed}, state}
    end
  end

  def handle_call(:reset, request_pid, %InternalState{} = server_state) do
    client_pid = resolve_pid(request_pid)

    with true <- Policy.allowed?(client_pid, :reset, server_state),
         {:ok, fresh_spec} <- fresh_state(server_state.state_spec, server_state.key) do
      broadcast(server_state, {:reset, fresh_spec.initial_state})
      {:reply, :ok, %{server_state | shared_state: fresh_spec.initial_stat}}
    else
      false ->
        {:reply, {:error, :not_allowed}, server_state}

      error ->
        {:reply, error, server_state}
    end
  end

  @impl true
  def handle_info(msg, internal_state)

  def handle_info({:DOWN, _ref, :process, pid}, state) do
    {:noreply, remove_client(state, pid)}
  end

  def handle_info({:DOWN, _ref, :process, pid, _reason}, state) do
    {:noreply, remove_client(state, pid)}
  end

  def handle_info(:ttl_check, %InternalState{clients: clients} = state) do
    if MapSet.size(clients) == 0 do
      {:stop, :normal, state}
    else
      {:noreply, state}
    end
  end

  @impl true
  def terminate(_reason, state) do
    broadcast(state, :shutdown)
    state
  end

  defp schedule_check(%InternalState{settings: %{ttl: ttl}}) do
    if ttl && ttl > 0 do
      Process.send_after(self(), :ttl_check, ttl)
    end
  end

  defp fresh_state(state_spec, key) do
    StateSpec.mount(state_spec, key)
  end

  defp remove_client(%InternalState{clients: clients} = state, pid) do
    schedule_check(state)
    updated_clients = MapSet.delete(clients, pid)
    %{state | clients: updated_clients}
  end

  defp broadcast(server_state, payload) do
    for client_pid <- server_state.clients do
      message = {PhoenixLiveState, server_state.key, payload}
      send(client_pid, message)
    end
  end

  defp resolve_pid(pid) when is_pid(pid), do: pid
  defp resolve_pid({pid, _aliases}) when is_pid(pid), do: pid
end
