defmodule BDS.Desktop.ShellLive.MiscEditor do
  @moduledoc false

  use Phoenix.LiveComponent

  import Phoenix.HTML, only: [raw: 1]
  import Ecto.Query

  alias BDS.{Embeddings, Generation, Git, HelpDocs, Posts, Repo}
  alias BDS.Desktop.ShellLive.Notify
  alias BDS.MapUtils
  alias BDS.Settings.Setting
  use Gettext, backend: BDS.Gettext

  embed_templates("misc_editor_html/*")

  @misc_routes [
    :documentation,
    :api_documentation,
    :site_validation,
    :metadata_diff,
    :translation_validation,
    :find_duplicates,
    :git_diff
  ]

  # ── LiveComponent lifecycle ────────────────────────────────────────────────

  @spec update(map(), Phoenix.LiveView.Socket.t()) :: {:ok, Phoenix.LiveView.Socket.t()}
  @impl true
  def update(%{current_tab: current_tab, tab_meta: tab_meta, project_id: project_id}, socket) do
    socket =
      socket
      |> assign(:current_tab, current_tab)
      |> assign(:project_id, project_id)
      |> assign(:tab_meta, tab_meta)
      |> ensure_state()
      |> build_data()

    {:ok, socket}
  end

  defp ensure_state(socket) do
    socket
    |> assign_new(:selected_pairs, fn -> MapSet.new() end)
    |> assign_new(:git_selected_file, fn -> nil end)
    |> assign_new(:active_tab, fn -> nil end)
    |> assign_new(:active_field, fn -> nil end)
  end

  defp build_data(socket) do
    misc_editor = do_build(socket.assigns)
    assign(socket, :misc_editor, misc_editor)
  end

  @spec render(map()) :: Phoenix.LiveView.Rendered.t()
  @impl true
  def render(assigns) do
    misc_editor(assigns)
  end

  # ── Event handlers ─────────────────────────────────────────────────────────

  @spec handle_event(String.t(), map(), Phoenix.LiveView.Socket.t()) ::
          {:noreply, Phoenix.LiveView.Socket.t()}
  @impl true
  def handle_event("rerun_misc_editor", _params, socket) do
    action = rerun_action(socket.assigns)

    if action do
      notify_command(action)
    end

    {:noreply, socket}
  end

  def handle_event("apply_site_validation", _params, socket) do
    meta = meta(socket.assigns)
    payload = Map.get(meta, :payload, %{})
    project_id = Map.get(meta, :project_id, socket.assigns.project_id)

    report = %{
      sitemap_path: Map.get(payload, :sitemap_path),
      sitemap_changed: Map.get(payload, :sitemap_changed, false),
      missing_url_paths: Map.get(payload, :missing_url_paths, []),
      extra_url_paths: Map.get(payload, :extra_url_paths, []),
      updated_post_url_paths: Map.get(payload, :updated_post_url_paths, []),
      expected_url_count: Map.get(payload, :expected_url_count, 0),
      existing_html_url_count: Map.get(payload, :existing_html_url_count, 0)
    }

    case Generation.apply_validation(project_id, report) do
      {:ok, result} ->
        notify_output(
          dgettext("ui", "Site Validation"),
          dgettext("ui", "Validation changes applied"),
          inspect(result)
        )

        notify_command("validate_site")
        {:noreply, socket}
    end
  rescue
    error ->
      notify_output(dgettext("ui", "Site Validation"), inspect(error), nil, "error")
      {:noreply, socket}
  end

  def handle_event("fix_translation_validation", _params, socket) do
    report =
      socket.assigns
      |> meta()
      |> Map.get(:payload, %{})
      |> normalize_translation_validation_report()

    {:ok, result} = Posts.fix_invalid_translations(report)

    notify_output(
      dgettext("ui", "Translation Validation"),
      dgettext(
        "ui",
        "Deleted %{dbRows} DB rows and %{files} files, flushed %{flushed} translations to disk",
        dbRows: result.deleted_database_rows,
        files: result.deleted_files,
        flushed: result.flushed_translations
      )
    )

    notify_command("validate_translations")
    {:noreply, socket}
  rescue
    error ->
      notify_output(dgettext("ui", "Translation Validation"), inspect(error), nil, "error")
      {:noreply, socket}
  end

  def handle_event("select_git_diff_file", %{"path" => path}, socket) do
    {:noreply, assign(socket, :git_selected_file, path) |> build_data()}
  end

  def handle_event("toggle_duplicate_pair", %{"pair-id" => pair_id}, socket) do
    current = socket.assigns.selected_pairs

    next =
      if MapSet.member?(current, pair_id) do
        MapSet.delete(current, pair_id)
      else
        MapSet.put(current, pair_id)
      end

    {:noreply, assign(socket, :selected_pairs, next) |> build_data()}
  end

  def handle_event(
        "dismiss_duplicate_pair",
        %{"post-id-a" => post_id_a, "post-id-b" => post_id_b},
        socket
      ) do
    case Embeddings.dismiss_duplicate_pair(post_id_a, post_id_b) do
      {:ok, _saved_pair} ->
        tab_type = socket.assigns.current_tab.type
        tab_id = socket.assigns.current_tab.id
        pair_id = pair_id(post_id_a, post_id_b)

        payload = Map.get(meta(socket.assigns), :payload, %{})

        next_pairs =
          update_in(payload[:pairs], fn pairs ->
            Enum.reject(pairs || [], fn pair ->
              pair_identity(pair) == pair_id
            end)
          end)

        next_payload = Map.put(payload, :pairs, next_pairs)
        notify_tab_meta(tab_type, tab_id, %{payload: next_payload})
        notify_output(dgettext("ui", "Find Duplicates"), dgettext("ui", "Pair dismissed"))

        selected = MapSet.delete(socket.assigns.selected_pairs, pair_id)
        {:noreply, assign(socket, :selected_pairs, selected) |> build_data()}

      {:error, reason} ->
        notify_output(dgettext("ui", "Find Duplicates"), inspect(reason), nil, "error")
        {:noreply, socket}
    end
  end

  def handle_event("dismiss_selected_duplicates", _params, socket) do
    selected =
      socket.assigns.selected_pairs
      |> MapSet.to_list()
      |> Enum.map(&decode_pair_id/1)
      |> Enum.reject(&is_nil/1)

    case Embeddings.dismiss_duplicate_pairs(selected) do
      {:ok, _saved_pairs} ->
        tab_type = socket.assigns.current_tab.type
        tab_id = socket.assigns.current_tab.id
        payload = Map.get(meta(socket.assigns), :payload, %{})

        next_pairs =
          update_in(payload[:pairs], fn pairs ->
            Enum.reject(pairs || [], fn pair -> pair_identity(pair) in selected end)
          end)

        next_payload = Map.put(payload, :pairs, next_pairs)
        notify_tab_meta(tab_type, tab_id, %{payload: next_payload})

        notify_output(
          dgettext("ui", "Find Duplicates"),
          dgettext("ui", "Selected pairs dismissed")
        )

        {:noreply, assign(socket, :selected_pairs, MapSet.new()) |> build_data()}

      {:error, reason} ->
        notify_output(dgettext("ui", "Find Duplicates"), inspect(reason), nil, "error")
        {:noreply, socket}
    end
  end

  def handle_event("repair_metadata_diff", %{"field" => field, "direction" => direction}, socket) do
    case metadata_diff_repair_request(socket.assigns, field, direction) do
      {:ok, params} ->
        notify_command("repair_metadata_diff", params)
        {:noreply, socket}

      {:error, message} ->
        notify_output(dgettext("ui", "Metadata Diff"), message, nil, "error")
        {:noreply, socket}
    end
  end

  def handle_event("import_metadata_diff_orphans", _params, socket) do
    case metadata_diff_orphan_import_request(socket.assigns) do
      {:ok, params} ->
        notify_command("import_metadata_diff_orphans", params)
        {:noreply, socket}

      {:error, message} ->
        notify_output(dgettext("ui", "Metadata Diff"), message, nil, "error")
        {:noreply, socket}
    end
  end

  def handle_event("select_metadata_diff_tab", %{"tab" => tab}, socket) do
    {:noreply,
     socket
     |> assign(:active_tab, tab)
     |> assign(:active_field, nil)
     |> build_data()}
  end

  def handle_event("toggle_metadata_diff_field", %{"field" => field}, socket) do
    current = socket.assigns.active_field
    next = if current == field, do: nil, else: field
    {:noreply, assign(socket, :active_field, next) |> build_data()}
  end

  def handle_event("open_duplicate_post", %{"id" => id, "title" => title}, socket) do
    notify_open_sidebar_item(
      %{"route" => "post", "id" => id, "title" => title, "subtitle" => "draft"},
      :preview
    )

    {:noreply, socket}
  end

  # ── Public helper functions (used by template) ─────────────────────────────

  @spec misc_class(atom()) :: String.t()
  def misc_class(:documentation), do: "help-doc-view"
  def misc_class(:api_documentation), do: "help-doc-view"
  def misc_class(:site_validation), do: "site-validation-view"
  def misc_class(:metadata_diff), do: "metadata-diff-view"
  def misc_class(:translation_validation), do: "translation-validation-view"
  def misc_class(:find_duplicates), do: "duplicates-view"
  def misc_class(:git_diff), do: "git-diff-view"

  @spec markdown_html(String.t()) :: Phoenix.HTML.safe()
  def markdown_html(content) do
    # Match Earmark's defaults (GFM tables, strikethrough, autolinks); escape
    # raw HTML to text rather than rendering it, as the old escape: true did.
    html =
      case MDEx.to_html(content || "",
             extension: [table: true, strikethrough: true, autolink: true],
             render: [escape: true]
           ) do
        {:ok, rendered} -> rendered
        {:error, _reason} -> ""
      end

    raw(html)
  end

  @spec refreshable?(atom()) :: boolean()
  def refreshable?(kind), do: kind not in [:documentation, :api_documentation]

  @spec summary_items(map()) :: [{String.t(), any()}]
  def summary_items(%{summary: summary}) when is_map(summary), do: Enum.to_list(summary)
  def summary_items(_misc), do: []

  @spec duplicate_checked?(map(), String.t()) :: boolean()
  def duplicate_checked?(misc, pair_id), do: MapSet.member?(misc.selected_pairs, pair_id)

  @spec pair_id_from_pair(map()) :: String.t()
  def pair_id_from_pair(pair), do: pair_identity(pair)

  @spec translation_issue_label(map()) :: String.t()
  def translation_issue_label(issue) do
    case issue_value(issue, :issue) do
      "same-language-as-canonical" ->
        dgettext("ui", "Translation language matches canonical post language")

      "do-not-translate-has-translations" ->
        dgettext("ui", "Post is marked as do-not-translate but has translations")

      "content-in-database" ->
        dgettext("ui", "Published translation has content stuck in DB instead of filesystem")

      _other ->
        dgettext("ui", "Translation points to a missing source post")
    end
  end

  @spec translation_issue_languages(map()) :: String.t()
  def translation_issue_languages(issue) do
    canonical_language = issue_value(issue, :canonical_language)
    translation_language = issue_value(issue, :translation_language)

    if canonical_language in [nil, ""] do
      translation_language
    else
      dgettext("ui", "%{canonical} = %{translation}",
        canonical: canonical_language,
        translation: translation_language
      )
    end
  end

  @spec translation_issue_value(map(), atom() | String.t()) :: any()
  def translation_issue_value(issue, key), do: issue_value(issue, key)

  @spec git_diff_language(String.t() | nil) :: String.t()
  def git_diff_language(nil), do: "plaintext"

  def git_diff_language(file_path) do
    case file_path |> Path.extname() |> String.downcase() do
      ".md" -> "markdown"
      ".markdown" -> "markdown"
      ".mdx" -> "markdown"
      ".ts" -> "typescript"
      ".tsx" -> "typescript"
      ".js" -> "javascript"
      ".jsx" -> "javascript"
      ".json" -> "json"
      ".css" -> "css"
      ".html" -> "html"
      ".yml" -> "yaml"
      ".yaml" -> "yaml"
      _other -> "plaintext"
    end
  end

  # ── Private helpers ────────────────────────────────────────────────────────

  defp notify_command(action, params \\ %{}) do
    Notify.command(action, params)
  end

  defp notify_output(title, message, detail \\ nil, level \\ "info") do
    Notify.output(title, message, detail, level)
  end

  defp notify_tab_meta(tab_type, tab_id, updates) do
    Notify.tab_meta_merge(tab_type, tab_id, updates)
  end

  defp notify_open_sidebar_item(params, intent) do
    Notify.open_sidebar_item(params, intent)
  end

  defp rerun_action(assigns) do
    case meta(assigns) do
      %{action: action} when is_binary(action) ->
        action

      _other ->
        misc_route_action(assigns.current_tab.type)
    end
  end

  defp do_build(%{current_tab: %{type: type}} = assigns) when type in @misc_routes do
    meta = meta(assigns)
    payload = Map.get(meta, :payload, %{})

    case type do
      :documentation -> build_help_doc(type, meta)
      :api_documentation -> build_help_doc(type, meta)
      :site_validation -> build_site_validation(meta, payload)
      :metadata_diff -> build_metadata_diff(assigns, meta, payload)
      :translation_validation -> build_translation_validation(meta, payload)
      :find_duplicates -> build_duplicates(assigns, meta, payload)
      :git_diff -> build_git_diff(assigns, meta)
    end
  end

  defp do_build(_assigns), do: nil

  defp build_help_doc(type, meta) do
    help_doc = HelpDocs.fetch(type)

    %{
      kind: type,
      title: Map.get(meta, :title, help_doc.title),
      subtitle: Map.get(meta, :subtitle, help_doc.subtitle),
      summary: %{},
      markdown: help_doc.markdown
    }
  end

  defp build_site_validation(meta, payload) do
    summary = Map.get(payload, :summary, %{})

    %{
      kind: :site_validation,
      title: Map.get(meta, :title, dgettext("ui", "Site Validation")),
      subtitle: Map.get(meta, :subtitle, ""),
      summary: %{
        expected: Map.get(summary, :expected_count, 0),
        existing: Map.get(summary, :existing_count, 0),
        missing: Map.get(summary, :missing_count, 0),
        extra: Map.get(summary, :extra_count, 0),
        updated: Map.get(summary, :updated_count, 0)
      },
      sitemap_path: Map.get(payload, :sitemap_path),
      sitemap_changed: Map.get(payload, :sitemap_changed, false),
      missing_url_paths: Map.get(payload, :missing_url_paths, []),
      extra_url_paths: Map.get(payload, :extra_url_paths, []),
      updated_post_url_paths: Map.get(payload, :updated_post_url_paths, []),
      expected_url_count: Map.get(payload, :expected_url_count, 0),
      existing_html_url_count: Map.get(payload, :existing_html_url_count, 0)
    }
  end

  defp build_metadata_diff(assigns, meta, payload) do
    items = Enum.map(Map.get(payload, :diff_reports, []), &normalize_metadata_diff_item/1)

    orphan_files =
      Enum.map(Map.get(payload, :orphan_reports, []), &normalize_metadata_diff_orphan/1)

    tabs = metadata_diff_tabs(items, orphan_files)
    active_tab = assigns.active_tab || metadata_diff_active_tab_fallback(tabs)
    active_field = assigns.active_field

    current_tab =
      Enum.find(tabs, &(&1.id == active_tab)) || List.first(tabs) || empty_metadata_diff_tab()

    filtered_items = metadata_diff_filtered_items(current_tab.items, active_field)

    %{
      kind: :metadata_diff,
      title: Map.get(meta, :title, dgettext("ui", "Metadata Diff")),
      subtitle: Map.get(meta, :subtitle, ""),
      summary: Map.get(payload, :summary, %{}),
      tabs:
        Enum.map(tabs, &Map.take(&1, [:id, :label, :badge_count, :diff_count, :orphan_count])),
      active_tab: current_tab.id,
      active_field: active_field,
      repair_enabled: metadata_diff_repairable_tab?(current_tab.id),
      field_summaries: field_summaries(current_tab.items),
      items: filtered_items,
      orphan_files: if(is_nil(active_field), do: current_tab.orphan_files, else: []),
      empty_message: dgettext("ui", "No items")
    }
  end

  defp build_translation_validation(meta, payload) do
    report = normalize_translation_validation_report(payload)

    %{
      kind: :translation_validation,
      title: Map.get(meta, :title, dgettext("ui", "Translation Validation")),
      subtitle: Map.get(meta, :subtitle, ""),
      summary: %{},
      summary_text:
        dgettext(
          "ui",
          "Checked DB rows: %{dbRows} · Checked files: %{files} · Invalid DB rows: %{invalidDb} · Invalid files: %{invalidFiles}",
          dbRows: report.checked_database_row_count,
          files: report.checked_filesystem_file_count,
          invalidDb: length(report.invalid_database_rows),
          invalidFiles: length(report.invalid_filesystem_files)
        ),
      invalid_database_rows: report.invalid_database_rows,
      invalid_filesystem_files: report.invalid_filesystem_files,
      can_fix?: report.invalid_database_rows != [] or report.invalid_filesystem_files != []
    }
  end

  defp build_duplicates(assigns, meta, payload) do
    %{
      kind: :find_duplicates,
      title: Map.get(meta, :title, dgettext("ui", "Find Duplicates")),
      subtitle: Map.get(meta, :subtitle, ""),
      summary: Map.get(payload, :summary, %{}),
      pairs: Map.get(payload, :pairs, []),
      selected_pairs: assigns.selected_pairs
    }
  end

  defp build_git_diff(assigns, meta) do
    project_id = assigns.project_id
    selected_file = assigns.git_selected_file

    {files, diff, error_message} =
      case Git.status(project_id) do
        {:ok, %{files: files}} ->
          file_paths =
            files
            |> Enum.map(&Map.get(&1, :path))
            |> Enum.reject(&is_nil/1)
            |> Enum.uniq()
            |> Enum.sort()

          selected_file_path =
            if selected_file in file_paths do
              selected_file
            else
              List.first(file_paths)
            end

          diff =
            case selected_file_path do
              nil ->
                empty_git_diff(project_id)

              file_path ->
                case Git.get_diff_content(project_id, file_path) do
                  {:ok, diff} ->
                    diff

                  {:error, reason} ->
                    Map.merge(empty_git_diff(project_id), %{
                      file_path: file_path,
                      error: inspect(reason)
                    })
                end
            end

          {file_paths, diff, nil}

        {:error, reason} ->
          {[], empty_git_diff(project_id), inspect(reason)}
      end

    preferences = git_diff_preferences()

    %{
      kind: :git_diff,
      title: Map.get(meta, :title, dgettext("ui", "Git Diff")),
      subtitle: Map.get(meta, :subtitle, ""),
      summary: %{},
      files: files,
      selected_file_path: diff.file_path,
      active_diff: Map.put(diff, :language, git_diff_language(diff.file_path)),
      preferences: preferences,
      empty_message: error_message || dgettext("ui", "No unstaged changes")
    }
  end

  defp meta(assigns) do
    Map.get(assigns.tab_meta, {assigns.current_tab.type, assigns.current_tab.id}, %{})
  end

  defp metadata_diff_repair_request(assigns, field, direction) do
    payload = Map.get(meta(assigns), :payload, %{})
    items = Enum.map(Map.get(payload, :diff_reports, []), &normalize_metadata_diff_item/1)
    tabs = metadata_diff_tabs(items, [])
    active_tab = assigns.active_tab || metadata_diff_active_tab_fallback(tabs)

    repair_items =
      items
      |> Enum.filter(&(&1.tab_id == active_tab))
      |> Enum.filter(fn item -> Enum.any?(item.differences, &(diff_name(&1) == field)) end)
      |> Enum.map(&%{"entity_type" => &1.entity_type, "entity_id" => &1.entity_id})

    cond do
      not metadata_diff_repairable_tab?(active_tab) ->
        {:error, dgettext("ui", "No repair action available")}

      repair_items == [] ->
        {:error, dgettext("ui", "No metadata diff items selected")}

      true ->
        {:ok,
         %{
           "direction" => direction,
           "field" => field,
           "tab" => active_tab,
           "items" => repair_items
         }}
    end
  end

  defp metadata_diff_orphan_import_request(assigns) do
    payload = Map.get(meta(assigns), :payload, %{})
    items = Enum.map(Map.get(payload, :diff_reports, []), &normalize_metadata_diff_item/1)

    orphan_files =
      Enum.map(Map.get(payload, :orphan_reports, []), &normalize_metadata_diff_orphan/1)

    tabs = metadata_diff_tabs(items, orphan_files)
    active_tab = assigns.active_tab || metadata_diff_active_tab_fallback(tabs)

    selected_orphans =
      orphan_files
      |> Enum.filter(&(&1.tab_id == active_tab))
      |> Enum.map(&%{"file_path" => &1.file_path})

    if selected_orphans == [] do
      {:error, dgettext("ui", "No orphan files selected")}
    else
      {:ok, %{"tab" => active_tab, "orphans" => selected_orphans}}
    end
  end

  defp pair_id(post_id_a, post_id_b), do: Enum.sort([post_id_a, post_id_b]) |> Enum.join("::")

  defp pair_identity(pair),
    do: pair_id(MapUtils.attr(pair, :post_id_a), MapUtils.attr(pair, :post_id_b))

  defp decode_pair_id(encoded) when is_binary(encoded) do
    case String.split(encoded, "::", parts: 2) do
      [post_id_a, post_id_b] -> {post_id_a, post_id_b}
      _other -> nil
    end
  end

  defp decode_pair_id(_encoded), do: nil

  defp field_summaries(items) do
    items
    |> Enum.flat_map(fn item -> MapUtils.attr(item, :differences) || [] end)
    |> Enum.group_by(&diff_name/1)
    |> Enum.map(fn {field, diffs} -> %{field_name: field, diff_count: length(diffs)} end)
    |> Enum.sort_by(&{&1.diff_count * -1, &1.field_name})
  end

  defp metadata_diff_tabs(items, orphan_files) do
    tab_ids =
      (Enum.map(items, & &1.tab_id) ++ Enum.map(orphan_files, & &1.tab_id))
      |> Enum.uniq()
      |> Enum.sort_by(&metadata_diff_tab_sort_key/1)

    if tab_ids == [] do
      [empty_metadata_diff_tab()]
    else
      Enum.map(tab_ids, fn tab_id ->
        tab_items = Enum.filter(items, &(&1.tab_id == tab_id))
        tab_orphans = Enum.filter(orphan_files, &(&1.tab_id == tab_id))

        %{
          id: tab_id,
          label: metadata_diff_tab_label(tab_id),
          items: tab_items,
          orphan_files: tab_orphans,
          diff_count: length(tab_items),
          orphan_count: length(tab_orphans),
          badge_count: length(tab_items) + length(tab_orphans)
        }
      end)
    end
  end

  defp empty_metadata_diff_tab do
    %{
      id: "posts",
      label: dgettext("ui", "Posts"),
      items: [],
      orphan_files: [],
      diff_count: 0,
      orphan_count: 0,
      badge_count: 0
    }
  end

  defp metadata_diff_active_tab_fallback(tabs) do
    tabs |> List.first() |> Map.get(:id)
  end

  defp metadata_diff_filtered_items(items, nil), do: items

  defp metadata_diff_filtered_items(items, field) do
    Enum.filter(items, fn item -> Enum.any?(item.differences, &(diff_name(&1) == field)) end)
  end

  defp normalize_metadata_diff_item(item) do
    entity_type = MapUtils.attr(item, :entity_type) || "post"
    entity_id = MapUtils.attr(item, :entity_id) || ""

    differences =
      item
      |> MapUtils.attr(:differences, [])
      |> Enum.map(&normalize_metadata_diff_difference/1)

    %{
      tab_id: metadata_diff_tab_id(entity_type),
      entity_type: entity_type,
      entity_id: entity_id,
      label: metadata_diff_item_label(item, entity_id),
      meta_label: metadata_diff_item_meta_label(item, entity_id),
      display_entity_type: metadata_diff_item_type_label(entity_type),
      differences: differences
    }
  end

  defp normalize_metadata_diff_difference(diff) do
    %{
      field: diff_name(diff),
      db_value: format_metadata_diff_value(MapUtils.attr(diff, :db_value)),
      file_value: format_metadata_diff_value(MapUtils.attr(diff, :file_value))
    }
  end

  defp normalize_metadata_diff_orphan(orphan) do
    path = MapUtils.attr(orphan, :file_path) || MapUtils.attr(orphan, :path) || ""
    entity_type = MapUtils.attr(orphan, :entity_type) || metadata_diff_orphan_entity_type(path)

    %{
      tab_id: metadata_diff_tab_id(entity_type),
      entity_type: entity_type,
      file_path: path,
      slug: Path.basename(path) |> String.trim(),
      id: MapUtils.attr(orphan, :id)
    }
  end

  defp metadata_diff_item_label(item, entity_id) do
    MapUtils.attr(item, :label) || MapUtils.attr(item, :title) || MapUtils.attr(item, :slug) ||
      entity_id
  end

  defp metadata_diff_item_meta_label(item, entity_id) do
    MapUtils.attr(item, :meta_label) || entity_id
  end

  defp metadata_diff_item_type_label("post"), do: dgettext("ui", "Post")
  defp metadata_diff_item_type_label("post_translation"), do: dgettext("ui", "Translations")
  defp metadata_diff_item_type_label("media"), do: dgettext("ui", "Media")
  defp metadata_diff_item_type_label("media_translation"), do: dgettext("ui", "Translations")
  defp metadata_diff_item_type_label("script"), do: dgettext("ui", "Script")
  defp metadata_diff_item_type_label("template"), do: dgettext("ui", "Template")
  defp metadata_diff_item_type_label("project"), do: dgettext("ui", "Project")
  defp metadata_diff_item_type_label("publishing"), do: dgettext("ui", "Publishing")
  defp metadata_diff_item_type_label("categories"), do: dgettext("ui", "Categories")
  defp metadata_diff_item_type_label("category_meta"), do: dgettext("ui", "Categories")
  defp metadata_diff_item_type_label("embedding"), do: dgettext("ui", "Embeddings")

  defp metadata_diff_item_type_label(entity_type),
    do: entity_type |> String.replace("_", " ") |> String.capitalize()

  defp metadata_diff_tab_id("post"), do: "posts"
  defp metadata_diff_tab_id("post_translation"), do: "posts"
  defp metadata_diff_tab_id("media"), do: "media"
  defp metadata_diff_tab_id("media_translation"), do: "media"
  defp metadata_diff_tab_id("script"), do: "scripts"
  defp metadata_diff_tab_id("template"), do: "templates"
  defp metadata_diff_tab_id("project"), do: "project"
  defp metadata_diff_tab_id("publishing"), do: "project"
  defp metadata_diff_tab_id("categories"), do: "project"
  defp metadata_diff_tab_id("category_meta"), do: "project"
  defp metadata_diff_tab_id("embedding"), do: "embeddings"
  defp metadata_diff_tab_id(_entity_type), do: "project"

  defp metadata_diff_tab_label("posts"), do: dgettext("ui", "Posts")
  defp metadata_diff_tab_label("media"), do: dgettext("ui", "Media")
  defp metadata_diff_tab_label("scripts"), do: dgettext("ui", "Scripts")
  defp metadata_diff_tab_label("templates"), do: dgettext("ui", "Templates")
  defp metadata_diff_tab_label("project"), do: dgettext("ui", "Project")
  defp metadata_diff_tab_label("embeddings"), do: dgettext("ui", "Embeddings")

  defp metadata_diff_tab_label(tab_id),
    do: tab_id |> String.replace("_", " ") |> String.capitalize()

  defp metadata_diff_tab_sort_key("posts"), do: 0
  defp metadata_diff_tab_sort_key("media"), do: 1
  defp metadata_diff_tab_sort_key("scripts"), do: 2
  defp metadata_diff_tab_sort_key("templates"), do: 3
  defp metadata_diff_tab_sort_key("project"), do: 4
  defp metadata_diff_tab_sort_key("embeddings"), do: 5
  defp metadata_diff_tab_sort_key(other), do: {6, other}

  defp metadata_diff_orphan_entity_type(path) do
    cond do
      String.starts_with?(path, "posts/") -> "post"
      String.starts_with?(path, "media/") -> "media"
      String.starts_with?(path, "scripts/") -> "script"
      String.starts_with?(path, "templates/") -> "template"
      true -> "project"
    end
  end

  defp metadata_diff_repairable_tab?(tab_id),
    do: tab_id in ["posts", "media", "scripts", "templates", "project", "embeddings"]

  defp misc_route_action(:site_validation), do: "validate_site"
  defp misc_route_action(:metadata_diff), do: "metadata_diff"
  defp misc_route_action(:translation_validation), do: "validate_translations"
  defp misc_route_action(:find_duplicates), do: "find_duplicates"
  defp misc_route_action(_route), do: nil

  defp format_metadata_diff_value(nil), do: "-"
  defp format_metadata_diff_value(""), do: "-"
  defp format_metadata_diff_value(value), do: to_string(value)

  defp diff_name(diff) do
    MapUtils.attr(diff, :field) || MapUtils.attr(diff, :name) || "value"
  end

  defp normalize_translation_validation_report(payload) when is_map(payload) do
    %{
      checked_database_row_count: issue_value(payload, :checked_database_row_count) || 0,
      checked_filesystem_file_count: issue_value(payload, :checked_filesystem_file_count) || 0,
      invalid_database_rows:
        payload
        |> issue_value(:invalid_database_rows)
        |> List.wrap(),
      invalid_filesystem_files:
        payload
        |> issue_value(:invalid_filesystem_files)
        |> List.wrap()
    }
  end

  defp normalize_translation_validation_report(_payload) do
    %{
      checked_database_row_count: 0,
      checked_filesystem_file_count: 0,
      invalid_database_rows: [],
      invalid_filesystem_files: []
    }
  end

  defp issue_value(issue, key) when is_map(issue) do
    Map.get(issue, key) || Map.get(issue, Atom.to_string(key))
  end

  defp issue_value(_issue, _key), do: nil

  defp empty_git_diff(file_path) do
    %{file_path: file_path, original: "", modified: "", error: nil}
  end

  defp git_diff_preferences do
    %{
      view_style: get_global_setting("ui.git_diff_view_style") || "inline",
      word_wrap: get_global_setting("ui.git_diff_word_wrap") == "true",
      hide_unchanged_regions: get_global_setting("ui.git_diff_hide_unchanged_regions") == "true"
    }
  end

  defp get_global_setting(key) do
    Repo.one(from setting in Setting, where: setting.key == ^key, select: setting.value)
  end
end
