defmodule Aprsme.DeviceIdentification do
  @moduledoc """
  Handles APRS device identification based on the APRS device identification database.
  """
  use Gettext, backend: AprsmeWeb.Gettext

  import Ecto.Query

  alias Aprsme.CircuitBreaker
  alias Aprsme.Devices
  alias Aprsme.Repo

  @device_patterns [
    {~r/^ \x00\x00$/, "Original MIC-E"},
    {~r/^>\x00\^$/, "Kenwood TH-D74"},
    {~r/^>\x00\x00$/, "Kenwood TH-D74A"},
    {~r/^]\x00=$/, "Kenwood DM-710"},
    {~r/^]\x00\x00$/, "Kenwood DM-700"},
    {~r/^`_ $/, "Yaesu VX-8"},
    {~r/^`_\"$/, "Yaesu FTM-350"},
    {~r/^`_#$/, "Yaesu VX-8G"},
    {~r/^`_\$$/, "Yaesu FT1D"},
    {~r/^`_%$/, "Yaesu FTM-400DR"},
    {~r/^`_\)$/, "Yaesu FTM-100D"},
    {~r/^`_\($/, "Yaesu FT2D"},
    {~r/^` X$/, "AP510"},
    {~r/^`\x00\x00$/, "Mic-Emsg"},
    {~r/^'\|3$/, "Byonics TinyTrack3"},
    {~r/^'\|4$/, "Byonics TinyTrack4"},
    {~r/^':4$/, "SCS GmbH & Co. P4dragon DR-7400 modems"},
    {~r/^':8$/, "SCS GmbH & Co. P4dragon DR-7800 modems"},
    {~r/^'\x00\x00$/, "McTrackr"},
    {~r/^\x00\"\x00$/, "Hamhud"},
    {~r/^\x00\/\x00$/, "Argent"},
    {~r/^\x00\^\x00$/, "HinzTec anyfrog"},
    {~r/^\x00\*\x00$/, "APOZxx www.KissOZ.dk Tracker. OZ1EKD and OZ7HVO"},
    {~r/^\x00~\x00$/, "Other"}
  ]

  @doc """
  Identifies the manufacturer and model of an APRS device based on its symbol pattern.
  Returns a tuple of {manufacturer, model} or "Unknown" if the device cannot be identified.

  ## Examples

      iex> Aprsme.DeviceIdentification.identify_device(">" <> <<0>> <> "^")
      "Kenwood TH-D74"

      iex> Aprsme.DeviceIdentification.identify_device("`_#")
      "Yaesu VX-8G"

      iex> Aprsme.DeviceIdentification.identify_device("not-a-match")
      "Unknown"
  """
  @spec identify_device(String.t()) :: String.t()
  def identify_device(symbols) do
    Enum.find_value(@device_patterns, gettext("Unknown"), fn {regex, name} ->
      if Regex.match?(regex, symbols) do
        name
      end
    end)
  end

  @doc """
  Returns a list of all known device manufacturers.

  ## Examples

      iex> "Kenwood" in Aprsme.DeviceIdentification.known_manufacturers()
      true

      iex> Enum.member?(Aprsme.DeviceIdentification.known_manufacturers(), "AP510")
      true
  """
  @spec known_manufacturers() :: [String.t()]
  def known_manufacturers do
    [
      "Original MIC-E",
      "Kenwood",
      "Yaesu",
      "AP510",
      "Byonics",
      "SCS GmbH & Co.",
      "McTrackr",
      "Hamhud",
      "Argent",
      "HinzTec",
      "APOZxx",
      "Other"
    ]
  end

  @doc """
  Returns a list of all known device models for a given manufacturer.

  ## Examples

      iex> Aprsme.DeviceIdentification.known_models("Kenwood")
      ["TH-D74", "TH-D74A", "DM-710", "DM-700"]

      iex> Aprsme.DeviceIdentification.known_models("Unknown")
      []
  """
  @spec known_models(String.t()) :: [String.t()]
  def known_models("Kenwood"), do: ["TH-D74", "TH-D74A", "DM-710", "DM-700"]

  def known_models("Yaesu"), do: ["VX-8", "FTM-350", "VX-8G", "FT1D", "FTM-400DR", "FTM-100D", "FT2D"]

  def known_models("Byonics"), do: ["TinyTrack3", "TinyTrack4"]
  def known_models("SCS GmbH & Co."), do: ["P4dragon DR-7400 modems", "P4dragon DR-7800 modems"]
  def known_models(_), do: []

  @url "https://aprs-deviceid.aprsfoundation.org/tocalls.dense.json"
  @week_seconds 7 * 24 * 60 * 60

  def maybe_refresh_devices do
    last = Repo.one(from d in Devices, order_by: [desc: d.updated_at], limit: 1)

    last_time =
      case last && last.updated_at do
        %NaiveDateTime{} = naive -> DateTime.from_naive!(naive, "Etc/UTC")
        %DateTime{} = dt -> dt
        _ -> nil
      end

    if last == nil or (last_time && DateTime.diff(DateTime.utc_now(), last_time) > @week_seconds) do
      fetch_and_upsert_devices()
    else
      :ok
    end
  end

  def fetch_and_upsert_devices do
    case CircuitBreaker.call(
           :aprs_foundation_api,
           fn ->
             case Req.get(@url) do
               {:ok, %Req.Response{status: 200, body: body}} ->
                 upsert_devices(body)

               {:ok, %Req.Response{status: status}} ->
                 {:error, {:http_error, status}}

               {:error, reason} ->
                 {:error, reason}
             end
           end,
           15_000
         ) do
      {:ok, result} -> result
      {:error, reason} -> {:error, reason}
    end
  end

  def upsert_devices(json) do
    tocalls = Map.get(json, "tocalls", %{})
    mice = Map.get(json, "mice", %{})
    micelegacy = Map.get(json, "micelegacy", %{})
    now = DateTime.utc_now()

    Repo.transaction(fn ->
      Repo.delete_all(Devices)

      Enum.each([tocalls, mice, micelegacy], fn group ->
        upsert_device_group(group, now)
      end)
    end)

    :ok
  end

  defp upsert_device_group(group, now) do
    Enum.each(group, fn {identifier, attrs} ->
      processed_attrs = process_device_attrs(attrs, identifier, now)
      %Devices{} |> Devices.changeset(processed_attrs) |> Repo.insert!()
    end)
  end

  defp process_device_attrs(attrs, identifier, now) do
    attrs
    |> Map.put("identifier", identifier)
    |> Map.update("features", nil, fn f ->
      if is_list(f), do: f, else: [f]
    end)
    |> Map.put("updated_at", now)
  end

  # Helper to enqueue the job
  def enqueue_refresh_job do
    # Oban.insert!(Worker.new(%{}))
  end

  @doc """
  Looks up a device by identifier, using ? as a single-character wildcard.
  Returns the device struct if found, or nil.

  Note: This function requires the devices table to be seeded and a running Repo context.
  """
  def lookup_device_by_identifier(nil), do: nil

  def lookup_device_by_identifier(identifier) when is_binary(identifier) do
    # Fetch all device patterns from DB
    devices = Repo.all(Devices)

    Enum.find(devices, fn device ->
      pattern = device.identifier

      cond do
        String.contains?(pattern, "?") ->
          try do
            regex = wildcard_pattern_to_regex(pattern)
            Regex.match?(regex, identifier)
          rescue
            _e in Regex.CompileError ->
              false
          end

        String.contains?(pattern, "*") ->
          # Compare literally if pattern contains * but not ?
          pattern == identifier

        true ->
          pattern == identifier
      end
    end)
  end

  # Converts a pattern with ? wildcards to a regex
  defp wildcard_pattern_to_regex(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 ->
      regex = "^" <> s <> "$"
      ~r/#{regex}/
    end)
  end
end
