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

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

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

  @impl true
  def handle_params(%{"id" => device_id, "sensor_type" => sensor_type} = params, _, socket) do
    _ =
      if connected?(socket) do
        Phoenix.PubSub.subscribe(Towerops.PubSub, "device:#{device_id}")
      end

    range = Map.get(params, "range", "24h")
    interface_id = Map.get(params, "interface_id")

    socket =
      socket
      |> assign(:device_id, device_id)
      |> assign(:sensor_type, sensor_type)
      |> assign(:range, range)
      |> assign(:interface_id, interface_id)
      |> load_graph_data()

    {:noreply, socket}
  end

  @impl true
  def handle_event("change_range", %{"range" => range}, socket) do
    params = build_graph_params(socket.assigns, range)

    {:noreply,
     push_patch(socket,
       to: ~p"/devices/#{socket.assigns.device_id}/graph/#{socket.assigns.sensor_type}?#{params}"
     )}
  end

  defp build_graph_params(assigns, range) do
    base_params = %{"range" => range}

    if assigns.interface_id do
      Map.put(base_params, "interface_id", assigns.interface_id)
    else
      base_params
    end
  end

  # Private functions

  defp load_graph_data(socket) do
    device_id = socket.assigns.device_id
    sensor_type = socket.assigns.sensor_type
    range = socket.assigns.range
    interface_id = socket.assigns.interface_id

    device = Devices.get_device!(device_id)

    {chart_data, title_suffix} =
      cond do
        sensor_type == "latency" ->
          {load_latency_chart_data(device_id, range), nil}

        sensor_type == "traffic" && interface_id ->
          {load_interface_traffic_chart_data(interface_id, range), get_interface_name(interface_id)}

        sensor_type == "traffic" ->
          {load_traffic_chart_data(device_id, range), nil}

        true ->
          sensor_types = get_sensor_types_for_chart(sensor_type)
          {load_sensor_chart_data(device_id, sensor_types, range), nil}
      end

    {title, unit, auto_scale} = get_chart_config(sensor_type)
    chart_title = if title_suffix, do: "#{title} - #{title_suffix}", else: title
    show_zero_line = sensor_type == "traffic"

    socket
    |> assign(:device, device)
    |> assign(:page_title, "#{device.name} - #{chart_title}")
    |> assign(:chart_title, chart_title)
    |> assign(:chart_data, chart_data)
    |> assign(:unit, unit)
    |> assign(:auto_scale, auto_scale)
    |> assign(:show_zero_line, show_zero_line)
  end

  defp get_sensor_types_for_chart("processors"), do: ["cpu_load"]
  defp get_sensor_types_for_chart("memory"), do: ["memory_usage"]
  defp get_sensor_types_for_chart("storage"), do: ["disk_usage"]
  defp get_sensor_types_for_chart("temperature"), do: ["temperature", "cpu_temperature", "celsius"]
  defp get_sensor_types_for_chart("voltage"), do: ["voltage", "volts"]
  defp get_sensor_types_for_chart("traffic"), do: nil
  defp get_sensor_types_for_chart(_), do: []

  defp get_chart_config("latency"), do: {"ICMP Latency", "ms", true}
  defp get_chart_config("processors"), do: {"Processor Usage", "%", false}
  defp get_chart_config("memory"), do: {"Memory Usage", "%", false}
  defp get_chart_config("storage"), do: {"Storage Usage", "%", false}
  defp get_chart_config("temperature"), do: {"Temperature", "°C", true}
  defp get_chart_config("voltage"), do: {"Voltage", "V", true}
  defp get_chart_config("traffic"), do: {"Overall Traffic", "bps", true}
  defp get_chart_config(_), do: {"Sensor Data", "", false}

  defp load_sensor_chart_data(device_id, sensor_types, range) do
    case Snmp.get_device_with_associations(device_id) do
      nil ->
        nil

      device ->
        build_sensor_chart_json(device, sensor_types, range)
    end
  end

  defp build_sensor_chart_json(device, sensor_types, range) 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
      since = get_datetime_from_range(range)
      limit = get_limit_for_range(range)
      datasets = Enum.map(sensors, &sensor_to_dataset(&1, since, limit))
      Jason.encode!(%{datasets: datasets})
    end
  end

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

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

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

  defp get_datetime_from_range("1h"), do: DateTime.add(DateTime.utc_now(), -1, :hour)
  defp get_datetime_from_range("6h"), do: DateTime.add(DateTime.utc_now(), -6, :hour)
  defp get_datetime_from_range("12h"), do: DateTime.add(DateTime.utc_now(), -12, :hour)
  defp get_datetime_from_range("24h"), do: DateTime.add(DateTime.utc_now(), -24, :hour)
  defp get_datetime_from_range("7d"), do: DateTime.add(DateTime.utc_now(), -7, :day)
  defp get_datetime_from_range("30d"), do: DateTime.add(DateTime.utc_now(), -30, :day)
  defp get_datetime_from_range(_), do: DateTime.add(DateTime.utc_now(), -24, :hour)

  defp get_limit_for_range("1h"), do: 360
  defp get_limit_for_range("6h"), do: 720
  defp get_limit_for_range("12h"), do: 1440
  defp get_limit_for_range("24h"), do: 2880
  defp get_limit_for_range("7d"), do: 10_080
  defp get_limit_for_range("30d"), do: 43_200
  defp get_limit_for_range(_), do: 2880

  defp load_traffic_chart_data(device_id, range) do
    case Snmp.get_device_with_associations(device_id) do
      nil -> nil
      device -> build_traffic_chart_json(device, range)
    end
  end

  defp load_interface_traffic_chart_data(interface_id, range) do
    case Snmp.get_interface(interface_id) do
      nil -> nil
      interface -> build_interface_traffic_chart_json(interface, range)
    end
  end

  defp get_interface_name(interface_id) do
    case Snmp.get_interface(interface_id) do
      nil -> "Unknown Interface"
      interface -> interface.if_name || interface.if_descr || "Interface #{interface.if_index}"
    end
  end

  defp load_latency_chart_data(device_id, range) do
    since = get_datetime_from_range(range)
    limit = get_limit_for_range(range)

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

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

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

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

  defp build_traffic_chart_json(device, range) do
    if Enum.empty?(device.interfaces), do: nil, else: build_traffic_datasets(device, range)
  end

  defp build_traffic_datasets(device, range) do
    since = get_datetime_from_range(range)
    limit = get_limit_for_range(range)
    active_interfaces = Enum.filter(device.interfaces, &(&1.if_oper_status == "up"))

    if Enum.empty?(active_interfaces), do: nil, else: encode_interface_datasets(active_interfaces, since, limit)
  end

  defp encode_interface_datasets(interfaces, since, limit) do
    datasets = build_per_interface_datasets(interfaces, since, limit)
    if Enum.empty?(datasets), do: nil, else: Jason.encode!(%{datasets: datasets})
  end

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

      if Enum.empty?(stats) do
        []
      else
        interface_name = interface.if_name || interface.if_descr
        in_data = calculate_interface_traffic_data(stats, :inbound)
        out_data = calculate_interface_traffic_data(stats, :outbound)

        [
          %{label: "#{interface_name} Out", data: out_data},
          %{label: "#{interface_name} In", data: in_data}
        ]
      end
    end)
  end

  defp build_interface_traffic_chart_json(interface, range) do
    since = get_datetime_from_range(range)
    limit = get_limit_for_range(range)

    stats =
      interface.id
      |> Snmp.get_interface_stats(since: since, limit: limit)
      |> Enum.reverse()

    if Enum.empty?(stats) do
      nil
    else
      in_data = calculate_interface_traffic_data(stats, :inbound)
      out_data = calculate_interface_traffic_data(stats, :outbound)

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

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

  defp calculate_interface_traffic_data(stats, :inbound) do
    stats
    |> Enum.chunk_every(2, 1, :discard)
    |> Enum.map(fn [stat1, stat2] ->
      time_diff = stat2.checked_at |> DateTime.diff(stat1.checked_at, :second) |> max(1)

      bps =
        ((stat2.if_in_octets - stat1.if_in_octets) * 8 / time_diff)
        |> max(0)
        |> :erlang.float()
        |> Float.round(2)

      %{
        x: DateTime.to_unix(stat2.checked_at, :millisecond),
        y: -bps
      }
    end)
  end

  defp calculate_interface_traffic_data(stats, :outbound) do
    stats
    |> Enum.chunk_every(2, 1, :discard)
    |> Enum.map(fn [stat1, stat2] ->
      time_diff = stat2.checked_at |> DateTime.diff(stat1.checked_at, :second) |> max(1)

      bps =
        ((stat2.if_out_octets - stat1.if_out_octets) * 8 / time_diff)
        |> max(0)
        |> :erlang.float()
        |> Float.round(2)

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