defmodule Towerops.Preseem.Insights do
  @moduledoc """
  Generates and manages Preseem insights based on baselines and fleet data.
  """
  import Ecto.Query

  alias Towerops.Preseem.AccessPoint
  alias Towerops.Preseem.DeviceBaseline
  alias Towerops.Preseem.FleetProfile
  alias Towerops.Preseem.Insight
  alias Towerops.Repo

  # --- Query functions ---

  @doc "List active insights for an organization, ordered by urgency then date."
  def list_insights(organization_id, opts \\ []) do
    status = Keyword.get(opts, :status, "active")
    type = Keyword.get(opts, :type)
    urgency = Keyword.get(opts, :urgency)
    source = Keyword.get(opts, :source)
    limit = Keyword.get(opts, :limit, 50)

    query =
      Insight
      |> where(organization_id: ^organization_id)
      |> where(status: ^status)
      |> order_by_urgency()
      |> limit(^limit)

    query = if type, do: where(query, type: ^type), else: query
    query = if urgency, do: where(query, urgency: ^urgency), else: query
    query = if source, do: where(query, source: ^source), else: query

    Repo.all(query)
  end

  @doc "List active insights for a specific site."
  def list_site_insights(site_id, opts \\ []) do
    limit = Keyword.get(opts, :limit, 50)

    Insight
    |> where(site_id: ^site_id, status: "active")
    |> order_by_urgency()
    |> limit(^limit)
    |> Repo.all()
  end

  @doc "Count active insights grouped by urgency for an organization."
  def count_active_by_urgency(organization_id) do
    results =
      Insight
      |> where(organization_id: ^organization_id, status: "active")
      |> group_by(:urgency)
      |> select([i], {i.urgency, count(i.id)})
      |> Repo.all()
      |> Map.new()

    %{
      critical: Map.get(results, "critical", 0),
      warning: Map.get(results, "warning", 0),
      info: Map.get(results, "info", 0)
    }
  end

  @doc "List active insights for a specific device."
  def list_device_insights(device_id) do
    Insight
    |> where(device_id: ^device_id, status: "active")
    |> order_by_urgency()
    |> Repo.all()
  end

  @doc "Dismiss an insight."
  def dismiss_insight(insight_id) do
    case Repo.get(Insight, insight_id) do
      nil ->
        {:error, :not_found}

      insight ->
        insight
        |> Insight.changeset(%{
          status: "dismissed",
          dismissed_at: DateTime.truncate(DateTime.utc_now(), :second)
        })
        |> Repo.update()
    end
  end

  @doc "Bulk dismiss insights by IDs."
  def dismiss_insights(insight_ids) when is_list(insight_ids) do
    now = DateTime.truncate(DateTime.utc_now(), :second)

    {count, _} =
      Insight
      |> where([i], i.id in ^insight_ids)
      |> where(status: "active")
      |> Repo.update_all(set: [status: "dismissed", dismissed_at: now])

    {:ok, count}
  end

  @doc """
  Insert an insight if no active insight with the same type exists for the same entity.

  Deduplicates by: device_id + type, site_id + type, or agent_token_id + type.
  Returns `{:ok, insight}` on insert, `{:ok, :duplicate}` if already exists.
  """
  def insert_insight_if_new(attrs) do
    dedup_query = build_dedup_query(attrs)

    if dedup_query && Repo.exists?(dedup_query) do
      {:ok, :duplicate}
    else
      case %Insight{} |> Insight.changeset(attrs) |> Repo.insert() do
        {:ok, insight} -> {:ok, insight}
        {:error, changeset} -> {:error, changeset}
      end
    end
  end

  defp build_dedup_query(%{device_id: device_id, type: type}) when not is_nil(device_id) do
    where(Insight, device_id: ^device_id, type: ^type, status: "active")
  end

  defp build_dedup_query(%{site_id: site_id, type: type}) when not is_nil(site_id) do
    where(Insight, site_id: ^site_id, type: ^type, status: "active")
  end

  defp build_dedup_query(%{agent_token_id: agent_token_id, type: type}) when not is_nil(agent_token_id) do
    where(Insight, agent_token_id: ^agent_token_id, type: ^type, status: "active")
  end

  defp build_dedup_query(%{organization_id: org_id, type: type, dedup_key: dedup_key})
       when not is_nil(org_id) and not is_nil(dedup_key) do
    Insight
    |> where(organization_id: ^org_id, type: ^type, status: "active")
    |> where([i], fragment("?->>'dedup_key' = ?", i.metadata, ^dedup_key))
  end

  defp build_dedup_query(_attrs), do: nil

  # --- Generation functions ---

  @doc "Generate insights for all matched APs in an organization."
  def generate_insights(organization_id) do
    matched_aps =
      AccessPoint
      |> where(organization_id: ^organization_id)
      |> where([ap], not is_nil(ap.device_id))
      |> Repo.all()

    fleet_profiles =
      FleetProfile
      |> where(organization_id: ^organization_id)
      |> Repo.all()

    count =
      Enum.reduce(matched_aps, 0, fn ap, acc ->
        baselines =
          DeviceBaseline
          |> where(preseem_access_point_id: ^ap.id)
          |> Repo.all()

        new_insights = generate_ap_insights(ap, baselines, fleet_profiles)
        acc + length(new_insights)
      end)

    {:ok, count}
  end

  defp generate_ap_insights(ap, baselines, fleet_profiles) do
    candidate_insights =
      check_qoe_degradation(ap, baselines) ++
        check_capacity_saturation(ap, baselines) ++
        check_model_underperforming(ap, fleet_profiles)

    Enum.flat_map(candidate_insights, fn attrs ->
      full_attrs =
        Map.merge(attrs, %{
          organization_id: ap.organization_id,
          preseem_access_point_id: ap.id,
          device_id: ap.device_id
        })

      insert_insight_if_new(ap.id, full_attrs)
    end)
  end

  defp insert_insight_if_new(ap_id, full_attrs) do
    existing =
      Repo.get_by(Insight,
        preseem_access_point_id: ap_id,
        type: full_attrs.type,
        status: "active"
      )

    if existing do
      []
    else
      case %Insight{} |> Insight.changeset(full_attrs) |> Repo.insert() do
        {:ok, insight} -> [insight]
        {:error, _} -> []
      end
    end
  end

  defp check_qoe_degradation(ap, baselines) do
    qoe_baseline = Enum.find(baselines, &(&1.metric_name == "qoe_score" and &1.period == "all"))

    cond do
      is_nil(qoe_baseline) ->
        []

      is_nil(ap.qoe_score) ->
        []

      ap.qoe_score < qoe_baseline.mean - 2 * qoe_baseline.stddev ->
        urgency =
          if ap.qoe_score < qoe_baseline.mean - 3 * qoe_baseline.stddev,
            do: "critical",
            else: "warning"

        [
          %{
            type: "qoe_degradation",
            urgency: urgency,
            channel: "proactive",
            title: "QoE degradation on #{ap.name}",
            description:
              "QoE score #{Float.round(ap.qoe_score, 1)} is significantly below the " <>
                "14-day baseline of #{Float.round(qoe_baseline.mean, 1)} " <>
                "(stddev: #{Float.round(qoe_baseline.stddev, 1)}).",
            metadata: %{
              "current" => ap.qoe_score,
              "baseline_mean" => qoe_baseline.mean,
              "baseline_stddev" => qoe_baseline.stddev
            }
          }
        ]

      true ->
        []
    end
  end

  defp check_capacity_saturation(ap, baselines) do
    sub_baseline =
      Enum.find(baselines, &(&1.metric_name == "subscriber_count" and &1.period == "all"))

    cond do
      is_nil(sub_baseline) ->
        []

      is_nil(ap.subscriber_count) ->
        []

      ap.subscriber_count > sub_baseline.p95 ->
        [
          %{
            type: "capacity_saturation",
            urgency: "warning",
            channel: "proactive",
            title: "#{ap.name} approaching capacity",
            description:
              "Subscriber count #{ap.subscriber_count} exceeds the 95th percentile " <>
                "(#{round(sub_baseline.p95)}) of the 14-day baseline.",
            metadata: %{
              "current_subscribers" => ap.subscriber_count,
              "p95" => sub_baseline.p95
            }
          }
        ]

      true ->
        []
    end
  end

  defp check_model_underperforming(ap, fleet_profiles) do
    matching_profile =
      Enum.find(fleet_profiles, fn fp ->
        ap.model != nil and fp.model == ap.model
      end)

    cond do
      is_nil(matching_profile) ->
        []

      is_nil(ap.qoe_score) or is_nil(matching_profile.avg_qoe_score) ->
        []

      ap.qoe_score < matching_profile.avg_qoe_score * 0.8 ->
        [
          %{
            type: "model_underperforming",
            urgency: "info",
            channel: "passive",
            title: "#{ap.name} underperforming fleet average",
            description:
              "QoE score #{Float.round(ap.qoe_score, 1)} is below the fleet average of " <>
                "#{Float.round(matching_profile.avg_qoe_score, 1)} for " <>
                "#{matching_profile.model} devices.",
            metadata: %{
              "current_qoe" => ap.qoe_score,
              "fleet_avg_qoe" => matching_profile.avg_qoe_score,
              "model" => matching_profile.model
            }
          }
        ]

      true ->
        []
    end
  end

  defp order_by_urgency(query) do
    order_by(query, [i], [
      fragment(
        "CASE ? WHEN 'critical' THEN 0 WHEN 'warning' THEN 1 WHEN 'info' THEN 2 END",
        i.urgency
      ),
      desc: i.inserted_at
    ])
  end
end
