defmodule Tailscale.Native do
  use Rustler,
    otp_app: :tailscale,
    crate: :ts_elixir

  @moduledoc false

  # The Elixir side of the Rustler bindings to `tailscale-rs`.
  #
  # The rest of this package adapts these bindings to a more Elixir-friendly module layout -- this is
  # where Rustler actually connects the Rust nifs to their Elixir names, so it's a flat module.
  #
  # Consider this module an internal implementation detail: we may break its API at our convenience
  # without a semver bump.

  @typedoc """
  A handle to a unique tailscale "identity" on a given tailnet.
  """
  @opaque device :: reference()

  @typedoc """
  A handle to a UDP socket.
  """
  @opaque udp_socket :: reference()

  @typedoc """
  A handle to a TCP listener.
  """
  @opaque tcp_listener :: reference()
  @typedoc """
  A handle to a TCP stream (connected socket).
  """
  @opaque tcp_stream :: reference()

  @typedoc """
  A handle to a running SOCKS5 loopback proxy. Dropping it (via `loopback_stop/1` or GC) stops the
  proxy listener.
  """
  @opaque loopback_handle :: reference()

  defp err, do: :erlang.nif_error(:nif_not_loaded)

  @doc """
  Open a new tailnet connection.

  See `t:Tailscale.options/0` for details on what options are supported.
  """
  @spec connect(%{}) :: {:ok, device()} | {:error, any()}
  def connect(_opts), do: err()

  @doc """
  Bind a new udp socket.

  ## Parameters

  - `dev`: the `m:Tailscale` device on which to create the socket.
  - `port`: the port to which the socket should bind.
  """
  @spec udp_bind(device(), Tailscale.ip_addr() | :ip4 | :ip6, :inet.port_number()) ::
          {:ok, udp_socket()} | {:error, any()}
  def udp_bind(_dev, _addr, _port), do: err()

  @doc """
  Send a packet to an address from a udp socket.

  ## Parameters

  - `sock`: the socket to send the packet from.
  - `ip`: the IP address to send the packet to.
  - `port`: the port to send the packet to.
  - `msg`: the packet to send.
  """
  @spec udp_send(udp_socket(), Tailscale.ip_addr(), :inet.port_number(), binary()) ::
          :ok | {:error, any()}
  def udp_send(_sock, _ip, _port, _msg), do: err()

  @doc """
  Receive an incoming UDP packet on the given socket.
  """
  @spec udp_recv(udp_socket()) ::
          {:ok, :inet.ip_address(), :inet.port_number(), binary()} | {:error, any()}
  def udp_recv(_sock), do: err()

  @doc """
  Get the local address to which the given UDP socket is bound.
  """
  @spec udp_local_addr(udp_socket()) :: {:inet.ip_address(), :inet.port_number()}
  def udp_local_addr(_sock), do: err()

  @doc """
  Start the Rust-side tracing machinery. This prints to stdout, so may conflict with erlang's
  logging setup.
  """
  @spec start_tracing() :: :ok
  def start_tracing(), do: err()

  @doc """
  Start a TCP listener on the given device, address, and port.
  """
  @spec tcp_listen(device(), Tailscale.ip_addr() | :ip4 | :ip6, :inet.port_number()) ::
          {:ok, tcp_listener()} | {:error, any()}
  def tcp_listen(_dev, _addr, _port), do: err()

  @doc """
  Get the local address to which the given TCP listener is bound.
  """
  @spec tcp_listen_local_addr(tcp_listener()) :: {:inet.ip_address(), :inet.port_number()}
  def tcp_listen_local_addr(_listener), do: err()

  @doc """
  Connect to the given TCP endpoint using the given device.
  """
  @spec tcp_connect(device(), Tailscale.ip_addr(), :inet.port_number()) ::
          {:ok, tcp_stream()} | {:error, any()}
  def tcp_connect(_dev, _addr, _port), do: err()

  @doc """
  Accept an incoming TCP connection. Blocks until one is available.
  """
  @spec tcp_accept(tcp_listener()) :: {:ok, tcp_stream()} | {:error, any()}
  def tcp_accept(_listener), do: err()

  @doc """
  Send a message to the remote peer on the given tcp socket, blocking until at least one byte can be
  sent.

  Returns the number of bytes actually written to the remote.
  """
  @spec tcp_send(tcp_stream(), binary()) :: {:ok, integer()} | {:error, any()}
  def tcp_send(_stream, _msg), do: err()

  @doc """
  Receive incoming data from the tcp socket, blocking until at least one byte can be received.
  """
  @spec tcp_recv(tcp_stream()) :: {:ok, binary()} | {:error, any()}
  def tcp_recv(_stream), do: err()

  @doc """
  Get the local address to which the given TCP stream is bound.
  """
  @spec tcp_local_addr(tcp_stream()) :: {:inet.ip_address(), :inet.port_number()}
  def tcp_local_addr(_stream), do: err()

  @doc """
  Get the remote address to which the given TCP stream is connected.
  """
  @spec tcp_remote_addr(tcp_stream()) :: {:inet.ip_address(), :inet.port_number()}
  def tcp_remote_addr(_stream), do: err()

  @doc """
  Retrieve the IPv4 address for the given tailscale device.

  Blocks until the device is connected and gets its address from control.
  """
  @spec ipv4_addr(device()) :: {:ok, :inet.ip4_address()} | {:error, any()}
  def ipv4_addr(_dev), do: err()

  @doc """
  Retrieve the IPv6 address for the given tailscale device.

  Blocks until the device is connected and gets its address from control.
  """
  @spec ipv6_addr(device()) :: {:ok, :inet.ip6_address()} | {:error, any()}
  def ipv6_addr(_dev), do: err()

  @doc """
  Retrieve a peer by name.
  """
  @spec peer_by_name(device(), String.t()) :: {:ok, %{} | nil} | {:error, any()}
  def peer_by_name(_dev, _name), do: err()

  @doc """
  Retrieve this node's info
  """
  @spec self_node(device()) :: {:ok, %{}} | {:error, any()}
  def self_node(_dev), do: err()

  @doc """
  Retrieve a peer by its tailnet IP.
  """
  @spec peer_by_tailnet_ip(device(), Tailscale.ip_addr()) :: {:ok, %{} | nil} | {:error, any()}
  def peer_by_tailnet_ip(_dev, _ip), do: err()

  @doc """
  Retrieve the most narrow set of peers that accept packets for the specified IP.
  """
  @spec peers_with_route(device(), Tailscale.ip_addr()) :: {:ok, [%{}]} | {:error, any()}
  def peers_with_route(_dev, _ip), do: err()

  @doc """
  Load key state from the specified path, generating a new state if the file doesn't exist.
  """
  @spec load_key_file(String.t()) :: {:ok, Tailscale.Keystate.t()} | {:error, any()}
  def load_key_file(_path), do: err()

  @doc """
  Snapshot this device and its tailnet peers (like `tailscale status`).
  """
  @spec status(device()) :: {:ok, Tailscale.Status.t()} | {:error, any()}
  def status(_dev), do: err()

  @doc """
  Map a tailnet source `{ip, port}` to the node that owns its IP (like `tsnet`'s `WhoIs`).
  Only the IP is used; the port is ignored.
  """
  @spec whois(device(), {Tailscale.ip_addr(), :inet.port_number()}) ::
          {:ok, Tailscale.WhoIs.t() | nil} | {:error, any()}
  def whois(_dev, _sockaddr), do: err()

  @doc """
  Snapshot the current netmap: the current set of peer `t:Tailscale.StatusNode.t/0`s.
  """
  @spec netmap(device()) :: {:ok, [Tailscale.StatusNode.t()]} | {:error, any()}
  def netmap(_dev), do: err()

  @doc """
  Resolve a tailnet peer (or this node) by MagicDNS name to its tailnet IPv4 address.

  Returns `{:ok, ip}` on a match, `{:ok, nil}` if no tailnet node has that name.
  """
  @spec resolve(device(), String.t()) ::
          {:ok, :inet.ip4_address() | nil} | {:error, any()}
  def resolve(_dev, _name), do: err()

  @doc """
  Connect to a tailnet peer by MagicDNS name and port over TCP.
  """
  @spec tcp_connect_by_name(device(), String.t(), :inet.port_number()) ::
          {:ok, tcp_stream()} | {:error, any()}
  def tcp_connect_by_name(_dev, _name, _port), do: err()

  @doc """
  Ping a tailnet peer over the overlay, returning the round-trip time in milliseconds.
  """
  @spec ping(device(), Tailscale.ip_addr(), non_neg_integer()) ::
          {:ok, float()} | {:error, any()}
  def ping(_dev, _addr, _timeout_ms), do: err()

  @doc """
  Obtain a TLS certificate for a node's MagicDNS `name` (fail-closed until ACME lands).
  """
  @spec get_certificate(device(), String.t()) :: {:ok, :ok} | {:error, any()}
  def get_certificate(_dev, _name), do: err()

  @doc """
  Build a TLS acceptor terminating TLS for a serve config (fail-closed until ACME lands).

  The config is a `{name, port, target}` tuple where `target` is `:accept` or `{:proxy, "host:port"}`.
  """
  @spec listen_tls(device(), {String.t(), :inet.port_number(), :accept | {:proxy, String.t()}}) ::
          {:ok, :ok} | {:error, any()}
  def listen_tls(_dev, _config), do: err()

  @doc """
  Expose a tailnet TLS service to the public internet via Tailscale Funnel (fail-closed until
  public-ingress relays + ACME land).

  The config is the same `{name, port, target}` tuple as `listen_tls/2`. `funnel_only` rejects
  tailnet-internal connections when `true`.
  """
  @spec listen_funnel(
          device(),
          {String.t(), :inet.port_number(), :accept | {:proxy, String.t()}},
          boolean()
        ) :: {:ok, :ok} | {:error, any()}
  def listen_funnel(_dev, _config, _funnel_only), do: err()

  @doc """
  Host a Tailscale VIP service (`svc:<label>`) on its control-assigned VIP.

  `mode` is a `{:tcp, port}` or `{:http, port}` tuple. Fail-closed: an untagged host or a missing
  control-assigned VIP returns `{:error, reason}` before any listener is bound.
  """
  @spec listen_service(device(), String.t(), {:tcp | :http, :inet.port_number()}) ::
          {:ok, :ok} | {:error, any()}
  def listen_service(_dev, _name, _mode), do: err()

  @doc """
  Request an OIDC ID token (a signed JWT) for this node, scoped to `audience`.
  """
  @spec fetch_id_token(device(), String.t()) :: {:ok, String.t()} | {:error, any()}
  def fetch_id_token(_dev, _audience), do: err()

  @doc """
  Snapshot this process's client metrics in Prometheus text exposition format.
  """
  @spec metrics(device()) :: String.t()
  def metrics(_dev), do: err()

  @doc """
  This node's key-expiry instant as Unix seconds, or `{:ok, nil}` if the key never expires.
  """
  @spec self_key_expiry_unix(device()) :: {:ok, integer() | nil} | {:error, any()}
  def self_key_expiry_unix(_dev), do: err()

  @doc """
  Whether this node's key has expired as of now.
  """
  @spec self_key_expired(device()) :: {:ok, boolean()} | {:error, any()}
  def self_key_expired(_dev), do: err()

  @doc """
  List the Taildrop files this device has fully received and not yet consumed.
  """
  @spec taildrop_waiting_files(device()) ::
          {:ok, [Tailscale.WaitingFile.t()]} | {:error, any()}
  def taildrop_waiting_files(_dev), do: err()

  @doc """
  Delete a received Taildrop file by name.
  """
  @spec taildrop_delete_file(device(), String.t()) :: {:ok, :ok} | {:error, any()}
  def taildrop_delete_file(_dev, _name), do: err()

  @doc """
  Save a received Taildrop file to `dst_path` by copying it there. Returns the bytes copied.
  """
  @spec taildrop_save_file(device(), String.t(), String.t()) ::
          {:ok, non_neg_integer()} | {:error, any()}
  def taildrop_save_file(_dev, _name, _dst_path), do: err()

  @doc """
  Send a local file at `src_path` to `peer_name` via Taildrop, naming it `file_name` on the peer.
  """
  @spec taildrop_send_file(device(), String.t(), String.t(), String.t()) ::
          {:ok, :ok} | {:error, any()}
  def taildrop_send_file(_dev, _peer_name, _file_name, _src_path), do: err()

  @doc """
  Begin a debug packet capture, writing a pcap of every dataplane packet to `dst_path`.
  """
  @spec capture_pcap(device(), String.t()) :: {:ok, :ok} | {:error, any()}
  def capture_pcap(_dev, _dst_path), do: err()

  @doc """
  Stop a debug packet capture started by `capture_pcap/2`. Idempotent.
  """
  @spec stop_capture(device()) :: {:ok, :ok} | {:error, any()}
  def stop_capture(_dev), do: err()

  @doc """
  Start the SOCKS5 loopback proxy, returning `{:ok, {addr, cred, handle}}`.
  """
  @spec loopback(device()) ::
          {:ok, {{:inet.ip_address(), :inet.port_number()}, String.t(), loopback_handle()}}
          | {:error, any()}
  def loopback(_dev), do: err()

  @doc """
  Stop a loopback proxy by dropping its handle. Idempotent.
  """
  @spec loopback_stop(loopback_handle()) :: :ok
  def loopback_stop(_handle), do: err()

  @doc """
  Fetch the current Tailnet Lock (TKA) status, or `{:ok, nil}` if control has sent none.
  """
  @spec tka_status(device()) :: {:ok, Tailscale.TkaStatus.t() | nil} | {:error, any()}
  def tka_status(_dev), do: err()

  @doc """
  Rotate the node key in a keystate for embedder-driven re-registration.
  """
  @spec rotate_node_key(Tailscale.Keystate.t()) ::
          {:ok, Tailscale.Keystate.t()} | {:error, any()}
  def rotate_node_key(_keys), do: err()
end
