defmodule Towerops.Accounts.HIBP do
  @moduledoc """
  Have I Been Pwned password breach checking using k-anonymity API.

  Uses the HIBP Pwned Passwords API v3 to check if a password appears
  in known data breaches. Implements k-anonymity by only sending the
  first 5 characters of the SHA-1 hash to the API.

  See: https://haveibeenpwned.com/API/v3#PwnedPasswords
  """

  require Logger

  @api_url "https://api.pwnedpasswords.com/range/"
  @timeout 5_000

  @doc """
  Checks if a password appears in known data breaches.

  Returns:
  - `{:ok, 0}` - Password not found in breaches
  - `{:ok, count}` - Password found in `count` breaches
  - `{:ok, :unknown}` - API error, status unknown (fail open)

  ## Examples

      iex> check_password("correct-horse-battery-staple")
      {:ok, 0}

      iex> check_password("password123")
      {:ok, 123456}
  """
  def check_password(password) when is_binary(password) and byte_size(password) > 0 do
    hash = hash_password(password)
    prefix = String.slice(hash, 0, 5)
    suffix = String.slice(hash, 5..-1//1)

    case fetch_breaches(prefix) do
      {:ok, results} ->
        check_suffix(results, suffix)

      {:error, reason} ->
        Logger.warning("HIBP API error: #{inspect(reason)}")
        {:ok, :unknown}
    end
  end

  def check_password(_), do: {:ok, :unknown}

  # Hash password to SHA-1 (HIBP uses SHA-1, not for security but for API compatibility)
  defp hash_password(password) do
    :sha
    |> :crypto.hash(password)
    |> Base.encode16()
    |> String.upcase()
  end

  # Fetch breaches from HIBP API
  defp fetch_breaches(prefix) do
    url = @api_url <> prefix

    case Req.get(url, receive_timeout: @timeout) do
      {:ok, %Req.Response{status: 200, body: body}} ->
        {:ok, body}

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

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

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

  # Parse API response and check if suffix exists
  defp check_suffix(results, suffix) do
    results
    |> String.split("\r\n", trim: true)
    |> Enum.find_value({:ok, 0}, fn line ->
      case String.split(line, ":") do
        [hash_suffix, count] when hash_suffix == suffix ->
          {:ok, String.to_integer(count)}

        _ ->
          nil
      end
    end)
  end
end
