defmodule Towerops.DeviceProfiles do
  @moduledoc """
  Context for managing device SNMP profiles.

  Provides functions for querying profiles, matching devices to profiles based on
  detection rules, and retrieving sensor/processor/mempool definitions.
  """

  import Ecto.Query

  alias Towerops.DeviceProfiles.DetectionRule
  alias Towerops.DeviceProfiles.DeviceProfile
  alias Towerops.DeviceProfiles.MemPoolDefinition
  alias Towerops.DeviceProfiles.OSDefinition
  alias Towerops.DeviceProfiles.ProcessorDefinition
  alias Towerops.DeviceProfiles.SensorDefinition
  alias Towerops.DeviceProfiles.SensorState
  alias Towerops.Repo

  @doc """
  Lists all enabled device profiles.
  """
  def list_profiles do
    Repo.all(from p in DeviceProfile, where: p.enabled == true, order_by: [asc: p.priority])
  end

  @doc """
  Gets a single device profile by OS name.
  """
  def get_profile(os) when is_binary(os) do
    Repo.get_by(DeviceProfile, os: os)
  end

  @doc """
  Gets a profile with all associations preloaded.
  """
  def get_profile_with_associations(os) when is_binary(os) do
    Repo.one(
      from p in DeviceProfile,
        where: p.os == ^os,
        preload: [
          :detection_rules,
          :os_definitions,
          :processor_definitions,
          :mempool_definitions,
          sensor_definitions: [:states]
        ]
    )
  end

  @doc """
  Matches a device to a profile based on system information.

  Takes a map with :sys_descr and :sys_object_id keys and returns the
  matching profile or nil.
  """
  def match_profile(system_info) do
    profiles = list_profiles_with_detection_rules()

    Enum.find(profiles, fn profile ->
      match_detection_rules?(profile.detection_rules, system_info)
    end)
  end

  @doc """
  Lists all sensor definitions for a given profile and sensor class.
  """
  def list_sensor_definitions(profile_id, sensor_class) do
    Repo.all(
      from s in SensorDefinition,
        where: s.device_profile_id == ^profile_id and s.sensor_class == ^sensor_class,
        preload: [:states]
    )
  end

  @doc """
  Creates a new device profile.
  """
  def create_profile(attrs \\ %{}) do
    %DeviceProfile{}
    |> DeviceProfile.changeset(attrs)
    |> Repo.insert()
  end

  @doc """
  Upserts a device profile by OS name.

  If a profile with the given OS exists, it updates it and deletes all associated
  definitions (detection rules, sensors, etc.) to allow re-import from scratch.
  Otherwise, creates a new profile.
  """
  def upsert_profile(attrs \\ %{}) do
    os = attrs[:os] || attrs["os"]

    case get_profile(os) do
      nil ->
        # Create new profile
        create_profile(attrs)

      existing_profile ->
        # Delete all associated definitions to get a clean slate
        delete_profile_associations(existing_profile.id)

        # Update the profile
        update_profile(existing_profile, attrs)
    end
  end

  @doc """
  Updates a device profile.
  """
  def update_profile(%DeviceProfile{} = profile, attrs) do
    profile
    |> DeviceProfile.changeset(attrs)
    |> Repo.update()
  end

  @doc """
  Deletes a device profile and all associated definitions.
  """
  def delete_profile(%DeviceProfile{} = profile) do
    Repo.delete(profile)
  end

  @doc """
  Creates a detection rule for a profile.
  """
  def create_detection_rule(attrs \\ %{}) do
    %DetectionRule{}
    |> DetectionRule.changeset(attrs)
    |> Repo.insert()
  end

  @doc """
  Creates a sensor definition for a profile.
  """
  def create_sensor_definition(attrs \\ %{}) do
    %SensorDefinition{}
    |> SensorDefinition.changeset(attrs)
    |> Repo.insert()
  end

  @doc """
  Creates a sensor state for a sensor definition.
  """
  def create_sensor_state(attrs \\ %{}) do
    %SensorState{}
    |> SensorState.changeset(attrs)
    |> Repo.insert()
  end

  @doc """
  Creates a processor definition for a profile.
  """
  def create_processor_definition(attrs \\ %{}) do
    %ProcessorDefinition{}
    |> ProcessorDefinition.changeset(attrs)
    |> Repo.insert()
  end

  @doc """
  Creates a memory pool definition for a profile.
  """
  def create_mempool_definition(attrs \\ %{}) do
    %MemPoolDefinition{}
    |> MemPoolDefinition.changeset(attrs)
    |> Repo.insert()
  end

  @doc """
  Creates an OS definition for a profile.
  """
  def create_os_definition(attrs \\ %{}) do
    %OSDefinition{}
    |> OSDefinition.changeset(attrs)
    |> Repo.insert()
  end

  # Private functions

  defp delete_profile_associations(profile_id) do
    # Delete in order to respect foreign key constraints
    Repo.delete_all(
      from s in SensorState,
        join: sd in SensorDefinition,
        on: s.sensor_definition_id == sd.id,
        where: sd.device_profile_id == ^profile_id
    )

    Repo.delete_all(from s in SensorDefinition, where: s.device_profile_id == ^profile_id)
    Repo.delete_all(from p in ProcessorDefinition, where: p.device_profile_id == ^profile_id)
    Repo.delete_all(from m in MemPoolDefinition, where: m.device_profile_id == ^profile_id)
    Repo.delete_all(from o in OSDefinition, where: o.device_profile_id == ^profile_id)
    Repo.delete_all(from d in DetectionRule, where: d.device_profile_id == ^profile_id)
  end

  defp list_profiles_with_detection_rules do
    Repo.all(
      from p in DeviceProfile,
        where: p.enabled == true,
        order_by: [asc: p.priority],
        preload: [:detection_rules]
    )
  end

  defp match_detection_rules?(rules, system_info) do
    Enum.any?(rules, &match_rule?(&1, system_info))
  end

  defp match_rule?(%DetectionRule{rule_type: "sysObjectID"} = rule, system_info) do
    sys_object_id = Map.get(system_info, :sys_object_id, "")

    # Normalize OID - prepend dot if not present for pattern matching
    # Device profiles use ".1.3.6.1..." format, SNMP returns "1.3.6.1..." format
    normalized_oid = if String.starts_with?(sys_object_id, "."), do: sys_object_id, else: "." <> sys_object_id

    cond do
      rule.pattern && rule.pattern != "" ->
        String.starts_with?(normalized_oid, rule.pattern)

      rule.value && rule.value != "" ->
        normalized_value = if String.starts_with?(rule.value, "."), do: rule.value, else: "." <> rule.value
        normalized_oid == normalized_value

      true ->
        false
    end
  end

  defp match_rule?(%DetectionRule{rule_type: "sysDescr"} = rule, system_info) do
    sys_descr = Map.get(system_info, :sys_descr, "")

    cond do
      rule.pattern && rule.pattern != "" ->
        # Handle regex patterns
        case Regex.compile(rule.pattern) do
          {:ok, regex} -> Regex.match?(regex, sys_descr)
          {:error, _} -> String.contains?(sys_descr, rule.pattern)
        end

      rule.value && rule.value != "" ->
        String.contains?(sys_descr, rule.value)

      true ->
        false
    end
  end

  defp match_rule?(%DetectionRule{rule_type: "sysDescr_regex"} = rule, system_info) do
    sys_descr = Map.get(system_info, :sys_descr, "")

    case Regex.compile(rule.pattern || "") do
      {:ok, regex} -> Regex.match?(regex, sys_descr)
      {:error, _} -> false
    end
  end

  defp match_rule?(_rule, _system_info), do: false
end
