defmodule Membrane.RTC.Engine.Endpoint.SIP.OutgoingCall do
  @moduledoc false

  use Membrane.RTC.Engine.Endpoint.SIP.Call
  require Membrane.Logger

  alias Membrane.RTC.Engine.Endpoint.SIP.{Call, SippetCore}
  alias Membrane.RTC.Engine.Endpoint.SIP.Call.SDP

  @death_timeout_ms 5000

  @response_trying 100
  @response_ringing 180
  @response_session_progress 183
  @response_ok 200
  @response_no_content 204
  @response_multiple_choices 300
  @response_moved_permanently 301
  @response_moved_temporarily 302
  @response_forbidden 403
  @response_busy_here 486
  @response_request_terminated 487
  @response_busy_everywhere 600
  @response_decline 603

  ## CALL MANAGEMENT API

  @spec cancel(Call.id()) :: :ok
  def cancel(call_id) do
    GenServer.cast(Call.registry_id(call_id), :cancel)
  end

  @spec bye(Call.id()) :: :ok
  def bye(call_id) do
    GenServer.cast(Call.registry_id(call_id), :bye)
  end

  ## SIP.Call CALLBACKS

  @impl Call
  def after_init(state) do
    proposal = SDP.proposal(state.external_ip, state.rtp_port)

    :invite
    |> Call.build_headers(state, %{
      content_length: byte_size(proposal),
      content_type: "application/sdp"
    })
    |> create_request(state)
    |> Map.replace(:body, proposal)
    |> Call.make_request(state)
  end

  @impl Call
  def handle_response(:invite, provisional, _response, state) when provisional in 100..199 do
    case provisional do
      @response_trying ->
        notify_endpoint(state.endpoint, :trying)

      @response_ringing ->
        notify_endpoint(state.endpoint, :ringing)

      @response_session_progress ->
        nil

      _other ->
        Logger.warning("SIP Client: Unknown provisional response #{provisional}. Ignoring")
    end

    state
  end

  @impl Call
  def handle_response(:invite, @response_ok, response, state) do
    send_ack(response, state)

    {_sn, last_request_method} = state.last_message.headers.cseq

    case SDP.parse(response.body) do
      # This is the case when we've already sent a CANCEL, but we received a response
      # to the INVITE before the CANCEL was processed -- we still need to send the BYE
      _any when last_request_method == :cancel ->
        {:noreply, state} = handle_cast(:bye, state)
        state

      {:ok, connection_info} ->
        notify_endpoint(state.endpoint, {:call_ready, connection_info})
        %{state | invite_dialog_complete?: true}

      {:error, reason} ->
        _state = build_and_send_request(:bye, state)

        # Give Sippet time to send the request (this is async)
        Process.sleep(20)

        raise "SIP Client: Received SDP answer is not matching our requirements: #{inspect(reason)}"
    end
  end

  @impl Call
  def handle_response(:invite, transfer, _response, state)
      when transfer in [
             @response_multiple_choices,
             @response_moved_permanently,
             @response_moved_temporarily
           ] do
    # Callee was updated by `Call.process_response/2`.
    # We need to clear the `to` header, otherwise we will receive 481 Call/Transaction Does Not Exist
    after_init(%{state | to: nil})
  end

  @impl Call
  def handle_response(:invite, declined, _response, state)
      when declined in [@response_forbidden, @response_decline] do
    notify_endpoint(state.endpoint, {:end, :declined})
    state
  end

  @impl Call
  def handle_response(:invite, busy, _response, state)
      when busy in [@response_busy_here, @response_busy_everywhere] do
    notify_endpoint(state.endpoint, {:end, :busy})
    state
  end

  @impl Call
  def handle_response(:invite, @response_request_terminated, _response, state) do
    # Request Terminated -- this is the case when the INVITE was cancelled
    # by a separate CANCEL request before receiving a final response
    state
  end

  @impl Call
  def handle_response(:invite, @response_no_content, _response, _state) do
    # 204 is a success response, but we need SDP from the response body to setup the session
    # This shouldn't ever happen, but if we don't handle it here it would get silently ignored
    raise "SIP Client: Received 204 No Content in response to INVITE"
  end

  @impl Call
  def handle_response(method, @response_ok, _response, state) when method in [:cancel, :bye] do
    schedule_death(:instant)
    state
  end

  @impl Call
  def handle_response(:cancel, _any, _response, state) do
    state
  end

  @impl Call
  def handle_response(_method, status_code, response, state) do
    Call.handle_generic_response(status_code, response, state)
  end

  @impl Call
  def handle_request(:notify, request, state) do
    respond(request, @response_ok)
    state
  end

  @impl Call
  def handle_request(:bye, request, state) do
    respond(request, @response_ok)
    notify_endpoint(state.endpoint, {:end, hangup_cause(request)})
    schedule_death()
    state
  end

  ## GenServer CALLBACKS

  @impl GenServer
  def handle_cast(:cancel, state) do
    state =
      if state.invite_dialog_complete? do
        build_and_send_request(:bye, state)
      else
        send_cancel(state)
      end

    notify_endpoint(state.endpoint, {:end, :cancelled})
    schedule_death()

    {:noreply, state}
  end

  @impl GenServer
  def handle_cast(:bye, state) do
    state = build_and_send_request(:bye, state)
    notify_endpoint(state.endpoint, {:end, :user_hangup})
    schedule_death()

    {:noreply, state}
  end

  @impl GenServer
  def handle_info(:die, state) do
    {:stop, :normal, state}
  end

  ## PRIVATE FUNCTIONS

  defp build_and_send_request(method, state) do
    method
    |> Call.build_headers(state)
    |> create_request(state)
    |> Call.make_request(state)
  end

  defp create_request(%{cseq: {_cseq, method}} = headers, state) do
    method
    |> Sippet.Message.build_request(to_string(state.callee))
    |> Map.put(:headers, headers)
  end

  defp respond(request, code) do
    request
    |> Sippet.Message.to_response(code)
    |> SippetCore.send_message()
  end

  defp notify_endpoint(endpoint, message) do
    send(endpoint, {:call_info, message})
  end

  defp send_ack(response, state) do
    %Sippet.Message{
      headers: %{
        to: to,
        via: [{_, _, _, %{"branch" => branch}}],
        cseq: {cseq, :invite}
      }
    } = response

    message =
      :ack
      |> Call.build_headers(state, %{to: to, cseq: {cseq, :ack}}, branch)
      |> create_request(state)

    message =
      if is_nil(state.target),
        do: message,
        else: Map.put(message, :target, state.target)

    SippetCore.send_message(message)
  end

  defp send_cancel(state) do
    headers =
      state.last_message.headers
      |> Map.drop([:content_length, :content_type, :proxy_authorization, :authorization])
      |> Map.update!(:cseq, fn {cseq, :invite} -> {cseq, :cancel} end)

    callee = %{state.registrar_credentials.uri | userinfo: state.phone_number}

    headers
    |> create_request(%{state | callee: callee})
    |> Call.make_request(state, false)
  end

  defp hangup_cause(request) do
    case Sippet.Message.get_header(request, "X-Asterisk-HangupCause", []) do
      ["Normal Clearing"] -> :normal_clearing
      [] -> :unknown
      [other | _rest] -> other
    end
  end

  defp schedule_death(), do: Process.send_after(self(), :die, @death_timeout_ms)
  defp schedule_death(:instant), do: send(self(), :die)
end
