defmodule ToweropsWeb.DeviceLive.Form do
  @moduledoc false
  use ToweropsWeb, :live_view

  alias Towerops.Admin.AuditLogger
  alias Towerops.Agents
  alias Towerops.Devices
  alias Towerops.Devices.Device, as: DeviceSchema
  alias Towerops.Organizations.SubscriptionLimits
  alias Towerops.Repo
  alias Towerops.Sites
  alias Towerops.Workers.DiscoveryWorker
  alias ToweropsWeb.Live.Helpers.AccessControl

  @impl true
  def mount(params, _session, socket) do
    organization = socket.assigns.current_scope.organization
    sites = Sites.list_organization_sites(organization.id)
    agents = Agents.list_organization_agent_tokens(organization.id)

    # Include cloud pollers in the available agents list
    cloud_pollers = Agents.list_cloud_pollers()
    all_agents = agents ++ cloud_pollers

    # Load device quota
    {current_devices, device_limit} =
      SubscriptionLimits.device_quota(organization)

    is_at_limit =
      case device_limit do
        :unlimited -> false
        limit -> current_devices >= limit
      end

    # Sites are now optional - devices can belong directly to organization
    {:ok,
     socket
     |> assign(:organization, organization)
     |> assign(:available_sites, sites)
     |> assign(:available_agents, all_agents)
     |> assign(:preselected_site_id, params["site_id"])
     |> assign(:snmp_test_result, nil)
     |> assign(:duplicate_device, nil)
     |> assign(:non_routable_ip_error, false)
     |> assign(:device_quota, %{
       current: current_devices,
       limit: device_limit,
       at_limit: is_at_limit
     })}
  end

  @impl true
  def handle_params(params, _url, socket) do
    {:noreply, apply_action(socket, socket.assigns.live_action, params)}
  end

  defp apply_action(socket, :new, params) do
    equipment_attrs = %{
      monitoring_enabled: true,
      check_interval_seconds: 300,
      snmp_enabled: true,
      snmp_version: "2c",
      snmp_port: 161
    }

    # Pre-fill from query params if available (e.g., from discovered devices)
    equipment_attrs =
      equipment_attrs
      |> merge_param(:name, params["name"])
      |> merge_param(:ip_address, params["ip_address"])
      |> merge_bool_param(:snmp_enabled, params["snmp_enabled"])

    equipment_attrs =
      cond do
        # Use preselected site from URL params if available
        socket.assigns.preselected_site_id ->
          Map.put(equipment_attrs, :site_id, socket.assigns.preselected_site_id)

        # Auto-select if there's only one site
        length(socket.assigns.available_sites) == 1 ->
          site = List.first(socket.assigns.available_sites)
          Map.put(equipment_attrs, :site_id, site.id)

        # Otherwise, leave it empty for user to select
        true ->
          equipment_attrs
      end

    changeset = Devices.change_device(%DeviceSchema{}, equipment_attrs)

    socket
    |> assign(:page_title, "New Device")
    |> assign(:device, %DeviceSchema{})
    |> assign(:form, to_form(changeset))
    |> assign(:monitoring_mode, "snmp_and_icmp")
  end

  defp apply_action(socket, :edit, %{"id" => id}) do
    organization = socket.assigns.current_scope.organization

    case AccessControl.verify_device_access(id, organization.id) do
      {:ok, device} ->
        # Preload necessary associations for agent resolution
        device = Repo.preload(device, [:organization, site: [organization: :default_agent_token]])
        apply_edit_action(socket, device)

      {:error, :not_found} ->
        socket
        |> put_flash(:error, t_equipment("Device not found"))
        |> push_navigate(to: ~p"/devices")

      {:error, :unauthorized} ->
        socket
        |> put_flash(:error, t_equipment("You don't have access to this device"))
        |> push_navigate(to: ~p"/devices")
    end
  end

  defp apply_edit_action(socket, device) do
    # Get current agent assignment if any
    current_assignment = Agents.get_device_assignment(device.id)

    # Get the effective agent and where it comes from
    {effective_agent_id, agent_source} = Agents.get_effective_agent_token_with_source(device)

    # For the form, we only want the device-specific assignment
    agent_token_id =
      if current_assignment do
        current_assignment.agent_token_id
      end

    # Get agent name for display
    effective_agent_name =
      if effective_agent_id do
        agent = Enum.find(socket.assigns.available_agents, &(&1.id == effective_agent_id))
        agent && agent.name
      end

    # Add agent_token_id to the changeset data
    device_with_agent = Map.put(device, :agent_token_id, agent_token_id)

    # Create changeset and validate to populate form.params for conditional rendering
    changeset =
      device_with_agent
      |> Devices.change_device(%{})
      |> Map.put(:action, :validate)

    # Get effective SNMP configuration and source
    snmp_config = Devices.get_snmp_config(device)

    # Get effective MikroTik configuration and source
    mikrotik_config = Devices.get_mikrotik_config(device)

    # Check if device is MikroTik (based on SNMP discovery)
    device_with_snmp = Repo.preload(device, :snmp_device, force: true)
    is_mikrotik = mikrotik_device?(device_with_snmp)

    # Determine monitoring mode based on current snmp_enabled value
    monitoring_mode = if device.snmp_enabled, do: "snmp_and_icmp", else: "icmp_only"

    socket
    |> assign(:page_title, "Edit Device")
    |> assign(:device, device_with_agent)
    |> assign(:form, to_form(changeset))
    |> assign(:agent_source, agent_source)
    |> assign(:effective_agent_name, effective_agent_name)
    |> assign(:snmp_config_source, snmp_config.source)
    |> assign(:effective_snmp_version, snmp_config.version)
    |> assign(:effective_snmp_community, snmp_config.community || "(not set)")
    |> assign(:mikrotik_config_source, mikrotik_config.source)
    |> assign(:effective_mikrotik_username, mikrotik_config.username || "(not set)")
    |> assign(:is_mikrotik_device, is_mikrotik)
    |> assign(:mikrotik_test_result, nil)
    |> assign(:monitoring_mode, monitoring_mode)
  end

  defp mikrotik_device?(device) do
    # Check if device is MikroTik based on SNMP discovery data
    # Must return boolean (not nil) to work with 'and' operator in templates
    not is_nil(device.snmp_device) and
      (String.contains?(device.snmp_device.manufacturer || "", "MikroTik") ||
         String.contains?(device.snmp_device.sys_descr || "", "RouterOS"))
  end

  @impl true
  def handle_event("switch_monitoring_mode", %{"mode" => mode}, socket) do
    # Update snmp_enabled based on the selected mode
    snmp_enabled = mode == "snmp_and_icmp"

    # Get current form params
    current_params = socket.assigns.form.params

    # Update params with new snmp_enabled value
    updated_params = Map.put(current_params, "snmp_enabled", snmp_enabled)

    # Create new changeset with updated params
    changeset =
      socket.assigns.device
      |> Devices.change_device(updated_params)
      |> Map.put(:action, :validate)

    socket
    |> assign(:monitoring_mode, mode)
    |> assign(:form, to_form(changeset))
    |> then(&{:noreply, &1})
  end

  @impl true
  def handle_event("validate", %{"device" => device_params}, socket) do
    require Logger

    device_params = sanitize_device_params(device_params)

    changeset =
      socket.assigns.device
      |> Devices.change_device(device_params)
      |> Map.put(:action, :validate)

    # Check for duplicate IP address within the same site
    ip_address = device_params["ip_address"]
    site_id = if device_params["site_id"] == "", do: nil, else: device_params["site_id"]
    exclude_id = if socket.assigns.live_action == :edit, do: socket.assigns.device.id

    duplicate_device =
      if ip_address && String.trim(ip_address) != "" && site_id do
        Devices.get_device_by_ip(String.trim(ip_address), site_id, exclude_id)
      end

    # Check for non-routable IP with cloud poller
    non_routable_ip_error = check_non_routable_ip_cloud_error(device_params, socket.assigns)

    {:noreply,
     socket
     |> assign(:form, to_form(changeset))
     |> assign(:duplicate_device, duplicate_device)
     |> assign(:non_routable_ip_error, non_routable_ip_error)}
  end

  @impl true
  def handle_event("save", %{"device" => device_params}, socket) do
    device_params = sanitize_device_params(device_params)

    # Inject organization_id from current context
    organization_id = socket.assigns.organization.id
    device_params = Map.put(device_params, "organization_id", organization_id)

    save_device(socket, socket.assigns.live_action, device_params)
  end

  @impl true
  def handle_event("delete", _params, socket) do
    device = socket.assigns.device

    case Devices.delete_device(device) do
      {:ok, _} ->
        # Log device deletion
        user = socket.assigns.current_scope.user
        AuditLogger.log_device_deleted(nil, user.id, device.id, device.name)

        {:noreply,
         socket
         |> put_flash(:info, t_equipment("Device deleted successfully"))
         |> push_navigate(to: ~p"/devices")}

      {:error, _} ->
        {:noreply, put_flash(socket, :error, t_equipment("Unable to delete device"))}
    end
  end

  @impl true
  def handle_event("test_snmp", _params, socket) do
    require Logger

    changeset = socket.assigns.form.source
    form_data = Ecto.Changeset.apply_changes(changeset)

    # Validate IP address is present
    if is_nil(form_data.ip_address) || to_string(form_data.ip_address) == "" do
      {:noreply,
       assign(socket, :snmp_test_result, %{
         success: false,
         message: "IP address is required to test SNMP connection"
       })}
    else
      # Resolve credentials with inheritance from site/org
      resolved_credentials = resolve_snmp_credentials_for_test(form_data, changeset, socket.assigns)

      device_map = build_device_map_with_credentials(form_data, resolved_credentials)
      effective_agent_id = determine_effective_agent_id(device_map, socket.assigns)

      # Debug logging for SNMPv3 credential testing
      Logger.debug(
        "SNMPv3 test credentials: version=#{device_map["snmp_version"]}, " <>
          "security_level=#{inspect(device_map["snmpv3_security_level"])}, " <>
          "username=#{inspect(device_map["snmpv3_username"])}, " <>
          "auth_protocol=#{inspect(device_map["snmpv3_auth_protocol"])}, " <>
          "auth_password_present=#{!is_nil(device_map["snmpv3_auth_password"])}, " <>
          "priv_protocol=#{inspect(device_map["snmpv3_priv_protocol"])}, " <>
          "priv_password_present=#{!is_nil(device_map["snmpv3_priv_password"])}"
      )

      handle_snmp_test(socket, device_map, effective_agent_id)
    end
  end

  @impl true
  def handle_event("trigger_discovery", _params, socket) do
    device = socket.assigns.device

    if device.snmp_enabled do
      _ = enqueue_discovery(device.id)

      {:noreply,
       socket
       |> put_flash(:info, t_equipment("Discovery started..."))
       |> push_navigate(to: ~p"/devices/#{device.id}")}
    else
      {:noreply, put_flash(socket, :error, t_equipment("SNMP is not enabled for this device"))}
    end
  end

  # Resolve all SNMP credentials with inheritance from site/org
  defp resolve_snmp_credentials_for_test(form_data, changeset, assigns) do
    snmp_version = form_data.snmp_version || "2c"

    if snmp_version == "3" do
      resolve_snmpv3_credentials_for_test(form_data, changeset, assigns)
    else
      resolve_snmp_community_for_test(form_data, changeset, assigns)
    end
  end

  # Resolve SNMPv3 credentials with site/org inheritance
  defp resolve_snmpv3_credentials_for_test(form_data, changeset, assigns) do
    cond do
      # Use device-level credentials if available
      form_data.snmpv3_username && form_data.snmpv3_username != "" ->
        build_device_snmpv3_creds(form_data, changeset.changes)

      # Inherit from site if available
      site_has_snmpv3_creds?(form_data, assigns) ->
        site = find_site(form_data.site_id, assigns.available_sites)
        build_snmpv3_creds(site)

      # Fall back to organization credentials
      assigns.organization.snmpv3_username ->
        build_snmpv3_creds(assigns.organization)

      # No credentials available
      true ->
        %{version: "3"}
    end
  end

  defp site_has_snmpv3_creds?(form_data, assigns) do
    site = find_site(form_data.site_id, assigns.available_sites)
    site && site.snmpv3_username
  end

  defp find_site(nil, _sites), do: nil
  defp find_site(site_id, sites), do: Enum.find(sites, &(&1.id == site_id))

  defp build_device_snmpv3_creds(form_data, changes) do
    %{
      version: "3",
      security_level: form_data.snmpv3_security_level,
      username: form_data.snmpv3_username,
      auth_protocol: form_data.snmpv3_auth_protocol,
      auth_password: Map.get(changes, :snmpv3_auth_password) || form_data.snmpv3_auth_password,
      priv_protocol: form_data.snmpv3_priv_protocol,
      priv_password: Map.get(changes, :snmpv3_priv_password) || form_data.snmpv3_priv_password
    }
  end

  defp build_snmpv3_creds(source) do
    %{
      version: "3",
      security_level: source.snmpv3_security_level,
      username: source.snmpv3_username,
      auth_protocol: source.snmpv3_auth_protocol,
      auth_password: source.snmpv3_auth_password,
      priv_protocol: source.snmpv3_priv_protocol,
      priv_password: source.snmpv3_priv_password
    }
  end

  # Resolve SNMP community with site/org inheritance
  defp resolve_snmp_community_for_test(form_data, changeset, assigns) do
    version = form_data.snmp_version || "2c"
    community = get_inherited_community(form_data, changeset, assigns)

    if community do
      %{version: version, community: community}
    else
      %{version: version}
    end
  end

  defp get_inherited_community(form_data, changeset, assigns) do
    changes = changeset.changes

    # Check if community is explicitly set in form
    if Map.has_key?(changes, :snmp_community) && form_data.snmp_community do
      form_data.snmp_community
    else
      # Try site-level, then org-level community
      site = form_data.site_id && Enum.find(assigns.available_sites, &(&1.id == form_data.site_id))

      if site && site.snmp_community do
        site.snmp_community
      else
        assigns.organization.snmp_community
      end
    end
  end

  defp build_device_map_with_credentials(form_data, credentials) do
    base_map = %{
      "agent_token_id" => Map.get(form_data, :agent_token_id),
      "site_id" => form_data.site_id,
      "ip_address" => form_data.ip_address,
      "snmp_port" => form_data.snmp_port || 161,
      "snmp_version" => credentials.version
    }

    if credentials.version == "3" do
      Map.merge(base_map, %{
        "snmpv3_security_level" => credentials[:security_level],
        "snmpv3_username" => credentials[:username],
        "snmpv3_auth_protocol" => credentials[:auth_protocol],
        "snmpv3_auth_password" => credentials[:auth_password],
        "snmpv3_priv_protocol" => credentials[:priv_protocol],
        "snmpv3_priv_password" => credentials[:priv_password]
      })
    else
      Map.put(base_map, "snmp_community", credentials[:community])
    end
  end

  defp handle_snmp_test(socket, _device_map, nil) do
    {:noreply,
     assign(socket, :snmp_test_result, %{
       success: false,
       message: "No agent available for testing. Please assign an agent or configure organization defaults."
     })}
  end

  defp handle_snmp_test(socket, device_map, effective_agent_id) do
    test_id = Ecto.UUID.generate()

    case send_credential_test_to_agent(effective_agent_id, device_map, test_id) do
      :ok ->
        Phoenix.PubSub.subscribe(Towerops.PubSub, "credential_test:#{test_id}")

        {:noreply,
         socket
         |> assign(:snmp_test_result, %{testing: true})
         |> assign(:test_id, test_id)}

      {:error, reason} ->
        {:noreply,
         assign(socket, :snmp_test_result, %{
           success: false,
           message: "Failed to send test: #{reason}"
         })}
    end
  end

  defp save_device(socket, :new, device_params) do
    # Extract agent_token_id before creating device
    agent_token_id = Map.get(device_params, "agent_token_id")
    device_params = Map.delete(device_params, "agent_token_id")

    # Add snmp_enabled based on monitoring mode
    snmp_enabled = socket.assigns.monitoring_mode == "snmp_and_icmp"
    device_params = Map.put(device_params, "snmp_enabled", snmp_enabled)

    # Check if user is superuser and bypass limits if so
    is_superuser = socket.assigns.current_scope.user.is_superuser
    opts = if is_superuser, do: [bypass_limits: true], else: []

    case Devices.create_device(device_params, opts) do
      {:ok, device} ->
        # Log device creation
        user = socket.assigns.current_scope.user
        AuditLogger.log_device_created(nil, user.id, device.id, device.name)

        # Handle agent assignment after device creation
        handle_agent_assignment(device.id, agent_token_id)

        flash_message = handle_device_creation(device)

        # Reload quota after device creation
        organization = socket.assigns.organization
        {current, limit} = SubscriptionLimits.device_quota(organization)

        is_at_limit =
          case limit do
            :unlimited -> false
            l -> current >= l
          end

        # Reset form for next device while staying on the add page
        fresh_attrs = %{
          monitoring_enabled: true,
          check_interval_seconds: 300,
          snmp_enabled: snmp_enabled,
          snmp_version: "2c",
          snmp_port: 161,
          # Keep the same site selected
          site_id: device.site_id
        }

        fresh_changeset = Devices.change_device(%DeviceSchema{}, fresh_attrs)

        {:noreply,
         socket
         |> put_flash(:info, flash_message)
         |> assign(:form, to_form(fresh_changeset))
         |> assign(:device_quota, %{current: current, limit: limit, at_limit: is_at_limit})
         |> assign(:snmp_test_result, nil)
         |> push_event("scroll_to_top", %{})}

      {:error, %Ecto.Changeset{} = changeset} ->
        {:noreply, assign(socket, :form, to_form(changeset))}
    end
  end

  defp save_device(socket, :edit, device_params) do
    old_device = socket.assigns.device

    # Extract agent_token_id before updating device
    agent_token_id = Map.get(device_params, "agent_token_id")
    device_params = Map.delete(device_params, "agent_token_id")

    # Add snmp_enabled based on monitoring mode
    snmp_enabled = socket.assigns.monitoring_mode == "snmp_and_icmp"
    device_params = Map.put(device_params, "snmp_enabled", snmp_enabled)

    case Devices.update_device(old_device, device_params) do
      {:ok, device} ->
        # Log device update
        user = socket.assigns.current_scope.user
        fields_changed = Map.keys(device_params)
        AuditLogger.log_device_updated(nil, user.id, device.id, fields_changed)

        # device update
        handle_agent_assignment(device.id, agent_token_id)

        flash_message = handle_device_update(old_device, device)

        {:noreply,
         socket
         |> put_flash(:info, flash_message)
         |> push_navigate(to: ~p"/devices/#{device.id}")}

      {:error, %Ecto.Changeset{} = changeset} ->
        {:noreply, assign(socket, :form, to_form(changeset))}
    end
  end

  defp handle_device_creation(device) do
    device_label = device.name || device.ip_address

    base_message =
      if device.snmp_enabled do
        t_equipment("Device created. SNMP discovery started. ")
      else
        t_equipment("Device created. ")
      end

    _ =
      if device.snmp_enabled do
        enqueue_discovery(device.id)
      end

    %{
      text: base_message,
      link_text: device_label,
      link_url: "/devices/#{device.id}"
    }
  end

  defp handle_device_update(old_device, device) do
    if should_trigger_snmp_discovery?(old_device, device) do
      _ = enqueue_discovery(device.id)
      t_equipment("Device updated successfully. SNMP discovery started in background.")
    else
      t_equipment("Device updated successfully")
    end
  end

  # Enqueue discovery job - safe to call in test environment
  defp enqueue_discovery(device_id) do
    if Application.get_env(:towerops, :env) == :test do
      # In test, skip discovery (tests can call Snmp.discover_device directly if needed)
      :ok
    else
      # In dev/prod, enqueue to Oban
      DiscoveryWorker.enqueue(device_id)
    end
  end

  defp should_trigger_snmp_discovery?(old_device, device) do
    device.snmp_enabled and
      (!old_device.snmp_enabled or
         snmp_config_changed?(old_device, device))
  end

  defp snmp_config_changed?(old_device, device) do
    device.snmp_community != old_device.snmp_community or
      device.snmp_version != old_device.snmp_version or
      device.snmp_port != old_device.snmp_port or
      device.ip_address != old_device.ip_address
  end

  defp handle_agent_assignment(device_id, agent_token_id) when is_binary(agent_token_id) do
    # Only assign if agent_token_id is not empty
    if agent_token_id == "" do
      Agents.update_device_assignment(device_id, nil)
    else
      Agents.update_device_assignment(device_id, agent_token_id)
    end
  end

  defp handle_agent_assignment(device_id, nil) do
    # No agent selected, remove any assignment
    Agents.update_device_assignment(device_id, nil)
  end

  defp handle_agent_assignment(_device_id, _), do: :ok

  # Sanitize device params - trim whitespace from IP address and handle device role source
  defp sanitize_device_params(params) do
    params
    |> sanitize_ip_address()
    |> sanitize_device_role()
  end

  defp sanitize_ip_address(params) do
    case Map.get(params, "ip_address") do
      ip when is_binary(ip) ->
        Map.put(params, "ip_address", String.trim(ip))

      _ ->
        params
    end
  end

  # Set device_role_source based on whether a role was manually selected
  defp sanitize_device_role(params) do
    case Map.get(params, "device_role") do
      role when is_binary(role) and role != "" ->
        Map.put(params, "device_role_source", "manual")

      _ ->
        # Auto-detect selected (empty string or nil) - clear role, set source to inferred
        params
        |> Map.put("device_role", nil)
        |> Map.put("device_role_source", "inferred")
    end
  end

  @impl true
  def handle_info({:credential_test_result, result}, socket) do
    require Logger

    Logger.info(
      "LiveView received credential test result: test_id=#{result.test_id} success=#{result.success} system_description=#{inspect(result.system_description)} error=#{inspect(result.error_message)}"
    )

    test_result =
      if result.success do
        %{
          success: true,
          message: "Connection successful! System: #{result.system_description}"
        }
      else
        %{
          success: false,
          message: result.error_message
        }
      end

    Logger.info("Setting snmp_test_result assign: #{inspect(test_result)}")

    {:noreply, assign(socket, :snmp_test_result, test_result)}
  end

  # Determine effective agent ID considering inheritance chain
  defp determine_effective_agent_id(device_params, assigns) do
    # Check device-level agent
    form_agent_id = device_params["agent_token_id"]

    if form_agent_id && form_agent_id != "" do
      form_agent_id
    else
      # Check site-level agent
      site_id = device_params["site_id"]
      site = site_id && Enum.find(assigns.available_sites, &(&1.id == site_id))
      site_agent_id = site && site.agent_token_id

      if site_agent_id do
        site_agent_id
      else
        # Check organization-level agent
        org = assigns.organization
        org_agent_id = org && org.default_agent_token_id

        org_agent_id
      end
    end
  end

  defp send_credential_test_to_agent(agent_token_id, device_map, test_id) do
    require Logger

    snmp_config = build_snmp_test_config(device_map)

    Logger.info("Broadcasting credential test request",
      agent_token_id: agent_token_id,
      test_id: test_id,
      topic: "agent:#{agent_token_id}:credential_test",
      ip: snmp_config[:ip],
      version: snmp_config[:version]
    )

    Phoenix.PubSub.broadcast(
      Towerops.PubSub,
      "agent:#{agent_token_id}:credential_test",
      {:credential_test_requested, test_id, snmp_config}
    )
  end

  defp build_snmp_test_config(device_map) do
    %{
      ip: get_value(device_map, "ip_address", ""),
      port: normalize_port(device_map["snmp_port"]),
      version: get_value(device_map, "snmp_version", "2c"),
      community: get_value(device_map, "snmp_community", ""),
      v3_security_level: get_value(device_map, "snmpv3_security_level", ""),
      v3_username: get_value(device_map, "snmpv3_username", ""),
      v3_auth_protocol: get_value(device_map, "snmpv3_auth_protocol", ""),
      v3_auth_password: get_value(device_map, "snmpv3_auth_password", ""),
      v3_priv_protocol: get_value(device_map, "snmpv3_priv_protocol", ""),
      v3_priv_password: get_value(device_map, "snmpv3_priv_password", "")
    }
  end

  defp get_value(map, key, default), do: map[key] || default

  defp normalize_port(port) when is_binary(port), do: String.to_integer(port)
  defp normalize_port(port) when is_integer(port), do: port
  defp normalize_port(_), do: 161

  # Check if a non-routable IP is being used with cloud poller
  # Skip this check in dev mode or for specific users
  defp check_non_routable_ip_cloud_error(device_params, assigns) do
    # Skip check in dev mode or for specific users
    if skip_non_routable_check?(assigns) do
      false
    else
      ip_address = device_params["ip_address"]

      # Only check if we have a valid IP address
      with true <- is_binary(ip_address) and String.trim(ip_address) != "",
           trimmed_ip = String.trim(ip_address),
           true <- non_routable_ip?(trimmed_ip),
           true <- using_cloud_poller?(device_params, assigns) do
        true
      else
        _ -> false
      end
    end
  end

  defp skip_non_routable_check?(assigns) do
    # Skip in dev mode or for specific users
    Application.get_env(:towerops, :env) == :dev ||
      (assigns[:current_scope] && assigns.current_scope.user &&
         assigns.current_scope.user.email == "graham@mcintire.me")
  end

  # Determine if the device would use cloud poller (no agent at any level)
  defp using_cloud_poller?(device_params, assigns) do
    # Check if agent is directly assigned in form
    form_agent_id = device_params["agent_token_id"]

    if form_agent_id && form_agent_id != "" do
      false
    else
      # Check if site has a default agent
      site_id = device_params["site_id"]
      site = site_id && Enum.find(assigns.available_sites, &(&1.id == site_id))
      site_agent_id = site && site.agent_token_id

      if site_agent_id do
        false
      else
        # Check if organization has a default agent
        org = assigns.organization
        org_agent_id = org && org.default_agent_token_id

        # Cloud poller if no agent at any level
        is_nil(org_agent_id)
      end
    end
  end

  # Merge a query param into attrs if present and non-empty
  defp merge_param(attrs, _key, nil), do: attrs
  defp merge_param(attrs, _key, ""), do: attrs

  defp merge_param(attrs, key, value) when is_binary(value) do
    Map.put(attrs, key, value)
  end

  # Merge a boolean query param into attrs if present
  defp merge_bool_param(attrs, _key, nil), do: attrs
  defp merge_bool_param(attrs, key, "true"), do: Map.put(attrs, key, true)
  defp merge_bool_param(attrs, key, "false"), do: Map.put(attrs, key, false)
  defp merge_bool_param(attrs, _key, _), do: attrs

  # Check if IP is non-routable (RFC1918 private or RFC6598 CGNAT)
  defp non_routable_ip?(ip_string) do
    case ip_string |> String.to_charlist() |> :inet.parse_address() do
      {:ok, {a, b, _c, _d}} -> non_routable_ipv4_range?(a, b)
      # IPv6 or invalid - don't flag as non-routable
      _ -> false
    end
  end

  # 10.0.0.0/8 (RFC1918)
  defp non_routable_ipv4_range?(10, _b), do: true
  # 172.16.0.0/12 (RFC1918)
  defp non_routable_ipv4_range?(172, b) when b >= 16 and b <= 31, do: true
  # 192.168.0.0/16 (RFC1918)
  defp non_routable_ipv4_range?(192, 168), do: true
  # 100.64.0.0/10 (RFC6598 CGNAT)
  defp non_routable_ipv4_range?(100, b) when b >= 64 and b <= 127, do: true
  defp non_routable_ipv4_range?(_a, _b), do: false
end
