defmodule Towerops.Devices do
  @moduledoc """
  The Devices context.
  """

  import Ecto.Query

  alias Towerops.Devices.Device, as: DeviceSchema
  alias Towerops.Devices.Event
  alias Towerops.Repo

  @doc """
  Returns the list of devices for a site.
  """
  def list_site_devices(site_id) do
    Repo.all(from(e in DeviceSchema, where: e.site_id == ^site_id, order_by: [asc: e.name]))
  end

  @doc """
  Returns the list of all devices for an organization (via sites).

  Supports filtering by:
  - site_id: Filter by specific site
  - status: Filter by status ("up", "down", "unknown")
  """
  def list_organization_devices(organization_id, filters \\ %{}) do
    query =
      from(e in DeviceSchema,
        join: s in assoc(e, :site),
        where: s.organization_id == ^organization_id,
        order_by: [asc: e.name],
        preload: [site: s]
      )

    query =
      if site_id = filters["site_id"] do
        where(query, [e], e.site_id == ^site_id)
      else
        query
      end

    query =
      if status = filters["status"] do
        where(query, [e], e.status == ^status)
      else
        query
      end

    Repo.all(query)
  end

  @doc """
  Returns the count of devices for an organization.
  """
  def count_organization_devices(organization_id) do
    Repo.aggregate(
      from(e in DeviceSchema,
        join: s in assoc(e, :site),
        where: s.organization_id == ^organization_id
      ),
      :count
    )
  end

  @doc """
  Returns the count of devices for a site.
  """
  def count_site_devices(site_id) do
    Repo.aggregate(
      from(e in DeviceSchema, where: e.site_id == ^site_id),
      :count
    )
  end

  @doc """
  Returns the count of devices that is down for a site.
  """
  def count_site_devices_down(site_id) do
    Repo.aggregate(
      from(e in DeviceSchema, where: e.site_id == ^site_id and e.status == "down"),
      :count
    )
  end

  @doc """
  Returns the list of all devices with monitoring enabled.
  """
  def list_monitored_devices do
    Repo.all(
      from(e in DeviceSchema,
        where: e.monitoring_enabled == true,
        order_by: [asc: e.name]
      )
    )
  end

  @doc """
  Returns the list of all devices with SNMP enabled.
  """
  def list_snmp_enabled_devices do
    Repo.all(
      from(e in DeviceSchema,
        where: e.snmp_enabled == true,
        order_by: [asc: e.name]
      )
    )
  end

  @doc """
  Gets a single device by ID, returns nil if not found.
  """
  def get_device(id) do
    DeviceSchema
    |> Repo.get(id)
    |> case do
      nil -> nil
      device -> Repo.preload(device, [:site])
    end
  end

  @doc """
  Gets a single device by ID, raises if not found.
  """
  def get_device!(id) do
    DeviceSchema
    |> Repo.get!(id)
    |> Repo.preload([:site])
  end

  @doc """
  Gets device with full details including SNMP device, interfaces, and sensors.
  """
  def get_device_with_details(id) do
    DeviceSchema
    |> Repo.get(id)
    |> case do
      nil ->
        nil

      device ->
        Repo.preload(device, [:site, snmp_device: [:interfaces, :sensors]])
    end
  end

  @doc """
  Gets a single device belonging to a specific site.
  """
  def get_site_device!(site_id, device_id) do
    Repo.one!(from(e in DeviceSchema, where: e.id == ^device_id, where: e.site_id == ^site_id, preload: [:site]))
  end

  @doc """
  Gets SNMP configuration for device with hierarchical fallback.

  Falls back in this order:
  1. Device-level configuration
  2. Site-level configuration
  3. Organization-level configuration

  Returns a map with:
  - version: SNMP version ("1", "2c", or "3")
  - community: SNMP community string
  - source: Where the config came from (:device, :site, or :organization)
  """
  def get_snmp_config(device_id) when is_binary(device_id) do
    device =
      DeviceSchema
      |> Repo.get!(device_id)
      |> Repo.preload(site: :organization)

    get_snmp_config(device)
  end

  def get_snmp_config(%DeviceSchema{} = device) do
    device = ensure_snmp_config_loaded(device)
    resolve_snmp_config(device)
  end

  defp ensure_snmp_config_loaded(device) do
    # Ensure site and organization associations are loaded
    needs_preload =
      !Ecto.assoc_loaded?(device.site) ||
        !Ecto.assoc_loaded?(device.site.organization)

    if needs_preload do
      Repo.preload(device, site: :organization)
    else
      device
    end
  end

  defp resolve_snmp_config(device) do
    cond do
      device.snmp_community != nil ->
        build_snmp_config(device.snmp_version, device.snmp_community, :device)

      device.site.snmp_community != nil ->
        build_snmp_config(device.site.snmp_version, device.site.snmp_community, :site)

      device.site.organization.snmp_community != nil ->
        build_snmp_config(
          device.site.organization.snmp_version,
          device.site.organization.snmp_community,
          :organization
        )

      true ->
        %{version: "2c", community: nil, source: :default}
    end
  end

  defp build_snmp_config(version, community, source) do
    %{
      version: version || "2c",
      community: community,
      source: source
    }
  end

  @doc """
  Creates device.
  """
  def create_device(attrs) do
    %DeviceSchema{}
    |> DeviceSchema.changeset(attrs)
    |> Repo.insert()
  end

  @doc """
  Updates device.
  """
  def update_device(%DeviceSchema{} = device, attrs) do
    device
    |> DeviceSchema.changeset(attrs)
    |> Repo.update()
  end

  @doc """
  Deletes device.
  """
  def delete_device(%DeviceSchema{} = device) do
    # Stop monitoring workers before deleting
    Towerops.Monitoring.Supervisor.stop_monitor(device.id)
    Towerops.Monitoring.Supervisor.stop_snmp_poller(device.id)

    Repo.delete(device)
  end

  @doc """
  Returns an `%Ecto.Changeset{}` for tracking device changes.
  """
  def change_device(%DeviceSchema{} = device, attrs \\ %{}) do
    DeviceSchema.changeset(device, attrs)
  end

  @doc """
  Updates the status of device.
  """
  def update_device_status(%DeviceSchema{} = device, new_status) do
    now = DateTime.truncate(DateTime.utc_now(), :second)

    changes = %{
      status: new_status,
      last_checked_at: now
    }

    changes =
      if device.status == new_status do
        changes
      else
        Map.put(changes, :last_status_change_at, now)
      end

    device
    |> Ecto.Changeset.change(changes)
    |> Repo.update()
  end

  @doc """
  Updates the last SNMP poll timestamp for device.
  Used for distributed coordination to prevent duplicate polling across pods.
  """
  def update_snmp_poll_time(%DeviceSchema{} = device) do
    now = DateTime.truncate(DateTime.utc_now(), :second)

    device
    |> Ecto.Changeset.change(%{last_snmp_poll_at: now})
    |> Repo.update()
  end

  ## Events

  @doc """
  Creates an device event.
  """
  def create_event(attrs) do
    %Event{}
    |> Event.changeset(attrs)
    |> Repo.insert()
  end

  @doc """
  Returns the list of events for device, ordered by most recent first.
  """
  def list_devices_events(device_id, limit \\ 100) do
    Repo.all(
      from(e in Event,
        where: e.device_id == ^device_id,
        order_by: [desc: e.occurred_at],
        limit: ^limit
      )
    )
  end
end
