defmodule Boruta.Oauth.AuthorizeResponse do
  @moduledoc """
  Response returned in case of authorization request success. Provides utilities and mandatory data needed to respond to the authorize part of implicit, code and hybrid flows.
  """

  @enforce_keys [:type, :redirect_uri]
  defstruct access_token: nil,
            code: nil,
            code_challenge: nil,
            code_challenge_method: nil,
            expires_in: nil,
            id_token: nil,
            redirect_uri: nil,
            response_mode: nil,
            state: nil,
            token_type: nil,
            type: nil

  @type t :: %__MODULE__{
          access_token: String.t() | nil,
          code: String.t() | nil,
          code_challenge: String.t() | nil,
          code_challenge_method: String.t() | nil,
          expires_in: integer(),
          id_token: String.t() | nil,
          redirect_uri: String.t(),
          response_mode: String.t() | nil,
          state: String.t() | nil,
          token_type: String.t() | nil,
          type: :token | :code | :hybrid
        }

  alias Boruta.Oauth.AuthorizeResponse
  alias Boruta.Oauth.CodeRequest
  alias Boruta.Oauth.Error
  alias Boruta.Oauth.HybridRequest
  alias Boruta.Oauth.Token
  alias Boruta.Oauth.TokenRequest
  alias Boruta.Openid.CredentialOfferResponse
  alias Boruta.Openid.SiopV2Response
  alias Boruta.Openid.VerifiablePresentationResponse

  @spec from_tokens(
          %{
            (type :: :siopv2_code | :preauthorized_code | :code | :token | :id_token) =>
              token :: Boruta.Oauth.Token.t() | String.t()
          },
          request :: CodeRequest.t() | TokenRequest.t() | HybridRequest.t()
        ) ::
          t()
          | CredentialOfferResponse.t()
          | SiopV2Response.t()
          | VerifiablePresentationResponse.t()
          | {:error, Boruta.Oauth.Error.t()}
  def from_tokens(
        %{
          code: %Token{
            expires_at: expires_at,
            value: value,
            redirect_uri: redirect_uri,
            state: state,
            code_challenge: code_challenge,
            code_challenge_method: code_challenge_method
          }
        } = params,
        request
      ) do
    {:ok, expires_at} = DateTime.from_unix(expires_at)
    expires_in = DateTime.diff(expires_at, DateTime.utc_now())

    type =
      case hybrid?(params) do
        true -> :hybrid
        false -> :code
      end

    response_mode =
      case request.__struct__ do
        HybridRequest -> request.response_mode
        _ -> nil
      end

    %AuthorizeResponse{
      type: type,
      redirect_uri: redirect_uri,
      code: value,
      id_token: params[:id_token] && params[:id_token].value,
      access_token: params[:token] && params[:token].value,
      expires_in: expires_in,
      state: state,
      code_challenge: code_challenge,
      code_challenge_method: code_challenge_method,
      token_type: if(has_token_type?(params), do: "bearer"),
      response_mode: response_mode
    }
  end

  def from_tokens(
        %{
          token: %Token{
            expires_at: expires_at,
            value: value,
            redirect_uri: redirect_uri,
            state: state
          }
        } = params,
        _request
      ) do
    {:ok, expires_at} = DateTime.from_unix(expires_at)
    expires_in = DateTime.diff(expires_at, DateTime.utc_now())

    %AuthorizeResponse{
      type: :token,
      redirect_uri: redirect_uri,
      access_token: value,
      id_token: params[:id_token] && params[:id_token].value,
      expires_in: expires_in,
      state: state,
      token_type: "bearer"
    }
  end

  def from_tokens(
        %{
          id_token: %Token{
            value: id_token,
            redirect_uri: redirect_uri,
            state: state
          }
        },
        _request
      ) do
    %AuthorizeResponse{
      type: :token,
      redirect_uri: redirect_uri,
      id_token: id_token,
      state: state
    }
  end

  def from_tokens(
        %{
          preauthorized_code: _preauthorized_code
        } = tokens,
        request
      ) do
    CredentialOfferResponse.from_tokens(tokens, request)
  end

  def from_tokens(
        %{
          siopv2_code: _siopv2_code
        } = tokens,
        request
      ) do
    SiopV2Response.from_tokens(tokens, request)
  end

  def from_tokens(
        %{
          vp_code: _siopv2_code
        } = tokens,
        request
      ) do
    VerifiablePresentationResponse.from_tokens(tokens, request)
  end

  def from_tokens(_params, _request) do
    {:error,
     %Error{
       status: :bad_request,
       error: :invalid_request,
       error_description:
         "Neither code, nor access_token, nor id_token could be created with given parameters."
     }}
  end

  defp has_token_type?(params) do
    hybrid?(params) && has_access_token?(params)
  end

  defp hybrid?(params) do
    !is_nil(params[:id_token] || params[:token])
  end

  defp has_access_token?(params) do
    Map.has_key?(params, :token)
  end

  @spec redirect_to_url(__MODULE__.t()) :: url :: String.t()
  def redirect_to_url(%__MODULE__{} = response) do
    params = params(response)
    url(response, params)
  end

  defp params(%__MODULE__{
         access_token: access_token,
         code: code,
         id_token: id_token,
         expires_in: expires_in,
         state: state,
         token_type: token_type
       }) do
    %{
      code: code,
      id_token: id_token,
      access_token: access_token,
      expires_in: expires_in,
      state: state,
      token_type: token_type
    }
    |> Enum.map(fn {param_type, value} ->
      value && {param_type, value}
    end)
    |> Enum.reject(&is_nil/1)
    |> Enum.into(%{})
  end

  defp url(%__MODULE__{type: :token, redirect_uri: redirect_uri}, params) do
    add_fragment_to_redirect_uri(redirect_uri, params)
  end

  defp url(%__MODULE__{type: :code, redirect_uri: redirect_uri}, params) do
    add_query_to_redirect_uri(redirect_uri, params)
  end

  defp url(
         %__MODULE__{type: :hybrid, response_mode: "query", redirect_uri: redirect_uri},
         params
       ) do
    add_query_to_redirect_uri(redirect_uri, params)
  end

  defp url(
         %__MODULE__{type: :hybrid, response_mode: "fragment", redirect_uri: redirect_uri},
         params
       ) do
    add_fragment_to_redirect_uri(redirect_uri, params)
  end

  # fallback to fragment since it is the hybrid default response mode
  defp url(
         %__MODULE__{type: :hybrid, response_mode: nil, redirect_uri: redirect_uri},
         params
       ) do
    add_fragment_to_redirect_uri(redirect_uri, params)
  end

  defp add_query_to_redirect_uri(redirect_uri, params) do
    redirect_uri = URI.parse(redirect_uri)

    query =
      (redirect_uri.query || "")
      |> URI.decode_query()
      |> Map.merge(params)
      |> URI.encode_query()

    URI.to_string(%{redirect_uri | query: query})
  end

  defp add_fragment_to_redirect_uri(redirect_uri, params) do
    redirect_uri = URI.parse(redirect_uri)
    fragment = URI.encode_query(params)

    URI.to_string(%{redirect_uri | fragment: fragment})
  end
end
