defmodule Livebook.Notebook.ContentLoader do
  @typedoc """
  A location from where content gets loaded.
  """
  @type location :: {:file, FileSystem.File.t()} | {:url, String.t()}

  @doc """
  Rewrite known URLs, so that they point to plain text file rather
  than HTML.

  Currently the rewerites handle:

    * GitHub files
    * Gist files

  """
  @spec rewrite_url(String.t()) :: String.t()
  def rewrite_url(url) do
    url
    |> URI.parse()
    |> do_rewrite_url()
    |> URI.to_string()
  end

  defp do_rewrite_url(%URI{host: "github.com"} = uri) do
    case String.split(uri.path, "/") do
      ["", owner, repo, "blob", hash | file_path] ->
        path = Enum.join(["", owner, repo, hash | file_path], "/")
        %{uri | path: path, host: "raw.githubusercontent.com"}

      _ ->
        uri
    end
  end

  defp do_rewrite_url(%URI{host: "gist.github.com"} = uri) do
    case String.split(uri.path, "/") do
      ["", owner, hash] ->
        path = Enum.join(["", owner, hash, "raw"], "/")
        %{uri | path: path, host: "gist.githubusercontent.com"}

      _ ->
        uri
    end
  end

  defp do_rewrite_url(uri), do: uri

  @doc """
  Loads binary content from the given URL and validates if it's plain text.
  """
  @spec fetch_content(String.t()) :: {:ok, String.t()} | {:error, String.t()}
  def fetch_content(url) do
    # Given the URL has arbitrary user-specified host, we specify
    # :pool_max_idle_time, so the Finch pool terminates eventually
    req = Req.new(pool_max_idle_time: 60_000) |> Livebook.Utils.req_attach_defaults()

    case Req.get(req, url: url) do
      {:ok, %{status: 200} = res} ->
        valid_content? =
          case Livebook.Utils.fetch_content_type(res) do
            {:ok, content_type} ->
              content_type in ["text/plain", "text/markdown", "application/octet-stream"]

            :error ->
              false
          end

        if valid_content? do
          {:ok, res.body}
        else
          {:error, "invalid content type, make sure the URL points to live markdown"}
        end

      _ ->
        {:error, "failed to download notebook from the given URL"}
    end
  end

  @doc """
  Loads a notebook content from the given location.
  """
  @spec fetch_content_from_location(location()) :: {:ok, String.t()} | {:error, String.t()}
  def fetch_content_from_location(location)

  def fetch_content_from_location({:file, file}) do
    case Livebook.FileSystem.File.read(file) do
      {:ok, content} -> {:ok, content}
      {:error, message} -> {:error, "failed to read #{file.path}, reason: #{message}"}
    end
  end

  def fetch_content_from_location({:url, url}) do
    url
    |> rewrite_url()
    |> fetch_content()
  end

  @doc """
  Normalizes the given URL into a location.
  """
  @spec url_to_location(String.t()) :: location()
  def url_to_location(url)

  def url_to_location("file://" <> path) do
    path = Path.expand(path)
    file = Livebook.FileSystem.File.local(path)
    {:file, file}
  end

  def url_to_location(url), do: {:url, url}

  @doc """
  Resolves the given relative path with regard to the given location.
  """
  @spec resolve_location(location(), String.t()) :: location()
  def resolve_location(location, relative_path)

  def resolve_location({:url, url}, relative_path) do
    {:url, Livebook.Utils.expand_url(url, relative_path)}
  end

  def resolve_location({:file, file}, relative_path) do
    {:file, Livebook.FileSystem.File.resolve(file, relative_path)}
  end
end
