defmodule BDS.UI.Sidebar do
  @moduledoc false

  import Ecto.Query

  alias BDS.AI.ChatConversation
  alias BDS.ImportDefinitions
  alias BDS.Media.Media
  alias BDS.Posts.Post
  alias BDS.Posts.Translation
  alias BDS.Repo
  alias BDS.Scripts.Script
  alias BDS.Tags.Tag
  alias BDS.Templates.Template

  @page_category "page"
  @default_page_size 500

  def snapshot(nil), do: empty_snapshot()

  def snapshot(project_id) when is_binary(project_id) do
    %{
      "posts" => view(project_id, "posts"),
      "pages" => view(project_id, "pages"),
      "media" => view(project_id, "media"),
      "scripts" => view(project_id, "scripts"),
      "templates" => view(project_id, "templates"),
      "tags" => view(project_id, "tags"),
      "chat" => view(project_id, "chat"),
      "import" =>
        entity_list_view(
          "Import",
          "Import definitions",
          "import",
          list_import_definitions(project_id)
        ),
      "git" => git_view(),
      "settings" => settings_nav_view()
    }
  end

  def view(project_id, view_id, params \\ %{})

  def view(nil, view_id, _params), do: empty_view(view_id)

  def view(project_id, view_id, params) when is_binary(project_id) do
    normalized_view = normalize_view_id(view_id)

    case normalized_view do
      "posts" ->
        posts_view(project_id, params, false)

      "pages" ->
        posts_view(project_id, params, true)

      "media" ->
        media_view(project_id, params)

      "scripts" ->
        entity_list_view("Scripts", "Automation helpers", "scripts", list_scripts(project_id))

      "templates" ->
        entity_list_view("Templates", "Site rendering", "templates", list_templates(project_id))

      "tags" ->
        tags_nav_view(list_tags(project_id))

      "chat" ->
        entity_list_view("Chat", "AI conversations", "chat", list_conversations())

      "import" ->
        entity_list_view(
          "Import",
          "Import definitions",
          "import",
          list_import_definitions(project_id)
        )

      "git" ->
        git_view()

      "settings" ->
        settings_nav_view()

      _other ->
        empty_view(normalized_view)
    end
  end

  def empty_snapshot do
    %{
      "posts" => empty_view("posts"),
      "pages" => empty_view("pages"),
      "media" => empty_view("media"),
      "scripts" => entity_list_view("Scripts", "Automation helpers", "scripts", []),
      "templates" => entity_list_view("Templates", "Site rendering", "templates", []),
      "tags" => tags_nav_view([]),
      "chat" => entity_list_view("Chat", "AI conversations", "chat", []),
      "import" => entity_list_view("Import", "Import definitions", "import", []),
      "git" => git_view(),
      "settings" => settings_nav_view()
    }
  end

  defp empty_view("posts"), do: posts_view_data([], [], %{}, false, empty_filter_params(), %{})
  defp empty_view("pages"), do: posts_view_data([], [], %{}, true, empty_filter_params(), %{})
  defp empty_view("media"), do: media_view_data([], [], empty_filter_params(), %{})
  defp empty_view("scripts"), do: entity_list_view("Scripts", "Automation helpers", "scripts", [])

  defp empty_view("templates"),
    do: entity_list_view("Templates", "Site rendering", "templates", [])

  defp empty_view("tags"), do: tags_nav_view([])
  defp empty_view("chat"), do: entity_list_view("Chat", "AI conversations", "chat", [])
  defp empty_view("import"), do: entity_list_view("Import", "Import definitions", "import", [])
  defp empty_view("git"), do: git_view()
  defp empty_view("settings"), do: settings_nav_view()

  defp empty_view(_other),
    do: %{title: "", subtitle: "", layout: "entity_list", items: [], empty_message: "No items"}

  defp posts_view(project_id, params, pages?) do
    posts = list_posts(project_id)
    translation_counts = translation_counts(project_id)
    tag_colors = tag_color_map(project_id)
    filters = normalize_filter_params(params)
    base_posts = Enum.filter(posts, &(page_post?(&1) == pages?))
    filtered_posts = apply_post_filters(base_posts, filters)

    posts_view_data(base_posts, filtered_posts, translation_counts, pages?, filters, tag_colors)
  end

  defp posts_view_data(
         base_posts,
         filtered_posts,
         translation_counts,
         pages?,
         filters,
         tag_colors
       ) do
    limited_posts = Enum.take(filtered_posts, filters.display_limit)
    grouped_posts = group_posts(limited_posts)
    available_tags = available_tags(base_posts, & &1.tags)
    available_categories = available_categories(base_posts, pages?)

    %{
      title: if(pages?, do: "Pages", else: "Posts"),
      subtitle:
        if(pages?, do: "Standalone pages", else: "Drafts, published entries, and archive history"),
      layout: "post_list",
      empty_message: if(pages?, do: "sidebar.noPagesYet", else: "sidebar.noPostsYet"),
      filters: %{
        enabled: true,
        search_placeholder:
          if(pages?, do: "sidebar.searchPagesPlaceholder", else: "sidebar.searchPostsPlaceholder"),
        toggle_filters_label: "sidebar.toggleFilters",
        archive_label: "render.archive",
        tags_label: "sidebar.tags",
        categories_label: "sidebar.categories",
        clear_tags_label: "sidebar.clearTags",
        clear_categories_label: "sidebar.clearCategories",
        clear_filters_label: "sidebar.clearFilters",
        results_label: "sidebar.results",
        results_for_label: "sidebar.resultsFor",
        no_results_label: "sidebar.noMatchingPosts",
        year_month_counts: year_month_counts(base_posts, &post_filter_timestamp/1),
        available_tags: available_tags,
        available_tag_colors: Map.take(tag_colors, available_tags),
        available_categories: available_categories,
        max_items: @default_page_size,
        display_limit: filters.display_limit,
        loaded_count: length(limited_posts),
        total_count: length(filtered_posts),
        has_more: length(filtered_posts) > filters.display_limit,
        has_active_filters: filter_active?(filters),
        selected: %{
          search: filters.search,
          year: filters.year,
          month: filters.month,
          tags: filters.tags,
          categories: filters.categories
        }
      },
      sections: [
        build_post_section("Drafts", :draft, grouped_posts.draft, translation_counts, false),
        build_post_section(
          "Published",
          :published,
          grouped_posts.published,
          translation_counts,
          true
        ),
        build_post_section(
          "Archived",
          :archived,
          grouped_posts.archived,
          translation_counts,
          false
        )
      ]
    }
  end

  defp media_view(project_id, params) do
    media_items = list_media(project_id)
    tag_colors = tag_color_map(project_id)
    filters = normalize_filter_params(params)
    filtered_media = apply_media_filters(media_items, filters)

    media_view_data(media_items, filtered_media, filters, tag_colors)
  end

  defp media_view_data(base_media, filtered_media, filters, tag_colors) do
    limited_media = Enum.take(filtered_media, filters.display_limit)
    available_tags = available_tags(base_media, & &1.tags)

    %{
      title: "Media",
      subtitle: "Images and files",
      layout: "media_grid",
      empty_message: "sidebar.noMediaFiles",
      filters: %{
        enabled: true,
        search_placeholder: "sidebar.searchMediaPlaceholder",
        toggle_filters_label: "sidebar.toggleFilters",
        archive_label: "render.archive",
        tags_label: "sidebar.tags",
        clear_tags_label: "sidebar.clearTags",
        clear_filters_label: "sidebar.clearFilters",
        results_label: "sidebar.results",
        results_for_label: "sidebar.resultsFor",
        no_results_label: "sidebar.noMediaFiles",
        year_month_counts: year_month_counts(base_media, &Map.get(&1, :updated_at)),
        available_tags: available_tags,
        available_tag_colors: Map.take(tag_colors, available_tags),
        available_categories: [],
        max_items: @default_page_size,
        display_limit: filters.display_limit,
        loaded_count: length(limited_media),
        total_count: length(filtered_media),
        has_more: length(filtered_media) > filters.display_limit,
        has_active_filters: filter_active?(filters),
        selected: %{
          search: filters.search,
          year: filters.year,
          month: filters.month,
          tags: filters.tags,
          categories: []
        }
      },
      items:
        Enum.map(limited_media, fn media ->
          %{
            id: media.id,
            title: display_media_title(media),
            meta: media_size_label(media.size),
            mime_type: media.mime_type,
            route: "media",
            updated_at: media.updated_at,
            tags: media.tags || [],
            search_blob: media_search_blob(media)
          }
        end)
    }
  end

  defp tags_nav_view(tags) do
    %{
      title: "Tags",
      subtitle: "Tag management",
      layout: "nav_list",
      items: [
        %{id: "tags-cloud", title: "Tag Cloud", icon: "☁️", route: "tags"},
        %{id: "tags-manage", title: "Create / Edit", icon: "✏️", route: "tags"},
        %{id: "tags-merge", title: "Merge Tags", icon: "🔀", route: "tags"}
      ],
      summary_badge: length(tags)
    }
  end

  defp settings_nav_view do
    %{
      title: "Settings",
      subtitle: "Project and publishing",
      layout: "nav_list",
      items: [
        %{id: "settings-project", title: "Project", icon: "📁", route: "settings"},
        %{id: "settings-editor", title: "Editor", icon: "📝", route: "settings"},
        %{id: "settings-content", title: "Content", icon: "📋", route: "settings"},
        %{id: "settings-ai", title: "AI", icon: "🤖", route: "settings"},
        %{id: "settings-technology", title: "Technology", icon: "⚙️", route: "settings"},
        %{id: "settings-publishing", title: "Publishing", icon: "🚀", route: "settings"},
        %{id: "settings-data", title: "Data", icon: "🗄️", route: "settings"},
        %{id: "settings-mcp", title: "MCP", icon: "🔌", route: "settings"},
        %{id: "settings-style", title: "Style", icon: "🎨", route: "style"}
      ]
    }
  end

  defp git_view do
    %{
      title: "Git",
      subtitle: "Working tree and history",
      layout: "entity_list",
      empty_message: "No items",
      items: [
        %{
          id: "git-working-tree",
          title: "Working tree",
          meta: "Working tree and history",
          route: "git_diff",
          updated_at: nil
        }
      ]
    }
  end

  defp entity_list_view(title, subtitle, route, items) do
    %{
      title: title,
      subtitle: subtitle,
      layout: "entity_list",
      empty_message: "No items",
      items:
        Enum.map(items, fn item ->
          %{
            id: item.id,
            title: item.title,
            meta: Map.get(item, :meta),
            updated_at: Map.get(item, :updated_at),
            route: route
          }
        end)
    }
  end

  defp build_post_section(title, status, posts, translation_counts, published_meta?) do
    %{
      id: Atom.to_string(status),
      title: title,
      status: Atom.to_string(status),
      count: length(posts),
      items:
        Enum.map(posts, fn post ->
          %{
            id: post.id,
            title: display_post_title(post),
            categories: post.categories || [],
            tags: post.tags || [],
            status: Atom.to_string(post.status),
            language_count: 1 + Map.get(translation_counts, post.id, 0),
            meta_timestamp:
              if(published_meta?, do: post.published_at || post.updated_at, else: post.updated_at),
            route: "post",
            search_blob: post_search_blob(post)
          }
        end)
    }
  end

  defp list_posts(project_id) do
    Repo.all(
      from post in Post,
        where: post.project_id == ^project_id,
        order_by: [desc: post.created_at],
        select: %{
          id: post.id,
          title: post.title,
          slug: post.slug,
          excerpt: post.excerpt,
          status: post.status,
          tags: post.tags,
          categories: post.categories,
          updated_at: post.updated_at,
          published_at: post.published_at,
          language: post.language
        }
    )
  end

  defp translation_counts(project_id) do
    Repo.all(
      from translation in Translation,
        where: translation.project_id == ^project_id,
        group_by: translation.translation_for,
        select: {translation.translation_for, count(translation.id)}
    )
    |> Map.new()
  end

  defp list_media(project_id) do
    Repo.all(
      from media in Media,
        where: media.project_id == ^project_id,
        order_by: [desc: media.created_at],
        select: %{
          id: media.id,
          title: media.title,
          original_name: media.original_name,
          mime_type: media.mime_type,
          size: media.size,
          tags: media.tags,
          alt: media.alt,
          caption: media.caption,
          updated_at: media.updated_at
        }
    )
  end

  defp list_scripts(project_id) do
    Repo.all(
      from script in Script,
        where: script.project_id == ^project_id,
        order_by: [desc: script.updated_at, desc: script.created_at],
        select: %{id: script.id, title: script.title, updated_at: script.updated_at}
    )
  end

  defp list_templates(project_id) do
    Repo.all(
      from template in Template,
        where: template.project_id == ^project_id,
        order_by: [desc: template.updated_at, desc: template.created_at],
        select: %{id: template.id, title: template.title, updated_at: template.updated_at}
    )
  end

  defp list_import_definitions(project_id) do
    ImportDefinitions.list_definitions(project_id)
  end

  defp list_tags(project_id) do
    Repo.all(
      from tag in Tag,
        where: tag.project_id == ^project_id,
        order_by: [asc: tag.name],
        select: %{id: tag.id, title: tag.name, updated_at: tag.updated_at}
    )
  end

  defp list_conversations do
    Repo.all(
      from conversation in ChatConversation,
        order_by: [desc: conversation.updated_at, desc: conversation.created_at],
        select: %{
          id: conversation.id,
          title: conversation.title,
          updated_at: conversation.updated_at
        }
    )
  end

  defp group_posts(posts) do
    Enum.reduce(posts, %{draft: [], published: [], archived: []}, fn post, acc ->
      case post.status do
        :draft -> %{acc | draft: acc.draft ++ [post]}
        :published -> %{acc | published: acc.published ++ [post]}
        :archived -> %{acc | archived: acc.archived ++ [post]}
        _other -> acc
      end
    end)
  end

  defp page_post?(post) do
    Enum.any?(post.categories || [], &(String.downcase(to_string(&1)) == @page_category))
  end

  defp normalize_view_id(view_id) when is_atom(view_id), do: Atom.to_string(view_id)
  defp normalize_view_id(view_id) when is_binary(view_id), do: view_id
  defp normalize_view_id(_other), do: ""

  defp normalize_filter_params(params) when is_map(params) do
    %{
      search: normalize_string(BDS.MapUtils.attr(params, :search)),
      year: normalize_integer(BDS.MapUtils.attr(params, :year)),
      month: normalize_integer(BDS.MapUtils.attr(params, :month)),
      tags: normalize_string_list(BDS.MapUtils.attr(params, :tags)),
      categories: normalize_string_list(BDS.MapUtils.attr(params, :categories)),
      display_limit:
        max(
          @default_page_size,
          normalize_integer(BDS.MapUtils.attr(params, :display_limit)) || @default_page_size
        )
    }
  end

  defp normalize_filter_params(_params), do: empty_filter_params()

  defp empty_filter_params do
    %{
      search: nil,
      year: nil,
      month: nil,
      tags: [],
      categories: [],
      display_limit: @default_page_size
    }
  end

  defp filter_active?(filters) do
    present?(filters.search) or not is_nil(filters.year) or filters.tags != [] or
      filters.categories != []
  end

  defp apply_post_filters(posts, filters) do
    Enum.filter(posts, fn post ->
      matches_search?(post_search_blob(post), filters.search) and
        matches_year_month?(post_filter_timestamp(post), filters.year, filters.month) and
        matches_overlap?(post.tags, filters.tags) and
        matches_overlap?(filtered_categories(post.categories), filters.categories)
    end)
  end

  defp apply_media_filters(media_items, filters) do
    Enum.filter(media_items, fn media ->
      matches_search?(media_search_blob(media), filters.search) and
        matches_year_month?(media.updated_at, filters.year, filters.month) and
        matches_overlap?(media.tags, filters.tags)
    end)
  end

  defp matches_search?(_text, nil), do: true

  defp matches_search?(text, search) do
    String.contains?(String.downcase(text), String.downcase(search))
  end

  defp matches_year_month?(_timestamp, nil, _month), do: true
  defp matches_year_month?(nil, _year, _month), do: false

  defp matches_year_month?(timestamp, year, month) do
    datetime = DateTime.from_unix!(timestamp, :millisecond)

    datetime.year == year and
      (is_nil(month) or datetime.month == month)
  end

  defp matches_overlap?(_values, []), do: true

  defp matches_overlap?(values, filters) do
    normalized_values = MapSet.new(Enum.map(values || [], &normalize_term/1))

    Enum.all?(filters, fn filter ->
      MapSet.member?(normalized_values, normalize_term(filter))
    end)
  end

  defp year_month_counts(items, timestamp_fun) do
    items
    |> Enum.reduce(%{}, fn item, acc ->
      case timestamp_fun.(item) do
        timestamp when is_integer(timestamp) ->
          datetime = DateTime.from_unix!(timestamp, :millisecond)
          Map.update(acc, {datetime.year, datetime.month}, 1, &(&1 + 1))

        _other ->
          acc
      end
    end)
    |> Enum.map(fn {{year, month}, count} -> %{year: year, month: month, count: count} end)
    |> Enum.sort_by(fn entry -> {-entry.year, -entry.month} end)
  end

  defp available_tags(items, getter) do
    items
    |> Enum.flat_map(fn item -> getter.(item) || [] end)
    |> Enum.map(&to_string/1)
    |> Enum.reject(&(&1 == ""))
    |> Enum.uniq_by(&String.downcase/1)
    |> Enum.sort_by(&String.downcase/1)
  end

  defp available_categories(posts, pages?) do
    posts
    |> Enum.flat_map(&filtered_categories(&1.categories || []))
    |> then(fn categories ->
      if pages?,
        do: Enum.reject(categories, &(normalize_term(&1) == @page_category)),
        else: categories
    end)
    |> Enum.map(&to_string/1)
    |> Enum.reject(&(&1 == ""))
    |> Enum.uniq_by(&String.downcase/1)
    |> Enum.sort_by(&String.downcase/1)
  end

  defp tag_color_map(project_id) do
    Repo.all(from tag in Tag, where: tag.project_id == ^project_id, select: {tag.name, tag.color})
    |> Enum.reduce(%{}, fn {name, color}, acc ->
      case String.trim(to_string(color || "")) do
        "" -> acc
        trimmed -> Map.put(acc, to_string(name), trimmed)
      end
    end)
  end

  defp filtered_categories(categories) do
    Enum.reject(categories || [], &(normalize_term(&1) == @page_category))
  end

  defp post_filter_timestamp(post), do: post.published_at || post.updated_at

  defp post_search_blob(post) do
    [
      post.title,
      post.slug,
      post.excerpt,
      Enum.join(post.tags || [], " "),
      Enum.join(post.categories || [], " ")
    ]
    |> Enum.reject(&is_nil/1)
    |> Enum.join(" ")
  end

  defp media_search_blob(media) do
    [media.title, media.original_name, media.alt, media.caption, Enum.join(media.tags || [], " ")]
    |> Enum.reject(&is_nil/1)
    |> Enum.join(" ")
  end

  defp normalize_integer(nil), do: nil
  defp normalize_integer(value) when is_integer(value), do: value

  defp normalize_integer(value) when is_binary(value) do
    case Integer.parse(value) do
      {integer, ""} -> integer
      _other -> nil
    end
  end

  defp normalize_integer(_value), do: nil

  defp normalize_string(value) when is_binary(value) do
    trimmed = String.trim(value)
    if trimmed == "", do: nil, else: trimmed
  end

  defp normalize_string(_value), do: nil

  defp normalize_string_list(values) when is_list(values) do
    values
    |> Enum.map(&normalize_string/1)
    |> Enum.reject(&is_nil/1)
  end

  defp normalize_string_list(_values), do: []

  defp normalize_term(value), do: value |> to_string() |> String.downcase()

  defp display_post_title(post) do
    cond do
      present?(post.title) -> post.title
      present?(post.slug) -> post.slug
      true -> "Untitled"
    end
  end

  defp display_media_title(media) do
    if present?(media.title), do: media.title, else: media.original_name || ""
  end

  defp media_size_label(size) when is_integer(size) and size < 1024, do: "#{size} B"

  defp media_size_label(size) when is_integer(size) and size < 1024 * 1024 do
    :erlang.float_to_binary(size / 1024, decimals: 1) <> " KB"
  end

  defp media_size_label(size) when is_integer(size) do
    :erlang.float_to_binary(size / (1024 * 1024), decimals: 1) <> " MB"
  end

  defp media_size_label(_size), do: "0 B"

  defp present?(value), do: value not in [nil, ""]
end
