defmodule BDS.AI.Runtime do
  @moduledoc false

  alias BDS.AI
  alias BDS.AI.Catalog
  alias BDS.AI.SecretBackend

  @model_preference_keys %{
    default: "ai.model.default",
    chat: "ai.model.chat",
    title: "ai.model.title",
    image_analysis: "ai.model.image_analysis",
    airplane_chat: "ai.airplane.model.chat",
    airplane_title: "ai.airplane.model.title",
    airplane_image_analysis: "ai.airplane.model.image_analysis"
  }

  @spec model_preference_keys() :: %{atom() => String.t()}
  def model_preference_keys, do: @model_preference_keys

  @spec resolve_target(atom(), keyword()) ::
          {:ok, map(), String.t(), :airplane | :online} | {:error, term()}
  def resolve_target(operation, extra) do
    mode = if AI.airplane_mode?(), do: :airplane, else: :online
    secret_backend = Keyword.get(extra, :secret_backend, SecretBackend)

    with {:ok, endpoint} <- fetch_endpoint_for_mode(mode, secret_backend),
         {:ok, model} <- resolve_model_for_operation(operation, mode, endpoint, extra) do
      {:ok, endpoint, model, mode}
    end
  end

  @spec validate_target(atom(), String.t(), :airplane | :online) :: :ok | {:error, term()}
  def validate_target(:analyze_image, model, _mode) do
    capabilities = Catalog.model_capabilities(model)

    cond do
      capabilities.supports_attachment ->
        :ok

      capabilities.supports_attachment == false ->
        {:error,
         %{kind: :model_capability_missing, capability: :supports_attachment, model: model}}

      true ->
        :ok
    end
  end

  def validate_target(_operation, _model, _mode), do: :ok

  @spec endpoint_with_model(map(), String.t()) :: map()
  def endpoint_with_model(endpoint, model), do: Map.put(endpoint, :model, model)

  @spec model_preference_value(atom()) :: String.t() | nil
  def model_preference_value(key) do
    case AI.get_model_preference(key) do
      {:ok, value} -> value
      _other -> nil
    end
  end

  defp resolve_model_for_operation(:chat, :airplane, endpoint, _extra) do
    {:ok, model_preference_value(:airplane_chat) || endpoint.model}
  end

  defp resolve_model_for_operation(:chat, :online, endpoint, conversation: conversation) do
    {:ok, conversation.model || model_preference_value(:chat) || endpoint.model}
  end

  defp resolve_model_for_operation(:chat, :online, endpoint, _extra) do
    {:ok, model_preference_value(:chat) || endpoint.model}
  end

  defp resolve_model_for_operation(:analyze_image, :airplane, endpoint, extra) do
    {:ok,
     Keyword.get(extra, :model) || model_preference_value(:airplane_image_analysis) ||
       endpoint.model}
  end

  defp resolve_model_for_operation(:analyze_image, :online, endpoint, extra) do
    {:ok, Keyword.get(extra, :model) || model_preference_value(:image_analysis) || endpoint.model}
  end

  defp resolve_model_for_operation(_operation, :airplane, endpoint, extra) do
    {:ok, Keyword.get(extra, :model) || model_preference_value(:airplane_title) || endpoint.model}
  end

  defp resolve_model_for_operation(_operation, :online, endpoint, extra) do
    {:ok, Keyword.get(extra, :model) || model_preference_value(:title) || endpoint.model}
  end

  defp fetch_endpoint_for_mode(mode, secret_backend) do
    with {:ok, endpoint} <- AI.get_endpoint(mode, secret_backend: secret_backend) do
      case endpoint do
        %{url: url, model: model} = loaded
        when is_binary(url) and url != "" and is_binary(model) and model != "" ->
          if mode == :online and blank?(loaded.api_key) do
            {:error, %{kind: :endpoint_not_configured, endpoint: mode}}
          else
            {:ok, loaded}
          end

        _other ->
          {:error, %{kind: :endpoint_not_configured, endpoint: mode}}
      end
    end
  end

  defp blank?(value), do: BDS.Values.blank?(value)
end
