defmodule SnmpKit.SnmpLib.PDU.Decoder do
  @moduledoc """
  ASN.1 BER decoding functions for SNMP PDUs and messages.

  This module handles the conversion of binary ASN.1 BER format to Elixir data structures
  for SNMP protocol communication.
  """

  import Bitwise

  alias SnmpKit.SnmpLib.PDU.Constants
  alias SnmpKit.SnmpLib.PDU.V3Encoder

  @type message :: Constants.message()
  @type pdu :: Constants.pdu()

  # Import constants for decoding
  @integer Constants.integer()
  @octet_string Constants.octet_string()
  @null Constants.null()
  @object_identifier Constants.object_identifier()
  @counter32 Constants.counter32()
  @gauge32 Constants.gauge32()
  @timeticks Constants.timeticks()
  @counter64 Constants.counter64()
  @ip_address Constants.ip_address()
  @opaque_type Constants.opaque_type()
  @no_such_object Constants.no_such_object()
  @no_such_instance Constants.no_such_instance()
  @end_of_mib_view Constants.end_of_mib_view()

  @doc """
  Decodes an SNMP message from binary format.
  """
  @spec decode_message(binary()) :: {:ok, message()} | {:error, atom()}
  def decode_message(data) when is_binary(data) do
    # Check if this is a SNMPv3 message by looking at version
    case peek_version(data) do
      {:ok, 3} ->
        # Delegate to SNMPv3 decoder
        V3Encoder.decode_message(data, nil)

      {:ok, _version} ->
        # Use standard v1/v2c decoder
        decode_snmp_message_comprehensive(data)

      {:error, reason} ->
        {:error, reason}
    end
  rescue
    error -> {:error, {:decoding_error, error}}
  catch
    error -> {:error, {:decoding_error, error}}
  end

  def decode_message(_), do: {:error, :invalid_input}

  @doc """
  Decodes an SNMP message with security user (SNMPv3).
  """
  @spec decode_message(binary(), map() | nil) :: {:ok, message()} | {:error, atom()}
  def decode_message(data, user) when is_binary(data) do
    case peek_version(data) do
      {:ok, 3} ->
        V3Encoder.decode_message(data, user)

      {:ok, _version} ->
        # v1/v2c messages don't use security users
        decode_message(data)

      {:error, reason} ->
        {:error, reason}
    end
  end

  @doc """
  Decodes a PDU from binary format.
  """
  @spec decode_pdu(binary()) :: {:ok, pdu()} | {:error, atom()}
  def decode_pdu(data) when is_binary(data) do
    case parse_pdu_comprehensive(data) do
      {:ok, pdu} -> {:ok, pdu}
      {:error, reason} -> {:error, reason}
    end
  rescue
    error -> {:error, {:decoding_error, error}}
  catch
    error -> {:error, {:decoding_error, error}}
  end

  @doc """
  Decodes an SNMP message from binary format (alias for decode_message/1).
  """
  @spec decode(binary()) :: {:ok, message()} | {:error, atom()}
  def decode(data) when is_binary(data) do
    decode_message(data)
  end

  # Private helper to peek at version without full decoding
  defp peek_version(<<0x30, _length, 0x02, _version_length, version, _rest::binary>>) do
    {:ok, version}
  end

  defp peek_version(data) when is_binary(data) do
    case parse_sequence(data) do
      {:ok, {content, _remaining}} ->
        case parse_integer(content) do
          {:ok, {version, _rest}} -> {:ok, version}
          {:error, reason} -> {:error, reason}
        end

      {:error, reason} ->
        {:error, reason}
    end
  end

  defp peek_version(_), do: {:error, :invalid_data}

  @doc """
  Alias for decode/1.
  """
  @spec decode_snmp_packet(binary()) :: {:ok, message()} | {:error, atom()}
  def decode_snmp_packet(data), do: decode(data)

  ## Private Implementation

  # Comprehensive decoding implementation (from SnmpSim)
  defp decode_snmp_message_comprehensive(<<0x30, rest::binary>>) do
    with {:ok, {_content_length, content}} <- parse_ber_length(rest),
         {:ok, {version, community, pdu_data}} <- parse_snmp_message_fields(content),
         {:ok, pdu} <- parse_pdu_comprehensive(pdu_data) do
      {:ok,
       %{
         version: version,
         community: community,
         pdu: pdu
       }}
    else
      {:error, reason} when is_tuple(reason) ->
        {:error, reason}

      {:error, reason} ->
        {:error, {:message_parse_error, reason}}
    end
  end

  defp decode_snmp_message_comprehensive(_), do: {:error, :invalid_message_format}

  defp parse_ber_length(<<length, rest::binary>>) when length < 128 do
    if byte_size(rest) >= length do
      content = binary_part(rest, 0, length)
      {:ok, {length, content}}
    else
      {:error, :insufficient_data}
    end
  end

  defp parse_ber_length(<<length_of_length, rest::binary>>) when length_of_length >= 128 do
    num_length_bytes = length_of_length - 128

    if num_length_bytes > 0 and num_length_bytes <= 4 and byte_size(rest) >= num_length_bytes do
      <<length_bytes::binary-size(num_length_bytes), remaining::binary>> = rest
      actual_length = :binary.decode_unsigned(length_bytes, :big)

      if byte_size(remaining) >= actual_length do
        content = binary_part(remaining, 0, actual_length)
        {:ok, {actual_length, content}}
      else
        {:error, :insufficient_data}
      end
    else
      {:error, :invalid_length_encoding}
    end
  end

  defp parse_ber_length(_), do: {:error, :invalid_length_format}

  defp parse_snmp_message_fields(data) do
    with {:ok, {version, rest1}} <- parse_integer(data),
         {:ok, {community, rest2}} <- parse_octet_string(rest1),
         {:ok, pdu_data} <- {:ok, rest2} do
      {:ok, {version, community, pdu_data}}
    end
  end

  defp parse_integer(<<@integer, rest::binary>>) do
    case parse_ber_length_and_remaining(rest) do
      {:ok, {_length, value_bytes, remaining}} ->
        if byte_size(value_bytes) > 0 do
          value = decode_integer_value(value_bytes)
          {:ok, {value, remaining}}
        else
          {:error, :invalid_integer_length}
        end

      {:error, reason} ->
        {:error, reason}
    end
  end

  defp parse_integer(_), do: {:error, :invalid_integer}

  defp parse_octet_string(<<@octet_string, rest::binary>>) do
    case parse_ber_length_and_remaining(rest) do
      {:ok, {_length, value_bytes, remaining}} ->
        {:ok, {value_bytes, remaining}}

      {:error, reason} ->
        {:error, reason}
    end
  end

  defp parse_octet_string(_), do: {:error, :invalid_octet_string}

  defp parse_ber_length_and_remaining(<<length, rest::binary>>) when length < 128 do
    if byte_size(rest) >= length do
      content = binary_part(rest, 0, length)
      remaining = binary_part(rest, length, byte_size(rest) - length)
      {:ok, {length, content, remaining}}
    else
      {:error, :insufficient_data}
    end
  end

  defp parse_ber_length_and_remaining(<<length_of_length, rest::binary>>) when length_of_length >= 128 do
    num_length_bytes = length_of_length - 128

    if num_length_bytes > 0 and num_length_bytes <= 4 and byte_size(rest) >= num_length_bytes do
      <<length_bytes::binary-size(num_length_bytes), remaining_with_content::binary>> = rest
      actual_length = :binary.decode_unsigned(length_bytes, :big)

      if byte_size(remaining_with_content) >= actual_length do
        content = binary_part(remaining_with_content, 0, actual_length)

        remaining =
          binary_part(
            remaining_with_content,
            actual_length,
            byte_size(remaining_with_content) - actual_length
          )

        {:ok, {actual_length, content, remaining}}
      else
        {:error, :insufficient_data}
      end
    else
      {:error, :invalid_length_encoding}
    end
  end

  defp parse_ber_length_and_remaining(_), do: {:error, :invalid_length_format}

  defp parse_pdu_comprehensive(<<tag, rest::binary>>) when tag in [0xA0, 0xA1, 0xA2, 0xA3, 0xA5] do
    pdu_type = tag_to_pdu_type(tag)

    case parse_ber_length_and_remaining(rest) do
      {:ok, {_length, pdu_content, _remaining}} ->
        build_pdu_from_content(pdu_type, pdu_content)

      {:error, _reason} ->
        build_empty_pdu(pdu_type)
    end
  end

  defp parse_pdu_comprehensive(_), do: {:error, :invalid_pdu}

  defp tag_to_pdu_type(tag) do
    case tag do
      0xA0 -> :get_request
      0xA1 -> :get_next_request
      0xA2 -> :get_response
      0xA3 -> :set_request
      0xA5 -> :get_bulk_request
    end
  end

  defp build_pdu_from_content(:get_bulk_request, pdu_content) do
    case parse_bulk_pdu_fields(pdu_content) do
      {:ok, {request_id, non_repeaters, max_repetitions, varbinds}} ->
        {:ok,
         %{
           type: :get_bulk_request,
           request_id: request_id,
           non_repeaters: non_repeaters,
           max_repetitions: max_repetitions,
           varbinds: varbinds
         }}

      {:error, _reason} ->
        {:ok, %{type: :get_bulk_request, varbinds: [], non_repeaters: 0, max_repetitions: 0}}
    end
  end

  defp build_pdu_from_content(pdu_type, pdu_content) do
    case parse_standard_pdu_fields(pdu_content) do
      {:ok, {request_id, error_status, error_index, varbinds}} ->
        {:ok,
         %{
           type: pdu_type,
           request_id: request_id,
           error_status: error_status,
           error_index: error_index,
           varbinds: varbinds
         }}

      {:error, _reason} ->
        {:ok, %{type: pdu_type, varbinds: [], error_status: 0, error_index: 0}}
    end
  end

  defp build_empty_pdu(:get_bulk_request) do
    {:ok, %{type: :get_bulk_request, varbinds: [], non_repeaters: 0, max_repetitions: 0}}
  end

  defp build_empty_pdu(pdu_type) do
    {:ok, %{type: pdu_type, varbinds: [], error_status: 0, error_index: 0}}
  end

  defp parse_standard_pdu_fields(data) do
    with {:ok, {request_id, rest1}} <- parse_integer(data),
         {:ok, {error_status, rest2}} <- parse_integer(rest1),
         {:ok, {error_index, rest3}} <- parse_integer(rest2),
         {:ok, varbinds} <- parse_varbinds(rest3) do
      {:ok, {request_id, error_status, error_index, varbinds}}
    end
  end

  defp parse_bulk_pdu_fields(data) do
    with {:ok, {request_id, rest1}} <- parse_integer(data),
         {:ok, {non_repeaters, rest2}} <- parse_integer(rest1),
         {:ok, {max_repetitions, rest3}} <- parse_integer(rest2),
         {:ok, varbinds} <- parse_varbinds(rest3) do
      {:ok, {request_id, non_repeaters, max_repetitions, varbinds}}
    end
  end

  defp parse_varbinds(data) do
    case parse_sequence(data) do
      {:ok, {varbind_data, _rest}} -> parse_varbind_list(varbind_data, [])
      {:error, _} -> {:ok, []}
    end
  end

  defp parse_sequence(<<0x30, rest::binary>>) do
    case parse_ber_length_and_remaining(rest) do
      {:ok, {_length, data, remaining}} ->
        {:ok, {data, remaining}}

      {:error, reason} ->
        {:error, reason}
    end
  end

  defp parse_sequence(_), do: {:error, :not_sequence}

  defp parse_varbind_list(<<>>, acc), do: {:ok, Enum.reverse(acc)}

  defp parse_varbind_list(data, acc) do
    case parse_sequence(data) do
      {:ok, {varbind_data, rest}} ->
        case parse_single_varbind(varbind_data) do
          {:ok, varbind} -> parse_varbind_list(rest, [varbind | acc])
          {:error, _} -> parse_varbind_list(rest, acc)
        end

      {:error, _} ->
        {:ok, Enum.reverse(acc)}
    end
  end

  defp parse_single_varbind(data) do
    with {:ok, {oid, rest1}} <- parse_oid(data),
         {:ok, {type, value, _rest2}} <- parse_value_with_type(rest1) do
      {:ok, {oid, type, value}}
    else
      _ -> {:error, :invalid_varbind}
    end
  end

  defp parse_oid(<<@object_identifier, rest::binary>>) do
    case parse_ber_length_and_remaining(rest) do
      {:ok, {_len, oid_data, rest2}} ->
        case decode_oid_data(oid_data) do
          {:ok, oid} -> {:ok, {oid, rest2}}
          error -> error
        end

      {:error, reason} ->
        {:error, reason}
    end
  end

  defp parse_oid(_), do: {:error, :invalid_oid}

  defp decode_oid_data(<<first, rest::binary>>) do
    first_subid = div(first, 40)
    second_subid = rem(first, 40)

    case decode_oid_subids(rest, [second_subid, first_subid]) do
      {:ok, subids} -> {:ok, Enum.reverse(subids)}
      error -> error
    end
  end

  defp decode_oid_data(_), do: {:error, :invalid_oid_data}

  defp decode_oid_subids(<<>>, acc), do: {:ok, acc}

  defp decode_oid_subids(data, acc) do
    case decode_oid_subid(data, 0) do
      {:ok, {subid, rest}} -> decode_oid_subids(rest, [subid | acc])
      error -> error
    end
  end

  defp decode_oid_subid(<<byte, rest::binary>>, acc) do
    new_acc = (acc <<< 7) + (byte &&& 0x7F)

    if (byte &&& 0x80) == 0 do
      {:ok, {new_acc, rest}}
    else
      decode_oid_subid(rest, new_acc)
    end
  end

  defp decode_oid_subid(<<>>, _), do: {:error, :incomplete_oid}

  defp parse_value_with_type(<<@octet_string, rest::binary>>) do
    case parse_ber_length_and_remaining(rest) do
      {:ok, {_len, value, rest2}} -> {:ok, {:octet_string, value, rest2}}
      {:error, reason} -> {:error, reason}
    end
  end

  defp parse_value_with_type(<<@integer, rest::binary>>) do
    case parse_ber_length_and_remaining(rest) do
      {:ok, {_len, value_bytes, rest2}} ->
        {:ok, {:integer, decode_integer_value(value_bytes), rest2}}

      {:error, reason} ->
        {:error, reason}
    end
  end

  defp parse_value_with_type(<<@null, 0, rest::binary>>) do
    {:ok, {:null, :null, rest}}
  end

  defp parse_value_with_type(<<@object_identifier, rest::binary>>) do
    case parse_ber_length_and_remaining(rest) do
      {:ok, {_len, oid_data, rest2}} ->
        case decode_oid_data(oid_data) do
          {:ok, oid_list} -> {:ok, {:object_identifier, oid_list, rest2}}
          {:error, _} -> {:error, :invalid_oid}
        end

      {:error, reason} ->
        {:error, reason}
    end
  end

  defp parse_value_with_type(<<@counter32, rest::binary>>) do
    case parse_ber_length_and_remaining(rest) do
      {:ok, {_len, value, rest2}} -> {:ok, {:counter32, decode_unsigned_integer(value), rest2}}
      {:error, reason} -> {:error, reason}
    end
  end

  defp parse_value_with_type(<<@gauge32, rest::binary>>) do
    case parse_ber_length_and_remaining(rest) do
      {:ok, {_len, value, rest2}} -> {:ok, {:gauge32, decode_unsigned_integer(value), rest2}}
      {:error, reason} -> {:error, reason}
    end
  end

  defp parse_value_with_type(<<@timeticks, rest::binary>>) do
    case parse_ber_length_and_remaining(rest) do
      {:ok, {_len, value, rest2}} -> {:ok, {:timeticks, decode_unsigned_integer(value), rest2}}
      {:error, reason} -> {:error, reason}
    end
  end

  defp parse_value_with_type(<<@counter64, rest::binary>>) do
    case parse_ber_length_and_remaining(rest) do
      {:ok, {_len, value, rest2}} -> {:ok, {:counter64, decode_counter64(value), rest2}}
      {:error, reason} -> {:error, reason}
    end
  end

  defp parse_value_with_type(<<@ip_address, rest::binary>>) do
    case parse_ber_length_and_remaining(rest) do
      {:ok, {_len, value, rest2}} -> {:ok, {:ip_address, value, rest2}}
      {:error, reason} -> {:error, reason}
    end
  end

  defp parse_value_with_type(<<@opaque_type, rest::binary>>) do
    case parse_ber_length_and_remaining(rest) do
      {:ok, {_len, value, rest2}} -> {:ok, {:opaque, value, rest2}}
      {:error, reason} -> {:error, reason}
    end
  end

  defp parse_value_with_type(<<@no_such_object, 0, rest::binary>>) do
    {:ok, {:no_such_object, nil, rest}}
  end

  defp parse_value_with_type(<<@no_such_instance, 0, rest::binary>>) do
    {:ok, {:no_such_instance, nil, rest}}
  end

  defp parse_value_with_type(<<@end_of_mib_view, 0, rest::binary>>) do
    {:ok, {:end_of_mib_view, nil, rest}}
  end

  defp parse_value_with_type(_), do: {:error, :invalid_value}

  defp decode_integer_value(<<byte>>) when byte < 128, do: byte
  defp decode_integer_value(<<byte>>) when byte >= 128, do: byte - 256

  defp decode_integer_value(data) do
    value = :binary.decode_unsigned(data, :big)
    bit_size = byte_size(data) * 8

    if value >= 1 <<< (bit_size - 1) do
      value - (1 <<< bit_size)
    else
      value
    end
  end

  defp decode_unsigned_integer(data) when byte_size(data) <= 4 do
    :binary.decode_unsigned(data, :big)
  end

  defp decode_unsigned_integer(data) when byte_size(data) == 5 do
    # Handle 5-byte case for large 32-bit unsigned values that require leading zero padding
    case data do
      <<0, rest::binary-size(4)>> ->
        # Leading zero byte for unsigned representation, decode the remaining 4 bytes
        :binary.decode_unsigned(rest, :big)

      _ ->
        # If first byte is not zero, this exceeds 32-bit range
        0
    end
  end

  defp decode_unsigned_integer(_), do: 0

  defp decode_counter64(data) when byte_size(data) <= 8 and byte_size(data) > 0 do
    :binary.decode_unsigned(data, :big)
  end

  defp decode_counter64(_), do: 0
end
