defmodule Towerops.Preseem.DeviceMatcher do
  @moduledoc """
  Attempts to match Preseem access points to Towerops devices.

  Matching strategy (ordered by confidence):
  1. MAC address (through interface -> snmp_device -> device)
  2. IP address (direct device match)
  3. Hostname (normalized name comparison)
  """

  import Ecto.Query

  alias Towerops.Devices.Device
  alias Towerops.Preseem.AccessPoint
  alias Towerops.Repo
  alias Towerops.Snmp.Device, as: SnmpDevice
  alias Towerops.Snmp.Interface

  @doc """
  Finds all unmatched access points for an organization and tries to match each one.

  Returns `{:ok, matched_count}` where matched_count is the number of APs
  that were successfully matched to a device.
  """
  def match_unmatched(organization_id) do
    unmatched_aps =
      AccessPoint
      |> where(organization_id: ^organization_id, match_confidence: "unmatched")
      |> Repo.all()

    matched_count =
      Enum.count(unmatched_aps, fn ap ->
        case match_access_point(ap) do
          {:ok, matched_ap} -> matched_ap.device_id != nil
          _ -> false
        end
      end)

    {:ok, matched_count}
  end

  @doc """
  Attempts to match a single access point against devices in the same organization.

  Skips APs with `match_confidence == "manual"` (user manually linked).
  Returns `{:ok, access_point}` with updated match_confidence and device_id.
  """
  def match_access_point(%AccessPoint{match_confidence: "manual"} = ap) do
    {:ok, ap}
  end

  def match_access_point(%AccessPoint{} = ap) do
    org_id = ap.organization_id

    result =
      try_mac_match(ap, org_id) ||
        try_ip_match(ap, org_id) ||
        try_hostname_match(ap, org_id)

    case result do
      {:single, device_id, confidence} ->
        update_match(ap, device_id, confidence)

      :ambiguous ->
        update_match(ap, nil, "ambiguous")

      nil ->
        {:ok, ap}
    end
  end

  defp try_mac_match(%AccessPoint{mac_address: nil}, _org_id), do: nil
  defp try_mac_match(%AccessPoint{mac_address: ""}, _org_id), do: nil

  defp try_mac_match(%AccessPoint{mac_address: mac}, org_id) do
    normalized_mac = normalize_mac(mac)

    # Query interface MAC addresses through interface -> snmp_device -> device chain,
    # then filter in Elixir since DB stores MACs in various formats
    device_ids =
      Interface
      |> join(:inner, [i], sd in SnmpDevice, on: i.snmp_device_id == sd.id)
      |> join(:inner, [_i, sd], d in Device, on: sd.device_id == d.id)
      |> where([_i, _sd, d], d.organization_id == ^org_id)
      |> where([i, _sd, _d], not is_nil(i.if_phys_address))
      |> select([i, _sd, d], {d.id, i.if_phys_address})
      |> Repo.all()
      |> Enum.filter(fn {_id, phys_addr} -> normalize_mac(phys_addr) == normalized_mac end)
      |> Enum.map(fn {id, _} -> id end)
      |> Enum.uniq()

    evaluate_matches(device_ids, "auto_mac")
  end

  defp try_ip_match(%AccessPoint{ip_address: nil}, _org_id), do: nil
  defp try_ip_match(%AccessPoint{ip_address: ""}, _org_id), do: nil

  defp try_ip_match(%AccessPoint{ip_address: ip}, org_id) do
    device_ids =
      Device
      |> where(organization_id: ^org_id, ip_address: ^ip)
      |> select([d], d.id)
      |> Repo.all()

    evaluate_matches(device_ids, "auto_ip")
  end

  defp try_hostname_match(%AccessPoint{name: nil}, _org_id), do: nil
  defp try_hostname_match(%AccessPoint{name: ""}, _org_id), do: nil

  defp try_hostname_match(%AccessPoint{name: ap_name}, org_id) do
    normalized_ap_name = normalize_hostname(ap_name)

    device_ids =
      Device
      |> where(organization_id: ^org_id)
      |> where([d], not is_nil(d.name))
      |> select([d], {d.id, d.name})
      |> Repo.all()
      |> Enum.filter(fn {_id, name} -> normalize_hostname(name) == normalized_ap_name end)
      |> Enum.map(fn {id, _} -> id end)

    evaluate_matches(device_ids, "auto_hostname")
  end

  defp evaluate_matches([], _confidence), do: nil
  defp evaluate_matches([device_id], confidence), do: {:single, device_id, confidence}
  defp evaluate_matches([_ | _], _confidence), do: :ambiguous

  defp update_match(ap, device_id, confidence) do
    ap
    |> AccessPoint.changeset(%{device_id: device_id, match_confidence: confidence})
    |> Repo.update()
  end

  @doc false
  def normalize_mac(mac) when is_binary(mac) do
    mac
    |> String.downcase()
    |> String.replace(~r/[:\-\.]/, "")
  end

  def normalize_mac(_), do: ""

  @doc false
  def normalize_hostname(name) when is_binary(name) do
    name
    |> String.downcase()
    |> String.split(".")
    |> List.first()
  end

  def normalize_hostname(_), do: ""
end
