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

  alias Towerops.Agents
  alias Towerops.Devices
  alias Towerops.Devices.Firmware
  alias Towerops.Devices.MikrotikBackups
  alias Towerops.Devices.VersionComparator
  alias Towerops.Gaiia
  alias Towerops.Monitoring
  alias Towerops.Repo
  alias Towerops.Snmp
  alias Towerops.Workers.DiscoveryWorker
  alias ToweropsWeb.CheckLive.FormComponent
  alias ToweropsWeb.Live.Helpers.AccessControl

  # SNMP interface type to category mappings
  @interface_type_categories %{
    6 => "Ethernet",
    24 => "Loopback",
    23 => "PPP",
    108 => "PPPoE",
    131 => "Tunnel",
    135 => "VLAN",
    136 => "VLAN",
    161 => "IEEE 802.11",
    244 => "WWP"
  }

  # Display order for interface categories
  @interface_category_order %{
    "Ethernet" => 1,
    "VLAN" => 2,
    "IEEE 802.11" => 3,
    "PPPoE" => 4,
    "PPP" => 5,
    "Tunnel" => 6,
    "Loopback" => 7,
    "WWP" => 8,
    "Other" => 99
  }

  # Check type to group key mapping for organizing checks by type
  @check_type_groups %{
    "snmp_sensor" => :snmp_sensors,
    "snmp_interface" => :snmp_interfaces,
    "snmp_processor" => :snmp_processors,
    "snmp_storage" => :snmp_storage,
    "http" => :http_checks,
    "tcp" => :tcp_checks,
    "dns" => :dns_checks,
    "ping" => :ping_checks
  }

  @impl true
  def mount(_params, _session, socket) do
    {:ok, assign(socket, :show_check_form, false)}
  end

  @impl true
  def handle_params(%{"id" => id} = params, _, socket) do
    organization = socket.assigns.current_scope.organization

    # Check if device exists and verify organization access
    case AccessControl.verify_device_access(id, organization.id) do
      {:ok, _device} ->
        maybe_subscribe_and_schedule_refresh(socket, id)

        # If no tab parameter, redirect to add it to the URL
        if Map.has_key?(params, "tab") do
          tab = params["tab"]
          page = params |> Map.get("page", "1") |> String.to_integer()

          socket =
            socket
            |> load_equipment_data(id)
            |> assign(:active_tab, tab)
            |> apply_backups_pagination(tab, page)

          {:noreply, socket}
        else
          # Redirect to add default tab to URL
          {:noreply, push_patch(socket, to: ~p"/devices/#{id}?tab=overview", replace: true)}
        end

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

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

  @impl true
  def handle_info(:refresh_data, socket) do
    # Check if device still exists before scheduling next refresh
    case Devices.get_device(socket.assigns.device.id) do
      nil ->
        # Device was deleted, don't schedule another refresh
        {:noreply,
         socket
         |> put_flash(:error, t_equipment("Device no longer exists"))
         |> push_navigate(to: ~p"/devices")}

      _device ->
        Process.send_after(self(), :refresh_data, 10_000)
        {:noreply, reload_active_tab_data(socket)}
    end
  end

  @impl true
  def handle_info({:device_status_changed, _device_id, _new_status, _response_time}, socket) do
    # Device status shows on all tabs in the header - reload base data only
    {:noreply, reload_base_data(socket)}
  end

  @impl true
  def handle_info({:discovery_completed, _device_id}, socket) do
    # Structural change - full reload needed
    {:noreply,
     socket
     |> load_equipment_data(socket.assigns.device.id)
     |> put_flash(:info, t_equipment("Discovery completed"))}
  end

  @impl true
  def handle_info({:sensors_updated, _device_id}, socket) do
    {:noreply, reload_if_tab(socket, ["overview"])}
  end

  @impl true
  def handle_info({:interfaces_updated, _device_id}, socket) do
    {:noreply, reload_if_tab(socket, ["overview", "ports"])}
  end

  @impl true
  def handle_info({:neighbors_updated, _device_id}, socket) do
    {:noreply, reload_if_tab(socket, ["neighbors"])}
  end

  @impl true
  def handle_info({:arp_updated, _device_id}, socket) do
    {:noreply, reload_if_tab(socket, ["arp"])}
  end

  @impl true
  def handle_info({:mac_updated, _device_id}, socket) do
    {:noreply, reload_if_tab(socket, ["mac"])}
  end

  @impl true
  def handle_info({:state_sensors_updated, _device_id}, socket) do
    {:noreply, reload_if_tab(socket, ["overview"])}
  end

  @impl true
  def handle_info({:processors_updated, _device_id}, socket) do
    {:noreply, reload_if_tab(socket, ["overview"])}
  end

  @impl true
  def handle_info({:storage_updated, _device_id}, socket) do
    {:noreply, reload_if_tab(socket, ["overview"])}
  end

  @impl true
  def handle_info({:monitoring_check_updated, _device_id}, socket) do
    {:noreply, reload_if_tab(socket, ["overview"])}
  end

  @impl true
  def handle_info({:device_event, _event_attrs}, socket) do
    # Device event logged (interface changes, sensor thresholds, etc.)
    {:noreply, reload_if_tab(socket, ["logs"])}
  end

  @impl true
  def handle_info({:agent_connected, agent_token_id, _organization_id}, socket) do
    # Reload agent info if this device's agent just connected
    if socket.assigns.agent_info.agent_token_id == agent_token_id do
      {:noreply, reload_base_data(socket)}
    else
      {:noreply, socket}
    end
  end

  @impl true
  def handle_info({:agent_disconnected, agent_token_id, _organization_id}, socket) do
    # Reload agent info if this device's agent just disconnected
    if socket.assigns.agent_info.agent_token_id == agent_token_id do
      {:noreply, reload_base_data(socket)}
    else
      {:noreply, socket}
    end
  end

  @impl true
  def handle_info({:agent_heartbeat, agent_token_id, _organization_id}, socket) do
    # Reload agent info if this device's agent sent a heartbeat
    if socket.assigns.agent_info.agent_token_id == agent_token_id do
      {:noreply, reload_base_data(socket)}
    else
      {:noreply, socket}
    end
  end

  @impl true
  def handle_info({:agents_stale, stale_agents}, socket) do
    # Reload agent info if this device's agent went stale
    device_agent_id = socket.assigns.agent_info.agent_token_id

    if device_agent_id && Enum.any?(stale_agents, &(&1.id == device_agent_id)) do
      {:noreply, reload_base_data(socket)}
    else
      {:noreply, socket}
    end
  end

  @impl true
  def handle_info({FormComponent, {:check_created, _check}}, socket) do
    {:noreply,
     socket
     |> assign(:show_check_form, false)
     |> put_flash(:info, "Check created successfully")
     |> load_equipment_data(socket.assigns.device.id)}
  end

  @impl true
  def handle_info({FormComponent, :close}, socket) do
    {:noreply, assign(socket, :show_check_form, false)}
  end

  # Private functions

  defp maybe_subscribe_and_schedule_refresh(socket, device_id) do
    _ =
      if connected?(socket) do
        _ = Phoenix.PubSub.subscribe(Towerops.PubSub, "device:#{device_id}")
        _ = Phoenix.PubSub.subscribe(Towerops.PubSub, "agents:health")
        Process.send_after(self(), :refresh_data, 10_000)
      end

    :ok
  end

  # -- Full data load (initial page load and discovery_completed) --

  # Loads all data for every tab. Called from handle_params (initial load)
  # and discovery_completed (structural change requiring full refresh).
  defp load_equipment_data(socket, device_id) do
    socket
    |> assign_base_data(device_id)
    |> assign_tab_nav_data(device_id)
    |> assign_overview_data(device_id)
    |> assign_ports_data()
    |> assign_logs_data(device_id)
    |> assign_backups_data(device_id)
    |> assign_checks_data(device_id)
    |> assign_preseem_data()
    |> assign_gaiia_data()
  end

  # -- Selective reload helpers (used by handle_info handlers) --

  # Reload only base data (device, agent_info, snmp_device).
  # Used when device status or agent state changes - these show in the header on all tabs.
  defp reload_base_data(socket) do
    assign_base_data(socket, socket.assigns.device.id)
  end

  # Reload base data + data for the currently active tab only.
  # Used by the periodic refresh timer.
  defp reload_active_tab_data(socket) do
    device_id = socket.assigns.device.id

    socket
    |> assign_base_data(device_id)
    |> assign_tab_nav_data(device_id)
    |> reload_current_tab_data(device_id)
  end

  # Reload data only if the active tab is one of the relevant tabs.
  # Otherwise does nothing - the event is irrelevant to what the user sees.
  defp reload_if_tab(socket, relevant_tabs) do
    if socket.assigns.active_tab in relevant_tabs do
      reload_current_tab_data(socket, socket.assigns.device.id)
    else
      socket
    end
  end

  # Dispatch to the appropriate tab-specific loader based on active_tab.
  defp reload_current_tab_data(socket, device_id) do
    case socket.assigns.active_tab do
      "overview" -> assign_overview_data(socket, device_id)
      "ports" -> assign_ports_data(socket)
      "logs" -> assign_logs_data(socket, device_id)
      "backups" -> assign_backups_data(socket, device_id)
      "checks" -> assign_checks_data(socket, device_id)
      "preseem" -> assign_preseem_data(socket)
      "gaiia" -> assign_gaiia_data(socket)
      # neighbors, arp, mac, vlans, ip_addresses, debug use data from tab_nav/base
      _ -> socket
    end
  end

  # -- Focused data loaders --

  # Base data needed by all tabs: device record, agent info, SNMP device/sensors/interfaces.
  defp assign_base_data(socket, device_id) do
    device = Devices.get_device!(device_id)
    device_with_associations = Repo.preload(device, [:organization, site: [organization: :default_agent_token]])
    {agent_token_id, agent_source} = Agents.get_effective_agent_token_with_source(device_with_associations)
    agent_info = load_agent_info(agent_token_id, agent_source)
    snmp_data = load_snmp_data(device_id)

    socket
    |> assign(:page_title, device.name)
    |> assign(:timezone, socket.assigns.current_scope.timezone)
    |> assign(:device, device)
    |> assign(:agent_info, agent_info)
    |> assign(:snmp_device, snmp_data.device)
    |> assign(:snmp_interfaces, snmp_data.interfaces)
    |> assign(:snmp_sensors, snmp_data.sensors)
  end

  # Data needed for the tab navigation bar to decide which tabs to show.
  # Neighbors, ARP, MAC, VLANs, and IP addresses determine tab visibility.
  defp assign_tab_nav_data(socket, device_id) do
    snmp_device = socket.assigns.snmp_device
    neighbors = Snmp.list_neighbors_with_equipment(device_id)
    arp_entries = Snmp.list_arp_entries(device_id)
    mac_addresses = Snmp.list_mac_addresses(device_id)
    vlans = load_vlans(snmp_device)
    ip_addresses = load_ip_addresses(snmp_device)

    socket
    |> assign(:neighbors, neighbors)
    |> assign(:arp_entries, arp_entries)
    |> assign(:mac_addresses, mac_addresses)
    |> assign(:vlans, vlans)
    |> assign(:ipv4_addresses, Enum.filter(ip_addresses, &(&1.ip_type == "ipv4")))
    |> assign(:ipv6_addresses, Enum.filter(ip_addresses, &(&1.ip_type == "ipv6")))
    |> assign(:ip_addresses_by_interface, group_ip_addresses_by_interface(ip_addresses))
  end

  # Overview tab: charts, sensor classifications, metrics, processors, storage, firmware.
  # This is the most expensive loader due to time-series chart data queries.
  defp assign_overview_data(socket, device_id) do
    device = socket.assigns.device
    snmp_device = socket.assigns.snmp_device
    sensors = socket.assigns.snmp_sensors

    processors = load_processors(snmp_device)
    storage = load_storage(snmp_device)
    latency_chart_data = load_latency_chart_data(device_id)
    processor_chart_data = load_processor_chart_data(processors)
    memory_chart_data = load_sensor_chart_data(device_id, ["memory_usage"])
    storage_chart_data = load_sensor_chart_data(device_id, ["disk_usage"])
    storage_volume_chart_data = load_storage_volume_chart_data(storage)
    traffic_chart_data = load_overall_traffic_chart_data(device_id)

    wireless_chart_data =
      load_sensor_chart_data(device_id, [
        "frequency",
        "power",
        "rssi",
        "ccq",
        "clients",
        "distance",
        "noise-floor",
        "quality",
        "rate",
        "utilization"
      ])

    # Classify sensors by type
    {transceiver_sensors, non_transceiver_sensors} =
      Enum.split_with(sensors, fn sensor ->
        sensor.sensor_descr && String.contains?(String.downcase(sensor.sensor_descr), "sfp")
      end)

    temperature_sensors = Enum.filter(non_transceiver_sensors, &temperature_sensor?/1)
    voltage_sensors = Enum.filter(non_transceiver_sensors, &voltage_sensor?/1)
    current_sensors = Enum.filter(non_transceiver_sensors, &current_sensor?/1)
    power_sensors = Enum.filter(non_transceiver_sensors, &power_sensor?/1)
    fan_sensors = Enum.filter(non_transceiver_sensors, &fan_sensor?/1)
    load_sensors = Enum.filter(non_transceiver_sensors, &load_sensor?/1)
    storage_sensors = Enum.filter(non_transceiver_sensors, &(&1.sensor_type in ["disk_usage"]))
    count_sensors = Enum.filter(non_transceiver_sensors, &counter_sensor?/1)

    {firewall_counters, general_counters} =
      Enum.split_with(count_sensors, &firewall_counter?/1)

    wireless_sensors = Enum.filter(non_transceiver_sensors, &wireless_sensor?/1)
    signal_sensors = Enum.filter(non_transceiver_sensors, &signal_sensor?/1)
    grouped_transceivers = group_transceiver_sensors(transceiver_sensors)
    metrics = calculate_metrics([], device)
    available_firmware = get_available_firmware(snmp_device)

    socket
    |> assign(:metrics, metrics)
    |> assign(:processors, processors)
    |> assign(:storage, storage)
    |> assign(:latency_chart_data, latency_chart_data)
    |> assign(:processor_chart_data, processor_chart_data)
    |> assign(:memory_chart_data, memory_chart_data)
    |> assign(:storage_chart_data, storage_chart_data)
    |> assign(:storage_volume_chart_data, storage_volume_chart_data)
    |> assign(:traffic_chart_data, traffic_chart_data)
    |> assign(:wireless_chart_data, wireless_chart_data)
    |> assign(:temperature_sensors, temperature_sensors)
    |> assign(:voltage_sensors, voltage_sensors)
    |> assign(:current_sensors, current_sensors)
    |> assign(:power_sensors, power_sensors)
    |> assign(:fan_sensors, fan_sensors)
    |> assign(:load_sensors, load_sensors)
    |> assign(:storage_sensors, storage_sensors)
    |> assign(:count_sensors, count_sensors)
    |> assign(:general_counters, general_counters)
    |> assign(:firewall_counters, firewall_counters)
    |> assign(:wireless_sensors, wireless_sensors)
    |> assign(:signal_sensors, signal_sensors)
    |> assign(:grouped_transceivers, grouped_transceivers)
    |> assign(:available_firmware, available_firmware)
    |> assign(:connected_devices, Towerops.Topology.list_connected_devices(device_id))
    |> assign(:recent_config_changes, load_recent_config_changes(device))
  end

  defp load_recent_config_changes(device) do
    if device.mikrotik_enabled do
      since = DateTime.add(DateTime.utc_now(), -30 * 24 * 3600, :second)
      Towerops.ConfigChanges.list_device_changes(device.id, after: since, limit: 5)
    else
      []
    end
  end

  # Ports tab: interfaces grouped by type.
  # Uses snmp_interfaces already loaded in base data.
  defp assign_ports_data(socket) do
    interfaces = socket.assigns.snmp_interfaces
    interfaces_by_type = group_interfaces_by_type(interfaces)

    assign(socket, :interfaces_by_type, interfaces_by_type)
  end

  # Logs/events tab data.
  defp assign_logs_data(socket, device_id) do
    events = Devices.list_devices_events(device_id, 100)
    assign(socket, :events, events)
  end

  # Backups tab data.
  defp assign_backups_data(socket, device_id) do
    device = socket.assigns.device

    mikrotik_backups =
      if device.mikrotik_enabled do
        MikrotikBackups.list_device_backups(device_id, limit: 50)
      else
        []
      end

    socket
    |> assign(:mikrotik_backups, mikrotik_backups)
    |> assign(:selected_backup_ids, MapSet.new())
  end

  # Checks tab data.
  defp assign_checks_data(socket, device_id) do
    # Load all checks for this device
    checks = Monitoring.list_checks(socket.assigns.device.organization_id, device_id: device_id)

    # Group checks by type for organized display
    grouped_checks =
      Enum.group_by(checks, fn check ->
        Map.get(@check_type_groups, check.check_type, :other_checks)
      end)

    socket
    |> assign(:checks, checks)
    |> assign(:grouped_checks, grouped_checks)
  end

  # Gaiia tab data.
  defp assign_gaiia_data(socket) do
    device = socket.assigns.device
    gaiia_item = Gaiia.get_inventory_item_for_device(device.id)

    {gaiia_account, gaiia_subscriptions, gaiia_network_site} =
      if gaiia_item do
        account =
          if gaiia_item.assigned_account_gaiia_id do
            Gaiia.get_account(device.organization_id, gaiia_item.assigned_account_gaiia_id)
          end

        subscriptions =
          if gaiia_item.assigned_account_gaiia_id do
            Gaiia.list_billing_subscriptions_for_account(
              device.organization_id,
              gaiia_item.assigned_account_gaiia_id
            )
          else
            []
          end

        network_site =
          if gaiia_item.assigned_network_site_gaiia_id do
            Gaiia.get_network_site(
              device.organization_id,
              gaiia_item.assigned_network_site_gaiia_id
            )
          end

        {account, subscriptions, network_site}
      else
        {nil, [], nil}
      end

    socket
    |> assign(:gaiia_item, gaiia_item)
    |> assign(:gaiia_account, gaiia_account)
    |> assign(:gaiia_subscriptions, gaiia_subscriptions)
    |> assign(:gaiia_network_site, gaiia_network_site)
  end

  # Preseem tab data.
  defp assign_preseem_data(socket) do
    device = socket.assigns.device
    preseem_ap = Towerops.Preseem.get_access_point_for_device(device.id)

    {metrics, insights} =
      if preseem_ap do
        {
          Towerops.Preseem.list_subscriber_metrics(preseem_ap.id, limit: 50),
          Towerops.Preseem.list_device_insights(device.id)
        }
      else
        {[], []}
      end

    socket
    |> assign(:preseem_access_point, preseem_ap)
    |> assign(:preseem_metrics, metrics)
    |> assign(:preseem_insights, insights)
  end

  defp get_available_firmware(nil), do: nil

  defp get_available_firmware(snmp_device) do
    vendor = determine_vendor(snmp_device.manufacturer)
    product_line = determine_product_line(snmp_device.manufacturer)

    # Only check firmware if both vendor and product_line are known
    # Currently only MikroTik/RouterOS is supported
    if vendor && product_line do
      Firmware.get_latest_firmware_release(vendor, product_line)
    end
  end

  defp determine_vendor(manufacturer) when is_binary(manufacturer) do
    manufacturer_lower = String.downcase(manufacturer)

    cond do
      String.contains?(manufacturer_lower, "mikrotik") -> "mikrotik"
      String.contains?(manufacturer_lower, "cisco") -> "cisco"
      String.contains?(manufacturer_lower, "ubiquiti") -> "ubiquiti"
      true -> nil
    end
  end

  defp determine_vendor(_), do: nil

  defp determine_product_line(manufacturer) when is_binary(manufacturer) do
    manufacturer_lower = String.downcase(manufacturer)

    if String.contains?(manufacturer_lower, "mikrotik") do
      "routeros"
    end
  end

  defp determine_product_line(_), do: nil

  defp firmware_update_available?(nil, _), do: false
  defp firmware_update_available?(_, nil), do: false

  defp firmware_update_available?(snmp_device, available_firmware) do
    current = snmp_device.firmware_version
    available = available_firmware.version

    # Extract clean version numbers for comparison
    # "RouterOS 7.20.6 (Level 6)" -> "7.20.6"
    # "7.21.2" -> "7.21.2"
    current_clean = extract_version_number(current)
    available_clean = extract_version_number(available)

    current_clean && available_clean && VersionComparator.newer?(current_clean, available_clean)
  end

  # Extract semantic version number from version string
  # Examples:
  #   "RouterOS 7.20.6 (Level 6)" -> "7.20.6"
  #   "7.21.2" -> "7.21.2"
  #   "v7.14.1" -> "7.14.1"
  defp extract_version_number(nil), do: nil
  defp extract_version_number(""), do: nil

  defp extract_version_number(version_string) when is_binary(version_string) do
    # Match semantic version pattern: X.Y or X.Y.Z
    case Regex.run(~r/\d+\.\d+(?:\.\d+)?/, version_string) do
      [version] -> version
      _ -> nil
    end
  end

  defp format_date(nil), do: ""

  defp format_date(date) do
    Calendar.strftime(date, "%B %-d, %Y")
  end

  defp load_agent_info(nil, :none) do
    %{
      agent_token_id: nil,
      type: :cloud,
      name: "Cloud Polling",
      source: nil,
      last_seen_at: nil,
      is_offline: false
    }
  end

  defp load_agent_info(agent_token_id, source) do
    agent_token = Agents.get_agent_token!(agent_token_id)
    is_offline = agent_offline?(agent_token)

    %{
      agent_token_id: agent_token_id,
      type: if(agent_token.is_cloud_poller, do: :cloud, else: :agent),
      name: agent_token.name,
      source: source,
      last_seen_at: agent_token.last_seen_at,
      is_offline: is_offline
    }
  rescue
    Ecto.NoResultsError ->
      # Agent token no longer exists, fall back to cloud polling display
      %{
        agent_token_id: nil,
        type: :cloud,
        name: "Cloud Polling (agent not found)",
        source: nil,
        last_seen_at: nil,
        is_offline: false
      }
  end

  # Agent is considered offline if it hasn't been seen in the last 5 minutes
  defp agent_offline?(%{is_cloud_poller: true}), do: false
  defp agent_offline?(%{last_seen_at: nil}), do: true

  defp agent_offline?(%{last_seen_at: last_seen_at}) do
    five_minutes_ago = DateTime.add(DateTime.utc_now(), -5, :minute)
    DateTime.before?(last_seen_at, five_minutes_ago)
  end

  defp calculate_metrics(checks, _equipment) do
    total_checks = length(checks)

    if total_checks > 0 do
      successful_checks = Enum.count(checks, &(&1.status == :success))
      uptime_percentage = Float.round(successful_checks / total_checks * 100, 1)

      %{
        uptime_percentage: uptime_percentage,
        total_checks: total_checks,
        successful_checks: successful_checks
      }
    else
      %{
        uptime_percentage: 0,
        total_checks: 0,
        successful_checks: 0
      }
    end
  end

  defp format_speed(speed_bps) when is_integer(speed_bps) do
    cond do
      speed_bps >= 1_000_000_000 ->
        "#{Float.round(speed_bps / 1_000_000_000, 1)} Gbps"

      speed_bps >= 1_000_000 ->
        "#{Float.round(speed_bps / 1_000_000, 1)} Mbps"

      speed_bps >= 1_000 ->
        "#{Float.round(speed_bps / 1_000, 1)} Kbps"

      true ->
        "#{speed_bps} bps"
    end
  end

  defp format_speed(_), do: "-"

  defp format_bytes(nil), do: "-"
  defp format_bytes(0), do: "0 B"

  defp format_bytes(bytes) when is_integer(bytes) do
    cond do
      bytes >= 1_099_511_627_776 ->
        "#{Float.round(bytes / 1_099_511_627_776, 1)} TB"

      bytes >= 1_073_741_824 ->
        "#{Float.round(bytes / 1_073_741_824, 1)} GB"

      bytes >= 1_048_576 ->
        "#{Float.round(bytes / 1_048_576, 1)} MB"

      bytes >= 1024 ->
        "#{Float.round(bytes / 1024, 1)} KB"

      true ->
        "#{bytes} B"
    end
  end

  defp format_bytes(_), do: "-"

  defp format_sensor_value(value, _divisor) when is_number(value) do
    # Value is already divided by divisor when stored in sensor_reading
    Float.round(value, 1)
  end

  defp format_sensor_value(_, _), do: "-"

  defp time_ago(nil), do: "Never"

  defp time_ago(datetime) do
    diff = DateTime.diff(DateTime.utc_now(), datetime, :second)

    cond do
      diff < 60 -> "#{diff}s ago"
      diff < 3600 -> "#{div(diff, 60)}m ago"
      diff < 86_400 -> "#{div(diff, 3600)}h ago"
      true -> "#{div(diff, 86_400)}d ago"
    end
  end

  defp format_uptime(nil), do: "N/A"

  defp format_uptime(timeticks) when is_integer(timeticks) do
    # Timeticks are in hundredths of a second
    seconds = div(timeticks, 100)
    format_duration(seconds)
  end

  defp format_uptime(_), do: "N/A"

  defp format_duration(total_seconds) do
    {weeks, remainder} = seconds_to_weeks(total_seconds)
    {days, remainder} = seconds_to_days(remainder)
    {hours, remainder} = seconds_to_hours(remainder)
    {minutes, seconds} = seconds_to_minutes(remainder)

    parts =
      Enum.reject(
        [
          format_time_part(weeks, "week"),
          format_time_part(days, "day"),
          format_time_part(hours, "hour"),
          format_time_part(minutes, "minute"),
          format_time_part(seconds, "second")
        ],
        &is_nil/1
      )

    case parts do
      [] -> "0 seconds"
      parts -> Enum.join(parts, " ")
    end
  end

  @spec seconds_to_weeks(integer()) :: {integer(), integer()}
  defp seconds_to_weeks(seconds), do: {div(seconds, 604_800), rem(seconds, 604_800)}

  @spec seconds_to_days(integer()) :: {integer(), integer()}
  defp seconds_to_days(seconds), do: {div(seconds, 86_400), rem(seconds, 86_400)}

  @spec seconds_to_hours(integer()) :: {integer(), integer()}
  defp seconds_to_hours(seconds), do: {div(seconds, 3600), rem(seconds, 3600)}

  @spec seconds_to_minutes(integer()) :: {integer(), integer()}
  defp seconds_to_minutes(seconds), do: {div(seconds, 60), rem(seconds, 60)}

  @spec format_time_part(integer(), String.t()) :: String.t() | nil
  defp format_time_part(0, _unit), do: nil
  defp format_time_part(1, unit), do: "1 #{unit}"
  defp format_time_part(count, unit), do: "#{count} #{unit}s"

  defp format_device_age(nil), do: "N/A"

  defp format_device_age(datetime) do
    diff = DateTime.diff(DateTime.utc_now(), datetime, :second)
    format_duration(diff) <> " ago"
  end

  defp format_event_type(event_type) do
    event_type
    |> String.replace("_", " ")
    |> String.capitalize()
  end

  defp load_snmp_data(device_id) do
    case Snmp.get_device_with_associations(device_id) do
      nil ->
        %{device: nil, interfaces: [], sensors: []}

      device ->
        # Batch load latest readings for all sensors in a single query
        sensor_ids = Enum.map(device.sensors, & &1.id)
        latest_readings_map = Snmp.get_latest_sensor_readings_batch(sensor_ids)

        sensors_with_readings =
          Enum.map(device.sensors, fn sensor ->
            latest_reading = Map.get(latest_readings_map, sensor.id)
            Map.put(sensor, :latest_reading, latest_reading)
          end)

        # Batch load latest stats for all interfaces in a single query
        interface_ids = Enum.map(device.interfaces, & &1.id)
        latest_stats_map = Snmp.get_latest_interface_stats_batch(interface_ids)

        interfaces_with_stats =
          Enum.map(device.interfaces, fn interface ->
            latest_stat = Map.get(latest_stats_map, interface.id)
            Map.put(interface, :latest_stat, latest_stat)
          end)

        %{
          device: device,
          interfaces: interfaces_with_stats,
          sensors: sensors_with_readings
        }
    end
  end

  defp load_vlans(nil), do: []
  defp load_vlans(snmp_device), do: Snmp.list_vlans(snmp_device.id)

  defp load_ip_addresses(nil), do: []
  defp load_ip_addresses(snmp_device), do: Snmp.list_ip_addresses(snmp_device.id)

  defp load_processors(nil), do: []
  defp load_processors(snmp_device), do: Snmp.list_processors(snmp_device.id)

  defp load_storage(nil), do: []
  defp load_storage(snmp_device), do: Snmp.list_storage(snmp_device.id)

  defp load_processor_chart_data([]), do: nil

  defp load_processor_chart_data(processors) do
    twenty_four_hours_ago = DateTime.add(DateTime.utc_now(), -24, :hour)

    datasets =
      processors
      |> Enum.map(&processor_to_chart_dataset(&1, twenty_four_hours_ago))
      |> Enum.reject(&Enum.empty?(&1.data))

    if Enum.empty?(datasets) do
      nil
    else
      Jason.encode!(%{datasets: datasets})
    end
  end

  defp processor_to_chart_dataset(processor, since) do
    readings =
      processor.id
      |> Snmp.get_processor_readings(since: since, limit: 1000)
      |> Enum.reverse()

    label =
      if processor.description do
        processor.description
      else
        "CPU #{processor.processor_index}"
      end

    %{
      label: label,
      data: Enum.map(readings, &processor_reading_to_data_point/1)
    }
  end

  defp processor_reading_to_data_point(reading) do
    %{
      x: DateTime.to_unix(reading.checked_at, :millisecond),
      y: if(reading.load_percent, do: Float.round(reading.load_percent, 1))
    }
  end

  defp load_storage_volume_chart_data([]), do: nil

  defp load_storage_volume_chart_data(storage_volumes) do
    twenty_four_hours_ago = DateTime.add(DateTime.utc_now(), -24, :hour)

    datasets =
      storage_volumes
      |> Enum.map(&storage_volume_to_chart_dataset(&1, twenty_four_hours_ago))
      |> Enum.reject(&Enum.empty?(&1.data))

    if Enum.empty?(datasets) do
      nil
    else
      Jason.encode!(%{datasets: datasets})
    end
  end

  defp storage_volume_to_chart_dataset(storage, since) do
    readings =
      storage.id
      |> Snmp.get_storage_readings(since: since, limit: 1000)
      |> Enum.reverse()

    label = storage.description || "Storage #{storage.storage_index}"

    %{
      label: label,
      data: Enum.map(readings, &storage_reading_to_data_point/1)
    }
  end

  defp storage_reading_to_data_point(reading) do
    %{
      x: DateTime.to_unix(reading.checked_at, :millisecond),
      y: if(reading.usage_percent, do: Float.round(reading.usage_percent, 1))
    }
  end

  defp load_sensor_chart_data(device_id, sensor_types) when is_list(sensor_types) do
    case Snmp.get_device_with_associations(device_id) do
      nil -> nil
      device -> build_sensor_datasets_json(device, sensor_types)
    end
  end

  defp build_sensor_datasets_json(device, sensor_types) do
    sensors =
      device.sensors
      |> Enum.filter(&(&1.sensor_type in sensor_types))
      |> Enum.sort_by(& &1.sensor_index)

    if Enum.empty?(sensors) do
      nil
    else
      twenty_four_hours_ago = DateTime.add(DateTime.utc_now(), -24, :hour)
      datasets = Enum.map(sensors, &sensor_to_chart_dataset(&1, twenty_four_hours_ago))
      Jason.encode!(%{datasets: datasets})
    end
  end

  defp sensor_to_chart_dataset(sensor, since) do
    readings =
      sensor.id
      |> Snmp.get_sensor_readings(since: since, limit: 1000)
      |> Enum.reverse()

    %{
      label: sensor.sensor_descr,
      data: Enum.map(readings, &reading_to_data_point/1)
    }
  end

  defp reading_to_data_point(reading) do
    %{
      x: DateTime.to_unix(reading.checked_at, :millisecond),
      y: if(reading.value, do: Float.round(reading.value, 1))
    }
  end

  defp load_latency_chart_data(device_id) do
    twenty_four_hours_ago = DateTime.add(DateTime.utc_now(), -24, :hour)

    checks =
      device_id
      |> Monitoring.get_latency_data(since: twenty_four_hours_ago, limit: 1000)
      |> Enum.reverse()

    if Enum.empty?(checks) do
      nil
    else
      dataset = %{
        label: "ICMP Latency",
        data: Enum.map(checks, &latency_check_to_data_point/1)
      }

      Jason.encode!(%{datasets: [dataset]})
    end
  end

  defp latency_check_to_data_point(check) do
    %{
      x: DateTime.to_unix(check.checked_at, :millisecond),
      y: check.response_time_ms
    }
  end

  defp load_overall_traffic_chart_data(device_id) do
    case Snmp.get_device_with_associations(device_id) do
      nil -> nil
      device -> build_overall_traffic_chart_json(device)
    end
  end

  defp build_overall_traffic_chart_json(device) do
    if Enum.empty?(device.interfaces) do
      nil
    else
      twenty_four_hours_ago = DateTime.add(DateTime.utc_now(), -24, :hour)
      all_stats = aggregate_interface_traffic_stats(device.interfaces, twenty_four_hours_ago)

      if Enum.empty?(all_stats) do
        nil
      else
        build_traffic_json_datasets(all_stats)
      end
    end
  end

  # Calculate BPS per interface first, then aggregate by timestamp
  # This avoids issues with misaligned timestamps between interfaces
  defp aggregate_interface_traffic_stats(interfaces, since) do
    interfaces
    |> Enum.flat_map(fn interface ->
      interface.id
      |> Snmp.get_interface_stats(since: since, limit: 1000)
      |> Enum.reverse()
      |> calculate_interface_bps()
    end)
    |> Enum.group_by(& &1.timestamp_ms)
    |> Enum.map(fn {ts, bps_list} ->
      total_in = Enum.reduce(bps_list, 0.0, fn x, acc -> acc + x.in_bps end)
      total_out = Enum.reduce(bps_list, 0.0, fn x, acc -> acc + x.out_bps end)
      %{timestamp_ms: ts, in_bps: total_in, out_bps: total_out}
    end)
    |> Enum.sort_by(& &1.timestamp_ms)
  end

  # Calculate BPS for a single interface's stats
  defp calculate_interface_bps(stats) do
    stats
    |> Enum.chunk_every(2, 1, :discard)
    |> Enum.map(fn [s1, s2] ->
      time_diff = s2.checked_at |> DateTime.diff(s1.checked_at, :second) |> max(1)

      in_bps =
        if s2.if_in_octets && s1.if_in_octets do
          max((s2.if_in_octets - s1.if_in_octets) * 8 / time_diff, 0)
        else
          0.0
        end

      out_bps =
        if s2.if_out_octets && s1.if_out_octets do
          max((s2.if_out_octets - s1.if_out_octets) * 8 / time_diff, 0)
        else
          0.0
        end

      # Round timestamp to nearest minute for aggregation (avoids microsecond misalignment)
      ts_seconds = DateTime.to_unix(s2.checked_at)
      rounded_ts_ms = div(ts_seconds, 60) * 60 * 1000

      %{timestamp_ms: rounded_ts_ms, in_bps: in_bps, out_bps: out_bps}
    end)
  end

  defp build_traffic_json_datasets(all_stats) do
    in_data = Enum.map(all_stats, fn s -> %{x: s.timestamp_ms, y: Float.round(s.in_bps, 2)} end)
    out_data = Enum.map(all_stats, fn s -> %{x: s.timestamp_ms, y: -Float.round(s.out_bps, 2)} end)

    datasets = [
      %{label: "Outbound", data: out_data},
      %{label: "Inbound", data: in_data}
    ]

    Jason.encode!(%{datasets: datasets})
  end

  # Group interfaces by type for organized display
  defp group_interfaces_by_type(interfaces) do
    interfaces
    |> Enum.group_by(&get_interface_type_category(&1.if_type))
    |> Enum.map(fn {category, interfaces} ->
      {category, Enum.sort_by(interfaces, & &1.if_index)}
    end)
    |> Enum.sort_by(fn {category, _} -> interface_type_order(category) end)
  end

  # Map SNMP interface types to human-readable categories
  defp get_interface_type_category(if_type) when is_integer(if_type) do
    Map.get(@interface_type_categories, if_type, "Other")
  end

  defp get_interface_type_category(_), do: "Other"

  # Define display order for interface categories
  defp interface_type_order(category) do
    Map.get(@interface_category_order, category, 99)
  end

  # Group transceiver sensors by transceiver name
  # Returns list of {name, sensors} tuples sorted by name
  defp group_transceiver_sensors(transceiver_sensors) do
    transceiver_sensors
    |> Enum.group_by(&extract_transceiver_name/1)
    |> Enum.sort_by(fn {name, _sensors} -> name end)
  end

  # Extract transceiver name from sensor description
  # Examples:
  #   "sfp-sfpplus1 Rx Power" -> "sfp-sfpplus1"
  #   "sfp-sfpplus2 Temperature" -> "sfp-sfpplus2"
  #   "SFP1 Voltage" -> "SFP1"
  defp extract_transceiver_name(sensor) do
    case sensor.sensor_descr do
      nil ->
        "unknown"

      descr ->
        # Extract the first word (transceiver identifier)
        descr
        |> String.split(" ")
        |> List.first()
        |> String.downcase()
    end
  end

  # Group IP addresses by interface ID for efficient lookup in templates
  defp group_ip_addresses_by_interface(ip_addresses) do
    Enum.group_by(ip_addresses, & &1.snmp_interface_id)
  end

  # Sensor type classification helpers
  # Trust sensor_type first (set during SNMP discovery based on OID)
  # Only fall back to unit/description matching if sensor_type is missing/unknown
  defp temperature_sensor?(sensor) do
    # Primary: Check sensor_type (set during discovery based on OID)
    type_match = sensor.sensor_type in ["temperature", "cpu_temperature", "celsius"]

    # Fallback: Check unit for devices that don't set sensor_type correctly
    unit_match = sensor.sensor_unit in ["°C", "C", "celsius"]

    # Only use description as last resort if type and unit don't match
    # and only for unambiguous temperature keywords
    descr_match =
      !type_match && !unit_match &&
        sensor.sensor_descr &&
        String.contains?(String.downcase(sensor.sensor_descr), "temperature") &&
        !String.contains?(String.downcase(sensor.sensor_descr), "voltage")

    (type_match || unit_match || descr_match) && !voltage_by_description?(sensor)
  end

  defp voltage_sensor?(sensor) do
    type_match = sensor.sensor_type in ["voltage", "volts"]
    unit_match = sensor.sensor_unit in ["V", "mV", "volts"]
    descr_match = voltage_by_description?(sensor)

    type_match || unit_match || descr_match
  end

  defp voltage_by_description?(sensor) do
    sensor.sensor_descr &&
      String.contains?(String.downcase(sensor.sensor_descr), "voltage")
  end

  defp wireless_sensor?(sensor) do
    sensor.sensor_type in [
      "frequency",
      "power",
      "rssi",
      "ccq",
      "clients",
      "distance",
      "noise-floor",
      "quality",
      "rate",
      "utilization"
    ]
  end

  # Counter sensors include various count and gauge types
  # (connections, sessions, clients, DHCP leases, etc.)
  defp counter_sensor?(sensor) do
    sensor.sensor_type in [
      "count",
      "pppoe_sessions",
      "connections",
      "clients",
      "leases",
      "sessions"
    ]
  end

  # Firewall counters are connection-related metrics
  defp firewall_counter?(sensor) do
    descr = String.downcase(sensor.sensor_descr || "")
    String.contains?(descr, "connection")
  end

  # Current sensors (amperes)
  defp current_sensor?(sensor) do
    type_match = sensor.sensor_type in ["current", "amperes"]
    unit_match = sensor.sensor_unit in ["A", "mA", "amperes"]

    type_match || unit_match
  end

  # Power sensors (watts) - separate from wireless power
  defp power_sensor?(sensor) do
    type_match = sensor.sensor_type in ["power", "watts"]
    unit_match = sensor.sensor_unit in ["W", "mW", "watts"]

    # Exclude wireless power sensors (those are in wireless category)
    (type_match || unit_match) && !wireless_sensor?(sensor)
  end

  # Fan speed sensors (RPM)
  defp fan_sensor?(sensor) do
    type_match = sensor.sensor_type in ["fanspeed", "rpm"]
    unit_match = sensor.sensor_unit in ["RPM", "rpm"]
    descr_match = sensor.sensor_descr && String.contains?(String.downcase(sensor.sensor_descr), "fan")

    type_match || unit_match || descr_match
  end

  # System load sensors (CPU load, etc.)
  defp load_sensor?(sensor) do
    type_match = sensor.sensor_type in ["load", "cpu_load", "system_load"]
    descr_match = sensor.sensor_descr && String.contains?(String.downcase(sensor.sensor_descr), "load")

    type_match || descr_match
  end

  # Signal quality sensors (SNR, RSSI, RSRP, etc.)
  defp signal_sensor?(sensor) do
    sensor.sensor_type in [
      "snr",
      "rssi",
      "rsrp",
      "rsrq",
      "sinr",
      "ssr",
      "mse",
      "noise",
      "noise-floor",
      "dbm"
    ]
  end

  @impl true
  def handle_event("add_check", _params, socket) do
    {:noreply, assign(socket, :show_check_form, true)}
  end

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

    # Queue discovery job
    case %{device_id: device.id} |> DiscoveryWorker.new() |> Oban.insert() do
      {:ok, _job} ->
        {:noreply, put_flash(socket, :info, "Discovery started for #{device.name}")}

      {:error, _} ->
        {:noreply, put_flash(socket, :error, "Failed to start discovery. Please try again.")}
    end
  end

  @impl true
  def handle_event("download_backup", %{"id" => backup_id}, socket) do
    backup = MikrotikBackups.get_backup!(backup_id)
    organization = socket.assigns.current_scope.organization

    # Verify the backup belongs to a device in the user's organization
    case AccessControl.verify_device_access(backup.device_id, organization.id) do
      {:ok, device} ->
        config_text = MikrotikBackups.decompress_config(backup.config_compressed)
        filename = "#{device.name}_backup_#{Calendar.strftime(backup.backed_up_at, "%Y%m%d_%H%M%S")}.rsc"

        {:noreply, push_event(socket, "download", %{content: config_text, filename: filename, mime_type: "text/plain"})}

      {:error, _} ->
        {:noreply, put_flash(socket, :error, t_equipment("You don't have permission to access this backup"))}
    end
  end

  @impl true
  def handle_event("toggle_backup_selection", %{"id" => backup_id}, socket) do
    selected = socket.assigns.selected_backup_ids

    updated_selected =
      if MapSet.member?(selected, backup_id) do
        MapSet.delete(selected, backup_id)
      else
        # Limit to 2 selections max
        if MapSet.size(selected) >= 2 do
          selected
        else
          MapSet.put(selected, backup_id)
        end
      end

    {:noreply, assign(socket, :selected_backup_ids, updated_selected)}
  end

  @impl true
  def handle_event("compare_selected", _params, socket) do
    device_id = socket.assigns.device.id
    selected = MapSet.to_list(socket.assigns.selected_backup_ids)

    case selected do
      [backup_a, backup_b] ->
        {:noreply,
         socket
         |> assign(:selected_backup_ids, MapSet.new())
         |> push_navigate(to: ~p"/devices/#{device_id}/backups/compare?backup_a=#{backup_a}&backup_b=#{backup_b}")}

      _ ->
        {:noreply, put_flash(socket, :error, t_equipment("Please select exactly 2 backups to compare"))}
    end
  end

  @impl true
  def handle_event("clear_selection", _params, socket) do
    {:noreply, assign(socket, :selected_backup_ids, MapSet.new())}
  end

  @impl true
  def handle_event("dismiss_insight", %{"id" => insight_id}, socket) do
    case Towerops.Preseem.dismiss_insight(insight_id) do
      {:ok, _} ->
        {:noreply, assign_preseem_data(socket)}

      {:error, _} ->
        {:noreply, put_flash(socket, :error, "Failed to dismiss insight")}
    end
  end

  @impl true
  def handle_event("delete_backup", %{"id" => backup_id}, socket) do
    import ToweropsWeb.Permissions

    if owner?(socket) do
      do_delete_backup(backup_id, socket)
    else
      {:noreply, put_flash(socket, :error, t_equipment("Only organization owners can delete backups"))}
    end
  end

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

    cond do
      is_nil(agent_token_id) ->
        {:noreply,
         put_flash(socket, :error, t_equipment("Device has no agent assigned. Assign an agent to create backups."))}

      !device.mikrotik_enabled ->
        {:noreply, put_flash(socket, :error, t_equipment("MikroTik API is not enabled for this device."))}

      true ->
        trigger_manual_backup(socket, device, agent_token_id)
    end
  end

  defp trigger_manual_backup(socket, device, agent_token_id) do
    alias Towerops.Agent.AgentJob
    alias Towerops.Agent.MikrotikCommand
    alias Towerops.Agent.MikrotikDevice
    alias Towerops.Devices.BackupRequests

    job_id = "backup:#{device.id}:#{DateTime.to_unix(DateTime.utc_now())}"
    mikrotik_config = Devices.get_mikrotik_config(device)

    # Generate unique filename for this backup (without .rsc extension, RouterOS adds it)
    backup_filename = "towerops-backup-#{DateTime.to_unix(DateTime.utc_now())}"

    # Build chunked read commands
    # Max chunk size is 32KB (32768 bytes), we'll read 10 chunks to cover 320KB
    chunk_size = 32_768
    num_chunks = 10

    read_commands =
      for i <- 0..(num_chunks - 1) do
        %MikrotikCommand{
          command: "/file/read",
          args: %{
            "file" => "#{backup_filename}.rsc",
            "offset" => to_string(i * chunk_size),
            "chunk-size" => to_string(chunk_size)
          }
        }
      end

    job = %AgentJob{
      job_id: job_id,
      job_type: :MIKROTIK,
      device_id: device.id,
      mikrotik_device: %MikrotikDevice{
        ip: device.ip_address,
        username: mikrotik_config.username,
        password: mikrotik_config.password,
        port: mikrotik_config.port,
        ssh_port: mikrotik_config.ssh_port,
        use_ssl: mikrotik_config.use_ssl
      },
      mikrotik_commands:
        [
          # Step 1: Export config to file on router (compact format, no defaults)
          %MikrotikCommand{
            command: "/export",
            args: %{"file" => backup_filename, "compact" => ""}
          }
        ] ++
          read_commands ++
          [
            # Final step: Delete the temporary file
            %MikrotikCommand{
              command: "/file/remove",
              args: %{"numbers" => "#{backup_filename}.rsc"}
            }
          ]
    }

    case BackupRequests.create_request(device.id, job_id, "manual") do
      {:ok, _request} ->
        Phoenix.PubSub.broadcast(
          Towerops.PubSub,
          "agent:#{agent_token_id}:backup",
          {:backup_requested, job}
        )

        {:noreply,
         socket
         |> put_flash(:info, t_equipment("Manual backup requested. Results will appear shortly."))
         |> load_equipment_data(device.id)}

      {:error, _reason} ->
        {:noreply, put_flash(socket, :error, t_equipment("Failed to create backup request. Please try again."))}
    end
  end

  # Private helper for deleting backups
  defp do_delete_backup(backup_id, socket) do
    backup = MikrotikBackups.get_backup!(backup_id)
    organization = socket.assigns.current_scope.organization

    # Verify the backup belongs to a device in the user's organization
    case AccessControl.verify_device_access(backup.device_id, organization.id) do
      {:ok, _device} ->
        case MikrotikBackups.delete_backup(backup) do
          {:ok, _} ->
            {:noreply,
             socket
             |> put_flash(:info, t_equipment("Backup deleted successfully"))
             |> load_equipment_data(socket.assigns.device.id)}

          {:error, _changeset} ->
            {:noreply, put_flash(socket, :error, t_equipment("Failed to delete backup"))}
        end

      {:error, _} ->
        {:noreply, put_flash(socket, :error, t_equipment("You don't have permission to access this backup"))}
    end
  end

  # Apply pagination to backups list if on the backups tab
  defp apply_backups_pagination(socket, "backups", page) when is_map_key(socket.assigns, :mikrotik_backups) do
    per_page = 20
    all_backups = socket.assigns.mikrotik_backups
    total_count = length(all_backups)
    total_pages = ceil(total_count / per_page)

    # Ensure page is within valid range
    page = max(1, min(page, max(1, total_pages)))

    # Slice backups for current page
    offset = (page - 1) * per_page
    backups_page = Enum.slice(all_backups, offset, per_page)

    socket
    |> assign(:mikrotik_backups, backups_page)
    |> assign(:pagination, %{
      page: page,
      per_page: per_page,
      total_count: total_count,
      total_pages: total_pages
    })
  end

  defp apply_backups_pagination(socket, _tab, _page), do: socket

  # Checks tab helpers

  defp group_title(:snmp_sensors), do: "SNMP Sensors"
  defp group_title(:snmp_interfaces), do: "SNMP Interfaces"
  defp group_title(:snmp_processors), do: "SNMP Processors"
  defp group_title(:snmp_storage), do: "SNMP Storage"
  defp group_title(:http_checks), do: "HTTP Checks"
  defp group_title(:tcp_checks), do: "TCP Checks"
  defp group_title(:dns_checks), do: "DNS Checks"
  defp group_title(:ping_checks), do: "Ping Checks"
  defp group_title(:other_checks), do: "Other Checks"

  defp render_status_badge(0) do
    assigns = %{}

    ~H"""
    <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400">
      OK
    </span>
    """
  end

  defp render_status_badge(1) do
    assigns = %{}

    ~H"""
    <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400">
      WARNING
    </span>
    """
  end

  defp render_status_badge(2) do
    assigns = %{}

    ~H"""
    <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400">
      CRITICAL
    </span>
    """
  end

  defp render_status_badge(3) do
    assigns = %{}

    ~H"""
    <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800 dark:bg-gray-900/30 dark:text-gray-400">
      UNKNOWN
    </span>
    """
  end

  defp render_status_badge(_) do
    assigns = %{}

    ~H"""
    <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800 dark:bg-gray-900/30 dark:text-gray-400">
      PENDING
    </span>
    """
  end

  defp get_latest_value(check) do
    # Get the latest check result to show current value
    case Monitoring.get_latest_check_result(check.id) do
      nil -> "-"
      result -> format_check_value(result, check)
    end
  end

  defp format_check_value(%{value: nil}, _check), do: "-"

  defp format_check_value(%{value: value}, check) when is_number(value) do
    case check.check_type do
      "snmp_sensor" -> format_check_sensor_value(value, check.config)
      "snmp_processor" -> "#{Float.round(value, 1)}%"
      "snmp_storage" -> "#{Float.round(value, 1)}%"
      _ -> value |> Float.round(2) |> to_string()
    end
  end

  defp format_check_value(_result, _check), do: "-"

  @sensor_value_formats %{
    "temperature" => {1, "°C"},
    "voltage" => {2, "V"},
    "current" => {2, "A"},
    "power" => {1, "W"},
    "frequency" => {0, "Hz"},
    "humidity" => {1, "%"}
  }

  defp format_check_sensor_value(value, %{"sensor_type" => "fanspeed", "sensor_unit" => unit}) do
    "#{round(value)}#{unit || " RPM"}"
  end

  defp format_check_sensor_value(value, config) do
    sensor_type = config["sensor_type"]
    unit = config["sensor_unit"]
    {precision, default_unit} = Map.get(@sensor_value_formats, sensor_type, {2, ""})
    "#{Float.round(value, precision)}#{unit || default_unit}"
  end

  defp format_relative_time(datetime) do
    alias ToweropsWeb.TimeHelpers

    TimeHelpers.format_time_ago(datetime)
  end

  defp config_change_dot_color(event) do
    cond do
      event.change_size > 50 -> "bg-red-500"
      event.change_size > 20 -> "bg-yellow-500"
      true -> "bg-green-500"
    end
  end
end
