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

  alias Towerops.Devices
  alias Towerops.Monitoring
  alias Towerops.Snmp

  # 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
  }

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

  @impl true
  def handle_params(%{"id" => id} = params, _, socket) do
    _ =
      if connected?(socket) do
        _ = Phoenix.PubSub.subscribe(Towerops.PubSub, "device:#{id}")
        Process.send_after(self(), :refresh_data, 10_000)
      end

    tab = Map.get(params, "tab", "overview")

    socket
    |> load_equipment_data(id)
    |> assign(:active_tab, tab)
    |> then(&{:noreply, &1})
  end

  @impl true
  def handle_info(:refresh_data, socket) do
    Process.send_after(self(), :refresh_data, 10_000)
    {:noreply, load_equipment_data(socket, socket.assigns.device.id)}
  end

  @impl true
  def handle_info({:device_status_changed, _device_id, _new_status, _response_time}, socket) do
    {:noreply, load_equipment_data(socket, socket.assigns.device.id)}
  end

  @impl true
  def handle_info({:discovery_completed, _device_id}, socket) do
    {:noreply,
     socket
     |> load_equipment_data(socket.assigns.device.id)
     |> put_flash(:info, "Discovery completed")}
  end

  @impl true
  def handle_info({:sensors_updated, _device_id}, socket) do
    {:noreply, load_equipment_data(socket, socket.assigns.device.id)}
  end

  @impl true
  def handle_info({:interfaces_updated, _device_id}, socket) do
    {:noreply, load_equipment_data(socket, socket.assigns.device.id)}
  end

  @impl true
  def handle_info({:neighbors_updated, _device_id}, socket) do
    {:noreply, load_equipment_data(socket, socket.assigns.device.id)}
  end

  @impl true
  def handle_info({:monitoring_check_updated, _device_id}, socket) do
    {:noreply, load_equipment_data(socket, socket.assigns.device.id)}
  end

  @impl true
  def handle_info({:device_event, _event_attrs}, socket) do
    # Device event logged (interface changes, sensor thresholds, etc.)
    # Reload to update events list
    {:noreply, load_equipment_data(socket, socket.assigns.device.id)}
  end

  # Private functions

  defp load_equipment_data(socket, device_id) do
    device = Devices.get_device!(device_id)
    recent_checks = Monitoring.list_devices_checks(device_id, 50)
    snmp_data = load_snmp_data(device_id)
    events = Devices.list_devices_events(device_id, 100)
    neighbors = Snmp.list_neighbors_with_equipment(device_id)
    latency_chart_data = load_latency_chart_data(device_id)
    cpu_chart_data = load_sensor_chart_data(device_id, ["cpu_load"])
    memory_chart_data = load_sensor_chart_data(device_id, ["memory_usage"])
    storage_chart_data = load_sensor_chart_data(device_id, ["disk_usage"])
    traffic_chart_data = load_overall_traffic_chart_data(device_id)

    # Filter sensors by type for temperature, voltage, and storage
    temperature_sensors =
      Enum.filter(snmp_data.sensors, &(&1.sensor_type in ["temperature", "cpu_temperature", "celsius"]))

    voltage_sensors = Enum.filter(snmp_data.sensors, &(&1.sensor_type in ["voltage", "volts"]))

    storage_sensors = Enum.filter(snmp_data.sensors, &(&1.sensor_type in ["disk_usage"]))

    # Calculate metrics for dashboard
    metrics = calculate_metrics(recent_checks, device)

    # Group interfaces by type for the ports tab
    interfaces_by_type = group_interfaces_by_type(snmp_data.interfaces)

    socket
    |> assign(:page_title, device.name)
    |> assign(:device, device)
    |> assign(:recent_checks, recent_checks)
    |> assign(:metrics, metrics)
    |> assign(:snmp_device, snmp_data.device)
    |> assign(:snmp_interfaces, snmp_data.interfaces)
    |> assign(:interfaces_by_type, interfaces_by_type)
    |> assign(:snmp_sensors, snmp_data.sensors)
    |> assign(:neighbors, neighbors)
    |> assign(:events, events)
    |> assign(:latency_chart_data, latency_chart_data)
    |> assign(:cpu_chart_data, cpu_chart_data)
    |> assign(:memory_chart_data, memory_chart_data)
    |> assign(:storage_chart_data, storage_chart_data)
    |> assign(:traffic_chart_data, traffic_chart_data)
    |> assign(:temperature_sensors, temperature_sensors)
    |> assign(:voltage_sensors, voltage_sensors)
    |> assign(:storage_sensors, storage_sensors)
  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_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_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

  defp aggregate_interface_traffic_stats(interfaces, since) do
    interfaces
    |> Enum.flat_map(fn interface ->
      stats =
        interface.id
        |> Snmp.get_interface_stats(since: since, limit: 1000)
        |> Enum.reverse()

      Enum.map(stats, &interface_stat_to_tuple/1)
    end)
    |> Enum.group_by(&elem(&1, 0))
    |> Enum.map(&sum_traffic_stats_by_timestamp/1)
    |> Enum.sort_by(&elem(&1, 0), DateTime)
  end

  defp interface_stat_to_tuple(stat) do
    {stat.checked_at, stat.if_in_octets, stat.if_out_octets}
  end

  defp sum_traffic_stats_by_timestamp({timestamp, stats}) do
    total_in = Enum.reduce(stats, 0, fn {_, in_octets, _}, acc -> acc + (in_octets || 0) end)
    total_out = Enum.reduce(stats, 0, fn {_, _, out_octets}, acc -> acc + (out_octets || 0) end)
    {timestamp, total_in, total_out}
  end

  defp build_traffic_json_datasets(all_stats) do
    in_data = calculate_bps_data(all_stats, :inbound)
    out_data = calculate_bps_data(all_stats, :outbound)

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

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

  defp calculate_bps_data(all_stats, :inbound) do
    all_stats
    |> Enum.chunk_every(2, 1, :discard)
    |> Enum.map(fn [{t1, in1, _out1}, {t2, in2, _out2}] ->
      time_diff = t2 |> DateTime.diff(t1, :second) |> max(1)
      bps = ((in2 - in1) * 8 / time_diff) |> max(0) |> :erlang.float() |> Float.round(2)

      %{
        x: DateTime.to_unix(t2, :millisecond),
        y: bps
      }
    end)
  end

  defp calculate_bps_data(all_stats, :outbound) do
    all_stats
    |> Enum.chunk_every(2, 1, :discard)
    |> Enum.map(fn [{t1, _in1, out1}, {t2, _in2, out2}] ->
      time_diff = t2 |> DateTime.diff(t1, :second) |> max(1)
      bps = ((out2 - out1) * 8 / time_diff) |> max(0) |> :erlang.float() |> Float.round(2)

      %{
        x: DateTime.to_unix(t2, :millisecond),
        y: -bps
      }
    end)
  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
end
