defmodule SnmpKit.SnmpLib.HostParser do
  @moduledoc """
  Comprehensive host and port parsing for all possible input formats.

  This module handles parsing of host and port information from any conceivable
  input format and returns exactly what gen_udp needs: an IP tuple and integer port.

  ## Supported Input Formats

  ### IPv4
  - Tuples: `{192, 168, 1, 1}`, `{{192, 168, 1, 1}, 161}`
  - Strings: `"192.168.1.1"`, `"192.168.1.1:161"`
  - Charlists: `'192.168.1.1'`, `'192.168.1.1:161'`
  - Hostnames: `"router.local"`, `"router.local:161"`

  ### IPv6
  - Tuples: `{0x2001, 0xdb8, 0, 0, 0, 0, 0, 1}`, `{{0x2001, 0xdb8, 0, 0, 0, 0, 0, 1}, 161}`
  - Strings: `"2001:db8::1"`, `"[2001:db8::1]:161"`
  - Charlists: `'2001:db8::1'`, `'[2001:db8::1]:161'`
  - Compressed: `"::1"`, `"[::1]:161"`

  ### Mixed Formats
  - Maps: `%{host: "192.168.1.1", port: 161}`
  - Keyword lists: `[host: "192.168.1.1", port: 161]`

  ## Returns

  `{:ok, {ip_tuple, port}}` where:
  - `ip_tuple` is a 4-tuple for IPv4 or 8-tuple for IPv6
  - `port` is an integer between 1-65535

  `{:error, reason}` for invalid input
  """

  require Logger

  @type ip4_tuple :: {0..255, 0..255, 0..255, 0..255}
  @type ip6_tuple ::
          {0..65_535, 0..65_535, 0..65_535, 0..65_535, 0..65_535, 0..65_535, 0..65_535, 0..65_535}
  @type ip_tuple :: ip4_tuple() | ip6_tuple()
  @type port_number :: 1..65_535
  @type parse_result :: {:ok, {ip_tuple(), port_number()}} | {:error, atom()}

  @default_port 161

  @doc """
  Parse host and port from any input format.

  ## Examples

      # IPv4 tuples
      iex> SnmpKit.SnmpLib.HostParser.parse({192, 168, 1, 1})
      {:ok, {{192, 168, 1, 1}, 161}}

      iex> SnmpKit.SnmpLib.HostParser.parse({{192, 168, 1, 1}, 8161})
      {:ok, {{192, 168, 1, 1}, 8161}}

      # IPv4 strings
      iex> SnmpKit.SnmpLib.HostParser.parse("192.168.1.1")
      {:ok, {{192, 168, 1, 1}, 161}}

      iex> SnmpKit.SnmpLib.HostParser.parse("192.168.1.1:8161")
      {:ok, {{192, 168, 1, 1}, 8161}}

      # IPv4 charlists
      iex> SnmpKit.SnmpLib.HostParser.parse('192.168.1.1')
      {:ok, {{192, 168, 1, 1}, 161}}

      iex> SnmpKit.SnmpLib.HostParser.parse('192.168.1.1:8161')
      {:ok, {{192, 168, 1, 1}, 8161}}

      # IPv6 strings
      iex> SnmpKit.SnmpLib.HostParser.parse("::1")
      {:ok, {{0, 0, 0, 0, 0, 0, 0, 1}, 161}}

      iex> SnmpKit.SnmpLib.HostParser.parse("[::1]:8161")
      {:ok, {{0, 0, 0, 0, 0, 0, 0, 1}, 8161}}

      # Error cases
      iex> SnmpKit.SnmpLib.HostParser.parse("invalid")
      {:error, :invalid_host}

      iex> SnmpKit.SnmpLib.HostParser.parse("192.168.1.1:99999")
      {:error, :invalid_port}
  """
  @spec parse(any()) :: parse_result()
  def parse(input, default_port \\ @default_port)

  # IPv4/IPv6 tuple without port
  def parse({a, b, c, d} = ip_tuple, default_port)
      when is_integer(a) and is_integer(b) and is_integer(c) and is_integer(d) do
    case validate_ipv4_tuple(ip_tuple) do
      :ok -> {:ok, {ip_tuple, default_port}}
      error -> error
    end
  end

  # IPv6 tuple without port
  # credo:disable-for-next-line Credo.Check.Refactor.CyclomaticComplexity
  def parse({a, b, c, d, e, f, g, h} = ip_tuple, default_port)
      when is_integer(a) and is_integer(b) and is_integer(c) and is_integer(d) and is_integer(e) and is_integer(f) and
             is_integer(g) and is_integer(h) do
    case validate_ipv6_tuple(ip_tuple) do
      :ok -> {:ok, {ip_tuple, default_port}}
      error -> error
    end
  end

  # Tuple with port: {ip_tuple, port}
  def parse({{a, b, c, d} = ip_tuple, port}, _default_port)
      when is_integer(a) and is_integer(b) and is_integer(c) and is_integer(d) and is_integer(port) do
    with :ok <- validate_ipv4_tuple(ip_tuple),
         :ok <- validate_port(port) do
      {:ok, {ip_tuple, port}}
    end
  end

  # IPv6 tuple with port: {{ip6_tuple}, port}
  def parse({{a, b, c, d, e, f, g, h} = ip_tuple, port}, _default_port)
      when is_integer(a) and is_integer(b) and is_integer(c) and is_integer(d) and is_integer(e) and is_integer(f) and
             is_integer(g) and is_integer(h) and is_integer(port) do
    with :ok <- validate_ipv6_tuple(ip_tuple),
         :ok <- validate_port(port) do
      {:ok, {ip_tuple, port}}
    end
  end

  # String input
  def parse(input, default_port) when is_binary(input) do
    parse_string(input, default_port)
  end

  # Charlist input
  def parse(input, default_port) when is_list(input) do
    # Validate charlist contains only valid ASCII/UTF-8 characters
    if valid_charlist?(input) do
      try do
        string_input = List.to_string(input)
        parse_string(string_input, default_port)
      rescue
        # Not a valid charlist
        _ -> {:error, :invalid_charlist}
      end
    else
      {:error, :invalid_charlist}
    end
  end

  # Map input: %{host: ..., port: ...}
  def parse(%{host: host} = map, default_port) do
    port = Map.get(map, :port, default_port)

    with {:ok, {ip_tuple, _}} <- parse(host, default_port),
         :ok <- validate_port(port) do
      {:ok, {ip_tuple, port}}
    end
  end

  # Keyword list input: [host: ..., port: ...]
  def parse([_ | _] = input, default_port) when is_list(input) do
    if Keyword.keyword?(input) do
      parse_keyword(input, default_port)
    else
      # Try as charlist
      parse_charlist(input, default_port)
    end
  end

  # Atom input (for hostnames like :localhost)
  def parse(input, default_port) when is_atom(input) do
    parse_string(Atom.to_string(input), default_port)
  end

  # Invalid input
  def parse(_input, _default_port) do
    {:error, :unsupported_format}
  end

  defp parse_keyword(input, default_port) do
    host = Keyword.get(input, :host)
    port = Keyword.get(input, :port, default_port)

    if host do
      with {:ok, {ip_tuple, _}} <- parse(host, default_port),
           :ok <- validate_port(port) do
        {:ok, {ip_tuple, port}}
      end
    else
      {:error, :missing_host}
    end
  end

  # Private helper functions

  defp parse_string(input, default_port) do
    input = String.trim(input)

    cond do
      # IPv6 with port: [::1]:8161
      String.starts_with?(input, "[") and String.contains?(input, "]:") ->
        parse_ipv6_with_port(input)

      # IPv4 with port: 192.168.1.1:8161
      String.contains?(input, ":") and not String.contains?(input, "::") ->
        parse_ipv4_with_port(input, default_port)

      # IPv6 without port or IPv4 without port
      true ->
        parse_ip_without_port(input, default_port)
    end
  end

  defp parse_charlist(input, default_port) do
    string_input = List.to_string(input)
    parse_string(string_input, default_port)
  rescue
    _ -> {:error, :invalid_charlist}
  end

  defp parse_ipv6_with_port(input) do
    # Format: [2001:db8::1]:8161
    case Regex.run(~r/^\[([^\]]+)\]:(\d+)$/, input) do
      [_full, ipv6_str, port_str] ->
        with {:ok, ip_tuple} <- parse_ipv6_address(ipv6_str),
             {port, ""} <- Integer.parse(port_str),
             :ok <- validate_port(port) do
          {:ok, {ip_tuple, port}}
        else
          _ -> {:error, :invalid_ipv6_with_port}
        end

      nil ->
        {:error, :invalid_ipv6_with_port}
    end
  end

  defp parse_ipv4_with_port(input, default_port) do
    # Handle multiple colons (might be IPv6)
    parts = String.split(input, ":")

    cond do
      length(parts) == 2 ->
        # Likely IPv4:port
        [host_part, port_part] = parts
        parse_host_with_port(host_part, port_part, input, default_port)

      length(parts) > 2 ->
        # Definitely IPv6
        parse_ip_without_port(input, default_port)

      true ->
        {:error, :invalid_format}
    end
  end

  defp parse_host_with_port(host_part, port_part, input, default_port) do
    case Integer.parse(port_part) do
      {port, ""} ->
        with {:ok, ip_tuple} <- parse_ipv4_address(host_part),
             :ok <- validate_port(port) do
          {:ok, {ip_tuple, port}}
        end

      _ ->
        # Port part is not a number, might be IPv6
        parse_ip_without_port(input, default_port)
    end
  end

  defp parse_ip_without_port(input, default_port) do
    parser =
      cond do
        String.contains?(input, ".") -> &parse_ipv4_address/1
        String.contains?(input, ":") -> &parse_ipv6_address/1
        true -> &resolve_hostname/1
      end

    case parser.(input) do
      {:ok, ip_tuple} -> {:ok, {ip_tuple, default_port}}
      error -> error
    end
  end

  defp parse_ipv4_address(input) do
    charlist_input = String.to_charlist(input)

    case :inet.parse_address(charlist_input) do
      {:ok, ip_tuple} ->
        case validate_ipv4_tuple(ip_tuple) do
          :ok -> {:ok, ip_tuple}
          error -> error
        end

      {:error, _} ->
        {:error, :invalid_ipv4}
    end
  end

  defp parse_ipv6_address(input) do
    charlist_input = String.to_charlist(input)

    case :inet.parse_address(charlist_input) do
      {:ok, ip_tuple} ->
        case validate_ipv6_tuple(ip_tuple) do
          :ok -> {:ok, ip_tuple}
          error -> error
        end

      {:error, _} ->
        {:error, :invalid_ipv6}
    end
  end

  defp resolve_hostname(hostname) do
    charlist_hostname = String.to_charlist(hostname)

    case :inet.gethostbyname(charlist_hostname) do
      {:ok, {:hostent, _name, _aliases, :inet, 4, [ip_tuple | _]}} ->
        {:ok, ip_tuple}

      {:ok, {:hostent, _name, _aliases, :inet6, 16, [ip_tuple | _]}} ->
        {:ok, ip_tuple}

      {:error, _reason} ->
        {:error, :hostname_resolution_failed}
    end
  end

  defp validate_ipv4_tuple({a, b, c, d}) do
    if valid_ipv4_octet?(a) and valid_ipv4_octet?(b) and
         valid_ipv4_octet?(c) and valid_ipv4_octet?(d) do
      :ok
    else
      {:error, :invalid_ipv4_tuple}
    end
  end

  defp validate_ipv4_tuple(_), do: {:error, :invalid_ipv4_tuple}

  defp valid_ipv4_octet?(n), do: is_integer(n) and n >= 0 and n <= 255

  defp validate_ipv6_tuple({a, b, c, d, e, f, g, h}) do
    octets = [a, b, c, d, e, f, g, h]

    if Enum.all?(octets, &valid_ipv6_segment?/1) do
      :ok
    else
      {:error, :invalid_ipv6_tuple}
    end
  end

  defp validate_ipv6_tuple(_), do: {:error, :invalid_ipv6_tuple}

  defp valid_ipv6_segment?(n), do: is_integer(n) and n >= 0 and n <= 65_535

  defp validate_port(port) when is_integer(port) and port >= 1 and port <= 65_535, do: :ok
  defp validate_port(_), do: {:error, :invalid_port}

  # Validate charlist contains only valid characters (0-255 for bytes)
  defp valid_charlist?(charlist) do
    # Additional check: ensure it would create valid UTF-8 when converted to string
    Enum.all?(charlist, fn char ->
      is_integer(char) and char >= 0 and char <= 255
    end) and
      try do
        _string = List.to_string(charlist)
        true
      rescue
        _ -> false
      end
  end

  @doc """
  Quick validation function to check if input can be parsed.

  ## Examples

      iex> SnmpKit.SnmpLib.HostParser.valid?("192.168.1.1")
      true

      iex> SnmpKit.SnmpLib.HostParser.valid?("invalid")
      false
  """
  @spec valid?(any()) :: boolean()
  def valid?(input) do
    case parse(input) do
      {:ok, _} -> true
      {:error, _} -> false
    end
  end

  @doc """
  Parse and return only the IP tuple, using default port.

  ## Examples

      iex> SnmpKit.SnmpLib.HostParser.parse_ip("192.168.1.1")
      {:ok, {192, 168, 1, 1}}
  """
  @spec parse_ip(any()) :: {:ok, ip_tuple()} | {:error, atom()}
  def parse_ip(input) do
    case parse(input) do
      {:ok, {ip_tuple, _port}} -> {:ok, ip_tuple}
      error -> error
    end
  end

  @doc """
  Parse and return only the port, using 161 as default.

  ## Examples

      iex> SnmpKit.SnmpLib.HostParser.parse_port("192.168.1.1:8161")
      {:ok, 8161}

      iex> SnmpKit.SnmpLib.HostParser.parse_port("192.168.1.1")
      {:ok, 161}
  """
  @spec parse_port(any()) :: {:ok, port_number()} | {:error, atom()}
  def parse_port(input) do
    case parse(input) do
      {:ok, {_ip_tuple, port}} -> {:ok, port}
      error -> error
    end
  end

  @doc """
  Format an IP tuple and port back to string representation.

  ## Examples

      iex> SnmpKit.SnmpLib.HostParser.format({{192, 168, 1, 1}, 161})
      "192.168.1.1:161"

      iex> SnmpKit.SnmpLib.HostParser.format({{0, 0, 0, 0, 0, 0, 0, 1}, 161})
      "[::1]:161"
  """
  @spec format({ip_tuple(), port_number()}) :: String.t()
  def format({{a, b, c, d}, port}) do
    "#{a}.#{b}.#{c}.#{d}:#{port}"
  end

  def format({{a, b, c, d, e, f, g, h}, port}) do
    ip_str = {a, b, c, d, e, f, g, h} |> :inet.ntoa() |> List.to_string()
    "[#{ip_str}]:#{port}"
  end
end
