defmodule Parrot.Sip.Message do
  @moduledoc """
  Represents a SIP message (request or response).

  This module provides a struct and functions for working with SIP messages as defined
  in RFC 3261 and related specifications. It provides a pure functional implementation
  that models both SIP requests and responses, along with utility functions for
  manipulation, analysis, and conversion.

  References:
  - RFC 3261 Section 7: SIP Messages
  - RFC 3261 Section 8.1: UAC Behavior
  - RFC 3261 Section 8.2: UAS Behavior
  - RFC 3261 Section 20: Header Fields
  """

  require Logger

  alias Parrot.Sip.DialogId
  alias Parrot.Sip.Headers.{CSeq, From, To, Via, CallId, Contact}
  alias Parrot.Sip.Method

  defstruct [
    # Atom like :invite, :register
    :method,
    # URI for requests
    :request_uri,
    # Integer for responses
    :status_code,
    # String for responses
    :reason_phrase,
    # String, typically "SIP/2.0"
    :version,
    # Map of header name to header struct
    :headers,
    # Binary string
    :body,
    # Source information for transport
    :source,
    # :request or :response
    :type,
    # :incoming or :outgoing
    :direction,
    # transaction_id
    :transaction_id,
    # dialog_id
    :dialog_id
  ]

  @type t :: %__MODULE__{
          method: Method.t() | nil,
          request_uri: String.t() | nil,
          status_code: integer() | nil,
          reason_phrase: String.t() | nil,
          version: String.t(),
          headers: map(),
          body: String.t(),
          source: map() | nil,
          type: :request | :response | nil,
          direction: :incoming | :outgoing | nil,
          transaction_id: String.t() | nil,
          dialog_id: String.t() | nil
        }

  @default_reason_phrases %{
    100 => "Trying",
    180 => "Ringing",
    181 => "Call Is Being Forwarded",
    182 => "Queued",
    183 => "Session Progress",
    199 => "Early Dialog Terminated",
    200 => "OK",
    202 => "Accepted",
    204 => "No Notification",
    300 => "Multiple Choices",
    301 => "Moved Permanently",
    302 => "Moved Temporarily",
    305 => "Use Proxy",
    380 => "Alternative Service",
    400 => "Bad Request",
    401 => "Unauthorized",
    402 => "Payment Required",
    403 => "Forbidden",
    404 => "Not Found",
    405 => "Method Not Allowed",
    406 => "Not Acceptable",
    407 => "Proxy Authentication Required",
    408 => "Request Timeout",
    409 => "Conflict",
    410 => "Gone",
    412 => "Conditional Request Failed",
    413 => "Request Entity Too Large",
    414 => "Request-URI Too Long",
    415 => "Unsupported Media Type",
    416 => "Unsupported URI Scheme",
    417 => "Unknown Resource-Priority",
    420 => "Bad Extension",
    421 => "Extension Required",
    422 => "Session Interval Too Small",
    423 => "Interval Too Brief",
    424 => "Bad Location Information",
    428 => "Use Identity Header",
    429 => "Provide Referrer Identity",
    430 => "Flow Failed",
    433 => "Anonymity Disallowed",
    436 => "Bad Identity-Info",
    437 => "Unsupported Certificate",
    438 => "Invalid Identity Header",
    439 => "First Hop Lacks Outbound Support",
    440 => "Max-Breadth Exceeded",
    470 => "Consent Needed",
    480 => "Temporarily Unavailable",
    481 => "Call/Transaction Does Not Exist",
    482 => "Loop Detected",
    483 => "Too Many Hops",
    484 => "Address Incomplete",
    485 => "Ambiguous",
    486 => "Busy Here",
    487 => "Request Terminated",
    488 => "Not Acceptable Here",
    489 => "Bad Event",
    491 => "Request Pending",
    493 => "Undecipherable",
    494 => "Security Agreement Required",
    500 => "Server Internal Error",
    501 => "Not Implemented",
    502 => "Bad Gateway",
    503 => "Service Unavailable",
    504 => "Server Time-out",
    505 => "Version Not Supported",
    513 => "Message Too Large",
    580 => "Precondition Failure",
    600 => "Busy Everywhere",
    603 => "Decline",
    604 => "Does Not Exist Anywhere",
    606 => "Not Acceptable",
    607 => "Unwanted",
    608 => "Rejected"
  }

  @doc """
  Returns the default reason phrase for a given SIP status code.
  """
  @spec default_reason_phrase(integer()) :: String.t()
  def default_reason_phrase(status_code) do
    Map.get(@default_reason_phrases, status_code, "Unknown")
  end

  @doc """
  Creates a new request message with the specified method, request URI, and headers.

  This function is the main entry point for creating SIP request messages.

  ## Parameters
  - method: The SIP method (atom) for the request
  - request_uri: The request target URI
  - headers: Optional map of initial headers

  ## Examples

      iex> Parrot.Sip.Message.new_request(:invite, "sip:alice@example.com")
      %Parrot.Sip.Message{
        method: :invite,
        request_uri: "sip:alice@example.com",
        version: "SIP/2.0",
        headers: %{},
        body: "",
        type: :request,
        direction: :outgoing
      }
  """
  @spec new_request(Method.t(), String.t(), map(), keyword()) :: t()
  def new_request(method, request_uri, headers \\ %{}, opts \\ []) do
    %__MODULE__{
      method: method,
      request_uri: request_uri,
      version: "SIP/2.0",
      headers: headers,
      body: "",
      type: :request,
      direction: :outgoing,
      dialog_id: Keyword.get(opts, :dialog_id, nil),
      transaction_id: Keyword.get(opts, :transaction_id, nil)
    }
  end

  @doc """
  Creates a new response message with the specified status code, reason phrase, and headers.

  ## Parameters
  - status_code: The SIP response status code (100-699)
  - reason_phrase: The reason phrase for the response
  - headers: Optional map of initial headers

  ## Examples

      iex> Parrot.Sip.Message.new_response(200, "OK")
      %Parrot.Sip.Message{
        status_code: 200,
        reason_phrase: "OK",
        version: "SIP/2.0",
        headers: %{},
        body: "",
        type: :response,
        direction: :outgoing
      }
  """
  @spec new_response(integer(), String.t(), map(), keyword()) :: t()
  def new_response(status_code, reason_phrase, headers, opts) do
    %__MODULE__{
      status_code: status_code,
      reason_phrase: reason_phrase,
      version: "SIP/2.0",
      headers: headers,
      body: "",
      type: :response,
      direction: :outgoing,
      dialog_id: Keyword.get(opts, :dialog_id, nil),
      transaction_id: Keyword.get(opts, :transaction_id, nil)
    }
  end

  @doc """
  Creates a new response message with standard reason phrase based on status code.

  If no reason phrase is provided, a standard one will be used based on the status code.

  ## Examples

      iex> Parrot.Sip.Message.new_response(200)
      %Parrot.Sip.Message{
        status_code: 200,
        reason_phrase: "OK",
        version: "SIP/2.0",
        headers: %{},
        body: "",
        type: :response,
        direction: :outgoing
      }
  """
  @spec new_response(integer()) :: t()
  def new_response(status_code) do
    reason_phrase = Map.get(@default_reason_phrases, status_code, "Unknown")
    new_response(status_code, reason_phrase, %{}, [])
  end

  @spec new_response(integer(), String.t()) :: t()
  def new_response(status_code, reason_phrase) when is_binary(reason_phrase) do
    new_response(status_code, reason_phrase, %{}, [])
  end

  @spec new_response(integer(), keyword()) :: t()
  def new_response(status_code, opts) when is_list(opts) do
    reason_phrase = Map.get(@default_reason_phrases, status_code, "Unknown")
    new_response(status_code, reason_phrase, %{}, opts)
  end

  @spec new_response(integer(), String.t(), map()) :: t()
  def new_response(status_code, reason_phrase, headers) do
    new_response(status_code, reason_phrase, headers, [])
  end

  @doc """
  Creates a response from a request, copying necessary headers and setting
  the status code and reason phrase.

  This function follows the requirements in RFC 3261 Section 8.2.6 for
  copying headers from requests to responses.

  ## Parameters
  - request: The SIP request message
  - status_code: The response status code
  - reason_phrase: The reason phrase for the response

  ## Examples

      iex> request = Parrot.Sip.Message.new_request(:invite, "sip:alice@example.com")
      iex> response = Parrot.Sip.Message.reply(request, 200, "OK")
      iex> response.status_code
      200
  """
  @spec reply(t(), integer(), String.t()) :: t()
  def reply(request, status_code, reason_phrase) when request.type == :request do
    # Copy headers from request to response with proper manipulation
    resp_headers = %{}
    # Add via headers from request
    resp_headers = Map.put(resp_headers, "via", request.headers["via"])
    # Add to/from headers
    resp_headers = Map.put(resp_headers, "to", request.headers["to"])
    resp_headers = Map.put(resp_headers, "from", request.headers["from"])
    # Add call-id
    resp_headers = Map.put(resp_headers, "call-id", request.headers["call-id"])
    # Add CSeq
    resp_headers = Map.put(resp_headers, "cseq", request.headers["cseq"])

    %__MODULE__{
      method: request.method,
      request_uri: request.request_uri,
      status_code: status_code,
      reason_phrase: reason_phrase,
      version: request.version,
      headers: resp_headers,
      body: Map.get(request, :body, ""),
      source: request.source,
      type: :response,
      direction: :outgoing,
      dialog_id: Map.get(request, :dialog_id),
      transaction_id: Map.get(request, :transaction_id)
    }
  end

  @doc """
  Creates a response from a request with standard reason phrase based on status code.

  ## Examples

      iex> request = Parrot.Sip.Message.new_request(:invite, "sip:alice@example.com")
      iex> response = Parrot.Sip.Message.reply(request, 200)
      iex> response.reason_phrase
      "OK"
  """
  @spec reply(t(), integer()) :: t()
  def reply(request, status_code) when request.type == :request do
    reason_phrase = Map.get(@default_reason_phrases, status_code, "Unknown")
    reply(request, status_code, reason_phrase)
  end

  @doc """
  Gets a header from the message by name.

  Header names are case-insensitive as per RFC 3261.
  If the header is a map, it will be converted to the appropriate struct.

  ## Examples

      iex> message = Parrot.Sip.Message.new_request(:invite, "sip:bob@example.com")
      iex> message = Parrot.Sip.Message.set_header(message, "Via", %{protocol: "SIP", version: "2.0", transport: "udp"})
      iex> Parrot.Sip.Message.get_header(message, "via")
      %Parrot.Sip.Headers.Via{protocol: "SIP", version: "2.0", transport: :udp}
  """
  @spec get_header(t(), String.t()) :: any()
  # Get Via header using direct pattern matching
  def get_header(message, "Via"), do: get_via_header(message)
  def get_header(message, "via"), do: get_via_header(message)
  def get_header(message, "v"), do: get_via_header(message)

  # Regular get_header function for other headers
  def get_header(message, name) do
    downcased = String.downcase(name)

    # Resolve compact headers to their full forms using the Serializer's function
    full_name = Parrot.Sip.Serializer.expand_compact_header(downcased)

    # Get the value
    value = Map.get(message.headers, full_name)

    # Return nil if not found
    if is_nil(value) do
      nil
    else
      # Special case for content-type when accessed directly
      if full_name == "content-type" && is_struct(value, Parrot.Sip.Headers.ContentType) &&
           downcased == "content-type" do
        Parrot.Sip.Headers.ContentType.format(value)
      else
        # Return the value as-is (it should already be a proper struct or primitive value)
        value
      end
    end
  end

  @doc """
  Gets all headers of a given name. Returns a list, even if only one header is present.

  Useful for headers that can appear multiple times like Record-Route or Via.
  """
  @spec get_headers(t(), String.t()) :: [any()]
  def get_headers(message, name) do
    downcased = String.downcase(name)
    full_name = Parrot.Sip.Serializer.expand_compact_header(downcased)

    values = Map.get(message.headers, full_name)

    cond do
      is_nil(values) ->
        []

      is_list(values) ->
        Enum.map(values, fn
          v when is_struct(v) ->
            v

          v when is_map(v) ->
            struct_for_header(full_name, v)

          v ->
            v
        end)

      true ->
        [get_header(message, name)]
    end
  end

  # Extracted helper
  # Headers are now parsed as structs during the initial parsing process,
  # so this function is no longer needed. However, we keep it for backward compatibility
  # with any code that might still rely on it.
  defp struct_for_header("record-route", v) when is_map(v) and not is_struct(v),
    do: struct(Parrot.Sip.Headers.RecordRoute, v)

  defp struct_for_header("route", v) when is_map(v) and not is_struct(v),
    do: struct(Parrot.Sip.Headers.Route, v)

  defp struct_for_header("via", v) when is_map(v) and not is_struct(v),
    do: struct(Parrot.Sip.Headers.Via, v)

  defp struct_for_header("contact", v) when is_map(v) and not is_struct(v),
    do: struct(Parrot.Sip.Headers.Contact, v)

  defp struct_for_header(_, v), do: v

  # Helper function to handle Via header logic
  defp get_via_header(message) do
    value = Map.get(message.headers, "via")

    cond do
      is_nil(value) ->
        nil

      is_struct(value) ->
        value

      is_binary(value) ->
        Parrot.Sip.Headers.Via.parse(value)

      is_list(value) && Enum.all?(value, &is_binary/1) ->
        Enum.map(value, &Parrot.Sip.Headers.Via.parse/1)

      is_list(value) && Enum.all?(value, &is_struct/1) ->
        Enum.map(value, fn via_struct ->
          if is_struct(via_struct, Parrot.Sip.Headers.Via) do
            via_struct
          else
            struct(Parrot.Sip.Headers.Via, via_struct)
          end
        end)

      is_list(value) ->
        Enum.map(value, fn via ->
          struct(Parrot.Sip.Headers.Via, via)
        end)

      true ->
        struct(Parrot.Sip.Headers.Via, value)
    end
  end

  @doc """
  Sets a header in the message.

  Header names are converted to lowercase for consistent storage and retrieval.

  ## Examples

      iex> message = Parrot.Sip.Message.new_request(:invite, "sip:bob@example.com")
      iex> from_header = %Parrot.Sip.Headers.From{}
      iex> message = Parrot.Sip.Message.set_header(message, "From", from_header)
      iex> message.headers["from"] == from_header
      true
  """
  @spec set_header(t(), String.t(), any()) :: t()
  def set_header(message, name, value) do
    downcased = String.downcase(name)
    %{message | headers: Map.put(message.headers, downcased, value)}
  end

  @doc """
  Sets the body of the message and updates the Content-Length header.

  This function automatically calculates and sets the Content-Length header
  based on the body's length.

  ## Examples

      iex> message = Parrot.Sip.Message.new_request(:invite, "sip:bob@example.com")
      iex> message = Parrot.Sip.Message.set_body(message, "Hello, world!")
      iex> message.body
      "Hello, world!"
      iex> message.headers["content-length"]
      13
  """
  @spec set_body(t(), String.t()) :: t()
  def set_body(message, body) do
    headers = Map.put(message.headers, "content-length", byte_size(body))
    %{message | body: body, headers: headers}
  end

  @doc """
  Check if a message is a request.
  """
  @spec is_request?(t()) :: boolean()
  def is_request?(message), do: message.type == :request

  @doc """
  Check if a message is a response.
  """
  @spec is_response?(t()) :: boolean()
  def is_response?(message), do: message.type == :response

  @doc """
  Convert the message to a string representation.
  """
  @spec to_s(t()) :: String.t()
  def to_s(message) do
    start_line =
      if is_request?(message) do
        "#{method_to_string(message.method)} #{message.request_uri} #{message.version}\r\n"
      else
        "#{message.version} #{message.status_code} #{message.reason_phrase}\r\n"
      end

    headers_str = headers_to_string(message.headers)

    start_line <> headers_str <> "\r\n" <> (message.body || "")
  end

  defp method_to_string(method) when is_atom(method) do
    Method.to_string(method)
  end

  defp headers_to_string(headers) do
    # RFC 3261 Section 7.3.1: Header Field Order
    # Via headers must appear before all other headers
    # Then: Route, Record-Route, Proxy-Require, Max-Forwards, 
    # Proxy-Authorization, To, From, Contact, etc.

    # Define header order priority (lower number = higher priority)
    header_order = %{
      "via" => 1,
      "route" => 2,
      "record-route" => 3,
      "max-forwards" => 4,
      "proxy-require" => 5,
      "proxy-authorization" => 6,
      "from" => 7,
      "to" => 8,
      "call-id" => 9,
      "cseq" => 10,
      "contact" => 11,
      "expires" => 12,
      "content-type" => 13,
      "content-length" => 14
    }

    # Default priority for headers not in the list
    default_priority = 100

    headers
    |> Enum.sort_by(fn {name, _value} ->
      normalized_name = name |> to_string() |> String.downcase()
      Map.get(header_order, normalized_name, default_priority)
    end)
    |> Enum.map(fn {name, value} -> format_header(name, value) end)
    |> Enum.join("")
  end

  defp format_header(name, value) do
    header_name =
      name
      |> to_string()
      |> String.split("-")
      |> Enum.map(&String.capitalize/1)
      |> Enum.join("-")

    "#{header_name}: #{format_header_value(name, value)}\r\n"
  end

  defp format_header_value(_name, value) when is_binary(value), do: value
  defp format_header_value(_name, value) when is_integer(value), do: Integer.to_string(value)

  defp format_header_value(_name, value) when is_list(value) do
    # For lists, check if all elements are structs with format function
    if Enum.all?(value, &(is_struct(&1) && function_exported?(&1.__struct__, :format, 1))) do
      # Check if the module has a format_list function
      first_struct = List.first(value)

      if first_struct && function_exported?(first_struct.__struct__, :format_list, 1) do
        first_struct.__struct__.format_list(value)
      else
        # Default: format each and join with comma
        value
        |> Enum.map(& &1.__struct__.format(&1))
        |> Enum.join(", ")
      end
    else
      inspect(value)
    end
  end

  defp format_header_value(_name, value) do
    # Try to call format function on header module
    if is_struct(value) && function_exported?(value.__struct__, :format, 1) do
      value.__struct__.format(value)
    else
      # Fall back to inspect for unhandled types
      inspect(value)
    end
  end

  @doc """
  Gets the CSeq header from a message.

  ## Examples

      iex> message = Parrot.Sip.Message.new_request(:invite, "sip:bob@example.com")
      iex> cseq = %Parrot.Sip.Headers.CSeq{sequence: 1, method: :invite}
      iex> message = Parrot.Sip.Message.set_header(message, "CSeq", cseq)
      iex> Parrot.Sip.Message.cseq(message)
      %Parrot.Sip.Headers.CSeq{sequence: 1, method: :invite}
  """
  @spec cseq(t()) :: CSeq.t() | nil
  def cseq(message) do
    get_header(message, "cseq")
  end

  @doc """
  Gets the From header from a message.

  ## Examples

      iex> message = Parrot.Sip.Message.new_request(:invite, "sip:bob@example.com")
      iex> from = %Parrot.Sip.Headers.From{uri: "sip:alice@example.com"}
      iex> message = Parrot.Sip.Message.set_header(message, "From", from)
      iex> Parrot.Sip.Message.from(message)
      %Parrot.Sip.Headers.From{uri: "sip:alice@example.com"}
  """
  @spec from(t()) :: From.t() | nil
  def from(message) do
    get_header(message, "from")
  end

  @doc """
  Gets the To header from a message.

  ## Examples

      iex> message = Parrot.Sip.Message.new_request(:invite, "sip:bob@example.com")
      iex> to = %Parrot.Sip.Headers.To{uri: "sip:bob@example.com"}
      iex> message = Parrot.Sip.Message.set_header(message, "To", to)
      iex> Parrot.Sip.Message.to(message)
      %Parrot.Sip.Headers.To{uri: "sip:bob@example.com"}
  """
  @spec to(t()) :: To.t() | nil
  def to(message) do
    get_header(message, "to")
  end

  @doc """
  Gets the Call-ID header from a message.

  ## Examples

      iex> message = Parrot.Sip.Message.new_request(:invite, "sip:bob@example.com")
      iex> message = Parrot.Sip.Message.set_header(message, "Call-ID", "abc123@example.com")
      iex> Parrot.Sip.Message.call_id(message)
      "abc123@example.com"
  """
  @spec call_id(t()) :: String.t() | CallId.t() | nil
  def call_id(message) do
    get_header(message, "call-id")
  end

  @doc """
  Gets the top Via header from a message.

  ## Examples

      iex> message = Parrot.Sip.Message.new_request(:invite, "sip:bob@example.com")
      iex> via = %Parrot.Sip.Headers.Via{host: "example.com", port: 5060}
      iex> message = Parrot.Sip.Message.set_header(message, "Via", via)
      iex> Parrot.Sip.Message.top_via(message)
      %Parrot.Sip.Headers.Via{host: "example.com", port: 5060}
  """
  @spec top_via(t()) :: Via.t() | nil
  def top_via(message) do
    case get_header(message, "via") do
      nil -> nil
      via when is_list(via) -> List.first(via)
      via -> via
    end
  end

  @doc """
  Gets all Via headers from a message.

  ## Examples

      iex> message = Parrot.Sip.Message.new_request(:invite, "sip:bob@example.com")
      iex> via = %Parrot.Sip.Headers.Via{host: "example.com", port: 5060}
      iex> message = Parrot.Sip.Message.set_header(message, "Via", via)
      iex> Parrot.Sip.Message.all_vias(message)
      [%Parrot.Sip.Headers.Via{host: "example.com", port: 5060}]
  """
  @spec all_vias(t()) :: list(Via.t())
  def all_vias(message) do
    case get_header(message, "via") do
      nil -> []
      via when is_list(via) -> via
      via -> [via]
    end
  end

  @doc """
  Gets the Contact header from a message.

  ## Examples

      iex> message = Parrot.Sip.Message.new_request(:invite, "sip:bob@example.com")
      iex> contact = %Parrot.Sip.Headers.Contact{uri: "sip:alice@example.com"}
      iex> message = Parrot.Sip.Message.set_header(message, "Contact", contact)
      iex> Parrot.Sip.Message.contact(message)
      %Parrot.Sip.Headers.Contact{uri: "sip:alice@example.com"}
  """
  @spec contact(t()) :: Contact.t() | nil
  def contact(message) do
    get_header(message, "contact")
  end

  @doc """
  Gets the branch parameter from the top Via header.

  ## Examples

      iex> message = Parrot.Sip.Message.new_request(:invite, "sip:bob@example.com")
      iex> via = %Parrot.Sip.Headers.Via{parameters: %{"branch" => "z9hG4bK123"}}
      iex> message = Parrot.Sip.Message.set_header(message, "Via", via)
      iex> Parrot.Sip.Message.branch(message)
      "z9hG4bK123"
  """
  @spec branch(t()) :: String.t() | nil
  def branch(message) do
    case top_via(message) do
      nil -> nil
      via -> Map.get(via.parameters, "branch")
    end
  end

  @doc """
  Gets a dialog ID from a message.

  ## Examples

      iex> message = Parrot.Sip.Message.new_request(:invite, "sip:bob@example.com")
      iex> from = %Parrot.Sip.Headers.From{parameters: %{"tag" => "123"}}
      iex> to = %Parrot.Sip.Headers.To{parameters: %{"tag" => "456"}}
      iex> message = message |> Parrot.Sip.Message.set_header("From", from)
      iex> message = message |> Parrot.Sip.Message.set_header("To", to)
      iex> message = message |> Parrot.Sip.Message.set_header("Call-ID", "abc@example.com")
      iex> dialog_id = Parrot.Sip.Message.dialog_id(message)
      iex> dialog_id.call_id
      "abc@example.com"
  """
  @spec dialog_id(t()) :: DialogId.t()
  def dialog_id(message) do
    Logger.debug("Getting dialog_id from message")
    DialogId.from_message(message)
  end

  @doc """
  Determines if a message is within a dialog.

  A message is within a dialog if it has a To tag and a From tag.

  ## Examples

      iex> message = Parrot.Sip.Message.new_request(:invite, "sip:bob@example.com")
      iex> from = %Parrot.Sip.Headers.From{parameters: %{"tag" => "123"}}
      iex> to = %Parrot.Sip.Headers.To{parameters: %{"tag" => "456"}}
      iex> message = message |> Parrot.Sip.Message.set_header("From", from)
      iex> message = message |> Parrot.Sip.Message.set_header("To", to)
      iex> Parrot.Sip.Message.in_dialog?(message)
      true
  """
  @spec in_dialog?(t()) :: boolean()
  def in_dialog?(message) do
    from_header = from(message)
    to_header = to(message)

    from_tag = if from_header, do: From.tag(from_header), else: nil
    to_tag = if to_header, do: To.tag(to_header), else: nil

    not is_nil(from_tag) and not is_nil(to_tag)
  end

  @doc """
  Gets the status class of a response message (1xx, 2xx, etc.).

  ## Examples

      iex> message = Parrot.Sip.Message.new_response(200, "OK")
      iex> Parrot.Sip.Message.status_class(message)
      2
  """
  @spec status_class(t()) :: integer() | nil
  def status_class(%__MODULE__{type: :response, status_code: status_code})
      when is_integer(status_code) do
    div(status_code, 100)
  end

  def status_class(_), do: nil

  @doc """
  Checks if a response message is provisional (1xx).

  ## Examples

      iex> message = Parrot.Sip.Message.new_response(180, "Ringing")
      iex> Parrot.Sip.Message.is_provisional?(message)
      true
  """
  @spec is_provisional?(t()) :: boolean()
  def is_provisional?(message) do
    status_class(message) == 1
  end

  @doc """
  Checks if a response message is successful (2xx).

  ## Examples

      iex> message = Parrot.Sip.Message.new_response(200, "OK")
      iex> Parrot.Sip.Message.is_success?(message)
      true
  """
  @spec is_success?(t()) :: boolean()
  def is_success?(message) do
    status_class(message) == 2
  end

  @doc """
  Checks if a response message is a redirection (3xx).

  ## Examples

      iex> message = Parrot.Sip.Message.new_response(302, "Moved Temporarily")
      iex> Parrot.Sip.Message.is_redirect?(message)
      true
  """
  @spec is_redirect?(t()) :: boolean()
  def is_redirect?(message) do
    status_class(message) == 3
  end

  @doc """
  Checks if a response message is a client error (4xx).

  ## Examples

      iex> message = Parrot.Sip.Message.new_response(404, "Not Found")
      iex> Parrot.Sip.Message.is_client_error?(message)
      true
  """
  @spec is_client_error?(t()) :: boolean()
  def is_client_error?(message) do
    status_class(message) == 4
  end

  @doc """
  Checks if a response message is a server error (5xx).

  ## Examples

      iex> message = Parrot.Sip.Message.new_response(500, "Server Internal Error")
      iex> Parrot.Sip.Message.is_server_error?(message)
      true
  """
  @spec is_server_error?(t()) :: boolean()
  def is_server_error?(message) do
    status_class(message) == 5
  end

  @doc """
  Checks if a response message is a global error (6xx).

  ## Examples

      iex> message = Parrot.Sip.Message.new_response(603, "Decline")
      iex> Parrot.Sip.Message.is_global_error?(message)
      true
  """
  @spec is_global_error?(t()) :: boolean()
  def is_global_error?(message) do
    status_class(message) == 6
  end

  @doc """
  Checks if a response message is a failure (4xx, 5xx, or 6xx).

  ## Examples

      iex> message = Parrot.Sip.Message.new_response(404, "Not Found")
      iex> Parrot.Sip.Message.is_failure?(message)
      true
  """
  @spec is_failure?(t()) :: boolean()
  def is_failure?(message) do
    class = status_class(message)
    class in [4, 5, 6]
  end

  @doc """
  Converts a message to binary format for transmission.

  ## Examples

      iex> message = Parrot.Sip.Message.new_request(:invite, "sip:bob@example.com")
      iex> binary = Parrot.Sip.Message.to_binary(message)
      iex> String.starts_with?(binary, "INVITE sip:bob@example.com SIP/2.0")
      true
  """
  @spec to_binary(t()) :: binary()
  def to_binary(message) do
    to_s(message)
  end
end
