defmodule Tortoise.Package.Connack do
  @moduledoc false

  @opcode 2

  alias Tortoise.Package

  @type status :: :accepted | {:refused, refusal_reasons()}
  @type refusal_reasons ::
          :unacceptable_protocol_version
          | :identifier_rejected
          | :server_unavailable
          | :bad_user_name_or_password
          | :not_authorized

  @opaque t :: %__MODULE__{
            __META__: Package.Meta.t(),
            session_present: boolean(),
            status: status() | nil
          }
  @enforce_keys [:status]
  defstruct __META__: %Package.Meta{opcode: @opcode, flags: 0},
            session_present: false,
            status: nil

  @spec decode(<<_::32>>) :: t
  def decode(<<@opcode::4, 0::4, 2, 0::7, session_present::1, return_code::8>>) do
    %__MODULE__{
      session_present: session_present == 1,
      status: coerce_return_code(return_code)
    }
  end

  defp coerce_return_code(return_code) do
    case return_code do
      0x00 -> :accepted
      0x01 -> {:refused, :unacceptable_protocol_version}
      0x02 -> {:refused, :identifier_rejected}
      0x03 -> {:refused, :server_unavailable}
      0x04 -> {:refused, :bad_user_name_or_password}
      0x05 -> {:refused, :not_authorized}
    end
  end

  defimpl Tortoise.Encodable do
    def encode(%Package.Connack{session_present: session_present, status: status} = t)
        when status != nil do
      [
        Package.Meta.encode(t.__META__),
        <<2, 0::7, flag(session_present)::1, to_return_code(status)::8>>
      ]
    end

    defp to_return_code(:accepted), do: 0x00

    defp to_return_code({:refused, reason}) do
      case reason do
        :unacceptable_protocol_version -> 0x01
        :identifier_rejected -> 0x02
        :server_unavailable -> 0x03
        :bad_user_name_or_password -> 0x04
        :not_authorized -> 0x05
      end
    end

    defp flag(f) when f in [0, nil, false], do: 0
    defp flag(_), do: 1
  end
end
