defmodule Towerops.Preseem.Baseline do
  @moduledoc """
  Computes rolling 14-day baselines for individual Preseem access points.
  """
  import Ecto.Query

  alias Towerops.Preseem.AccessPoint
  alias Towerops.Preseem.DeviceBaseline
  alias Towerops.Preseem.SubscriberMetric
  alias Towerops.Repo

  @baseline_days 14
  @metrics ~w(avg_latency avg_jitter avg_loss avg_throughput p95_latency subscriber_count)

  @doc "Compute baselines for all matched APs in an organization."
  def compute_baselines(organization_id) do
    cutoff = DateTime.add(DateTime.utc_now(), -@baseline_days * 86_400, :second)
    now = DateTime.truncate(DateTime.utc_now(), :second)

    matched_aps =
      AccessPoint
      |> where(organization_id: ^organization_id)
      |> where([ap], not is_nil(ap.device_id))
      |> Repo.all()

    count =
      Enum.reduce(matched_aps, 0, fn ap, acc ->
        metrics = get_metrics_in_window(ap.id, cutoff)

        if length(metrics) >= 3 do
          compute_and_upsert_baselines(ap.id, metrics, now)
          acc + 1
        else
          acc
        end
      end)

    {:ok, count}
  end

  defp get_metrics_in_window(ap_id, cutoff) do
    SubscriberMetric
    |> where(preseem_access_point_id: ^ap_id)
    |> where([m], m.recorded_at >= ^cutoff)
    |> order_by(:recorded_at)
    |> Repo.all()
  end

  defp compute_and_upsert_baselines(ap_id, metrics, now) do
    for metric_name <- @metrics do
      values =
        metrics
        |> Enum.map(&Map.get(&1, String.to_existing_atom(metric_name)))
        |> Enum.reject(&is_nil/1)

      if length(values) >= 3 do
        stats = compute_stats(values)

        upsert_baseline(ap_id, %{
          metric_name: metric_name,
          period: "all",
          mean: stats.mean,
          stddev: stats.stddev,
          p5: stats.p5,
          p95: stats.p95,
          sample_count: length(values),
          computed_at: now
        })
      end
    end
  end

  defp compute_stats(values) do
    sorted = Enum.sort(values)
    n = length(sorted)
    mean = Enum.sum(sorted) / n

    variance =
      sorted
      |> Enum.map(fn v -> (v - mean) * (v - mean) end)
      |> Enum.sum()
      |> Kernel./(max(n - 1, 1))

    stddev = :math.sqrt(variance)
    p5_idx = max(round(n * 0.05) - 1, 0)
    p95_idx = min(round(n * 0.95) - 1, n - 1)

    %{
      mean: Float.round(mean / 1, 4),
      stddev: Float.round(stddev / 1, 4),
      p5: sorted |> Enum.at(p5_idx) |> to_float(),
      p95: sorted |> Enum.at(p95_idx) |> to_float()
    }
  end

  defp to_float(v) when is_float(v), do: Float.round(v, 4)
  defp to_float(v) when is_integer(v), do: v * 1.0

  defp upsert_baseline(ap_id, attrs) do
    %DeviceBaseline{}
    |> DeviceBaseline.changeset(Map.put(attrs, :preseem_access_point_id, ap_id))
    |> Repo.insert(
      on_conflict: {:replace_all_except, [:id, :preseem_access_point_id]},
      conflict_target: [:preseem_access_point_id, :metric_name, :period]
    )
  end
end
