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

  alias Towerops.Devices
  alias Towerops.Gaiia
  alias Towerops.Monitoring
  alias Towerops.Organizations.SubscriptionLimits
  alias Towerops.Sites
  alias Towerops.Snmp
  alias Towerops.Workers.DiscoveryWorker
  alias ToweropsWeb.Live.Helpers.AccessControl

  @impl true
  def mount(_params, _session, socket) do
    organization = socket.assigns.current_scope.organization
    devices = Devices.list_organization_devices(organization.id)
    sites = Sites.list_organization_sites(organization.id)

    if connected?(socket) do
      Phoenix.PubSub.subscribe(Towerops.PubSub, "devices:org:#{organization.id}")
      schedule_tick()
    end

    # Load device quota
    {current, limit} = SubscriptionLimits.device_quota(organization)

    site_subscribers = load_site_subscribers(devices)

    {:ok,
     socket
     |> assign(:page_title, t_equipment("Devices"))
     |> assign(:timezone, socket.assigns.current_scope.timezone)
     |> assign(:has_devices, devices != [])
     |> stream(:device_rows, build_device_rows(devices))
     |> assign(:sites_enabled, organization.use_sites)
     |> assign(:has_sites, sites != [])
     |> assign(:reorder_mode, false)
     |> assign(:device_quota, %{current: current, limit: limit})
     |> assign(:site_subscribers, site_subscribers)
     |> assign(:pending_reload_ref, nil)}
  end

  @impl true
  def handle_params(params, _url, socket) do
    tab = Map.get(params, "tab", "existing")
    {:noreply, apply_action(socket, socket.assigns.live_action, params, tab)}
  end

  defp apply_action(socket, :index, params, tab) do
    organization = socket.assigns.current_scope.organization

    case tab do
      "discovered" ->
        page = params |> Map.get("page", "1") |> String.to_integer()
        per_page = 20

        # Load all discovered devices - this is temporarily kept in memory for slicing
        # For very large datasets, consider database-level pagination
        all_discovered = Snmp.list_discovered_devices_for_organization(organization.id)
        total_count = length(all_discovered)
        total_pages = ceil(total_count / per_page)

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

        offset = (page - 1) * per_page
        # Only keep the current page in assigns - don't store all_discovered
        discovered_devices = Enum.slice(all_discovered, offset, per_page)

        socket
        |> assign(:active_tab, "discovered")
        # Use temporary assign - will be cleared after render
        |> assign_new(:discovered_devices, fn -> discovered_devices end)
        |> assign(:pagination, %{
          page: page,
          per_page: per_page,
          total_count: total_count,
          total_pages: total_pages
        })

      _ ->
        # Reload device stream to ensure immediate rendering when switching back to existing tab.
        # Without this, the table won't appear until the next PubSub update or tick event.
        devices = Devices.list_organization_devices(organization.id)

        socket
        |> assign(:active_tab, "existing")
        |> assign(:discovered_devices, [])
        |> assign(:pagination, nil)
        |> stream(:device_rows, build_device_rows(devices), reset: true)
    end
  end

  @impl true
  def handle_event("force_rediscover_all", _params, socket) do
    organization = socket.assigns.current_scope.organization
    devices = Devices.list_organization_devices(organization.id)
    snmp_devices = Enum.filter(devices, & &1.snmp_enabled)

    if Enum.empty?(snmp_devices) do
      {:noreply, put_flash(socket, :error, t_equipment("No SNMP-enabled devices found"))}
    else
      Enum.each(snmp_devices, fn device ->
        enqueue_discovery(device.id)
      end)

      count = length(snmp_devices)
      message = t_equipment("Discovery started for %{count} device", count: count) <> if count == 1, do: "", else: "s"

      {:noreply, put_flash(socket, :info, message)}
    end
  end

  @impl true
  def handle_event("reorder_site", %{"site_id" => site_id, "new_position" => new_position}, socket) do
    organization = socket.assigns.current_scope.organization

    # Verify site belongs to current organization
    case AccessControl.verify_site_access(site_id, organization.id) do
      {:ok, _site} ->
        perform_site_reorder(socket, site_id, new_position, organization.id)

      {:error, :not_found} ->
        {:noreply, put_flash(socket, :error, t_equipment("Site not found"))}

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

  @impl true
  def handle_event("reorder_device", %{"device_id" => device_id, "new_position" => new_position}, socket) do
    organization = socket.assigns.current_scope.organization

    # Verify device belongs to current organization
    case AccessControl.verify_device_access(device_id, organization.id) do
      {:ok, _device} ->
        perform_device_reorder(socket, device_id, new_position, organization.id)

      {:error, :not_found} ->
        {:noreply, put_flash(socket, :error, t_equipment("Device not found"))}

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

  @impl true
  def handle_event("toggle_reorder_mode", _params, socket) do
    {:noreply, assign(socket, :reorder_mode, !socket.assigns.reorder_mode)}
  end

  @impl true
  def handle_event("reset_order", _params, socket) do
    organization_id = socket.assigns.current_scope.organization.id

    Sites.reset_site_order(organization_id)
    Devices.reset_organization_device_order(organization_id)

    # Reload devices with alphabetical order
    devices = Devices.list_organization_devices(organization_id)

    {:noreply,
     socket
     |> assign(:has_devices, devices != [])
     |> stream(:device_rows, build_device_rows(devices), reset: true)
     |> put_flash(:info, t_equipment("Order reset to alphabetical"))}
  end

  def handle_event("add_discovered_device", params, socket) do
    identifier = Jason.decode!(params["identifier"])
    discovered = Enum.find(socket.assigns.discovered_devices, &(&1.identifier == identifier))

    prefill_params = %{
      "name" => discovered.hostname || "",
      "ip_address" => List.first(discovered.ip_addresses) || "",
      "snmp_enabled" => "true"
    }

    {:noreply, push_navigate(socket, to: ~p"/devices/new?#{prefill_params}")}
  end

  defp perform_site_reorder(socket, site_id, new_position, organization_id) do
    position = if is_integer(new_position), do: new_position, else: String.to_integer(new_position)
    result = Sites.reorder_site(site_id, position)
    perform_reorder(socket, result, "site", organization_id)
  end

  defp perform_device_reorder(socket, device_id, new_position, organization_id) do
    position = if is_integer(new_position), do: new_position, else: String.to_integer(new_position)
    result = Devices.reorder_device(device_id, position)
    perform_reorder(socket, result, "device", organization_id)
  end

  defp perform_reorder(socket, reorder_result, resource_name, organization_id) do
    case reorder_result do
      {:ok, _} ->
        # Reload devices with updated order
        devices = Devices.list_organization_devices(organization_id)

        success_message =
          case resource_name do
            "site" -> t_equipment("Site order updated")
            "device" -> t_equipment("Device order updated")
          end

        {:noreply,
         socket
         |> assign(:has_devices, devices != [])
         |> stream(:device_rows, build_device_rows(devices), reset: true)
         |> put_flash(:info, success_message)}

      {:error, _changeset} ->
        error_message =
          case resource_name do
            "site" -> t_equipment("Failed to reorder site")
            "device" -> t_equipment("Failed to reorder device")
          end

        {:noreply, put_flash(socket, :error, error_message)}
    end
  end

  # Periodic tick to refresh "time ago" timestamps between poll events
  @impl true
  def handle_info(:tick, socket) do
    schedule_tick()

    if socket.assigns.active_tab == "existing" do
      {:noreply, schedule_debounced_reload(socket, :tick)}
    else
      {:noreply, socket}
    end
  end

  # Structural changes (create/delete) need full reload with quota recalculation
  @impl true
  def handle_info({event, _org_id}, socket) when event in [:device_created, :device_deleted] do
    {:noreply, reload_devices(socket)}
  end

  # Status changes and updates are debounced to avoid redundant reloads
  # when many devices change status simultaneously (e.g., network event)
  @impl true
  def handle_info({event, _org_id}, socket) when event in [:device_status_changed, :device_updated] do
    {:noreply, schedule_debounced_reload(socket, event)}
  end

  @impl true
  def handle_info(:debounced_device_reload, socket) do
    {:noreply, reload_device_stream(socket)}
  end

  @tick_interval_ms if Mix.env() == :test, do: :infinity, else: to_timeout(second: 15)

  defp schedule_tick do
    if @tick_interval_ms != :infinity do
      Process.send_after(self(), :tick, @tick_interval_ms)
    end
  end

  @debounce_ms if Mix.env() == :test, do: 0, else: 100

  defp schedule_debounced_reload(socket, event) do
    # Cancel any pending debounced reload
    if ref = socket.assigns.pending_reload_ref do
      Process.cancel_timer(ref)
    end

    # For status changes, skip reload entirely if viewing the discovered tab
    if event == :device_status_changed && socket.assigns.active_tab == "discovered" do
      assign(socket, :pending_reload_ref, nil)
    else
      if @debounce_ms == 0 do
        # In test: reload immediately so render(view) sees the update
        reload_device_stream(socket)
      else
        ref = Process.send_after(self(), :debounced_device_reload, @debounce_ms)
        assign(socket, :pending_reload_ref, ref)
      end
    end
  end

  # Full reload: stream + quota (for structural changes like create/delete)
  defp reload_devices(socket) do
    organization = socket.assigns.current_scope.organization
    devices = Devices.list_organization_devices(organization.id)
    {current, limit} = SubscriptionLimits.device_quota(organization)

    socket
    |> stream(:device_rows, build_device_rows(devices), reset: true)
    |> assign(:has_devices, devices != [])
    |> assign(:device_quota, %{current: current, limit: limit})
    |> assign(:site_subscribers, load_site_subscribers(devices))
  end

  # Lightweight reload: stream only, skip quota query (for status/update changes)
  defp reload_device_stream(socket) do
    organization = socket.assigns.current_scope.organization
    devices = Devices.list_organization_devices(organization.id)

    socket
    |> stream(:device_rows, build_device_rows(devices), reset: true)
    |> assign(:has_devices, devices != [])
    |> assign(:pending_reload_ref, nil)
  end

  defp enqueue_discovery(device_id) do
    if Application.get_env(:towerops, :env) == :test do
      _ = Task.start(fn -> Snmp.discover_device(Devices.get_device!(device_id)) end)
    else
      DiscoveryWorker.enqueue(device_id)
    end
  end

  defp build_device_rows(devices) do
    device_ids = Enum.map(devices, & &1.id)
    health_data = Monitoring.get_device_health_summary(device_ids)
    response_data = Monitoring.get_device_latest_response_times(device_ids)
    subscriber_data = Gaiia.get_device_subscriber_counts(device_ids)

    devices
    |> group_devices_by_site()
    |> Enum.flat_map(fn {site, site_devices, stats} ->
      site_id = if site, do: site.id, else: "no-site"

      site_row = %{
        id: "site-header-#{site_id}",
        type: :site_header,
        site: site,
        stats: stats,
        devices: site_devices
      }

      device_rows =
        Enum.with_index(site_devices, fn device, idx ->
          %{
            id: "device-#{device.id}",
            type: :device,
            device: device,
            site: site,
            device_index: idx,
            health: Map.get(health_data, device.id),
            response: Map.get(response_data, device.id),
            subscribers: Map.get(subscriber_data, device.id)
          }
        end)

      [site_row | device_rows]
    end)
  end

  defp group_devices_by_site(devices) do
    devices
    |> Enum.group_by(& &1.site)
    |> Enum.map(fn {site, site_devices} ->
      {site, site_devices, calculate_site_stats(site_devices)}
    end)
    |> Enum.sort_by(
      fn {site, _devices, _stats} ->
        if site do
          {site.display_order || 999_999, site.name}
        else
          {0, ""}
        end
      end,
      :asc
    )
  end

  defp calculate_site_stats(devices) do
    status_counts = Enum.frequencies_by(devices, & &1.status)

    %{
      total: length(devices),
      up: Map.get(status_counts, :up, 0),
      down: Map.get(status_counts, :down, 0),
      unknown: Map.get(status_counts, :unknown, 0)
    }
  end

  defp device_type_icon(:router), do: "hero-signal"
  defp device_type_icon(:switch), do: "hero-squares-2x2"
  defp device_type_icon(:wireless), do: "hero-wifi"
  defp device_type_icon(:server), do: "hero-server"
  defp device_type_icon(:workstation), do: "hero-computer-desktop"
  defp device_type_icon(_), do: "hero-question-mark-circle"

  defp device_type_label(type) do
    type |> to_string() |> String.capitalize()
  end

  defp load_site_subscribers(devices) do
    devices
    |> Enum.map(& &1.site_id)
    |> Enum.uniq()
    |> Enum.reject(&is_nil/1)
    |> Map.new(fn site_id -> {site_id, Gaiia.get_site_subscriber_summary(site_id)} end)
  end

  # Health indicator helpers

  defp health_dot_color(nil), do: "bg-gray-400"
  defp health_dot_color(%{worst_status: 0}), do: "bg-green-500"
  defp health_dot_color(%{worst_status: 1}), do: "bg-yellow-500"
  defp health_dot_color(%{worst_status: 2}), do: "bg-red-500"
  defp health_dot_color(%{worst_status: 3}), do: "bg-gray-400"
  defp health_dot_color(_), do: "bg-gray-400"

  defp health_dot_title(nil), do: "No check data"
  defp health_dot_title(%{worst_status: 0}), do: "All checks passing"
  defp health_dot_title(%{worst_status: 1}), do: "Warning"
  defp health_dot_title(%{worst_status: 2}), do: "Critical"
  defp health_dot_title(%{worst_status: 3}), do: "Unknown"
  defp health_dot_title(_), do: "No check data"

  defp last_seen_text(nil), do: {"Never", "text-gray-400 dark:text-gray-600"}

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

    cond do
      diff_seconds < 0 -> {"just now", "text-green-600 dark:text-green-400"}
      diff_seconds < 60 -> {"#{diff_seconds}s ago", "text-green-600 dark:text-green-400"}
      diff_seconds < 600 -> {"#{div(diff_seconds, 60)}m ago", "text-green-600 dark:text-green-400"}
      diff_seconds < 3600 -> {"#{div(diff_seconds, 60)}m ago", "text-yellow-600 dark:text-yellow-400"}
      diff_seconds < 86400 -> {"#{div(diff_seconds, 3600)}h ago", "text-red-600 dark:text-red-400"}
      true -> {"#{div(diff_seconds, 86400)}d ago", "text-red-600 dark:text-red-400"}
    end
  end

  defp response_time_badge(nil), do: {"-", "text-gray-400"}

  defp response_time_badge(%{response_time_ms: nil}), do: {"-", "text-gray-400"}

  defp response_time_badge(%{response_time_ms: ms}) do
    formatted =
      cond do
        ms < 1 -> "<1ms"
        ms < 1000 -> "#{round(ms)}ms"
        true -> "#{Float.round(ms / 1000, 1)}s"
      end

    color =
      cond do
        ms < 20 -> "text-green-600 dark:text-green-400"
        ms <= 100 -> "text-yellow-600 dark:text-yellow-400"
        true -> "text-red-600 dark:text-red-400"
      end

    {formatted, color}
  end

  defp format_subscriber_count(nil), do: nil

  defp format_subscriber_count(%{subscriber_count: count}) when count > 0 do
    "#{count} subs"
  end

  defp format_subscriber_count(_), do: nil

  # Generate pagination range with ellipsis for large page counts
  # Shows: [1] ... [4] [5] [6] ... [20] when on page 5 of 20
  defp pagination_range(current_page, total_pages) do
    cond do
      total_pages <= 7 ->
        # Show all pages if 7 or fewer
        Enum.to_list(1..total_pages)

      current_page <= 4 ->
        # Near the start
        [1, 2, 3, 4, 5, :ellipsis, total_pages]

      current_page >= total_pages - 3 ->
        # Near the end
        [1, :ellipsis, total_pages - 4, total_pages - 3, total_pages - 2, total_pages - 1, total_pages]

      true ->
        # In the middle
        [1, :ellipsis, current_page - 1, current_page, current_page + 1, :ellipsis, total_pages]
    end
  end
end
