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

  use Phoenix.LiveComponent

  alias BDS.{AI, Metadata, Posts, Preview}
  alias BDS.Desktop.ShellData
  alias BDS.Desktop.ShellLive.{EditorImageDrop, Notify}
  alias BDS.Desktop.ShellLive.PostEditor.{DraftManagement, ListValues, Persistence, PostMetadata}
  alias BDS.Posts.Post
  alias BDS.Tags

  import DraftManagement,
    only: [
      current_draft: 4,
      editing_canonical_language?: 3,
      normalize_language: 2,
      normalize_mode: 1,
      normalize_params: 3,
      persisted_form: 3,
      persisted_form: 4,
      record_status: 1,
      record_title: 2,
      save_state_for_action: 1
    ]

  import ListValues,
    only: [
      category_suggestions: 3,
      category_values: 1,
      csv_to_list: 1,
      ensure_list_value: 3,
      field_key: 1,
      normalize_list_entry: 1,
      query_addable?: 4,
      tag_chips: 2,
      tag_suggestions: 3,
      tag_values: 1
    ]

  import Persistence,
    only: [
      discard: 3,
      discard_label: 1,
      discard_title: 1,
      has_published_version?: 1,
      persist: 5
    ]

  use Gettext, backend: BDS.Gettext

  import PostMetadata,
    only: [
      blank?: 1,
      blank_to_nil: 1,
      canonical_language: 2,
      display_title: 3,
      footer: 4,
      gallery_count: 1,
      languages: 1,
      linked_media: 1,
      post_links: 1,
      preview_url: 4,
      project_metadata: 1,
      template_options: 1,
      translation_flags: 4,
      translations: 1
    ]

  defdelegate tag_chip_style(color), to: ListValues

  embed_templates("post_editor_html/*")

  @spec update(map(), Phoenix.LiveView.Socket.t()) :: {:ok, Phoenix.LiveView.Socket.t()}
  @impl true
  def update(%{action: :save} = assigns, socket) do
    socket =
      socket
      |> assign(Map.drop(assigns, [:action]))
      |> do_save()

    {:ok, socket}
  end

  def update(%{action: :publish} = assigns, socket) do
    socket =
      socket
      |> assign(Map.drop(assigns, [:action]))
      |> do_publish()

    {:ok, socket}
  end

  def update(%{action: :close_quick_actions} = assigns, socket) do
    socket =
      socket
      |> assign(Map.drop(assigns, [:action]))
      |> assign(:quick_actions_open?, false)
      |> build_data()

    {:ok, socket}
  end

  def update(%{action: :insert_content, content: content} = assigns, socket) do
    socket =
      socket
      |> assign(Map.drop(assigns, [:action, :content]))
      |> Phoenix.LiveView.push_event("post-editor-insert-content", %{
        id: socket.assigns.post_id,
        content: content
      })
      |> assign(:shell_overlay, nil)

    {:ok, socket}
  end

  def update(%{action: :translate, language: language} = assigns, socket) do
    socket =
      socket
      |> assign(Map.drop(assigns, [:action, :language]))
      |> do_translate(language)

    {:ok, socket}
  end

  def update(%{action: :apply_ai_suggestions, fields: fields} = assigns, socket) do
    socket =
      socket
      |> assign(Map.drop(assigns, [:action, :fields]))
      |> do_apply_ai_suggestions(fields)

    {:ok, socket}
  end

  def update(assigns, socket) do
    socket =
      socket
      |> assign(assigns)
      |> ensure_state()
      |> build_data()

    {:ok, socket}
  end

  @spec render(map()) :: Phoenix.LiveView.Rendered.t()
  @impl true
  def render(%{post_editor: nil} = assigns), do: ~H"<div></div>"

  def render(assigns) do
    post_editor(assigns)
  end

  @spec handle_event(String.t(), map(), Phoenix.LiveView.Socket.t()) ::
          {:noreply, Phoenix.LiveView.Socket.t()}
  @impl true
  def handle_event("change_post_editor", %{"post_editor" => params}, socket) do
    post_id = socket.assigns.post_id
    current_language = socket.assigns.active_language
    metadata = socket.assigns.project_metadata
    post = socket.assigns.post

    requested_language = normalize_language(Map.get(params, "language"), current_language)

    next_language =
      if current_language == socket.assigns.canonical_language do
        requested_language
      else
        current_language
      end

    draft = normalize_params(params, current_language, next_language)
    current = component_current_draft(socket, post, metadata, next_language)
    dirty? = draft != current
    was_dirty? = socket.assigns.dirty?

    socket =
      socket
      |> assign(:tag_query, Map.get(params, "tag_query", ""))
      |> assign(:category_query, Map.get(params, "category_query", ""))
      |> maybe_update_component_draft(next_language, draft)
      |> assign(:dirty?, dirty?)
      |> build_data()

    if dirty? != was_dirty? do
      Notify.dirty(:post, post_id, dirty?)
    end

    if dirty? do
      Notify.schedule_auto_save(:post, post_id)
    end

    {:noreply, socket}
  end

  def handle_event("save_post_editor", _params, socket) do
    {:noreply, do_save(socket)}
  end

  def handle_event("publish_post_editor", _params, socket) do
    {:noreply, do_publish(socket)}
  end

  def handle_event("discard_post_editor", _params, socket) do
    {:noreply, do_discard(socket)}
  end

  def handle_event("delete_post_editor", _params, socket) do
    {:noreply, do_delete(socket)}
  end

  def handle_event("archive_post_editor", _params, socket) do
    {:noreply, do_archive(socket)}
  end

  def handle_event("editor_image_dropped", %{"path" => path}, socket)
      when is_binary(path) do
    {:noreply, do_image_drop(socket, path)}
  end

  def handle_event("unarchive_post_editor", _params, socket) do
    {:noreply, do_unarchive(socket)}
  end

  def handle_event("set_post_editor_mode", %{"mode" => mode}, socket) do
    normalized_mode = normalize_mode(mode)

    if normalized_mode == :preview do
      case socket.assigns.post do
        %Post{} = post -> _ = Preview.ensure_preview(post.project_id)
        _other -> :ok
      end
    end

    socket =
      socket
      |> assign(:mode, normalized_mode)
      |> build_data()

    {:noreply, socket}
  end

  def handle_event("toggle_post_metadata", _params, socket) do
    socket =
      socket
      |> assign(:expanded, Map.update(socket.assigns.expanded, :metadata, false, &(!&1)))
      |> build_data()

    {:noreply, socket}
  end

  def handle_event("toggle_post_excerpt", _params, socket) do
    socket =
      socket
      |> assign(:expanded, Map.update(socket.assigns.expanded, :excerpt, false, &(!&1)))
      |> build_data()

    {:noreply, socket}
  end

  def handle_event("select_post_editor_language", %{"language" => language}, socket) do
    socket =
      socket
      |> assign(:active_language, normalize_language(language, language))
      |> build_data()

    {:noreply, socket}
  end

  def handle_event("toggle_post_editor_quick_actions", _params, socket) do
    socket =
      socket
      |> assign(:quick_actions_open?, not socket.assigns.quick_actions_open?)
      |> build_data()

    {:noreply, socket}
  end

  def handle_event("detect_post_editor_language", _params, socket) do
    {:noreply, do_detect_language(socket)}
  end

  def handle_event("add_post_editor_tag", %{"tag" => tag}, socket) do
    {:noreply, do_add_list_value(socket, :tags, tag)}
  end

  def handle_event("remove_post_editor_tag", %{"tag" => tag}, socket) do
    {:noreply, do_remove_list_value(socket, :tags, tag)}
  end

  def handle_event("add_post_editor_category", %{"category" => category}, socket) do
    {:noreply, do_add_list_value(socket, :categories, category)}
  end

  def handle_event("remove_post_editor_category", %{"category" => category}, socket) do
    {:noreply, do_remove_list_value(socket, :categories, category)}
  end

  def handle_event("insert_content", %{"content" => content}, socket) do
    socket =
      socket
      |> Phoenix.LiveView.push_event("post-editor-insert-content", %{
        id: socket.assigns.post_id,
        content: content
      })
      |> assign(:shell_overlay, nil)

    {:noreply, socket}
  end

  def handle_event("close_quick_actions", _params, socket) do
    socket =
      socket
      |> assign(:quick_actions_open?, false)
      |> build_data()

    {:noreply, socket}
  end

  defp component_current_draft(socket, post, metadata, active_language) do
    persisted = persisted_form(post, metadata, active_language)
    Map.get(socket.assigns.drafts, active_language, persisted)
  end

  defp ensure_state(socket) do
    post_id = socket.assigns.current_tab.id
    post = Posts.get_post(post_id)
    metadata = project_metadata(post && post.project_id)
    canonical = if post, do: canonical_language(post, metadata), else: "en"

    defaults = %{
      post_id: post_id,
      post: post,
      project_metadata: metadata,
      canonical_language: canonical,
      active_language: canonical,
      drafts: %{},
      tag_query: "",
      category_query: "",
      quick_actions_open?: false,
      mode: :markdown,
      expanded: %{metadata: post && blank?(post.title), excerpt: post && not blank?(post.excerpt)},
      save_state: :idle,
      dirty?: false
    }

    Enum.reduce(defaults, socket, fn {key, default}, acc ->
      if is_nil(Map.get(acc.assigns, key)) do
        assign(acc, key, default)
      else
        acc
      end
    end)
  end

  defp build_data(socket) do
    case socket.assigns.post do
      nil ->
        assign(socket, :post_editor, nil)

      %Post{} = post ->
        metadata = socket.assigns.project_metadata
        canonical_language = socket.assigns.canonical_language
        active_language = socket.assigns.active_language
        translations = translations(post.id)
        persisted = persisted_form(post, metadata, active_language, translations)

        form =
          socket.assigns.drafts
          |> Map.get(active_language, persisted)

        expanded = socket.assigns.expanded
        current_translation = Map.get(translations, active_language)

        data = %{
          id: post.id,
          display_title: display_title(form["title"], post.slug, post.id),
          subtitle: nil,
          slug: post.slug || post.id,
          status: post.status,
          dirty?: socket.assigns.dirty?,
          save_state: socket.assigns.save_state,
          quick_actions_open?: socket.assigns.quick_actions_open?,
          metadata_expanded: Map.get(expanded, :metadata, false),
          excerpt_expanded: Map.get(expanded, :excerpt, false),
          mode: socket.assigns.mode,
          editing_canonical?:
            editing_canonical_language?(translations, active_language, canonical_language),
          can_publish?: post.status == :draft,
          can_delete?: post.status == :published,
          can_archive?: post.status in [:draft, :published],
          can_unarchive?: post.status == :archived,
          has_published_version?: has_published_version?(post),
          discard_label: discard_label(post),
          discard_title: discard_title(post),
          detect_language_enabled?:
            not blank?(Map.get(form, "title")) or not blank?(Map.get(form, "content")),
          can_translate?: Enum.any?(languages(metadata), &(&1 != canonical_language)),
          languages: languages(metadata),
          form: form,
          template_options: template_options(post.project_id),
          show_template_selector?: template_options(post.project_id) != [],
          tag_options: Tags.list_tags(post.project_id),
          tag_values: tag_values(form),
          tag_chips: tag_chips(form, Tags.list_tags(post.project_id)),
          tag_query: socket.assigns.tag_query,
          tag_query_addable?:
            query_addable?(
              socket.assigns.tag_query,
              tag_values(form),
              Tags.list_tags(post.project_id),
              fn option -> option.name end
            ),
          category_values: category_values(form),
          category_query: socket.assigns.category_query,
          category_options: metadata.categories || [],
          category_query_addable?:
            query_addable?(
              socket.assigns.category_query,
              category_values(form),
              metadata.categories || [],
              & &1
            ),
          tag_suggestions:
            tag_suggestions(
              form,
              Tags.list_tags(post.project_id),
              socket.assigns.tag_query
            ),
          category_suggestions:
            category_suggestions(
              form,
              metadata.categories || [],
              socket.assigns.category_query
            ),
          gallery_count: gallery_count(form),
          preview_url:
            preview_url(
              post,
              active_language,
              canonical_language,
              socket.assigns.mode
            ),
          translation_flags:
            translation_flags(post, canonical_language, active_language, translations),
          linked_media: linked_media(post.id),
          post_links: post_links(post.id),
          footer: footer(post, current_translation, active_language, canonical_language)
        }

        assign(socket, :post_editor, data)
    end
  end

  defp do_save(socket) do
    post = socket.assigns.post

    case post do
      nil ->
        socket

      %Post{} = post ->
        metadata = socket.assigns.project_metadata
        active_language = socket.assigns.active_language
        draft = component_current_draft(socket, post, metadata, active_language)

        case persist(post, draft, active_language, metadata, :save) do
          {:ok, record} ->
            refreshed_post = Posts.get_post!(post.id)
            _refreshed_form = persisted_form(refreshed_post, metadata, active_language)

            socket =
              socket
              |> assign(:post, refreshed_post)
              |> assign(:drafts, Map.delete(socket.assigns.drafts, active_language))
              |> assign(:save_state, save_state_for_action(:save))
              |> assign(:dirty?, false)
              |> build_data()

            Notify.tab_meta(
              :post,
              post.id,
              record_title(record, refreshed_post),
              Atom.to_string(record_status(record))
            )

            Notify.dirty(:post, post.id, false)
            Notify.cancel_auto_save(:post, post.id)
            notify_output(socket, dgettext("ui", "Post"), dgettext("ui", "Post saved"))
            socket

          {:error, reason} ->
            notify_output(socket, dgettext("ui", "Post"), inspect(reason), "error")
            |> build_data()
        end
    end
  end

  defp do_publish(socket) do
    post = socket.assigns.post

    case post do
      nil ->
        socket

      %Post{} = post ->
        metadata = socket.assigns.project_metadata
        active_language = socket.assigns.active_language
        draft = component_current_draft(socket, post, metadata, active_language)

        case persist(post, draft, active_language, metadata, :publish) do
          {:ok, record} ->
            refreshed_post = Posts.get_post!(post.id)
            _refreshed_form = persisted_form(refreshed_post, metadata, active_language)

            socket =
              socket
              |> assign(:post, refreshed_post)
              |> assign(:drafts, Map.delete(socket.assigns.drafts, active_language))
              |> assign(:save_state, save_state_for_action(:publish))
              |> assign(:dirty?, false)
              |> build_data()

            Notify.tab_meta(
              :post,
              post.id,
              record_title(record, refreshed_post),
              Atom.to_string(record_status(record))
            )

            Notify.dirty(:post, post.id, false)
            notify_output(socket, dgettext("ui", "Post"), dgettext("ui", "Post published"))
            socket

          {:error, reason} ->
            notify_output(socket, dgettext("ui", "Post"), inspect(reason), "error")
            |> build_data()
        end
    end
  end

  defp do_discard(socket) do
    post = socket.assigns.post

    case post do
      nil ->
        socket

      %Post{} = post ->
        metadata = socket.assigns.project_metadata
        active_language = socket.assigns.active_language

        case discard(post, active_language, metadata) do
          {:ok, restored_post} ->
            socket =
              socket
              |> assign(:post, restored_post)
              |> assign(:drafts, Map.delete(socket.assigns.drafts, active_language))
              |> assign(:save_state, :discarded)
              |> assign(:dirty?, false)
              |> build_data()

            Notify.tab_meta(
              :post,
              post.id,
              restored_post.title || restored_post.slug || restored_post.id,
              Atom.to_string(restored_post.status || :draft)
            )

            Notify.dirty(:post, post.id, false)
            socket

          {:error, reason} ->
            notify_output(socket, dgettext("ui", "Post"), inspect(reason), "error")
            |> build_data()
        end
    end
  end

  defp do_delete(socket) do
    post_id = socket.assigns.post_id

    case Posts.delete_post(post_id) do
      {:ok, :deleted} ->
        Notify.close_tab(:post, post_id)
        socket

      {:error, reason} ->
        notify_output(socket, dgettext("ui", "Post"), inspect(reason), "error")
        |> build_data()
    end
  end

  defp do_archive(socket) do
    case socket.assigns.post do
      nil ->
        socket

      %Post{} = post ->
        case Posts.archive_post(post.id) do
          {:ok, archived_post} ->
            socket =
              socket
              |> assign(:post, archived_post)
              |> assign(:drafts, %{})
              |> assign(:dirty?, false)
              |> assign(:quick_actions_open?, false)
              |> build_data()

            Notify.tab_meta(
              :post,
              post.id,
              archived_post.title || archived_post.slug || archived_post.id,
              "archived"
            )

            Notify.dirty(:post, post.id, false)
            notify_output(socket, dgettext("ui", "Post"), dgettext("ui", "Post archived"))

          {:error, reason} ->
            notify_output(socket, dgettext("ui", "Post"), inspect(reason), "error")
            |> build_data()
        end
    end
  end

  # Drag-and-drop image chain (action_patterns.allium DragDropImageChain).
  # Steps 1-4 run synchronously while the user waits; steps 5-6 (AI analysis +
  # auto-translate) run in the background and are gated behind airplane mode.
  defp do_image_drop(socket, path) do
    case socket.assigns.post do
      %Post{} = post ->
        case EditorImageDrop.import_and_link(post.project_id, post.id, path) do
          {:ok, media, markdown} ->
            maybe_enrich_dropped_image(media, post)

            socket
            |> Phoenix.LiveView.push_event("post-editor-insert-content", %{
              id: socket.assigns.post_id,
              content: markdown
            })
            |> notify_output(
              dgettext("ui", "Insert Image"),
              dgettext("ui", "Added %{name}", name: media.original_name)
            )

          {:error, reason} ->
            notify_output(
              socket,
              dgettext("ui", "Insert Image"),
              dgettext("ui", "Failed to import %{path}: %{reason}",
                path: Path.basename(path),
                reason: inspect(reason)
              ),
              "error"
            )
        end

      _other ->
        socket
    end
  end

  defp maybe_enrich_dropped_image(media, post) do
    unless AI.airplane_mode?() do
      {:ok, metadata} = Metadata.get_project_metadata(post.project_id)
      language = metadata.main_language || "en"

      Task.Supervisor.start_child(BDS.TCP.TaskSupervisor, fn ->
        EditorImageDrop.enrich(media, post.id, language)
      end)
    end

    :ok
  end

  defp do_unarchive(socket) do
    case socket.assigns.post do
      nil ->
        socket

      %Post{} = post ->
        case Posts.unarchive_post(post.id) do
          {:ok, unarchived_post} ->
            socket =
              socket
              |> assign(:post, unarchived_post)
              |> assign(:drafts, %{})
              |> assign(:dirty?, false)
              |> assign(:quick_actions_open?, false)
              |> build_data()

            Notify.tab_meta(
              :post,
              post.id,
              unarchived_post.title || unarchived_post.slug || unarchived_post.id,
              "draft"
            )

            Notify.dirty(:post, post.id, false)
            notify_output(socket, dgettext("ui", "Post"), dgettext("ui", "Post unarchived"))

          {:error, reason} ->
            notify_output(socket, dgettext("ui", "Post"), inspect(reason), "error")
            |> build_data()
        end
    end
  end

  defp do_detect_language(socket) do
    if Map.get(socket.assigns, :offline_mode, true) and not AI.airplane_endpoint_configured?() do
      notify_output(
        socket,
        dgettext("ui", "Detect Language"),
        dgettext("ui", "Automatic AI actions stay gated by airplane mode."),
        "info"
      )
      |> build_data()
    else
      post = socket.assigns.post

      case post do
        nil ->
          socket

        %Post{} = post ->
          metadata = socket.assigns.project_metadata
          active_language = socket.assigns.active_language
          draft = component_current_draft(socket, post, metadata, active_language)
          text = Enum.join([Map.get(draft, "title", ""), Map.get(draft, "content", "")], "\n\n")

          case AI.detect_language(text) do
            {:ok, %{language_code: language_code}}
            when is_binary(language_code) and language_code != "" ->
              socket
              |> put_component_draft_field(
                "language",
                normalize_language(language_code, socket.assigns.canonical_language)
              )
              |> build_data()

            {:error, reason} ->
              notify_output(socket, dgettext("ui", "Detect Language"), inspect(reason), "error")
              |> build_data()

            _other ->
              notify_output(
                socket,
                dgettext("ui", "Detect Language"),
                dgettext("ui", "Language detection failed."),
                "error"
              )
              |> build_data()
          end
      end
    end
  end

  defp do_translate(socket, language) do
    if Map.get(socket.assigns, :offline_mode, true) and not AI.airplane_endpoint_configured?() do
      notify_output(
        socket,
        dgettext("ui", "Translate"),
        dgettext("ui", "Automatic AI actions stay gated by airplane mode."),
        "info"
      )
      |> build_data()
    else
      post_id = socket.assigns.post_id
      normalized_language = normalize_language(language, "")
      source_language = socket.assigns.canonical_language

      case AI.translate_post(post_id, normalized_language, source_language: source_language) do
        {:ok, translation} ->
          with {:ok, _saved_translation} <-
                 Posts.upsert_post_translation(post_id, normalized_language, %{
                   title: translation.title,
                   excerpt: translation.excerpt,
                   content: translation.content
                 }) do
            socket =
              socket
              |> assign(:active_language, normalized_language)
              |> assign(:drafts, Map.delete(socket.assigns.drafts, normalized_language))
              |> assign(:quick_actions_open?, false)
              |> build_data()

            Notify.dirty(:post, post_id, false)
            socket
          else
            {:error, reason} ->
              notify_output(socket, dgettext("ui", "Translate"), inspect(reason), "error")
              |> build_data()
          end

        {:error, reason} ->
          notify_output(socket, dgettext("ui", "Translate"), inspect(reason), "error")
          |> build_data()
      end
    end
  end

  defp do_apply_ai_suggestions(socket, fields) do
    post_id = socket.assigns.post_id

    case Posts.get_post(post_id) do
      nil ->
        socket

      %Post{} = _post ->
        attrs =
          fields
          |> Enum.reduce(%{}, fn field, acc ->
            case field.key do
              "title" -> Map.put(acc, :title, blank_to_nil(field.suggested_value))
              "excerpt" -> Map.put(acc, :excerpt, blank_to_nil(field.suggested_value))
              "slug" -> Map.put(acc, :slug, blank_to_nil(field.suggested_value))
              _other -> acc
            end
          end)

        if map_size(attrs) == 0 do
          assign(socket, :shell_overlay, nil)
        else
          case Posts.update_post(post_id, attrs) do
            {:ok, updated_post} ->
              metadata = project_metadata(updated_post.project_id)
              active_language = socket.assigns.active_language
              refreshed_form = persisted_form(updated_post, metadata, active_language)

              socket =
                socket
                |> assign(:post, updated_post)
                |> assign(:project_metadata, metadata)
                |> assign(
                  :drafts,
                  Map.put(socket.assigns.drafts, active_language, refreshed_form)
                )
                |> assign(:save_state, :dirty)
                |> assign(:dirty?, true)
                |> assign(:shell_overlay, nil)
                |> build_data()

              Notify.dirty(:post, post_id, true)
              socket

            {:error, reason} ->
              notify_output(socket, dgettext("ui", "AI Suggestions"), inspect(reason), "error")
              |> build_data()
          end
        end
    end
  end

  defp do_add_list_value(socket, kind, value) do
    post = socket.assigns.post

    case post do
      nil ->
        socket

      %Post{} = post ->
        metadata = socket.assigns.project_metadata
        active_language = socket.assigns.active_language
        draft = component_current_draft(socket, post, metadata, active_language)
        normalized = normalize_list_entry(value)

        if normalized == "" do
          socket
        else
          ensure_list_value(post.project_id, kind, normalized)

          updated =
            draft
            |> Map.get(field_key(kind), "")
            |> csv_to_list()
            |> Kernel.++([normalized])
            |> Enum.uniq()
            |> Enum.join(", ")

          socket =
            socket
            |> assign_query(kind, "")
            |> put_component_draft_field(field_key(kind), updated)
            |> build_data()

          Notify.dirty(:post, socket.assigns.post_id, true)
          assign(socket, :dirty?, true)
        end
    end
  end

  defp do_remove_list_value(socket, kind, value) do
    post = socket.assigns.post

    case post do
      nil ->
        socket

      %Post{} = post ->
        metadata = socket.assigns.project_metadata
        active_language = socket.assigns.active_language
        draft = component_current_draft(socket, post, metadata, active_language)

        updated =
          draft
          |> Map.get(field_key(kind), "")
          |> csv_to_list()
          |> Enum.reject(&(&1 == value))
          |> Enum.join(", ")

        socket =
          socket
          |> put_component_draft_field(field_key(kind), updated)
          |> build_data()

        Notify.dirty(:post, socket.assigns.post_id, true)
        assign(socket, :dirty?, true)
    end
  end

  defp maybe_update_component_draft(socket, next_language, draft) do
    current_language = socket.assigns.active_language

    cond do
      current_language == next_language ->
        assign(socket, :drafts, Map.put(socket.assigns.drafts, next_language, draft))

      Map.has_key?(socket.assigns.drafts, current_language) ->
        socket
        |> assign(:active_language, next_language)
        |> assign(:drafts, Map.put(socket.assigns.drafts, next_language, draft))

      true ->
        socket
        |> assign(:active_language, next_language)
        |> assign(:drafts, Map.put(%{}, next_language, draft))
    end
  end

  defp put_component_draft_field(socket, field, value) do
    active_language = socket.assigns.active_language
    post = socket.assigns.post
    metadata = socket.assigns.project_metadata
    draft = current_draft(socket.assigns, post, metadata, active_language)
    updated = Map.put(draft, field, value)
    assign(socket, :drafts, Map.put(socket.assigns.drafts, active_language, updated))
  end

  defp assign_query(socket, :tags, value), do: assign(socket, :tag_query, value)
  defp assign_query(socket, :categories, value), do: assign(socket, :category_query, value)

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

  @spec post_status_label(term()) :: term()
  def post_status_label(status), do: ShellData.dashboard_status_label(status)

  @spec post_editor_save_state_label(term()) :: term()
  def post_editor_save_state_label(:dirty), do: dgettext("ui", "Unsaved")
  def post_editor_save_state_label(:saved), do: dgettext("ui", "Saved")
  def post_editor_save_state_label(:published), do: dgettext("ui", "Published")
  def post_editor_save_state_label(:discarded), do: dgettext("ui", "Reverted")
  def post_editor_save_state_label(_state), do: dgettext("ui", "Idle")

  @spec post_editor_mode_label(term()) :: term()
  def post_editor_mode_label(:markdown), do: dgettext("ui", "Markdown")
  def post_editor_mode_label(:preview), do: dgettext("ui", "Preview")
end
