defmodule Aprsme.DeviceCache do
  @moduledoc """
  Caches device information and provides efficient lookup by identifier with wildcard matching.
  """

  use GenServer

  alias Aprsme.Cache
  alias Aprsme.Devices
  alias Aprsme.Repo

  require Logger

  @cache_name :device_cache
  @refresh_interval Cache.to_timeout(day: 1)

  # Client API

  @doc """
  Starts the DeviceCache GenServer.
  """
  def start_link(_opts) do
    GenServer.start_link(__MODULE__, [], name: __MODULE__)
  end

  @doc """
  Looks up a device by identifier using wildcard matching.
  Returns the device struct if found, or nil.
  """
  def lookup_device(nil), do: nil

  def lookup_device(identifier) when is_binary(identifier) do
    case Cache.get(@cache_name, :all_devices) do
      {:ok, nil} ->
        # Cache miss - trigger async refresh and return nil for now
        GenServer.cast(__MODULE__, :refresh_cache_async)
        nil

      {:ok, devices} when is_list(devices) ->
        find_matching_device(devices, identifier)

      _ ->
        nil
    end
  end

  @doc """
  Refreshes the device cache from the database.
  """
  def refresh_cache do
    case Process.whereis(__MODULE__) do
      nil -> :ok
      _pid -> GenServer.call(__MODULE__, :refresh_cache)
    end
  end

  # Server callbacks

  @impl true
  def init(_) do
    # Delay initial load to allow Redis connections to establish
    Process.send_after(self(), :initial_load, Application.get_env(:aprsme, :device_cache_initial_load_delay_ms, 1_000))

    {:ok, %{initial_load_done: false}}
  end

  @impl true
  def handle_call(:refresh_cache, _from, state) do
    result = load_devices_into_cache()
    {:reply, result, state}
  end

  @impl true
  def handle_cast(:refresh_cache_async, state) do
    load_devices_into_cache()
    {:noreply, state}
  end

  @impl true
  def handle_info(:initial_load, state) do
    load_devices_into_cache()

    # Schedule periodic refresh
    Process.send_after(self(), :refresh_cache, @refresh_interval)
    {:noreply, %{state | initial_load_done: true}}
  end

  def handle_info(:refresh_cache, state) do
    load_devices_into_cache()

    # Schedule next refresh
    Process.send_after(self(), :refresh_cache, @refresh_interval)

    {:noreply, state}
  end

  # Private functions

  defp load_devices_into_cache do
    devices = load_devices()

    case Cache.put(@cache_name, :all_devices, devices) do
      {:ok, true} -> :ok
      _ -> :ok
    end
  end

  defp load_devices, do: load_devices_impl()

  defp load_devices_impl do
    case Application.get_env(:aprsme, :env) do
      :test -> []
      _ -> load_from_db()
    end
  end

  defp load_from_db do
    Repo.all(Devices)
  rescue
    error ->
      Logger.error("Failed to load devices from database: #{inspect(error)}")
      []
  end

  defp find_matching_device(devices, identifier) do
    Enum.find(devices, fn device ->
      pattern_matches?(device.identifier, identifier)
    end)
  end

  # Patterns containing `?` wildcards route through regex; everything else is
  # compared verbatim. The old `*` branch was dead code — it did the same
  # exact-match compare as the fallback — so it's gone.
  defp pattern_matches?(pattern, identifier) do
    if String.contains?(pattern, "?") do
      matches_wildcard_pattern?(pattern, identifier)
    else
      pattern == identifier
    end
  end

  defp matches_wildcard_pattern?(pattern, identifier) do
    regex_pattern = wildcard_pattern_to_regex_string(pattern)

    case Aprsme.RegexCache.get_or_compile(regex_pattern) do
      {:ok, regex} -> Regex.match?(regex, identifier)
      {:error, _} -> false
    end
  end

  # Converts a pattern with ? wildcards to a regex string (for caching)
  defp wildcard_pattern_to_regex_string(pattern) when is_binary(pattern) do
    # Replace ? with a placeholder, escape all regex metacharacters except the placeholder,
    # then replace placeholder with .
    pattern
    |> String.replace("?", "__WILDCARD__")
    # Escape all regex metacharacters
    |> String.replace(~r/([\\.\+\*\?\[\^\]\$\(\)\{\}=!<>\|:\-])/, "\\\\\\1")
    |> String.replace("__WILDCARD__", ".")
    |> then(fn s -> "^" <> s <> "$" end)
  end
end
