defmodule Towerops.ConfigChanges.Correlator do
  @moduledoc """
  Performance correlation engine for config change events.

  Compares QoE metrics from Preseem in a window around a config change:
  2 hours before and 4 hours after. Detects significant degradation and
  creates insights when correlation is found.
  """

  import Ecto.Query

  alias Towerops.ConfigChanges.ConfigChangeEvent
  alias Towerops.Preseem.AccessPoint
  alias Towerops.Preseem.Insights
  alias Towerops.Preseem.SubscriberMetric
  alias Towerops.Repo

  require Logger

  @pre_window_hours 2
  @post_window_hours 4

  # Thresholds for "significant" impact
  @latency_increase_threshold 0.20
  @loss_increase_threshold 0.50
  @throughput_drop_threshold 0.15

  @doc """
  Correlate a config change event with QoE metrics.

  Returns `{:ok, :no_impact}`, `{:ok, :no_data}`, or `{:ok, insight}` when
  significant degradation is detected.
  """
  def correlate(%ConfigChangeEvent{} = event) do
    # Find Preseem access points matched to this device
    ap_ids = get_matched_ap_ids(event.device_id)

    if Enum.empty?(ap_ids) do
      {:ok, :no_data}
    else
      pre_start = DateTime.add(event.changed_at, -@pre_window_hours * 3600, :second)
      post_end = DateTime.add(event.changed_at, @post_window_hours * 3600, :second)

      pre_metrics = avg_metrics(ap_ids, pre_start, event.changed_at)
      post_metrics = avg_metrics(ap_ids, event.changed_at, post_end)

      case {pre_metrics, post_metrics} do
        {nil, _} -> {:ok, :no_data}
        {_, nil} -> {:ok, :no_data}
        {pre, post} -> evaluate_impact(event, pre, post)
      end
    end
  end

  @doc """
  Correlate all recent uncorrelated change events for an organization.
  """
  def correlate_recent(organization_id, opts \\ []) do
    hours_back = Keyword.get(opts, :hours_back, 24)
    since = DateTime.add(DateTime.utc_now(), -hours_back * 3600, :second)

    events =
      ConfigChangeEvent
      |> where(organization_id: ^organization_id)
      |> where([e], e.changed_at >= ^since)
      |> Repo.all()

    results = Enum.map(events, &correlate/1)
    {:ok, results}
  end

  # Find Preseem AP IDs matched to a device
  defp get_matched_ap_ids(device_id) do
    AccessPoint
    |> where(device_id: ^device_id)
    |> select([ap], ap.id)
    |> Repo.all()
  end

  # Average metrics for given APs in a time window
  defp avg_metrics(ap_ids, from, to) do
    result =
      SubscriberMetric
      |> where([m], m.preseem_access_point_id in ^ap_ids)
      |> where([m], m.recorded_at >= ^from and m.recorded_at < ^to)
      |> select([m], %{
        avg_latency: avg(m.avg_latency),
        avg_jitter: avg(m.avg_jitter),
        avg_loss: avg(m.avg_loss),
        avg_throughput: avg(m.avg_throughput)
      })
      |> Repo.one()

    if is_nil(result) || is_nil(result.avg_latency) do
      nil
    else
      result
    end
  end

  defp evaluate_impact(event, pre, post) do
    deltas = %{
      latency_delta: safe_pct_change(pre.avg_latency, post.avg_latency),
      jitter_delta: safe_pct_change(pre.avg_jitter, post.avg_jitter),
      loss_delta: safe_pct_change(pre.avg_loss, post.avg_loss),
      throughput_delta: safe_pct_change(pre.avg_throughput, post.avg_throughput)
    }

    significant? =
      deltas.latency_delta > @latency_increase_threshold ||
        deltas.loss_delta > @loss_increase_threshold ||
        deltas.throughput_delta < -@throughput_drop_threshold

    if significant? do
      create_suspect_insight(event, deltas, pre, post)
    else
      {:ok, :no_impact}
    end
  end

  defp create_suspect_insight(event, deltas, pre, post) do
    severity = determine_severity(deltas)

    attrs = %{
      organization_id: event.organization_id,
      device_id: event.device_id,
      type: "suspect_config_change",
      urgency: severity,
      channel: "proactive",
      source: "system",
      title: "Config change may have degraded performance",
      description: build_description(deltas, event),
      metadata: %{
        "change_event_id" => event.id,
        "metric_deltas" => %{
          "latency_pct" => Float.round(deltas.latency_delta * 100, 1),
          "jitter_pct" => Float.round(deltas.jitter_delta * 100, 1),
          "loss_pct" => Float.round(deltas.loss_delta * 100, 1),
          "throughput_pct" => Float.round(deltas.throughput_delta * 100, 1)
        },
        "pre_metrics" => stringify_metrics(pre),
        "post_metrics" => stringify_metrics(post),
        "sections_changed" => event.sections_changed
      }
    }

    Insights.insert_insight_if_new(attrs)
  end

  defp determine_severity(deltas) do
    cond do
      deltas.latency_delta > 0.50 || deltas.loss_delta > 1.0 || deltas.throughput_delta < -0.30 ->
        "critical"

      deltas.latency_delta > @latency_increase_threshold ||
        deltas.loss_delta > @loss_increase_threshold ||
          deltas.throughput_delta < -@throughput_drop_threshold ->
        "warning"

      true ->
        "info"
    end
  end

  defp build_description(deltas, event) do
    parts =
      []
      |> maybe_add("latency +#{round(deltas.latency_delta * 100)}%", deltas.latency_delta > 0.10)
      |> maybe_add("loss +#{round(deltas.loss_delta * 100)}%", deltas.loss_delta > 0.10)
      |> maybe_add("throughput #{round(deltas.throughput_delta * 100)}%", deltas.throughput_delta < -0.05)

    sections =
      if Enum.any?(event.sections_changed),
        do: " (sections: #{Enum.join(event.sections_changed, ", ")})",
        else: ""

    "After config change#{sections}: #{Enum.join(parts, ", ")}."
  end

  defp maybe_add(list, text, true), do: list ++ [text]
  defp maybe_add(list, _text, false), do: list

  defp safe_pct_change(old, new) when is_number(old) and is_number(new) and old > 0 do
    (new - old) / old
  end

  defp safe_pct_change(_, _), do: 0.0

  defp stringify_metrics(metrics) do
    %{
      "avg_latency" => metrics.avg_latency && Float.round(metrics.avg_latency, 2),
      "avg_jitter" => metrics.avg_jitter && Float.round(metrics.avg_jitter, 2),
      "avg_loss" => metrics.avg_loss && Float.round(metrics.avg_loss, 4),
      "avg_throughput" => metrics.avg_throughput && Float.round(metrics.avg_throughput, 2)
    }
  end
end
