defmodule BDS.Rendering do
  @moduledoc false

  import Ecto.Query

  alias BDS.Frontmatter
  alias BDS.Persistence
  alias BDS.Media.Media, as: MediaAsset
  alias BDS.Menu
  alias BDS.Metadata
  alias BDS.PreviewAssets
  alias BDS.PostLinks
  alias BDS.Projects
  alias BDS.I18n
  alias BDS.Rendering.FileSystem
  alias BDS.Rendering.Filters
  alias BDS.Repo
  alias BDS.StarterTemplates
  alias BDS.Posts.Post
  alias BDS.Posts.Translation
  alias BDS.Tags.Tag
  alias BDS.Templates.Template

  def render_post_page(project_id, template_slug, assigns)
      when is_binary(project_id) and is_map(assigns) do
    with {:ok, template_source} <- load_template_source(project_id, :post, template_slug),
         {:ok, rendered} <-
           render_template(project_id, template_source, post_assigns(project_id, assigns)) do
      {:ok, rendered}
    end
  end

  def render_list_page(project_id, assigns) when is_binary(project_id) and is_map(assigns) do
    with {:ok, template_source} <- load_template_source(project_id, :list, nil),
         {:ok, rendered} <-
           render_template(project_id, template_source, list_assigns(project_id, assigns)) do
      {:ok, rendered}
    end
  end

  def render_not_found_page(project_id, assigns \\ %{})
      when is_binary(project_id) and is_map(assigns) do
    with {:ok, template_source} <- load_template_source(project_id, :not_found, nil),
         {:ok, rendered} <-
           render_template(project_id, template_source, not_found_assigns(project_id, assigns)) do
      {:ok, rendered}
    end
  end

  defp load_template_source(project_id, kind, slug) do
    project = Projects.get_project!(project_id)

    case select_template(project_id, kind, slug) do
      %Template{} = template ->
        case published_template_body(template) do
          {:ok, _source} = ok ->
            ok

          {:error, reason} = error ->
            maybe_load_bundled_template_source(project, kind, slug, template, reason, error)
        end

      nil ->
        load_bundled_template_source(project, kind, slug)
    end
  end

  defp select_template(project_id, kind, slug) when is_binary(slug) and slug != "" do
    Repo.one(
      from template in Template,
        where:
          template.project_id == ^project_id and template.kind == ^kind and
            template.status == :published and
            template.enabled == true and template.slug == ^slug,
        limit: 1
    )
  end

  defp select_template(project_id, :post, nil) do
    case StarterTemplates.default_slug(:post) do
      nil ->
        nil

      default_slug ->
        Repo.one(
          from template in Template,
            where:
              template.project_id == ^project_id and template.kind == :post and
                template.status == :published and
                template.enabled == true and template.slug == ^default_slug,
            limit: 1
        )
    end
  end

  defp select_template(project_id, kind, nil) do
    Repo.one(
      from template in Template,
        where:
          template.project_id == ^project_id and template.kind == ^kind and
            template.status == :published and
            template.enabled == true,
        order_by: [desc: template.created_at, desc: template.slug],
        limit: 1
    )
  end

  defp published_template_body(%Template{content: content}) when is_binary(content),
    do: {:ok, content}

  defp published_template_body(%Template{} = template) do
    project = Projects.get_project!(template.project_id)
    full_path = Path.join(Projects.project_data_dir(project), template.file_path)

    case File.read(full_path) do
      {:ok, contents} ->
        case Frontmatter.parse_document(contents) do
          {:ok, %{body: body}} -> {:ok, body}
          {:error, reason} -> {:error, reason}
        end

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

  defp render_template(project_id, source, assigns) do
    with {:ok, template_ast} <- Liquex.parse(source) do
      project = Projects.get_project!(project_id)

      context =
        Liquex.Context.new(assigns,
          static_environment: assigns,
          filter_module: Filters,
          file_system: FileSystem.new(StarterTemplates.template_roots(project))
        )

      {result, _context} = Liquex.render!(template_ast, context)
      {:ok, IO.iodata_to_binary(result)}
    end
  rescue
    error -> {:error, error}
  end

  defp load_bundled_template_source(project, kind, slug) do
    desired_slug = bundled_template_slug(kind, slug)

    if is_binary(desired_slug) do
      file_system = project |> StarterTemplates.template_roots() |> FileSystem.new()
      source = Liquex.FileSystem.read_template_file(file_system, desired_slug)

      case Frontmatter.parse_document(source) do
        {:ok, %{body: body}} -> {:ok, body}
        {:error, :invalid_frontmatter} -> {:ok, source}
      end
    else
      {:error, :template_not_found}
    end
  rescue
    error in [Liquex.Error] ->
      _ = error
      {:error, :template_not_found}
  end

  defp maybe_load_bundled_template_source(project, kind, slug, template, reason, error)
       when reason in [:enoent, :template_not_found] do
    if template.content in [nil, ""] and StarterTemplates.default_template?(kind, template.slug) do
      load_bundled_template_source(project, kind, slug)
    else
      error
    end
  end

  defp maybe_load_bundled_template_source(_project, _kind, _slug, _template, _reason, error),
    do: error

  defp bundled_template_slug(_kind, slug) when is_binary(slug) and slug != "", do: slug
  defp bundled_template_slug(kind, _slug), do: StarterTemplates.default_slug(kind)

  defp post_assigns(project_id, assigns) do
    metadata = project_metadata(project_id)
    template_context = template_render_context(project_id)

    language =
      Map.get(assigns, :language, Map.get(assigns, "language", metadata.main_language || "en"))

    main_language = metadata.main_language || language
    post_record = load_post_record(assigns)
    canonical_post = canonical_post_record(post_record)
    post_id = canonical_post_id(post_record, assigns)
    post_categories = Map.get(post_record || %{}, :categories, []) || []
    post_tags = Map.get(post_record || %{}, :tags, []) || []
    canonical_post_paths = canonical_post_path_by_slug(project_id, main_language)
    canonical_media_paths = canonical_media_path_by_source_path(project_id)
    raw_content = Map.get(assigns, :content, Map.get(assigns, "content"))
    rendered_content = render_post_content(raw_content, canonical_post_paths, canonical_media_paths, language, template_context)
    incoming_links = link_contexts(project_id, post_id, :incoming, main_language)
    outgoing_links = link_contexts(project_id, post_id, :outgoing, main_language)

    post_assigns =
      assigns
      |> Map.put(:content, rendered_content)
      |> Map.put(:raw_content, raw_content)

    %{
      language: language,
      language_prefix:
        Map.get(
          assigns,
          :language_prefix,
          Map.get(assigns, "language_prefix", language_prefix(language, main_language))
        ),
      page_title:
        Map.get(
          assigns,
          :page_title,
          Map.get(assigns, "page_title", Map.get(assigns, :title, Map.get(assigns, "title")))
        ),
      pico_stylesheet_href:
        Map.get(
          assigns,
          :pico_stylesheet_href,
          Map.get(assigns, "pico_stylesheet_href", default_pico_stylesheet_href(metadata.pico_theme))
        ),
      html_theme_attribute:
        Map.get(
          assigns,
          :html_theme_attribute,
          Map.get(assigns, "html_theme_attribute")
        ),
      blog_languages: blog_languages(metadata, language),
      alternate_links: alternate_links(canonical_post, project_id, main_language),
      menu_items: menu_items(project_id),
      calendar_initial_year: calendar_initial_year(post_record),
      calendar_initial_month: calendar_initial_month(post_record),
      post_categories: post_categories,
      post_tags: post_tags,
      tag_color_by_name: tag_color_by_name(project_id),
      backlinks: backlinks(incoming_links),
      canonical_post_path_by_slug: canonical_post_paths,
      canonical_media_path_by_source_path: canonical_media_paths,
      post_data_json_by_id: post_data_json(post_assigns, post_record),
      post: build_post_context(post_assigns, post_record, incoming_links, outgoing_links)
    }
  end

  defp list_assigns(project_id, assigns) do
    metadata = project_metadata(project_id)
    template_context = template_render_context(project_id)

    language =
      Map.get(assigns, :language, Map.get(assigns, "language", metadata.main_language || "en"))

    main_language = metadata.main_language || language
    archive_context = Map.get(assigns, :archive_context, Map.get(assigns, "archive_context", %{}))

    canonical_post_paths = canonical_post_path_by_slug(project_id, main_language)
    canonical_media_paths = canonical_media_path_by_source_path(project_id)
    posts =
      normalize_list_posts(
        Map.get(assigns, :posts, Map.get(assigns, "posts", [])),
        canonical_post_paths,
        canonical_media_paths,
        language,
        template_context
      )

    pagination =
      normalize_pagination(Map.get(assigns, :pagination, Map.get(assigns, "pagination")), posts)

    day_blocks = build_day_blocks(posts)
    min_date = min_date(posts)
    max_date = max_date(posts)
    normalized_archive_context = normalize_archive_context(archive_context)

    %{
      language: language,
      language_prefix:
        Map.get(
          assigns,
          :language_prefix,
          Map.get(assigns, "language_prefix", language_prefix(language, main_language))
        ),
      page_title: Map.get(assigns, :page_title, Map.get(assigns, "page_title")),
      posts: posts,
      pico_stylesheet_href:
        Map.get(
          assigns,
          :pico_stylesheet_href,
          Map.get(assigns, "pico_stylesheet_href", default_pico_stylesheet_href(metadata.pico_theme))
        ),
      html_theme_attribute:
        Map.get(
          assigns,
          :html_theme_attribute,
          Map.get(assigns, "html_theme_attribute")
        ),
      blog_languages: blog_languages(metadata, language),
      alternate_links: [],
      menu_items: menu_items(project_id),
      calendar_initial_year: calendar_initial_year_from_posts(posts),
      calendar_initial_month: calendar_initial_month_from_posts(posts),
      archive_context: normalized_archive_context,
      show_archive_range_heading: show_archive_range_heading?(normalized_archive_context, day_blocks),
      min_date: min_date,
      max_date: max_date,
      is_list_page: true,
      is_first_page: pagination.current_page <= 1,
      is_last_page: pagination.current_page >= pagination.total_pages,
      has_prev_page: pagination.has_prev_page,
      has_next_page: pagination.has_next_page,
      prev_page_href: pagination.prev_page_href,
      next_page_href: pagination.next_page_href,
      current_page: pagination.current_page,
      total_pages: pagination.total_pages,
      total_items: pagination.total_items,
      items_per_page: pagination.items_per_page,
      canonical_post_path_by_slug: canonical_post_paths,
      canonical_media_path_by_source_path: canonical_media_paths,
      post_data_json_by_id:
        Enum.into(posts, %{}, fn post -> {post.id, post_data_json_value(post)} end),
      day_blocks: day_blocks
    }
  end

  defp not_found_assigns(project_id, assigns) do
    metadata = project_metadata(project_id)

    language =
      Map.get(assigns, :language, Map.get(assigns, "language", metadata.main_language || "en"))

    main_language = metadata.main_language || language

    %{
      page_title: Map.get(assigns, :page_title, Map.get(assigns, "page_title", "404")),
      language: language,
      language_prefix:
        Map.get(
          assigns,
          :language_prefix,
          Map.get(assigns, "language_prefix", language_prefix(language, main_language))
        ),
      pico_stylesheet_href:
        Map.get(
          assigns,
          :pico_stylesheet_href,
          Map.get(assigns, "pico_stylesheet_href", default_pico_stylesheet_href(metadata.pico_theme))
        ),
      html_theme_attribute:
        Map.get(
          assigns,
          :html_theme_attribute,
          Map.get(assigns, "html_theme_attribute")
        ),
      blog_languages: blog_languages(metadata, language),
      menu_items: menu_items(project_id),
      alternate_links: [],
      not_found_message:
        Map.get(
          assigns,
          :not_found_message,
          Map.get(
            assigns,
            "not_found_message",
            I18n.translate(language, "render.notFound.message")
          )
        ),
      not_found_back_label:
        Map.get(
          assigns,
          :not_found_back_label,
          Map.get(
            assigns,
            "not_found_back_label",
            I18n.translate(language, "render.notFound.back")
          )
        )
    }
  end

  defp project_metadata(project_id) do
    {:ok, metadata} = Metadata.get_project_metadata(project_id)
    metadata
  end

  defp menu_items(project_id) do
    {:ok, %{items: items}} = Menu.get_menu(project_id)
    Enum.map(items, &to_template_menu_item/1)
  end

  defp to_template_menu_item(item) do
    kind = Map.get(item, :kind)
    children = Enum.map(Map.get(item, :children, []), &to_template_menu_item/1)

    %{
      title: Map.get(item, :label, ""),
      href: menu_item_href(item),
      has_children: children != [],
      children: children,
      kind: kind
    }
  end

  defp menu_item_href(%{kind: :home}), do: "/"

  defp menu_item_href(%{kind: :page, slug: slug}) when is_binary(slug) and slug != "",
    do: "/#{slug}/"

  defp menu_item_href(%{kind: :category_archive, slug: slug}) when is_binary(slug) and slug != "",
    do: "/category/#{URI.encode(slug)}/"

  defp menu_item_href(%{kind: :submenu}), do: "#"
  defp menu_item_href(_item), do: "#"

  defp blog_languages(metadata, current_language) do
    ([metadata.main_language] ++ (metadata.blog_languages || []))
    |> Enum.reject(&(&1 in [nil, ""]))
    |> Enum.uniq()
    |> Enum.map(fn language ->
      normalized = I18n.normalize_language(language)
      href_prefix = language_prefix(normalized, metadata.main_language || current_language)

      %{
        code: normalized,
        flag: I18n.flag(normalized),
        href: href_for_language(href_prefix),
        href_prefix: href_prefix,
        is_current: normalized == I18n.normalize_language(current_language)
      }
    end)
  end

  defp tag_color_by_name(project_id) do
    Repo.all(from tag in Tag, where: tag.project_id == ^project_id, select: {tag.name, tag.color})
    |> Enum.into(%{}, fn {name, color} -> {name, color} end)
  end

  defp load_post_record(assigns) do
    case Map.get(assigns, :id, Map.get(assigns, "id")) do
      nil -> nil
      post_id -> Repo.get(Post, post_id) || Repo.get(Translation, post_id)
    end
  end

  defp canonical_post_record(%Translation{translation_for: post_id}), do: Repo.get(Post, post_id)
  defp canonical_post_record(%Post{} = post), do: post
  defp canonical_post_record(_other), do: nil

  defp canonical_post_id(%Translation{translation_for: post_id}, _assigns), do: post_id
  defp canonical_post_id(%Post{id: post_id}, _assigns), do: post_id
  defp canonical_post_id(_post_record, assigns), do: Map.get(assigns, :id, Map.get(assigns, "id"))

  defp post_data_json(assigns, post_record) do
    id = Map.get(assigns, :id, Map.get(assigns, "id"))

    if is_binary(id) do
      incoming_links = link_contexts(Map.get(post_record || %{}, :project_id), canonical_post_id(post_record, assigns), :incoming, Map.get(post_record || %{}, :language))
      outgoing_links = link_contexts(Map.get(post_record || %{}, :project_id), canonical_post_id(post_record, assigns), :outgoing, Map.get(post_record || %{}, :language))

      %{
        id => post_data_json_value(build_post_context(assigns, post_record, incoming_links, outgoing_links))
      }
    else
      %{}
    end
  end

  defp post_data_json_value(post_context) do
    Jason.encode!(%{
      id: Map.get(post_context, :id),
      title: Map.get(post_context, :title),
      slug: Map.get(post_context, :slug),
      excerpt: Map.get(post_context, :excerpt),
      author: Map.get(post_context, :author),
      language: Map.get(post_context, :language),
      published_at: Map.get(post_context, :published_at),
      created_at: Map.get(post_context, :created_at),
      updated_at: Map.get(post_context, :updated_at),
      tags: Map.get(post_context, :tags, []),
      categories: Map.get(post_context, :categories, [])
    })
  end

  defp canonical_post_path_by_slug(project_id, main_language) do
    posts =
      Repo.all(
        from post in Post, where: post.project_id == ^project_id and post.status == :published
      )

    translations =
      Repo.all(
        from translation in Translation,
          where: translation.project_id == ^project_id and translation.status == :published
      )

    post_by_id = Map.new(posts, fn post -> {post.id, post} end)

    post_paths =
      Enum.into(posts, %{}, fn post ->
        {post.slug, post_path(post, nil)}
      end)

    Enum.reduce(translations, post_paths, fn translation, acc ->
      case Map.get(post_by_id, translation.translation_for) do
        nil -> acc
        post -> Map.put(acc, post.slug, post_path(post, translation.language, main_language))
      end
    end)
  end

  defp alternate_links(nil, _project_id, _main_language), do: []

  defp alternate_links(%Post{} = post, project_id, main_language) do
    translations =
      Repo.all(
        from translation in Translation,
          where:
            translation.project_id == ^project_id and
              translation.translation_for == ^post.id and
              translation.status == :published,
          order_by: [asc: translation.language]
      )

    [%{href: post_path(post, nil), hreflang: normalize_language(post.language, main_language)}] ++
      Enum.map(translations, fn translation ->
        %{href: post_path(post, translation.language, main_language), hreflang: translation.language}
      end)
  end

  defp backlinks(incoming_links) do
    Enum.map(incoming_links, fn link ->
      %{path: link.href, display_slug: link.display_slug, title: link.title}
    end)
  end

  defp link_contexts(_project_id, nil, _direction, _main_language), do: []

  defp link_contexts(project_id, post_id, :incoming, main_language) do
    PostLinks.list_incoming_links(post_id)
    |> Enum.map(&link_context(project_id, &1, :incoming, main_language))
    |> Enum.reject(&is_nil/1)
  end

  defp link_contexts(project_id, post_id, :outgoing, main_language) do
    PostLinks.list_outgoing_links(post_id)
    |> Enum.map(&link_context(project_id, &1, :outgoing, main_language))
    |> Enum.reject(&is_nil/1)
  end

  defp link_context(_project_id, link, direction, main_language) do
    linked_post_id =
      case direction do
        :incoming -> link.source_post_id
        :outgoing -> link.target_post_id
      end

    case Repo.get(Post, linked_post_id) do
      nil -> nil
      linked_post -> %{href: post_path(linked_post, nil), title: linked_post.title, display_slug: linked_post.slug, language: normalize_language(linked_post.language, main_language)}
    end
  end

  defp canonical_media_path_by_source_path(project_id) do
    Repo.all(from media in MediaAsset, where: media.project_id == ^project_id)
    |> Enum.reduce(%{}, fn media, acc ->
      datetime = Persistence.from_unix_ms!(media.created_at)

      source_key =
        Path.join([
          "media",
          Integer.to_string(datetime.year),
          String.pad_leading(Integer.to_string(datetime.month), 2, "0"),
          media.original_name
        ])
        |> String.downcase()

      Map.put(acc, source_key, "/" <> media.file_path)
    end)
  end

  defp post_path(post, language_prefix)
       when is_binary(language_prefix) and language_prefix != "" do
    language_prefix <> post_path(post, nil)
  end

  defp post_path(post, ""), do: post_path(post, nil)

  defp post_path(post, nil) do
    datetime = Persistence.from_unix_ms!(post.created_at)

    Path.join([
      Integer.to_string(datetime.year),
      String.pad_leading(Integer.to_string(datetime.month), 2, "0"),
      String.pad_leading(Integer.to_string(datetime.day), 2, "0"),
      post.slug,
      "index.html"
    ])
    |> then(&("/" <> String.trim_trailing(&1, "index.html")))
  end

  defp post_path(post, language, main_language) do
    prefix = language_prefix(language, main_language)
    post_path(post, prefix)
  end

  defp normalize_list_posts(posts, canonical_post_paths, canonical_media_paths, language, template_context) do
    Enum.map(posts, fn post ->
      post_record = load_post_record(post)
      raw_content =
        Map.get(
          post,
          :content,
          Map.get(post, "content", Map.get(post, :excerpt, Map.get(post, "excerpt", "")))
        )

      %{
        id: Map.get(post, :id, Map.get(post, "id")),
        slug: Map.get(post, :slug, Map.get(post, "slug")),
        title: Map.get(post, :title, Map.get(post, "title")),
        content:
          render_post_content(
            raw_content,
            canonical_post_paths,
            canonical_media_paths,
            language,
            template_context
          ),
        raw_content: raw_content,
        excerpt:
          Map.get(post, :excerpt, Map.get(post, "excerpt", Map.get(post_record || %{}, :excerpt))),
        author: Map.get(post, :author, Map.get(post, "author", Map.get(post_record || %{}, :author))),
        language:
          Map.get(
            post,
            :language,
            Map.get(post, "language", Map.get(post_record || %{}, :language))
          ),
        published_at:
          Map.get(post, :published_at, Map.get(post, "published_at", Map.get(post_record || %{}, :published_at))),
        created_at:
          Map.get(post, :created_at, Map.get(post, "created_at", Map.get(post_record || %{}, :created_at))),
        updated_at:
          Map.get(post, :updated_at, Map.get(post, "updated_at", Map.get(post_record || %{}, :updated_at))),
        tags: Map.get(post, :tags, Map.get(post, "tags", Map.get(post_record || %{}, :tags, []))) || [],
        categories:
          Map.get(post, :categories, Map.get(post, "categories", Map.get(post_record || %{}, :categories, []))) || [],
        template_slug:
          Map.get(post, :template_slug, Map.get(post, "template_slug", Map.get(post_record || %{}, :template_slug))),
        do_not_translate:
          Map.get(post, :do_not_translate, Map.get(post, "do_not_translate", Map.get(post_record || %{}, :do_not_translate, false))),
        href: Map.get(post, :href, Map.get(post, "href")),
        show_title: true,
        linked_media: [],
        outgoing_links: [],
        incoming_links: []
      }
    end)
  end

  defp build_post_context(assigns, post_record, incoming_links, outgoing_links) do
    %{
      id: Map.get(assigns, :id, Map.get(assigns, "id")),
      slug: Map.get(assigns, :slug, Map.get(assigns, "slug")),
      title: Map.get(assigns, :title, Map.get(assigns, "title")),
      content: Map.get(assigns, :content, Map.get(assigns, "content")),
      raw_content: Map.get(assigns, :raw_content, Map.get(assigns, "raw_content")),
      excerpt:
        Map.get(
          assigns,
          :excerpt,
          Map.get(assigns, "excerpt", Map.get(post_record || %{}, :excerpt))
        ),
      author: Map.get(post_record || %{}, :author),
      language:
        Map.get(
          assigns,
          :language,
          Map.get(assigns, "language", Map.get(post_record || %{}, :language))
        ),
      show_title: true,
      published_at: Map.get(post_record || %{}, :published_at),
      created_at: Map.get(post_record || %{}, :created_at),
      updated_at: Map.get(post_record || %{}, :updated_at),
      tags: Map.get(post_record || %{}, :tags, []) || [],
      categories: Map.get(post_record || %{}, :categories, []) || [],
      template_slug:
        Map.get(
          post_record || %{},
          :template_slug,
          Map.get(assigns, :template_slug, Map.get(assigns, "template_slug"))
        ),
      do_not_translate: Map.get(post_record || %{}, :do_not_translate, false),
      linked_media: [],
      outgoing_links: outgoing_links,
      incoming_links: incoming_links
    }
  end

  defp render_post_content(content, canonical_post_paths, canonical_media_paths, language, template_context) do
    Filters.render_markdown(content, canonical_post_paths, canonical_media_paths, language, template_context)
  end

  defp template_render_context(project_id) do
    project = Projects.get_project!(project_id)

    Liquex.Context.new(%{},
      static_environment: %{},
      filter_module: Filters,
      file_system: FileSystem.new(StarterTemplates.template_roots(project))
    )
  end

  defp normalize_pagination(nil, posts) do
    total_items = length(posts)

    %{
      current_page: 1,
      total_pages: 1,
      total_items: total_items,
      items_per_page: total_items,
      has_prev_page: false,
      prev_page_href: "",
      has_next_page: false,
      next_page_href: ""
    }
  end

  defp normalize_pagination(%{} = pagination, posts) do
    total_items =
      Map.get(pagination, :total_items, Map.get(pagination, "total_items", length(posts)))

    items_per_page =
      Map.get(pagination, :items_per_page, Map.get(pagination, "items_per_page", total_items))

    %{
      current_page: Map.get(pagination, :current_page, Map.get(pagination, "current_page", 1)),
      total_pages: Map.get(pagination, :total_pages, Map.get(pagination, "total_pages", 1)),
      total_items: total_items,
      items_per_page: items_per_page,
      has_prev_page:
        Map.get(pagination, :has_prev_page, Map.get(pagination, "has_prev_page", false)),
      prev_page_href:
        Map.get(pagination, :prev_page_href, Map.get(pagination, "prev_page_href", "")),
      has_next_page:
        Map.get(pagination, :has_next_page, Map.get(pagination, "has_next_page", false)),
      next_page_href:
        Map.get(pagination, :next_page_href, Map.get(pagination, "next_page_href", ""))
    }
  end

  defp normalize_archive_context(nil), do: nil

  defp normalize_archive_context(%{} = archive_context) do
    %{
      kind: Map.get(archive_context, :kind, Map.get(archive_context, "kind")),
      name: Map.get(archive_context, :name, Map.get(archive_context, "name")),
      month: Map.get(archive_context, :month, Map.get(archive_context, "month")),
      year: Map.get(archive_context, :year, Map.get(archive_context, "year")),
      day: Map.get(archive_context, :day, Map.get(archive_context, "day"))
    }
  end

  defp build_day_blocks(posts) do
    grouped_blocks =
      posts
      |> Enum.filter(&is_integer(Map.get(&1, :created_at)))
      |> Enum.group_by(&(Map.get(&1, :created_at) |> Persistence.from_unix_ms!() |> DateTime.to_date() |> Date.to_iso8601()))
      |> Enum.sort_by(fn {label, _posts} -> label end)

    grouped_blocks
    |> Enum.with_index()
    |> Enum.map(fn {{date_label, grouped_posts}, index} ->
      %{
        date_label: date_label,
        show_date_marker: true,
        show_separator: index < length(grouped_blocks) - 1,
        posts: Enum.sort_by(grouped_posts, &Map.get(&1, :created_at))
      }
    end)
    |> case do
      [] -> [%{date_label: "", show_date_marker: false, show_separator: false, posts: posts}]
      blocks -> blocks
    end
  end

  defp min_date(posts) do
    posts
    |> Enum.map(&Map.get(&1, :created_at))
    |> Enum.filter(&is_integer/1)
    |> Enum.min(fn -> nil end)
  end

  defp max_date(posts) do
    posts
    |> Enum.map(&Map.get(&1, :created_at))
    |> Enum.filter(&is_integer/1)
    |> Enum.max(fn -> nil end)
  end

  defp show_archive_range_heading?(%{kind: "date"}, _day_blocks), do: true
  defp show_archive_range_heading?(_archive_context, _day_blocks), do: false

  defp default_pico_stylesheet_href(theme), do: PreviewAssets.stylesheet_href(theme)

  defp href_for_language(""), do: "/"
  defp href_for_language(prefix), do: prefix <> "/"

  defp calendar_initial_year(%{created_at: created_at}) when is_integer(created_at),
    do: Persistence.from_unix_ms!(created_at).year

  defp calendar_initial_year(_post), do: nil

  defp calendar_initial_month(%{created_at: created_at}) when is_integer(created_at),
    do: Persistence.from_unix_ms!(created_at).month

  defp calendar_initial_month(_post), do: nil

  defp calendar_initial_year_from_posts([post | _rest]), do: calendar_initial_year(post)
  defp calendar_initial_year_from_posts([]), do: nil

  defp calendar_initial_month_from_posts([post | _rest]), do: calendar_initial_month(post)
  defp calendar_initial_month_from_posts([]), do: nil

  defp language_prefix(language, main_language) when language == main_language, do: ""
  defp language_prefix(nil, _main_language), do: ""
  defp language_prefix(language, _main_language), do: "/#{language}"

  defp normalize_language(nil, fallback), do: fallback
  defp normalize_language("", fallback), do: fallback

  defp normalize_language(language, _fallback) do
    language
    |> to_string()
    |> String.downcase()
    |> String.split("-", parts: 2)
    |> hd()
  end
end
