defmodule Blogex.Router do
  @moduledoc """
  Plug router that serves blog pages and feeds.

  Serves HTML to browsers (Accept: text/html) and JSON to API clients.

  Mount this in your host app's router:

      # In your Phoenix router
      scope "/blog" do
        pipe_through :browser
        forward "/engineering", Blogex.Router, blog: MyApp.EngineeringBlog
        forward "/releases", Blogex.Router, blog: MyApp.ReleaseNotes
      end

  Or use the convenience macro:

      import Blogex.Router, only: [blogex_routes: 2]

      scope "/blog" do
        pipe_through :browser
        blogex_routes "/engineering", MyApp.EngineeringBlog
        blogex_routes "/releases", MyApp.ReleaseNotes
      end

  ## Routes served

    * `GET /` — post index (paginated)
    * `GET /:slug` — individual post
    * `GET /tag/:tag` — posts by tag
    * `GET /feed.xml` — RSS feed
    * `GET /atom.xml` — Atom feed
  """

  use Plug.Router

  plug :match
  plug :dispatch

  get "/feed.xml" do
    blog = conn.private[:blogex_blog]
    base_url = Blogex.Router.Helpers.base_url(conn)

    xml = Blogex.Feed.rss(blog, base_url)

    conn
    |> put_resp_content_type("application/rss+xml")
    |> send_resp(200, xml)
  end

  get "/atom.xml" do
    blog = conn.private[:blogex_blog]
    base_url = Blogex.Router.Helpers.base_url(conn)

    xml = Blogex.Feed.atom(blog, base_url)

    conn
    |> put_resp_content_type("application/atom+xml")
    |> send_resp(200, xml)
  end

  get "/tag/:tag" do
    blog = conn.private[:blogex_blog]
    posts = blog.posts_by_tag(tag)

    if wants_html?(conn) do
      assigns = %{
        blog_title: blog.title(),
        tag: tag,
        posts: posts,
        base_path: blog.base_path()
      }

      send_html(conn, Blogex.Layout.tag_page(assigns))
    else
      conn
      |> put_resp_content_type("application/json")
      |> send_resp(
        200,
        Jason.encode!(%{
          blog: blog.blog_id(),
          tag: tag,
          posts: Enum.map(posts, &post_json/1)
        })
      )
    end
  end

  get "/:slug" do
    blog = conn.private[:blogex_blog]

    case blog.get_post(slug) do
      nil ->
        conn |> send_resp(404, "Post not found")

      post ->
        if wants_html?(conn) do
          assigns = %{post: post, base_path: blog.base_path()}
          send_html(conn, Blogex.Layout.show_page(assigns))
        else
          conn
          |> put_resp_content_type("application/json")
          |> send_resp(200, Jason.encode!(post_json(post)))
        end
    end
  end

  get "/" do
    blog = conn.private[:blogex_blog]
    page = (conn.params["page"] || "1") |> String.to_integer()
    result = blog.paginate(page)

    if wants_html?(conn) do
      assigns = %{
        blog_title: blog.title(),
        blog_description: blog.description(),
        posts: result.entries,
        base_path: blog.base_path(),
        page: result.page,
        total_pages: result.total_pages
      }

      send_html(conn, Blogex.Layout.index_page(assigns))
    else
      conn
      |> put_resp_content_type("application/json")
      |> send_resp(
        200,
        Jason.encode!(%{
          blog: blog.blog_id(),
          title: blog.title(),
          posts: Enum.map(result.entries, &post_json/1),
          page: result.page,
          total_pages: result.total_pages,
          total_entries: result.total_entries
        })
      )
    end
  end

  match _ do
    send_resp(conn, 404, "Not found")
  end

  @doc false
  def init(opts), do: opts

  @doc false
  def call(conn, opts) do
    blog = Keyword.fetch!(opts, :blog)

    conn
    |> Plug.Conn.put_private(:blogex_blog, blog)
    |> super(opts)
  end

  defp wants_html?(conn) do
    case Plug.Conn.get_req_header(conn, "accept") do
      [accept | _] -> String.contains?(accept, "text/html")
      _ -> false
    end
  end

  defp send_html(conn, rendered) do
    html =
      rendered
      |> Phoenix.HTML.Safe.to_iodata()
      |> IO.iodata_to_binary()

    conn
    |> put_resp_content_type("text/html")
    |> send_resp(200, html)
  end

  defp post_json(post) do
    %{
      id: post.id,
      title: post.title,
      author: post.author,
      date: Date.to_iso8601(post.date),
      description: post.description,
      tags: post.tags,
      body: post.body
    }
  end

  defmodule Helpers do
    @moduledoc false

    def base_url(conn) do
      scheme = if conn.scheme == :https, do: "https", else: "http"
      port_suffix = port_suffix(conn.scheme, conn.port)
      "#{scheme}://#{conn.host}#{port_suffix}"
    end

    defp port_suffix(:http, 80), do: ""
    defp port_suffix(:https, 443), do: ""
    defp port_suffix(_, port), do: ":#{port}"
  end
end
