defmodule SnmpKit.SnmpMgr.Target do
  @moduledoc """
  Target parsing and validation for SNMP requests.

  Handles parsing of various target formats and resolves hostnames to IP addresses.
  """

  @default_port 161

  @doc """
  Parses a target string into a structured format.

  ## Examples

      iex> SnmpKit.SnmpMgr.Target.parse("192.168.1.1:161")
      {:ok, %{host: {192, 168, 1, 1}, port: 161}}

      iex> SnmpKit.SnmpMgr.Target.parse("device.local")
      {:ok, %{host: "device.local", port: 161}}

      iex> SnmpKit.SnmpMgr.Target.parse("192.168.1.1")
      {:ok, %{host: {192, 168, 1, 1}, port: 161}}
  """
  def parse(target) when is_binary(target) do
    case String.split(target, ":") do
      [host] ->
        parse_host_and_port(host, @default_port)

      [host, port_str] ->
        case Integer.parse(port_str) do
          {port, ""} when port > 0 and port <= 65_535 ->
            parse_host_and_port(host, port)

          _ ->
            {:error, {:invalid_port, port_str}}
        end

      _ ->
        {:error, {:invalid_target_format, target}}
    end
  end

  def parse(target) when is_tuple(target) and tuple_size(target) == 4 do
    # Already an IP tuple
    {:ok, %{host: target, port: @default_port}}
  end

  def parse(%{host: _host, port: _port} = target) do
    # Already parsed
    {:ok, target}
  end

  def parse(_target) do
    {:error, :invalid_target_format}
  end

  @doc """
  Resolves a target to a parsed struct, with fallback for unparseable targets.

  Unlike `parse/1` which returns `{:ok, target}` or `{:error, reason}`,
  this function always returns a target struct, using the raw input as
  the host with default port 161 if parsing fails.

  This is the canonical function for target resolution across all modules.

  ## Examples

      iex> SnmpKit.SnmpMgr.Target.resolve("192.168.1.1:161")
      %{host: {192, 168, 1, 1}, port: 161}

      iex> SnmpKit.SnmpMgr.Target.resolve("device.local")
      %{host: "device.local", port: 161}

      iex> SnmpKit.SnmpMgr.Target.resolve(%{host: {127, 0, 0, 1}, port: 162})
      %{host: {127, 0, 0, 1}, port: 162}
  """
  def resolve(target) when is_binary(target) do
    case parse(target) do
      {:ok, parsed} -> parsed
      {:error, _} -> %{host: target, port: @default_port}
    end
  end

  def resolve(%{host: _, port: _} = target), do: target

  def resolve(target) when is_tuple(target) and tuple_size(target) == 4 do
    %{host: target, port: @default_port}
  end

  def resolve(target), do: %{host: target, port: @default_port}

  @doc """
  Resolves a hostname to an IP address if needed.

  If the host is already an IP tuple, returns it unchanged.
  """
  def resolve_hostname(%{host: host, port: _port} = target) when is_tuple(host) do
    {:ok, target}
  end

  def resolve_hostname(%{host: hostname, port: port}) when is_binary(hostname) do
    case parse_ip_address(hostname) do
      {:ok, ip_tuple} ->
        {:ok, %{host: ip_tuple, port: port}}

      :error ->
        case :inet.gethostbyname(String.to_charlist(hostname)) do
          {:ok, {:hostent, _name, _aliases, :inet, 4, [ip_tuple | _]}} ->
            {:ok, %{host: ip_tuple, port: port}}

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

  @doc """
  Validates that a target is reachable (basic connectivity check).
  """
  def validate_connectivity(%{host: host, port: port}, timeout \\ 5000) do
    case :gen_tcp.connect(host, port, [], timeout) do
      {:ok, socket} ->
        :gen_tcp.close(socket)
        :ok

      {:error, :econnrefused} ->
        # This is actually good - the port responded (even if it refused)
        :ok

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

  # Private functions

  defp parse_host_and_port(host, port) do
    case parse_ip_address(host) do
      {:ok, ip_tuple} ->
        {:ok, %{host: ip_tuple, port: port}}

      :error ->
        # Assume it's a hostname
        {:ok, %{host: host, port: port}}
    end
  end

  defp parse_ip_address(ip_string) do
    case :inet.parse_address(String.to_charlist(ip_string)) do
      {:ok, ip_tuple} -> {:ok, ip_tuple}
      {:error, _} -> :error
    end
  end
end
