defmodule Ontogen do
  use GenServer

  alias Ontogen.{Service, Repository, ConfigError}
  alias Ontogen.Operation.{Query, Command}

  import Ontogen.Operation, only: [include_api: 1]

  include_api Ontogen.Operations.BootCommand
  include_api Ontogen.Operations.SetupCommand

  include_api Ontogen.Operations.CleanCommand
  include_api Ontogen.Operations.CommitCommand
  include_api Ontogen.Operations.RevertCommand
  include_api Ontogen.Operations.EffectiveChangesetQuery
  include_api Ontogen.Operations.ChangesetQuery
  include_api Ontogen.Operations.RevisionQuery
  include_api Ontogen.Operations.HistoryQuery
  include_api Ontogen.Operations.DatasetQuery
  include_api Ontogen.Operations.RepositoryQuery

  @allow_configless_mode Application.compile_env(:ontogen, :allow_configless_mode, false)

  @env Application.compile_env(:ontogen, :env, Mix.env())
  def env, do: @env

  def ansi_enabled? do
    Application.get_env(:ontogen, :ansi_enabled, true)
  end

  def start_link(args) do
    GenServer.start_link(__MODULE__, args, name: __MODULE__)
  end

  def status, do: GenServer.call(__MODULE__, :status)

  def service, do: GenServer.call(__MODULE__, :service)

  def store, do: GenServer.call(__MODULE__, :store)

  def dataset_info, do: GenServer.call(__MODULE__, :dataset_info)

  def history_info, do: GenServer.call(__MODULE__, :history_info)

  def head, do: GenServer.call(__MODULE__, :head)

  def reload, do: GenServer.call(__MODULE__, :reload)

  ############################################################################

  # Server (callbacks)

  @impl true
  def init(opts) do
    {:ok, nil, {:continue, {:boot, opts}}}
  end

  @impl true
  def handle_continue({:boot, opts}, _) do
    case BootCommand.call(opts) do
      {:ok, service} ->
        {:noreply, service}

      {:error, error} ->
        if @allow_configless_mode do
          {:noreply, {:error, error}}
        else
          {:stop, error}
        end
    end
  end

  @impl true
  def handle_call(%BootCommand{} = operation, from, {:error, _}),
    do: handle_call(operation, from, nil)

  def handle_call(%BootCommand{} = operation, from, nil),
    do: handle_call(operation, from, Ontogen.Config.service!())

  def handle_call(%BootCommand{} = operation, _from, service) do
    case BootCommand.call(operation, service) do
      {:ok, service} -> {:reply, {:ok, service}, service}
      {:error, _} = error -> {:reply, error, service}
    end
  end

  def handle_call(:status, _from, {:error, error}),
    do: {:reply, {:error, ConfigError.exception(reason: error)}, {:error, error}}

  def handle_call(:status, _from, nil), do: {:reply, :unconfigured, nil}
  def handle_call(:status, _from, service), do: {:reply, service.status, service}

  def handle_call(_operation, _from, nil) do
    {:reply, {:error, ConfigError.exception(reason: :missing)}, nil}
  end

  def handle_call(_operation, _from, {:error, error}) do
    {:reply, {:error, ConfigError.exception(reason: error)}, {:error, error}}
  end

  def handle_call(:service, _from, service), do: {:reply, service, service}
  def handle_call(:store, _from, service), do: {:reply, service.store, service}

  def handle_call(:dataset_info, _from, service),
    do: {:reply, service.repository.dataset, service}

  def handle_call(:history_info, _from, service),
    do: {:reply, service.repository.history, service}

  def handle_call(%SetupCommand{} = operation, _from, %Service{status: :not_setup} = service) do
    case SetupCommand.call(operation, service) do
      {:ok, service} -> {:reply, {:ok, service}, service}
      {:error, _} = error -> {:reply, error, service}
    end
  end

  def handle_call(%SetupCommand{}, _from, service) do
    {:reply, {:error, Repository.SetupError.exception(service: service, reason: :already_setup)},
     service}
  end

  def handle_call(operation, _from, %{status: :not_setup} = service) do
    {:reply, {:error, Repository.NotSetupError.exception(service: service, operation: operation)},
     service}
  end

  def handle_call(:head, _from, service) do
    {:reply, service.repository.head, service}
  end

  def handle_call(%operation{type: Query} = query, _from, service) do
    case apply(operation, :call, [query, service]) do
      {:ok, result} -> {:reply, {:ok, result}, service}
      {:error, _} = error -> {:reply, error, service}
    end
  end

  def handle_call(%operation{type: Command} = command, _from, service) do
    case apply(operation, :call, [command, service]) do
      {:ok, %Service{} = service} -> {:reply, :ok, service}
      {:ok, %Service{} = service, result} -> {:reply, {:ok, result}, service}
      {:error, _} = error -> {:reply, error, service}
    end
  end
end
