defmodule Towerops.Workers.FirmwareVersionFetcherWorker do
  @moduledoc """
  Oban worker that fetches the latest MikroTik RouterOS firmware version from RSS feed.

  Runs daily to check for new firmware releases and updates the firmware_releases table.
  """

  use Oban.Worker, queue: :maintenance, max_attempts: 3

  import SweetXml

  alias Towerops.Devices.Firmware

  require Logger

  @rss_url "https://cdn.mikrotik.com/routeros/latest-stable.rss"
  @changelog_url "https://mikrotik.com/download/changelogs"

  @impl Oban.Worker
  @spec perform(Oban.Job.t()) :: :ok | {:error, term()}
  def perform(%Oban.Job{}) do
    Logger.info("Fetching MikroTik RouterOS firmware version from RSS feed")

    with {:ok, rss_body} <- fetch_rss_feed(),
         {:ok, release_data} <- parse_rss_feed(rss_body),
         {:ok, _release} <- Firmware.upsert_firmware_release(release_data) do
      Logger.info("Successfully updated firmware release: #{release_data.version}")
      :ok
    else
      {:error, reason} = error ->
        Logger.error("Failed to fetch firmware version: #{inspect(reason)}")
        error
    end
  end

  @doc """
  Fetches the RSS feed from MikroTik CDN.

  Returns `{:ok, body}` on success or `{:error, reason}` on failure.
  """
  def fetch_rss_feed do
    case Req.get(@rss_url, receive_timeout: 10_000) do
      {:ok, %Req.Response{status: 200, body: body}} ->
        {:ok, body}

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

      {:error, %Req.TransportError{}} ->
        {:error, :network_error}

      {:error, reason} ->
        {:error, reason}
    end
  end

  @doc """
  Parses RSS feed XML and extracts firmware release information.

  ## Examples

      iex> parse_rss_feed(valid_rss_xml)
      {:ok, %{vendor: "mikrotik", version: "7.14.1", ...}}

      iex> parse_rss_feed(invalid_xml)
      {:error, :parse_error}
  """
  def parse_rss_feed(xml_string) do
    # Parse channel title and first item
    channel_title = xpath(xml_string, ~x"//channel/title/text()"s)

    # Check if any items exist
    items = xpath(xml_string, ~x"//item"l)

    if Enum.empty?(items) do
      {:error, :no_items}
    else
      # Extract data from first item
      version = xpath(xml_string, ~x"//item/title/text()"s)
      download_url = xpath(xml_string, ~x"//item/link/text()"s)
      description = xpath(xml_string, ~x"//item/description/text()"s)
      pub_date_string = xpath(xml_string, ~x"//item/pubDate/text()"s)

      # Validate required fields
      if version == "" or is_nil(version) do
        {:error, :invalid_data}
      else
        # Extract clean version number from RSS title
        # "RouterOS 7.21.2 [stable]" -> "7.21.2"
        clean_version = extract_version_number(version)

        # Parse RFC822 date
        release_date = parse_rfc822_date(pub_date_string)

        {:ok,
         %{
           vendor: "mikrotik",
           product_line: "routeros",
           version: clean_version,
           release_date: release_date,
           download_url: download_url,
           changelog_url: @changelog_url,
           fetched_at: DateTime.utc_now(),
           metadata: %{
             "rss_title" => channel_title,
             "rss_description" => description,
             "raw_version" => version
           }
         }}
      end
    end
  rescue
    _ ->
      {:error, :parse_error}
  catch
    :exit, _ ->
      {:error, :parse_error}
  end

  # Extract clean version number from RSS title
  # Examples:
  #   "RouterOS 7.21.2 [stable]" -> "7.21.2"
  #   "7.14.1" -> "7.14.1"
  defp extract_version_number(version_string) do
    # Match pattern: one or more digits, followed by dot, followed by digits
    # This extracts semantic version numbers like "7.21.2"
    case Regex.run(~r/\d+\.\d+(?:\.\d+)?/, version_string) do
      [version] -> version
      # Fallback to original if no match
      _ -> version_string
    end
  end

  # Parse RFC822 date format used in RSS feeds
  # Example: "Wed, 15 Jan 2025 12:00:00 +0000"
  defp parse_rfc822_date(date_string) when is_binary(date_string) and date_string != "" do
    # RFC822 format: "Day, DD Mon YYYY HH:MM:SS +ZZZZ"
    # We only care about DD Mon YYYY
    case Regex.run(~r/\d{1,2}\s+\w{3}\s+\d{4}/, date_string) do
      [date_part] ->
        parse_date_part(date_part)

      _ ->
        nil
    end
  end

  defp parse_rfc822_date(_), do: nil

  # Parse the date portion "15 Jan 2025"
  defp parse_date_part(date_string) do
    with [day_str, month_str, year_str] <- String.split(date_string),
         {day, ""} <- Integer.parse(day_str),
         {year, ""} <- Integer.parse(year_str),
         month when is_integer(month) <- parse_month(month_str),
         {:ok, date} <- Date.new(year, month, day) do
      date
    else
      _ -> nil
    end
  end

  # Convert three-letter month abbreviation to month number
  defp parse_month("Jan"), do: 1
  defp parse_month("Feb"), do: 2
  defp parse_month("Mar"), do: 3
  defp parse_month("Apr"), do: 4
  defp parse_month("May"), do: 5
  defp parse_month("Jun"), do: 6
  defp parse_month("Jul"), do: 7
  defp parse_month("Aug"), do: 8
  defp parse_month("Sep"), do: 9
  defp parse_month("Oct"), do: 10
  defp parse_month("Nov"), do: 11
  defp parse_month("Dec"), do: 12
  defp parse_month(_), do: nil
end
