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

  import Ecto.Query

  alias Towerops.Alerts.Alert
  alias Towerops.Gaiia.ImpactAnalysis
  alias Towerops.Repo

  @doc """
  Creates an alert for device status change.

  For device_down alerts, automatically computes and stores Gaiia impact analysis
  if the device belongs to an organization with a Gaiia integration.
  """
  @spec create_alert(map()) :: {:ok, Alert.t()} | {:error, Ecto.Changeset.t()}
  def create_alert(attrs) do
    with {:ok, alert} <- Repo.insert(Alert.changeset(%Alert{}, attrs)) do
      maybe_compute_gaiia_impact(alert)
      {:ok, alert}
    end
  end

  defp maybe_compute_gaiia_impact(%Alert{alert_type: :device_down, device_id: device_id} = alert)
       when not is_nil(device_id) do
    device = Towerops.Devices.get_device(device_id)

    if device do
      impact = ImpactAnalysis.analyze_device_impact(device.organization_id, device_id)

      if impact.total_subscribers > 0 do
        store_gaiia_impact(alert, ImpactAnalysis.to_json(impact))
      end
    end
  rescue
    _ -> :ok
  end

  defp maybe_compute_gaiia_impact(_alert), do: :ok

  @doc """
  Returns the list of alerts for an device.
  """
  @spec list_devices_alerts(String.t(), integer()) :: [Alert.t()]
  def list_devices_alerts(device_id, limit \\ 100) do
    Repo.all(
      from(a in Alert,
        where: a.device_id == ^device_id,
        order_by: [desc: a.triggered_at],
        limit: ^limit,
        preload: [:device, :acknowledged_by]
      )
    )
  end

  @doc """
  Returns the list of active (unresolved) alerts for an organization.
  Only returns equipment_down alerts, as equipment_up alerts are informational.
  """
  @spec list_organization_active_alerts(String.t()) :: [Alert.t()]
  def list_organization_active_alerts(organization_id) do
    Repo.all(
      from(a in Alert,
        join: e in assoc(a, :device),
        join: s in assoc(e, :site),
        where: s.organization_id == ^organization_id,
        where: a.alert_type == :device_down,
        where: is_nil(a.resolved_at),
        order_by: [desc: a.triggered_at],
        preload: [:acknowledged_by, device: {e, site: s}]
      )
    )
  end

  @doc """
  Returns the count of active (unresolved) alerts for an organization.
  """
  @spec count_active_alerts(String.t()) :: integer()
  def count_active_alerts(organization_id) do
    Repo.aggregate(
      from(a in Alert,
        join: e in assoc(a, :device),
        join: s in assoc(e, :site),
        where: s.organization_id == ^organization_id,
        where: a.alert_type == :device_down,
        where: is_nil(a.resolved_at)
      ),
      :count
    )
  end

  @doc "Returns the count of active alerts for devices at a specific site."
  def count_site_active_alerts(site_id) do
    Repo.aggregate(
      from(a in Alert,
        join: e in assoc(a, :device),
        where: e.site_id == ^site_id,
        where: a.alert_type == :device_down,
        where: is_nil(a.resolved_at)
      ),
      :count
    )
  end

  @doc "Returns active (unresolved) alerts for devices at a specific site."
  def list_site_active_alerts(site_id) do
    Repo.all(
      from(a in Alert,
        join: e in assoc(a, :device),
        where: e.site_id == ^site_id,
        where: a.alert_type == :device_down,
        where: is_nil(a.resolved_at),
        order_by: [desc: a.triggered_at],
        preload: [device: e]
      )
    )
  end

  @doc """
  Returns the list of all alerts for an organization.

  Accepts either an integer limit or a map of filters:
  - limit (integer): Max number of alerts to return
  - filters (map):
    - status: Filter by status ("active", "acknowledged", "resolved")
    - limit: Max number of alerts to return
  """
  @spec list_organization_alerts(String.t(), integer() | map()) :: [Alert.t()]
  def list_organization_alerts(organization_id, limit) when is_integer(limit) do
    list_organization_alerts(organization_id, %{"limit" => limit})
  end

  def list_organization_alerts(organization_id, filters) when is_map(filters) do
    limit = filters["limit"] || 100

    query =
      from(a in Alert,
        join: e in assoc(a, :device),
        join: s in assoc(e, :site),
        where: s.organization_id == ^organization_id,
        order_by: [desc: a.triggered_at],
        limit: ^limit,
        preload: [device: {e, site: s}, acknowledged_by: []]
      )

    query =
      case filters["status"] do
        "active" -> where(query, [a], is_nil(a.resolved_at) and is_nil(a.acknowledged_at))
        "acknowledged" -> where(query, [a], is_nil(a.resolved_at) and not is_nil(a.acknowledged_at))
        "resolved" -> where(query, [a], not is_nil(a.resolved_at))
        _ -> query
      end

    Repo.all(query)
  end

  @doc """
  Returns alerts for multiple organizations (for GDPR data access).
  Accepts a `since` option to filter alerts created after a specific date.
  """
  def list_alerts_for_organizations(organization_ids, opts \\ []) when is_list(organization_ids) do
    since = Keyword.get(opts, :since)

    query =
      from(a in Alert,
        join: e in assoc(a, :device),
        join: s in assoc(e, :site),
        where: s.organization_id in ^organization_ids,
        order_by: [desc: a.triggered_at],
        preload: [device: {e, site: s}]
      )

    query =
      if since do
        where(query, [a], a.inserted_at >= ^since)
      else
        query
      end

    Repo.all(query)
  end

  @doc """
  Gets a single alert.
        where: s.organization_id == ^organization_id,
        order_by: [desc: a.triggered_at],
        limit: ^limit,
        preload: [device: {e, site: s}, acknowledged_by: []]
      )
    )
  end

  @doc \"""
  Gets a single alert.
  """
  def get_alert!(id) do
    Alert
    |> Repo.get!(id)
    |> Repo.preload([:device, :acknowledged_by])
  end

  @doc """
  Gets a single alert. Returns nil if not found.
  """
  def get_alert(id) do
    case Repo.get(Alert, id) do
      nil -> nil
      alert -> Repo.preload(alert, [:device, :acknowledged_by])
    end
  end

  @doc """
  Acknowledges an alert.
  """
  def acknowledge_alert(alert, user_id) do
    alert
    |> Alert.changeset(%{
      acknowledged_at: DateTime.truncate(DateTime.utc_now(), :second),
      acknowledged_by_id: user_id
    })
    |> Repo.update()
  end

  @doc """
  Resolves an alert.
  """
  def resolve_alert(alert) do
    alert
    |> Alert.changeset(%{resolved_at: DateTime.truncate(DateTime.utc_now(), :second)})
    |> Repo.update()
  end

  @doc """
  Stores Gaiia impact analysis data on an alert record.
  """
  def store_gaiia_impact(%Alert{} = alert, impact_data) when is_map(impact_data) do
    alert
    |> Alert.changeset(%{gaiia_impact: impact_data})
    |> Repo.update()
  end

  @doc """
  Checks if there's an active alert of the same type for the device.
  """
  def has_active_alert?(device_id, alert_type) do
    Repo.exists?(
      from(a in Alert,
        where: a.device_id == ^device_id,
        where: a.alert_type == ^alert_type,
        where: is_nil(a.resolved_at)
      )
    )
  end

  @doc """
  Gets the latest active alert of a specific type for device.
  """
  def get_active_alert(device_id, alert_type) do
    Repo.one(
      from(a in Alert,
        where: a.device_id == ^device_id,
        where: a.alert_type == ^alert_type,
        where: is_nil(a.resolved_at),
        order_by: [desc: a.triggered_at],
        limit: 1
      )
    )
  end

  @doc """
  Checks if there's an active alert for a check.
  """
  def has_active_check_alert?(check_id) do
    Repo.exists?(
      from(a in Alert,
        where: a.check_id == ^check_id,
        where: is_nil(a.resolved_at)
      )
    )
  end

  @doc """
  Resolves all active alerts for a check.
  """
  def resolve_check_alerts(check_id) do
    now = DateTime.truncate(DateTime.utc_now(), :second)

    Repo.update_all(from(a in Alert, where: a.check_id == ^check_id, where: is_nil(a.resolved_at)),
      set: [resolved_at: now]
    )
  end
end
