defmodule Towerops.Devices.VersionComparator do
  @moduledoc """
  Compares semantic version strings.

  Supports versions with 0-3 parts (e.g., "7", "7.14", "7.14.1").
  Versions with 4+ parts are considered invalid and fall back to {0, 0, 0}.

  ## Parsing Rules

  - Strips "v" or "V" prefix (case-insensitive)
  - Trims whitespace
  - Splits on "-" or " " and ignores suffixes (e.g., "7.14.1-beta" → "7.14.1")
  - Pads missing parts with 0 (e.g., "7.14" → {7, 14, 0})
  - 0-3 parts: VALID
  - 4+ parts: INVALID → fallback to {0, 0, 0}
  - Non-numeric parts: INVALID
  - Negative numbers: INVALID
  - Leading zeros: VALID (e.g., "007.014.001" → {7, 14, 1})

  ## Examples

      iex> VersionComparator.compare("7.14.1", "7.14.2")
      :lt

      iex> VersionComparator.compare("8.0.0", "7.14.1")
      :gt

      iex> VersionComparator.compare("7.14.1", "7.14.1")
      :eq

      iex> VersionComparator.newer?("7.14.1", "7.14.2")
      true

      iex> VersionComparator.newer?("7.14.2", "7.14.1")
      false
  """

  @doc """
  Compares two version strings.

  Returns `:lt` if the first version is older, `:gt` if newer, or `:eq` if equal.

  ## Examples

      iex> VersionComparator.compare("7.14.1", "7.14.2")
      :lt

      iex> VersionComparator.compare("8.0.0", "7.14.1")
      :gt

      iex> VersionComparator.compare("v7.14.1", "V7.14.1")
      :eq
  """
  def compare(version1, version2) do
    v1_tuple = parse_version(version1)
    v2_tuple = parse_version(version2)

    cond do
      v1_tuple < v2_tuple -> :lt
      v1_tuple > v2_tuple -> :gt
      true -> :eq
    end
  end

  @doc """
  Returns true if the second version is newer than the first.

  ## Examples

      iex> VersionComparator.newer?("7.14.1", "7.14.2")
      true

      iex> VersionComparator.newer?("7.14.2", "7.14.1")
      false

      iex> VersionComparator.newer?("7.14.1", "7.14.1")
      false
  """
  def newer?(current_version, new_version) do
    compare(current_version, new_version) == :lt
  end

  # Private helper to parse version string into {major, minor, patch} tuple
  defp parse_version(version) when is_binary(version) do
    version
    |> String.trim()
    |> strip_prefix()
    |> strip_suffix()
    |> parse_parts()
  end

  # Strip "v" or "V" prefix
  defp strip_prefix("v" <> rest), do: rest
  defp strip_prefix("V" <> rest), do: rest
  defp strip_prefix(version), do: version

  # Strip suffixes after "-" or " "
  defp strip_suffix(version) do
    version
    |> String.split(["-", " "], parts: 2)
    |> List.first()
  end

  # Parse version parts into {major, minor, patch} tuple
  defp parse_parts(""), do: {0, 0, 0}

  defp parse_parts(version) do
    parts = String.split(version, ".")

    # 4+ parts is invalid
    if length(parts) > 3 do
      {0, 0, 0}
    else
      parse_numeric_parts(parts)
    end
  end

  # Convert parts to integers, pad with 0s, validate
  defp parse_numeric_parts(parts) do
    case parse_and_validate(parts) do
      {:ok, numbers} -> pad_to_three(numbers)
      :error -> {0, 0, 0}
    end
  end

  # Parse each part as integer and validate (no negatives, must be numeric)
  defp parse_and_validate(parts) do
    results =
      Enum.map(parts, fn part ->
        case Integer.parse(part) do
          {num, ""} when num >= 0 -> {:ok, num}
          _ -> :error
        end
      end)

    if Enum.all?(results, &match?({:ok, _}, &1)) do
      {:ok, Enum.map(results, fn {:ok, num} -> num end)}
    else
      :error
    end
  end

  # Pad list to 3 elements with 0s, then convert to tuple
  defp pad_to_three(numbers) do
    padded = numbers ++ List.duplicate(0, 3 - length(numbers))
    List.to_tuple(Enum.take(padded, 3))
  end
end
