defmodule ExTracker.Types.AnnounceRequest do

  require Logger

  def parse(params) do
    # mandatory fields
    with {:ok, info_hash} <- fetch_field_info_hash(params),
      {:ok, peer_id} <- fetch_field_peer_id(params),
      {:ok, port} <- fetch_field_port(params),
      {:ok, uploaded} <- fetch_field_uploaded(params),
      {:ok, downloaded} <- fetch_field_downloaded(params),
      {:ok, left} <- fetch_field_left(params)
    do
      mandatories = %{
        info_hash: info_hash, peer_id: peer_id, port: port,
        uploaded: uploaded, downloaded: downloaded, left: left
      }

      # optional fields
      optionals = %{}
        |> add_field_compact(params)
        |> add_field_event(params)
        |> add_field_no_peer_id(params)
        |> add_field_numwant(params)
        |> add_field_ip(params)
        #|> add_field_key(params)
        #|> add_field_trackerid(params)

      request = Map.merge(mandatories, optionals)
      {:ok, request}
    else
      {:error, message} -> {:error, message}
      _ -> {:error, "unknown error"}
    end
  end

  #==========================================================================
  # Mandatory Fields
  #==========================================================================

  # info_hash: urlencoded 20-byte SHA1 hash of the value of the info key from the Metainfo file.
  defp fetch_field_info_hash(params) do
    case Map.fetch(params, "info_hash") do
      {:ok, info_hash} ->
        case ExTracker.Utils.validate_hash(info_hash) do
          {:ok, decoded_hash} -> {:ok, decoded_hash}
          {:error, error} ->
            Logger.warning("invalid 'info_hash' parameter: size: #{byte_size(info_hash)} value: #{inspect(info_hash)}")
            {:error, "invalid 'info_hash' parameter: #{error}"}
        end
      :error -> {:error, "missing 'info_hash' parameter"}
    end
  end

  # peer_id: urlencoded 20-byte string used as a unique ID for the client, generated by the client at startup. This is allowed to be any value, and may be binary data.
  defp fetch_field_peer_id(params) do
    case Map.fetch(params, "peer_id") do
      {:ok, peer_id} ->
        case byte_size(peer_id) do
          20 -> {:ok,peer_id}
          _ ->
            Logger.warning("invalid 'peer_id' parameter: size: #{byte_size(peer_id)} value: #{inspect(peer_id)}")
            {:error, "invalid 'peer_id' parameter"}
        end
      :error -> {:error, "missing 'peer_id' parameter"}
    end
  end

  # port: The port number that the client is listening on. Ports reserved for BitTorrent are typically 6881-6889.
  defp fetch_field_port(params) do
    case Map.fetch(params, "port") do
      {:ok, port} when is_integer(port) ->
        {:ok, port}
      {:ok, port} ->
        case Integer.parse(port) do
          {number, _rest} when number >= 0 and number <= 65535 -> {:ok, number}
          {number, _rest} -> {:error, "invalid 'port' parameter: '#{number}' is not yet handled"}
          :error -> {:error, "invalid 'port' parameter"}
        end
      :error -> {:error, "missing 'port' parameter"}
    end
  end

  # downloaded: The total amount downloaded (since the client sent the 'started' event to the tracker) in base ten ASCII.
  # While not explicitly stated in the official specification, the consensus is that this should be the total number of bytes downloaded.
  defp fetch_field_downloaded(params) do
    case Map.fetch(params, "downloaded") do
      {:ok, downloaded} when is_integer(downloaded) ->
        {:ok, downloaded}
      {:ok, downloaded} ->
        case Integer.parse(downloaded) do
          {number, _rest} when number >= 0 -> {:ok, number}
          :error -> {:error, "invalid 'downloaded' parameter"}
        end
      :error -> {:error, "missing 'downloaded' parameter"}
    end
  end

  # uploaded: The total amount uploaded (since the client sent the 'started' event to the tracker) in base ten ASCII.
  # While not explicitly stated in the official specification, the consensus is that this should be the total number of bytes uploaded.
  defp fetch_field_uploaded(params) do
    case Map.fetch(params, "uploaded") do
      {:ok, uploaded} when is_integer(uploaded) ->
        {:ok, uploaded}
      {:ok, uploaded} ->
        case Integer.parse(uploaded) do
          {number, _rest} when number >= 0 -> {:ok, number}
          :error -> {:error, "invalid 'uploaded' parameter"}
        end
      :error -> {:error, "missing 'uploaded' parameter"}
    end
  end

  # left: The number of bytes this client still has to download in base ten ASCII.
  # Clarification: The number of bytes needed to download to be 100% complete and get all the included files in the torrent.
  defp fetch_field_left(params) do
    case Map.fetch(params, "left") do
      {:ok, left} when is_integer(left) ->
        {:ok, left}
      {:ok, left} ->
        case Integer.parse(left) do
          {number, _rest} when number >= 0 -> {:ok, number}
          :error -> {:error, "invalid 'left' parameter"}
        end
      :error -> {:error, "missing 'left' parameter"}
    end
  end

  #==========================================================================
  # Optional Fields
  #==========================================================================

  # compact: Setting this to 1 indicates that the client accepts a compact response. The peers list is replaced by a peers string with 6 bytes per peer.
  # The first four bytes are the host (in network byte order), the last two bytes are the port (in network byte order).
  # It should be noted that some trackers only support compact responses (for saving bandwidth) and either refuse requests without "compact=1"
  # or simply send a compact response unless the request contains "compact=0" (in which case they will refuse the request.)
  defp add_field_compact(request, params) do
    case Map.fetch(params, "compact") do
      {:ok, compact} when is_integer(compact) -> Map.put(request, :compact, compact != 0)
      {:ok, compact} -> Map.put(request, :compact, compact != "0")
      :error -> Map.put(request, :compact, true)
    end
  end

  # no_peer_id: Indicates that the tracker can omit peer id field in peers dictionary. This option is ignored if compact is enabled.
  defp add_field_no_peer_id(request, params) do
    case Map.fetch(params, "no_peer_id") do
      {:ok, no_peer_id} when is_integer(no_peer_id) -> Map.put(request, :no_peer_id, no_peer_id == 1)
      {:ok, no_peer_id} -> Map.put(request, :no_peer_id, no_peer_id == "1")
      :error -> Map.put(request, :no_peer_id, false)
    end
  end

  # event: If specified, must be one of started, completed, stopped, (or empty which is the same as not being specified). If not specified, then this request is one performed at regular intervals.
  #   started: The first request to the tracker must include the event key with this value.
  #   stopped: Must be sent to the tracker if the client is shutting down gracefully.
  #   completed: Must be sent to the tracker when the download completes. However, must not be sent if the download was already 100 % complete when the client started.
  #              Presumably, this is to allow the tracker to increment the "completed downloads" metric based solely on this event.
  defp add_field_event(request, params) do
    case Map.fetch(params, "event") do
      {:ok, "started"} -> Map.put(request, :event, :started)
      {:ok, "stopped"} -> Map.put(request, :event, :stopped)
      {:ok, "completed"} -> Map.put(request, :event, :completed)
      {:ok, "paused"} -> Map.put(request, :event, :paused)
      {:ok, "unknown"} -> Map.put(request, :event, :updated) # this one is annoying in the wild
      {:ok, ""} -> Map.put(request, :event, :updated)
      {:ok, other} ->
        Logger.warning("invalid 'event' parameter: size: #{byte_size(other)} value: #{inspect(other)}")
        Map.put(request, :event, :updated) #:invalid
      :error -> Map.put(request, :event, :updated)
    end
  end

  # numwant: Optional. Number of peers that the client would like to receive from the tracker. This value is permitted to be zero. If omitted, typically defaults to 50 peers.
  defp add_field_numwant(request, params) do
    case Map.fetch(params, "numwant") do
      {:ok, numwant} when is_integer(numwant) ->
        Map.put(request, :numwant, numwant)
      {:ok, numwant} ->
        case Integer.parse(numwant) do
          {number, _rest} -> Map.put(request, :numwant, number)
          :error -> Map.put(request, :numwant, 25)
        end
      :error -> Map.put(request, :numwant, 25)
    end
  end

  # ip: Optional. The true IP address of the client machine, in dotted quad format or rfc3513 defined hexed IPv6 address.
  # In general this parameter is not necessary as the address of the client can be determined from the IP address from which the HTTP request came.
  # The parameter is only needed in the case where the IP address that the request came in on is not the IP address of the client.
  # This happens if the client is communicating to the tracker through a proxy (or a transparent web proxy/cache.)
  # It also is necessary when both the client and the tracker are on the same local side of a NAT gateway.
  # The reason for this is that otherwise the tracker would give out the internal(RFC1918) address of the client, which is not routable.
  # Therefore the client must explicitly state its (external, routable) IP address to be given out to external peers.
  # Various trackers treat this parameter differently. Some only honor it only if the IP address that the request came in on is in RFC1918 space.
  # Others honor it unconditionally, while others ignore it completely. In case of IPv6 address(e.g. :2001:db8:1:2::100) it indicates only that client can communicate via IPv6.
  defp add_field_ip(request, params) do
    case Map.fetch(params, "ip") do
      {:ok, requested_ip} -> Map.put(request, :ip, requested_ip)
      :error -> Map.put(request, :ip, nil)
    end
  end

  # key: Optional. An additional identification that is not shared with any other peers. It is intended to allow a client to prove their identity should their IP address change.
  defp add_field_key(request, params) do
    case Map.fetch(params, "key") do
      {:ok, key} -> Map.put(request, :key, key)
      :error -> Map.put(request, :key, nil)
    end
  end

  # trackerid: Optional. If a previous announce contained a tracker id, it should be set here.
  defp add_field_trackerid(request, params) do
    case Map.fetch(params, "trackerid") do
      {:ok, trackerid} -> Map.put(request, :trackerid, trackerid)
      :error -> Map.put(request, :trackerid, nil)
    end
  end
end
