defmodule Towerops.Trace do
  @moduledoc """
  Context for the Subscriber Trace / Help Desk Mode feature.

  Provides unified search across Gaiia accounts, inventory items, devices,
  and Preseem access points, then assembles a full "patient chart" view
  of a subscriber's network status.
  """

  import Ecto.Query

  alias Towerops.Alerts.Alert
  alias Towerops.ConfigChanges.ConfigChangeEvent
  alias Towerops.Devices.Device
  alias Towerops.Gaiia.Account
  alias Towerops.Gaiia.BillingSubscription
  alias Towerops.Gaiia.InventoryItem
  alias Towerops.Preseem.AccessPoint
  alias Towerops.Preseem.Insight
  alias Towerops.Preseem.SubscriberMetric
  alias Towerops.Repo

  # ---------------------------------------------------------------------------
  # Search
  # ---------------------------------------------------------------------------

  @doc """
  Search across multiple data sources for a given query string.
  Returns a list of `%{type, id, label, sublabel, data}` maps.
  """
  def search(organization_id, query) when is_binary(query) do
    query = String.trim(query)

    if String.length(query) < 2 do
      []
    else
      pattern = "%#{sanitize_like(query)}%"

      accounts = search_accounts(organization_id, pattern)
      inventory = search_inventory(organization_id, pattern)
      devices = search_devices(organization_id, pattern)
      access_points = search_access_points(organization_id, pattern)

      (accounts ++ inventory ++ devices ++ access_points)
      |> Enum.take(25)
    end
  end

  defp search_accounts(organization_id, pattern) do
    Account
    |> where(organization_id: ^organization_id)
    |> where([a], ilike(a.name, ^pattern) or ilike(a.readable_id, ^pattern) or ilike(a.gaiia_id, ^pattern))
    |> limit(10)
    |> Repo.all()
    |> Enum.map(fn a ->
      address_str = format_address(a.address)
      sublabel = Enum.join(Enum.reject([a.readable_id, a.status, address_str], &is_nil/1), " · ")

      %{type: :account, id: a.id, gaiia_id: a.gaiia_id, label: a.name || a.gaiia_id, sublabel: sublabel, data: a}
    end)
  end

  defp search_inventory(organization_id, pattern) do
    InventoryItem
    |> where(organization_id: ^organization_id)
    |> where([i], ilike(i.ip_address, ^pattern) or ilike(i.serial_number, ^pattern) or ilike(i.name, ^pattern) or ilike(i.mac_address, ^pattern))
    |> limit(10)
    |> preload(:device)
    |> Repo.all()
    |> Enum.map(fn i ->
      sublabel = Enum.join(Enum.reject([i.ip_address, i.model_name, i.serial_number], &is_nil/1), " · ")

      %{type: :inventory_item, id: i.id, label: i.name || i.ip_address || i.gaiia_id, sublabel: sublabel, data: i}
    end)
  end

  defp search_devices(organization_id, pattern) do
    Device
    |> where(organization_id: ^organization_id)
    |> where([d], ilike(d.ip_address, ^pattern) or ilike(d.name, ^pattern))
    |> limit(10)
    |> preload(:site)
    |> Repo.all()
    |> Enum.map(fn d ->
      site_name = if d.site && d.site.name, do: d.site.name
      sublabel = Enum.join(Enum.reject([d.ip_address, site_name, to_string(d.status)], &is_nil/1), " · ")

      %{type: :device, id: d.id, label: d.name || d.ip_address, sublabel: sublabel, data: d}
    end)
  end

  defp search_access_points(organization_id, pattern) do
    AccessPoint
    |> where(organization_id: ^organization_id)
    |> where([ap], ilike(ap.name, ^pattern) or ilike(ap.ip_address, ^pattern))
    |> limit(10)
    |> preload(:device)
    |> Repo.all()
    |> Enum.map(fn ap ->
      sublabel = Enum.join(Enum.reject([ap.ip_address, ap.model, "#{ap.subscriber_count || 0} subscribers"], &is_nil/1), " · ")

      %{type: :access_point, id: ap.id, label: ap.name, sublabel: sublabel, data: ap}
    end)
  end

  # ---------------------------------------------------------------------------
  # Trace Assembly
  # ---------------------------------------------------------------------------

  @doc """
  Given a search result, assemble the full trace view.
  Returns a map with all sections of the "patient chart".
  """
  def assemble_trace(organization_id, type, id) do
    case type do
      :account -> trace_from_account(organization_id, id)
      :inventory_item -> trace_from_inventory(organization_id, id)
      :device -> trace_from_device(organization_id, id)
      :access_point -> trace_from_access_point(organization_id, id)
      _ -> nil
    end
  end

  defp trace_from_account(organization_id, account_id) do
    account = Repo.get_by(Account, id: account_id, organization_id: organization_id)
    unless account, do: throw(:not_found)

    subscriptions = list_account_subscriptions(organization_id, account.gaiia_id)
    inventory_items = list_account_inventory(organization_id, account.gaiia_id)

    # Find connected device through inventory items
    {device, access_point} = find_device_and_ap(inventory_items)

    build_trace(account, subscriptions, inventory_items, device, access_point, organization_id)
  catch
    :not_found -> nil
  end

  defp trace_from_inventory(organization_id, item_id) do
    item =
      InventoryItem
      |> where(id: ^item_id, organization_id: ^organization_id)
      |> preload(:device)
      |> Repo.one()

    unless item, do: throw(:not_found)

    # Find account if linked
    account =
      if item.assigned_account_gaiia_id do
        Repo.get_by(Account, organization_id: organization_id, gaiia_id: item.assigned_account_gaiia_id)
      end

    subscriptions = if account, do: list_account_subscriptions(organization_id, account.gaiia_id), else: []
    inventory_items = if account, do: list_account_inventory(organization_id, account.gaiia_id), else: [item]

    device = if item.device_id, do: Repo.get(Device, item.device_id) |> Repo.preload(:site)
    access_point = if device, do: Repo.get_by(AccessPoint, device_id: device.id)

    build_trace(account, subscriptions, inventory_items, device, access_point, organization_id)
  catch
    :not_found -> nil
  end

  defp trace_from_device(organization_id, device_id) do
    device =
      Device
      |> where(id: ^device_id, organization_id: ^organization_id)
      |> preload(:site)
      |> Repo.one()

    unless device, do: throw(:not_found)

    access_point = Repo.get_by(AccessPoint, device_id: device.id)

    # Find accounts served by this device through inventory items
    inventory_items =
      InventoryItem
      |> where(organization_id: ^organization_id, device_id: ^device_id)
      |> Repo.all()

    account =
      case inventory_items do
        [first | _] when not is_nil(first.assigned_account_gaiia_id) ->
          Repo.get_by(Account, organization_id: organization_id, gaiia_id: first.assigned_account_gaiia_id)
        _ -> nil
      end

    subscriptions = if account, do: list_account_subscriptions(organization_id, account.gaiia_id), else: []

    build_trace(account, subscriptions, inventory_items, device, access_point, organization_id)
  catch
    :not_found -> nil
  end

  defp trace_from_access_point(organization_id, ap_id) do
    access_point =
      AccessPoint
      |> where(id: ^ap_id, organization_id: ^organization_id)
      |> preload(:device)
      |> Repo.one()

    unless access_point, do: throw(:not_found)

    device = if access_point.device_id, do: access_point.device |> Repo.preload(:site)

    build_trace(nil, [], [], device, access_point, organization_id)
  catch
    :not_found -> nil
  end

  # ---------------------------------------------------------------------------
  # Build the trace map
  # ---------------------------------------------------------------------------

  defp build_trace(account, subscriptions, inventory_items, device, access_point, organization_id) do
    device_id = if device, do: device.id

    %{
      subscriber: build_subscriber_info(account, subscriptions),
      inventory_items: inventory_items,
      device: device,
      access_point: access_point,
      qoe_metrics: build_qoe_metrics(access_point),
      peer_impact: build_peer_impact(access_point, organization_id),
      alerts: load_device_alerts(device_id),
      config_changes: load_recent_config_changes(device_id),
      insights: load_device_insights(device_id, organization_id)
    }
  end

  defp build_subscriber_info(nil, _subscriptions), do: nil

  defp build_subscriber_info(account, subscriptions) do
    primary_sub = List.first(subscriptions)

    %{
      name: account.name,
      account_id: account.readable_id || account.gaiia_id,
      status: account.status,
      address: format_address(account.address),
      mrr: account.mrr,
      customer_since: account.inserted_at,
      plan: if(primary_sub, do: primary_sub.product_name),
      plan_mrr: if(primary_sub, do: primary_sub.mrr_amount),
      speed_download: if(primary_sub, do: primary_sub.speed_download),
      speed_upload: if(primary_sub, do: primary_sub.speed_upload),
      subscription_count: length(subscriptions),
      subscriptions: subscriptions
    }
  end

  defp build_qoe_metrics(nil), do: nil

  defp build_qoe_metrics(access_point) do
    latest_metric =
      SubscriberMetric
      |> where(preseem_access_point_id: ^access_point.id)
      |> order_by(desc: :recorded_at)
      |> limit(1)
      |> Repo.one()

    %{
      qoe_score: access_point.qoe_score,
      capacity_score: access_point.capacity_score,
      rf_score: access_point.rf_score,
      subscriber_count: access_point.subscriber_count,
      airtime_utilization: access_point.airtime_utilization,
      latest_metric: latest_metric
    }
  end

  defp build_peer_impact(nil, _org_id), do: nil

  defp build_peer_impact(access_point, organization_id) do
    # Count subscribers on same AP
    subscriber_count = access_point.subscriber_count || 0

    # Find other APs at the same device (same tower)
    peer_aps =
      if access_point.device_id do
        AccessPoint
        |> where(organization_id: ^organization_id)
        |> where(device_id: ^access_point.device_id)
        |> where([ap], ap.id != ^access_point.id)
        |> Repo.all()
      else
        []
      end

    # Check for active alerts on the device
    device_alerts =
      if access_point.device_id do
        Alert
        |> where(device_id: ^access_point.device_id)
        |> where([a], is_nil(a.resolved_at))
        |> Repo.aggregate(:count)
      else
        0
      end

    %{
      subscriber_count: subscriber_count,
      peer_ap_count: length(peer_aps),
      peer_aps: peer_aps,
      active_alert_count: device_alerts
    }
  end

  defp load_device_alerts(nil), do: []

  defp load_device_alerts(device_id) do
    Alert
    |> where(device_id: ^device_id)
    |> order_by(desc: :triggered_at)
    |> limit(10)
    |> preload([:device, :acknowledged_by])
    |> Repo.all()
  end

  defp load_recent_config_changes(nil), do: []

  defp load_recent_config_changes(device_id) do
    since = DateTime.utc_now() |> DateTime.add(-24 * 3600, :second)

    ConfigChangeEvent
    |> where(device_id: ^device_id)
    |> where([c], c.detected_at >= ^since)
    |> order_by(desc: :detected_at)
    |> limit(10)
    |> Repo.all()
  end

  defp load_device_insights(nil, _org_id), do: []

  defp load_device_insights(device_id, _organization_id) do
    Insight
    |> where(device_id: ^device_id)
    |> where(status: "active")
    |> order_by(desc: :inserted_at)
    |> limit(10)
    |> Repo.all()
  end

  # ---------------------------------------------------------------------------
  # Helpers
  # ---------------------------------------------------------------------------

  defp find_device_and_ap(inventory_items) do
    # Find the first inventory item that has a linked device
    item_with_device = Enum.find(inventory_items, & &1.device_id)

    if item_with_device do
      device = Repo.get(Device, item_with_device.device_id) |> Repo.preload(:site)
      access_point = Repo.get_by(AccessPoint, device_id: device.id)
      {device, access_point}
    else
      {nil, nil}
    end
  end

  defp list_account_subscriptions(organization_id, account_gaiia_id) do
    BillingSubscription
    |> where(organization_id: ^organization_id, account_gaiia_id: ^account_gaiia_id)
    |> order_by(:product_name)
    |> Repo.all()
  end

  defp list_account_inventory(organization_id, account_gaiia_id) do
    InventoryItem
    |> where(organization_id: ^organization_id, assigned_account_gaiia_id: ^account_gaiia_id)
    |> preload(:device)
    |> Repo.all()
  end

  defp format_address(nil), do: nil

  defp format_address(addr) when is_map(addr) do
    parts =
      [
        addr["street1"] || addr["line1"],
        addr["street2"] || addr["line2"],
        addr["city"],
        addr["state"] || addr["province"],
        addr["zip"] || addr["postal_code"]
      ]
      |> Enum.reject(&is_nil/1)
      |> Enum.reject(&(String.trim(&1) == ""))

    if parts == [], do: nil, else: Enum.join(parts, ", ")
  end

  defp format_address(_), do: nil

  defp sanitize_like(query) do
    query
    |> String.replace("\\", "\\\\")
    |> String.replace("%", "\\%")
    |> String.replace("_", "\\_")
  end
end
