defmodule Towerops.Monitoring.DeviceMonitor do
  @moduledoc """
  GenServer that monitors a single piece of device by periodically pinging it.
  """
  use GenServer

  alias Towerops.Alerts
  alias Towerops.Devices
  alias Towerops.Monitoring

  require Logger

  # Allow dependency injection for testing
  @ping_module Application.compile_env(:towerops, :ping_module, Towerops.Monitoring.Ping)

  # Suppress warnings for Mox modules that are defined at runtime during tests
  @compile {:no_warn_undefined, Towerops.Monitoring.PingMock}

  # Client API

  @doc """
  Starts a monitor for the given device ID.
  """
  def start_link(device_id) do
    GenServer.start_link(__MODULE__, device_id, name: via_tuple(device_id))
  end

  @doc """
  Triggers an immediate check for the device.
  Cluster-aware: finds the monitor regardless of which node it's on.
  """
  def trigger_check(device_id) do
    # Check if monitor process is running (cluster-aware lookup)
    case Horde.Registry.lookup(Towerops.Monitoring.Registry, device_id) do
      [{_pid, _}] ->
        # Process exists, send cast
        GenServer.cast(via_tuple(device_id), :check_now)

      [] ->
        # Process doesn't exist (monitoring disabled), enqueue check job
        enqueue_check(device_id)
    end
  end

  # Server Callbacks

  @impl true
  def init(device_id) do
    device = Devices.get_device!(device_id)

    if device.monitoring_enabled do
      # Perform immediate check when monitoring starts
      send(self(), :check_device)
    end

    {:ok, %{device_id: device_id}}
  end

  @impl true
  def handle_info(:check_device, state) do
    _ = perform_check(state.device_id)
    {:noreply, state}
  end

  @impl true
  def handle_cast(:check_now, state) do
    _ = perform_check(state.device_id)
    {:noreply, state}
  end

  # Private Functions

  defp perform_check(device_id) do
    device = Devices.get_device!(device_id)

    # For SNMP-enabled devices, use recent SNMP poll success as health indicator
    # For ICMP-only devices, use ping
    check_result = check_device_health(device)

    now = DateTime.truncate(DateTime.utc_now(), :second)

    {status, response_time_ms} =
      case check_result do
        {:ok, time} -> {:success, time}
        {:error, _reason} -> {:failure, nil}
      end

    # Save the check result
    case Monitoring.create_check(%{
           device_id: device_id,
           status: status,
           response_time_ms: response_time_ms,
           checked_at: now
         }) do
      {:ok, _check} ->
        :ok

      {:error, changeset} ->
        Logger.error("Failed to create monitoring check for device #{device_id}: #{inspect(changeset.errors)}")
    end

    # device status if it changed
    new_status = if status == :success, do: :up, else: :down
    old_status = device.status

    _ = Devices.update_device_status(device, new_status)

    # Create alerts if status changed
    _ =
      if old_status != new_status do
        handle_status_change(device, old_status, new_status)
      end

    # Broadcast status change via PubSub
    _ =
      Phoenix.PubSub.broadcast(
        Towerops.PubSub,
        "device:#{device_id}",
        {:device_status_changed, device_id, new_status, nil}
      )

    # Only schedule next check if monitoring is enabled
    if device.monitoring_enabled do
      schedule_next_check(device.check_interval_seconds)
    end
  end

  defp check_device_health(device) do
    # Always perform actual ICMP ping to measure real latency
    # Don't rely solely on SNMP poll success since that doesn't give us latency data
    @ping_module.ping(device.ip_address)
  end

  defp handle_status_change(device, old_status, new_status) do
    now = DateTime.truncate(DateTime.utc_now(), :second)

    case {old_status, new_status} do
      {_, :down} ->
        handle_equipment_down(device, now)

      {_, :up} ->
        handle_equipment_up(device, now)
    end
  end

  defp handle_equipment_down(device, now) do
    if Alerts.has_active_alert?(device.id, :device_down) do
      :ok
    else
      create_device_down_alert(device, now)
    end
  end

  defp create_device_down_alert(device, now) do
    alert_message = get_down_alert_message(device)

    {:ok, _alert} =
      Alerts.create_alert(%{
        device_id: device.id,
        alert_type: :device_down,
        triggered_at: now,
        message: alert_message
      })

    Phoenix.PubSub.broadcast(
      Towerops.PubSub,
      "alerts:new",
      {:new_alert, device.id, :device_down}
    )
  end

  defp get_down_alert_message(device) do
    if device.snmp_enabled do
      "Device is not responding to SNMP"
    else
      "Device is not responding to ping"
    end
  end

  defp handle_equipment_up(device, now) do
    recovery_message = get_recovery_message(device)

    {:ok, _alert} =
      Alerts.create_alert(%{
        device_id: device.id,
        alert_type: :device_up,
        triggered_at: now,
        message: recovery_message
      })

    resolve_down_alert(device)

    Phoenix.PubSub.broadcast(
      Towerops.PubSub,
      "alerts:resolved",
      {:alert_resolved, device.id, :device_down}
    )
  end

  defp get_recovery_message(device) do
    if device.snmp_enabled do
      "Device is now responding to SNMP"
    else
      "Device is now responding to ping"
    end
  end

  defp resolve_down_alert(device) do
    case Alerts.get_active_alert(device.id, :device_down) do
      nil -> :ok
      alert -> Alerts.resolve_alert(alert)
    end
  end

  defp schedule_next_check(interval_seconds) do
    Process.send_after(self(), :check_device, interval_seconds * 1000)
  end

  # Enqueue monitoring check job - safe to call in test environment
  defp enqueue_check(device_id) do
    if Application.get_env(:towerops, :env) == :test do
      # In test, run synchronously
      Task.start(fn -> perform_check(device_id) end)
    else
      # In dev/prod, enqueue to Exq
      {:ok, _job} = Exq.enqueue(Exq, "monitoring", Towerops.Workers.MonitorWorker, [device_id])
    end
  end

  defp via_tuple(device_id) do
    {:via, Horde.Registry, {Towerops.Monitoring.Registry, device_id}}
  end
end
