defmodule BerrypodWeb.PageEditorHook do
  @moduledoc """
  LiveView on_mount hook for the unified on-site editor.

  Mounted in the public_shop live_session. When an admin visits any shop
  page, this hook enables editing capabilities:

  1. **Page editing** — loads a working copy of the page's blocks, attaches
     event handlers for block manipulation
  2. **Theme editing** — provides live theme customisation on the actual shop

  The hook manages a tabbed editor panel with Page, Theme, and Settings tabs.
  Non-admin users are unaffected — the hook just assigns `editing: false`.

  ## Actions

  - `:mount_page_editor` — sets up editing assigns and attaches hooks
  """

  import Phoenix.Component, only: [assign: 3]
  import Phoenix.LiveView, only: [attach_hook: 4, push_navigate: 2, push_patch: 2]

  alias Berrypod.{Media, Settings}
  alias Berrypod.Pages
  alias Berrypod.Pages.{BlockEditor, BlockTypes, Defaults}
  alias Berrypod.Theme.{Contrast, CSSGenerator, Presets}
  alias BerrypodWeb.SiteEditorState

  def on_mount(:mount_page_editor, _params, _session, socket) do
    socket =
      socket
      # Page editing state
      |> assign(:editing, false)
      |> assign(:editing_blocks, nil)
      |> assign(:editor_page_slug, nil)
      |> assign(:editor_dirty, false)
      |> assign(:editor_at_defaults, true)
      |> assign(:editor_history, [])
      |> assign(:editor_future, [])
      |> assign(:editor_expanded, MapSet.new())
      |> assign(:editor_show_picker, false)
      |> assign(:editor_picker_filter, "")
      |> assign(:editor_allowed_blocks, nil)
      |> assign(:editor_live_region_message, nil)
      |> assign(:editor_current_path, nil)
      |> assign(:editor_sidebar_open, true)
      |> assign(:editor_sheet_state, :collapsed)
      |> assign(:editor_image_picker_block_id, nil)
      |> assign(:editor_image_picker_field_key, nil)
      |> assign(:editor_image_picker_images, [])
      |> assign(:editor_image_picker_search, "")
      |> assign(:editor_save_status, :idle)
      # Unified editor tab state (:page | :theme | :settings)
      |> assign(:editor_active_tab, :page)
      # Theme editing state
      |> assign(:theme_editing, false)
      |> assign(:theme_dirty, false)
      |> assign(:theme_editor_original, nil)
      |> assign(:theme_editor_settings, nil)
      |> assign(:theme_editor_active_preset, nil)
      |> assign(:theme_editor_logo_image, nil)
      |> assign(:theme_editor_header_image, nil)
      |> assign(:theme_editor_icon_image, nil)
      |> assign(:theme_editor_contrast_warning, :ok)
      |> assign(:theme_editor_customise_open, false)
      |> assign(:theme_editor_presets, Presets.all_with_descriptions())
      # Settings editing state
      |> assign(:settings_dirty, false)
      |> assign(:settings_save_status, :idle)
      # Site editing state (using SiteEditorState struct)
      |> assign(:site_editing, false)
      |> assign(:site_state, nil)
      |> assign(:site_dirty, false)
      |> assign(:site_nav_pages, [])
      # Navigation warning state
      |> assign(:editor_nav_blocked, nil)
      |> attach_hook(:editor_params, :handle_params, &handle_editor_params/3)
      |> attach_hook(:editor_events, :handle_event, &handle_editor_event/3)
      |> attach_hook(:editor_info, :handle_info, &handle_editor_info/2)

    {:cont, socket}
  end

  @doc """
  Called by Shop.Page after @page is assigned. If edit mode was requested via
  URL but couldn't be entered because @page wasn't available yet, enters it now.
  """
  def maybe_enter_pending_page_mode(socket) do
    if socket.assigns[:editor_pending_page_mode] && socket.assigns[:page] &&
         !socket.assigns.editing do
      socket
      |> assign(:editor_pending_page_mode, false)
      |> enter_edit_mode()
    else
      socket
    end
  end

  # ── handle_params: track current path and restore editor state ────

  defp handle_editor_params(params, uri, socket) do
    parsed = URI.parse(uri)

    socket =
      socket
      |> assign(:editor_current_path, parsed.path)
      |> maybe_restore_editor_state(params)

    {:cont, socket}
  end

  # Restore editor state from URL params on navigation
  # Only activates state if not already in the requested state (avoids loops)
  defp maybe_restore_editor_state(socket, params) do
    if socket.assigns.is_admin do
      requested_tab = params["edit"]
      current_tab = socket.assigns.editor_active_tab
      current_state = socket.assigns.editor_sheet_state

      # If already in the correct state, don't re-apply
      already_correct? =
        current_state == :open && requested_tab &&
          String.to_existing_atom(requested_tab) == current_tab

      if already_correct? do
        socket
      else
        case requested_tab do
          "theme" ->
            socket
            |> assign(:editor_sheet_state, :open)
            |> assign(:editor_active_tab, :theme)
            |> maybe_enter_theme_mode()

          "page" ->
            socket
            |> assign(:editor_sheet_state, :open)
            |> assign(:editor_active_tab, :page)
            |> maybe_enter_page_mode()

          "settings" ->
            socket
            |> assign(:editor_sheet_state, :open)
            |> assign(:editor_active_tab, :settings)
            |> maybe_enter_theme_mode()

          "site" ->
            socket
            |> assign(:editor_sheet_state, :open)
            |> assign(:editor_active_tab, :site)
            |> maybe_enter_site_mode()

          nil ->
            # No edit param - collapse the editor (supports browser back)
            assign(socket, :editor_sheet_state, :collapsed)

          _ ->
            # Unknown edit value - ignore
            socket
        end
      end
    else
      socket
    end
  end

  defp maybe_enter_theme_mode(socket) do
    if socket.assigns.theme_editing do
      socket
    else
      load_theme_state(socket)
    end
  end

  defp maybe_enter_page_mode(socket) do
    if socket.assigns.editing do
      socket
    else
      if socket.assigns[:page] do
        enter_edit_mode(socket)
      else
        # Page not yet loaded - mark that we need to enter edit mode when it is
        assign(socket, :editor_pending_page_mode, true)
      end
    end
  end

  defp maybe_enter_site_mode(socket) do
    if socket.assigns.site_editing do
      socket
    else
      # Site tab uses branding settings from theme, so load theme state too
      socket
      |> maybe_enter_theme_mode()
      |> load_site_state()
    end
  end

  # ── handle_info ─────────────────────────────────────────────────

  defp handle_editor_info(:editor_clear_save_status, socket) do
    {:halt, assign(socket, :editor_save_status, :idle)}
  end

  defp handle_editor_info(_msg, socket), do: {:cont, socket}

  # ── handle_event: editor_* events ────────────────────────────────

  # toggle_editing can be called even when not editing (to enter edit mode)
  defp handle_editor_event("editor_toggle_editing", _params, socket) do
    if socket.assigns.is_admin and socket.assigns[:page] do
      if socket.assigns.editing do
        {:halt, exit_edit_mode(socket)}
      else
        {:halt, enter_edit_mode(socket)}
      end
    else
      {:cont, socket}
    end
  end

  # set_sheet_state can be called even when not editing (from JS click-outside)
  defp handle_editor_event("editor_set_sheet_state", %{"state" => state_str}, socket) do
    if socket.assigns.is_admin do
      state = if state_str == "open", do: :open, else: :collapsed

      socket =
        socket
        |> assign(:editor_sheet_state, state)
        |> sync_edit_url_param(state)

      {:halt, socket}
    else
      {:cont, socket}
    end
  end

  # Tab switching for unified editor
  defp handle_editor_event("editor_set_tab", %{"tab" => tab_str}, socket) do
    if socket.assigns.is_admin do
      tab = String.to_existing_atom(tab_str)

      socket =
        case tab do
          :theme ->
            # Load theme state if not already loaded
            if socket.assigns.theme_editing do
              assign(socket, :editor_active_tab, :theme)
            else
              enter_theme_edit_mode(socket)
            end

          :page ->
            # Enter page edit mode if we have a page and aren't already editing
            socket = assign(socket, :editor_active_tab, :page)

            if socket.assigns[:page] && !socket.assigns.editing do
              enter_edit_mode(socket)
            else
              socket
            end

          :settings ->
            # Ensure theme state is loaded for settings that need it
            socket =
              if socket.assigns.theme_editing do
                socket
              else
                load_theme_state(socket)
              end

            # Initialize settings form from current page if not already set
            # Init settings form for pages with editable settings
            # Custom pages: all settings editable
            # System pages: only url_slug editable (except home)
            socket =
              if is_nil(socket.assigns[:settings_form]) && socket.assigns[:page] do
                init_settings_form(socket)
              else
                socket
              end

            assign(socket, :editor_active_tab, :settings)

          :site ->
            # Site tab shows site-wide content editors
            # Load theme state for branding settings (will be moved here from theme tab)
            socket =
              if socket.assigns.theme_editing do
                socket
              else
                load_theme_state(socket)
              end

            # Load site state if not already loaded
            socket =
              if socket.assigns.site_editing do
                socket
              else
                load_site_state(socket)
              end

            assign(socket, :editor_active_tab, :site)
        end

      # Open the sheet and sync URL with new tab
      socket =
        socket
        |> assign(:editor_sheet_state, :open)
        |> sync_edit_url_param(:open)

      {:halt, socket}
    else
      {:cont, socket}
    end
  end

  # Toggle theme editing mode
  defp handle_editor_event("editor_toggle_theme", _params, socket) do
    if socket.assigns.is_admin do
      if socket.assigns.theme_editing do
        {:halt, exit_theme_edit_mode(socket)}
      else
        {:halt, enter_theme_edit_mode(socket)}
      end
    else
      {:cont, socket}
    end
  end

  # Unified save works for all tabs regardless of editing mode
  defp handle_editor_event("editor_save_all", _params, socket) do
    if socket.assigns.is_admin do
      socket = save_all_tabs(socket)
      {:halt, socket}
    else
      {:cont, socket}
    end
  end

  # Navigation blocked by unsaved changes
  defp handle_editor_event("editor_nav_blocked", %{"href" => href}, socket) do
    {:halt, assign(socket, :editor_nav_blocked, href)}
  end

  defp handle_editor_event("editor_save_and_navigate", _params, socket) do
    href = socket.assigns.editor_nav_blocked
    socket = save_all_tabs(socket)

    socket =
      socket
      |> assign(:editor_nav_blocked, nil)
      |> Phoenix.LiveView.push_event("editor_navigate", %{href: href})

    {:halt, socket}
  end

  defp handle_editor_event("editor_discard_and_navigate", _params, socket) do
    href = socket.assigns.editor_nav_blocked
    socket = revert_all_tabs(socket)

    socket =
      socket
      |> assign(:editor_nav_blocked, nil)
      |> Phoenix.LiveView.push_event("editor_navigate", %{href: href})

    {:halt, socket}
  end

  defp handle_editor_event("editor_cancel_navigate", _params, socket) do
    socket =
      socket
      |> assign(:editor_nav_blocked, nil)
      |> assign(:editor_sheet_state, :open)

    {:halt, socket}
  end

  defp handle_editor_event("editor_" <> action, params, socket) do
    if socket.assigns.editing do
      handle_editor_action(action, params, socket)
    else
      {:cont, socket}
    end
  end

  # Theme editing events
  defp handle_editor_event("theme_" <> action, params, socket) do
    if socket.assigns.is_admin && socket.assigns.theme_editing do
      handle_theme_action(action, params, socket)
    else
      {:cont, socket}
    end
  end

  # Settings editing events (custom page settings)
  defp handle_editor_event("settings_" <> action, params, socket) do
    if socket.assigns.is_admin do
      handle_settings_action(action, params, socket)
    else
      {:cont, socket}
    end
  end

  # Site editing events (announcement bar, footer, etc.)
  defp handle_editor_event("site_" <> action, params, socket) do
    if socket.assigns.is_admin do
      handle_site_action(action, params, socket)
    else
      {:cont, socket}
    end
  end

  defp handle_editor_event(_event, _params, socket), do: {:cont, socket}

  # Sync URL ?edit param with editor state
  defp sync_edit_url_param(socket, :collapsed) do
    path = socket.assigns.editor_current_path || "/"
    push_patch(socket, to: path)
  end

  defp sync_edit_url_param(socket, :open) do
    path = socket.assigns.editor_current_path || "/"
    tab = socket.assigns.editor_active_tab
    push_patch(socket, to: "#{path}?edit=#{tab}")
  end

  # ── Block manipulation actions ───────────────────────────────────

  defp handle_editor_action("move_up", %{"id" => id}, socket) do
    case BlockEditor.move_up(socket.assigns.editing_blocks, id) do
      {:ok, new_blocks, message} ->
        {:halt, apply_mutation(socket, new_blocks, message, :reorder)}

      :noop ->
        {:halt, socket}
    end
  end

  defp handle_editor_action("move_down", %{"id" => id}, socket) do
    case BlockEditor.move_down(socket.assigns.editing_blocks, id) do
      {:ok, new_blocks, message} ->
        {:halt, apply_mutation(socket, new_blocks, message, :reorder)}

      :noop ->
        {:halt, socket}
    end
  end

  defp handle_editor_action("remove_block", %{"id" => id}, socket) do
    {:ok, new_blocks, message} = BlockEditor.remove_block(socket.assigns.editing_blocks, id)
    {:halt, apply_mutation(socket, new_blocks, message, :content)}
  end

  defp handle_editor_action("duplicate_block", %{"id" => id}, socket) do
    case BlockEditor.duplicate_block(socket.assigns.editing_blocks, id) do
      {:ok, new_blocks, message} ->
        {:halt, apply_mutation(socket, new_blocks, message, :content)}

      :noop ->
        {:halt, socket}
    end
  end

  defp handle_editor_action("add_block", %{"type" => type}, socket) do
    case BlockEditor.add_block(socket.assigns.editing_blocks, type) do
      {:ok, new_blocks, message} ->
        socket =
          socket
          |> apply_mutation(new_blocks, message, :content)
          |> assign(:editor_show_picker, false)

        {:halt, socket}

      :noop ->
        {:halt, socket}
    end
  end

  defp handle_editor_action("update_block_settings", params, socket) do
    block_id = params["block_id"]
    new_settings = params["block_settings"] || %{}

    case BlockEditor.update_settings(socket.assigns.editing_blocks, block_id, new_settings) do
      {:ok, new_blocks} ->
        {:halt, apply_mutation(socket, new_blocks, "Settings updated", :content)}

      :noop ->
        {:halt, socket}
    end
  end

  # ── Repeater actions ─────────────────────────────────────────────

  defp handle_editor_action(
         "repeater_add",
         %{"block-id" => block_id, "field" => field_key},
         socket
       ) do
    case BlockEditor.repeater_add(socket.assigns.editing_blocks, block_id, field_key) do
      {:ok, new_blocks, message} ->
        {:halt, apply_mutation(socket, new_blocks, message, :content)}

      :noop ->
        {:halt, socket}
    end
  end

  defp handle_editor_action(
         "repeater_remove",
         %{"block-id" => block_id, "field" => field_key, "index" => index_str},
         socket
       ) do
    index = String.to_integer(index_str)

    case BlockEditor.repeater_remove(socket.assigns.editing_blocks, block_id, field_key, index) do
      {:ok, new_blocks, message} ->
        {:halt, apply_mutation(socket, new_blocks, message, :content)}

      :noop ->
        {:halt, socket}
    end
  end

  defp handle_editor_action(
         "repeater_move",
         %{"block-id" => block_id, "field" => field_key, "index" => index_str, "dir" => dir},
         socket
       ) do
    index = String.to_integer(index_str)

    case BlockEditor.repeater_move(
           socket.assigns.editing_blocks,
           block_id,
           field_key,
           index,
           dir
         ) do
      {:ok, new_blocks, message} ->
        {:halt, apply_mutation(socket, new_blocks, message, :reorder)}

      :noop ->
        {:halt, socket}
    end
  end

  # ── UI state actions ─────────────────────────────────────────────

  defp handle_editor_action("toggle_expand", %{"id" => block_id}, socket) do
    expanded = socket.assigns.editor_expanded
    block = Enum.find(socket.assigns.editing_blocks, &(&1["id"] == block_id))
    block_name = BlockEditor.block_display_name(block)

    {new_expanded, action} =
      if MapSet.member?(expanded, block_id) do
        {MapSet.delete(expanded, block_id), "collapsed"}
      else
        {MapSet.put(expanded, block_id), "expanded"}
      end

    {:halt,
     socket
     |> assign(:editor_expanded, new_expanded)
     |> assign(:editor_live_region_message, "#{block_name} settings #{action}")}
  end

  defp handle_editor_action("toggle_sidebar", _params, socket) do
    {:halt, assign(socket, :editor_sidebar_open, !socket.assigns.editor_sidebar_open)}
  end

  defp handle_editor_action("show_picker", _params, socket) do
    {:halt,
     socket
     |> assign(:editor_show_picker, true)
     |> assign(:editor_picker_filter, "")}
  end

  defp handle_editor_action("hide_picker", _params, socket) do
    {:halt, assign(socket, :editor_show_picker, false)}
  end

  defp handle_editor_action("filter_picker", %{"value" => value}, socket) do
    {:halt, assign(socket, :editor_picker_filter, value)}
  end

  # ── Image picker actions ───────────────────────────────────────

  defp handle_editor_action(
         "show_image_picker",
         %{"block-id" => block_id, "field" => field_key},
         socket
       ) do
    images = Berrypod.Media.list_images()

    {:halt,
     socket
     |> assign(:editor_image_picker_block_id, block_id)
     |> assign(:editor_image_picker_field_key, field_key)
     |> assign(:editor_image_picker_images, images)
     |> assign(:editor_image_picker_search, "")}
  end

  defp handle_editor_action("hide_image_picker", _params, socket) do
    {:halt,
     socket
     |> assign(:editor_image_picker_block_id, nil)
     |> assign(:editor_image_picker_field_key, nil)}
  end

  defp handle_editor_action("image_picker_search", %{"value" => value}, socket) do
    {:halt, assign(socket, :editor_image_picker_search, value)}
  end

  defp handle_editor_action("pick_image", %{"image-id" => image_id}, socket) do
    block_id = socket.assigns.editor_image_picker_block_id
    field_key = socket.assigns.editor_image_picker_field_key

    case BlockEditor.update_settings(socket.assigns.editing_blocks, block_id, %{
           field_key => image_id
         }) do
      {:ok, new_blocks} ->
        socket =
          socket
          |> apply_mutation(new_blocks, "Image selected", :content)
          |> assign(:editor_image_picker_block_id, nil)
          |> assign(:editor_image_picker_field_key, nil)

        {:halt, socket}

      :noop ->
        {:halt, socket}
    end
  end

  defp handle_editor_action(
         "clear_image",
         %{"block-id" => block_id, "field" => field_key},
         socket
       ) do
    case BlockEditor.update_settings(socket.assigns.editing_blocks, block_id, %{
           field_key => ""
         }) do
      {:ok, new_blocks} ->
        {:halt, apply_mutation(socket, new_blocks, "Image cleared", :content)}

      :noop ->
        {:halt, socket}
    end
  end

  # ── Undo / redo ──────────────────────────────────────────────────

  defp handle_editor_action("undo", _params, socket) do
    case socket.assigns.editor_history do
      [prev | rest] ->
        future = [socket.assigns.editing_blocks | socket.assigns.editor_future]
        at_defaults = Defaults.matches_defaults?(socket.assigns.page.slug, prev)

        socket =
          socket
          |> assign(:editing_blocks, prev)
          |> assign(:editor_history, rest)
          |> assign(:editor_future, future)
          |> assign(:editor_dirty, true)
          |> assign(:editor_at_defaults, at_defaults)
          |> assign(:editor_live_region_message, "Undone")
          |> reload_block_data(prev)

        {:halt, socket}

      [] ->
        {:halt, socket}
    end
  end

  defp handle_editor_action("redo", _params, socket) do
    case socket.assigns.editor_future do
      [next | rest] ->
        history = [socket.assigns.editing_blocks | socket.assigns.editor_history]
        at_defaults = Defaults.matches_defaults?(socket.assigns.page.slug, next)

        socket =
          socket
          |> assign(:editing_blocks, next)
          |> assign(:editor_history, history)
          |> assign(:editor_future, rest)
          |> assign(:editor_dirty, true)
          |> assign(:editor_at_defaults, at_defaults)
          |> assign(:editor_live_region_message, "Redone")
          |> reload_block_data(next)

        {:halt, socket}

      [] ->
        {:halt, socket}
    end
  end

  # ── Page actions ─────────────────────────────────────────────────

  defp handle_editor_action("save", _params, socket) do
    %{page: page, editing_blocks: blocks} = socket.assigns

    case Pages.save_page(page.slug, %{title: page.title, blocks: blocks}) do
      {:ok, _saved_page} ->
        updated_page = Pages.get_page(page.slug)
        at_defaults = Defaults.matches_defaults?(page.slug, updated_page.blocks)
        Process.send_after(self(), :editor_clear_save_status, 2500)

        socket =
          socket
          |> assign(:page, updated_page)
          |> assign(:editing_blocks, updated_page.blocks)
          |> assign(:editor_dirty, false)
          |> assign(:editor_at_defaults, at_defaults)
          |> assign(:editor_history, [])
          |> assign(:editor_future, [])
          |> assign(:editor_save_status, :saved)

        {:halt, socket}

      {:error, _changeset} ->
        {:halt, assign(socket, :editor_save_status, :error)}
    end
  end

  defp handle_editor_action("save_all", _params, socket) do
    socket = save_all_tabs(socket)
    {:halt, socket}
  end

  defp handle_editor_action("reset_defaults", _params, socket) do
    slug = socket.assigns.page.slug
    default_blocks = Berrypod.Pages.Defaults.for_slug(slug).blocks

    # Treat reset like any other mutation: push to history, mark dirty
    {:halt, apply_mutation(socket, default_blocks, "Reset to defaults", :content)}
  end

  defp handle_editor_action("done", _params, socket) do
    path = socket.assigns.editor_current_path || "/"
    {:halt, push_navigate(socket, to: path)}
  end

  # Catch-all for unknown editor actions
  defp handle_editor_action(_action, _params, socket), do: {:halt, socket}

  # ── Theme editing actions ───────────────────────────────────────────

  # Settings stored outside the theme JSON (site_name, site_description)
  @standalone_settings ~w(site_name site_description)

  defp handle_theme_action("apply_preset", %{"preset" => preset_name}, socket) do
    preset_atom = String.to_existing_atom(preset_name)

    # Get preset values and apply in-memory (don't persist yet)
    case Presets.get(preset_atom) do
      nil ->
        {:halt, socket}

      preset_values ->
        # Merge preset values into current settings
        current = socket.assigns.theme_editor_settings
        theme_settings = struct(current, preset_values)

        generated_css =
          CSSGenerator.generate(theme_settings, &BerrypodWeb.Endpoint.static_path/1)

        header_image = socket.assigns.theme_editor_header_image
        contrast_warning = compute_header_contrast(theme_settings, header_image)

        socket =
          socket
          # Update editor state
          |> assign(:theme_editor_settings, theme_settings)
          |> assign(:theme_editor_active_preset, preset_atom)
          |> assign(:theme_editor_contrast_warning, contrast_warning)
          |> assign(:theme_dirty, true)
          # Update shop state so layout reflects changes live
          |> assign(:theme_settings, theme_settings)
          |> assign(:generated_css, generated_css)

        {:halt, socket}
    end
  end

  defp handle_theme_action(
         "update_setting",
         %{"field" => field, "setting_value" => value},
         socket
       )
       when field in @standalone_settings do
    # Standalone settings (site_name, site_description) save immediately for now
    # TODO: Track these separately for proper revert
    Settings.put_setting(field, value, "string")
    # Also update the main assigns so ThemeHook sees the change
    {:halt, assign(socket, String.to_existing_atom(field), value)}
  end

  defp handle_theme_action(
         "update_setting",
         %{"field" => field, "setting_value" => value},
         socket
       ) do
    field_atom = String.to_existing_atom(field)
    update_theme_setting(socket, %{field_atom => value}, field)
  end

  defp handle_theme_action("update_setting", %{"field" => field} = params, socket)
       when field in @standalone_settings do
    value = params[field]

    if value do
      # Standalone settings save immediately for now
      # TODO: Track these separately for proper revert
      Settings.put_setting(field, value, "string")
      {:halt, assign(socket, String.to_existing_atom(field), value)}
    else
      {:halt, socket}
    end
  end

  defp handle_theme_action("update_setting", %{"field" => field} = params, socket) do
    value = params[field] || params["#{field}_text"] || params["value"]

    if value do
      field_atom = String.to_existing_atom(field)
      update_theme_setting(socket, %{field_atom => value}, field)
    else
      {:halt, socket}
    end
  end

  defp handle_theme_action("update_color", %{"field" => field, "value" => value}, socket) do
    field_atom = String.to_existing_atom(field)
    update_theme_setting(socket, %{field_atom => value}, field)
  end

  defp handle_theme_action("toggle_setting", %{"field" => field}, socket) do
    field_atom = String.to_existing_atom(field)
    current_value = Map.get(socket.assigns.theme_editor_settings, field_atom)
    new_value = !current_value

    # Prevent turning off show_site_name when there's no logo
    if field_atom == :show_site_name && new_value == false && !has_valid_logo?(socket) do
      {:halt, socket}
    else
      update_theme_setting(socket, %{field_atom => new_value}, field)
    end
  end

  defp handle_theme_action("toggle_customise", _params, socket) do
    {:halt,
     assign(socket, :theme_editor_customise_open, !socket.assigns.theme_editor_customise_open)}
  end

  defp handle_theme_action("remove_logo", _params, socket) do
    # Delete the image immediately (this is a destructive action)
    if logo = socket.assigns.theme_editor_logo_image do
      Media.delete_image(logo)
    end

    # Update settings in memory only
    current = socket.assigns.theme_editor_settings
    theme_settings = %{current | logo_image_id: nil, show_site_name: true}
    generated_css = CSSGenerator.generate(theme_settings)

    socket =
      socket
      |> assign(:theme_editor_logo_image, nil)
      |> assign(:logo_image, nil)
      |> assign(:theme_editor_settings, theme_settings)
      |> assign(:theme_dirty, true)
      |> assign(:generated_css, generated_css)

    {:halt, socket}
  end

  defp handle_theme_action("remove_header", _params, socket) do
    # Delete the image immediately (this is a destructive action)
    if header = socket.assigns.theme_editor_header_image do
      Media.delete_image(header)
    end

    # Update settings in memory only
    current = socket.assigns.theme_editor_settings
    theme_settings = %{current | header_image_id: nil}
    generated_css = CSSGenerator.generate(theme_settings)

    socket =
      socket
      |> assign(:theme_editor_header_image, nil)
      |> assign(:header_image, nil)
      |> assign(:theme_editor_settings, theme_settings)
      |> assign(:theme_dirty, true)
      |> assign(:theme_editor_contrast_warning, :ok)
      |> assign(:generated_css, generated_css)

    {:halt, socket}
  end

  defp handle_theme_action("remove_icon", _params, socket) do
    # Delete the image immediately (this is a destructive action)
    if icon = socket.assigns.theme_editor_icon_image do
      Media.delete_image(icon)
    end

    # Update settings in memory only
    current = socket.assigns.theme_editor_settings
    theme_settings = %{current | icon_image_id: nil}

    socket =
      socket
      |> assign(:theme_editor_icon_image, nil)
      |> assign(:icon_image, nil)
      |> assign(:theme_editor_settings, theme_settings)
      |> assign(:theme_dirty, true)

    {:halt, socket}
  end

  defp handle_theme_action(
         "cancel_upload",
         %{"ref" => ref, "upload" => upload_name},
         socket
       ) do
    upload_atom = String.to_existing_atom(upload_name)
    {:halt, Phoenix.LiveView.cancel_upload(socket, upload_atom, ref)}
  end

  # Catch-all for unknown theme actions
  defp handle_theme_action(_action, _params, socket), do: {:halt, socket}

  # ── Settings editing actions (page settings) ────────────────

  # Validate page settings form (live as-you-type)
  defp handle_settings_action("validate_page", %{"page" => params}, socket) do
    page = socket.assigns.page

    # Allow editing for custom pages (all fields) or system pages (url_slug only)
    if page && page.type in ["custom", "system"] do
      socket =
        socket
        |> assign(:settings_form, params)
        |> assign(:settings_dirty, has_settings_changed?(page, params))
        |> assign(:settings_save_status, :idle)

      {:halt, socket}
    else
      {:halt, socket}
    end
  end

  # Save page settings
  defp handle_settings_action("save_page", %{"page" => params}, socket) do
    page = socket.assigns.page

    cond do
      # Custom pages: save all settings including url_slug
      page && page.type == "custom" ->
        save_custom_page_settings(socket, page, params)

      # System pages with url_prefix (collections, products, orders)
      page && page.type == "system" && params["url_prefix"] ->
        save_system_page_url_prefix(socket, page, params["url_prefix"])

      # System pages: only save url_slug (other fields are read-only)
      page && page.type == "system" && params["url_slug"] ->
        save_system_page_url_slug(socket, page, params["url_slug"])

      true ->
        {:halt, socket}
    end
  end

  defp handle_settings_action(_action, _params, socket), do: {:halt, socket}

  defp save_custom_page_settings(socket, page, params) do
    # Normalize checkbox fields (unchecked checkboxes aren't sent)
    params =
      params
      |> Map.put_new("published", "false")
      |> Map.put_new("show_in_nav", "false")

    old_slug = page.slug

    # Fetch the Page struct from DB (assigns.page may be a map from cache)
    page_struct = Pages.get_page_struct(page.slug)

    # Handle url_slug change separately to ensure redirects are created
    url_slug = params["url_slug"]
    old_url_slug = page[:url_slug]

    # First update the main page settings
    case Pages.update_custom_page(page_struct, params) do
      {:ok, updated_page} ->
        # If url_slug changed, handle redirect creation
        socket =
          if url_slug != old_url_slug do
            Pages.update_page_url_slug(updated_page, url_slug)
            # Reload page to get updated url_slug
            updated_page = Pages.get_page(updated_page.slug)
            assign(socket, :page, updated_page)
          else
            assign(socket, :page, updated_page)
          end

        socket =
          socket
          |> assign(:settings_form, nil)
          |> assign(:settings_dirty, false)
          |> assign(:settings_save_status, :saved)

        # Reinit form with new page values
        socket = init_settings_form(socket)

        # If slug changed, redirect to new URL
        socket =
          if updated_page.slug != old_slug do
            push_navigate(socket, to: "/#{updated_page.slug}")
          else
            socket
          end

        {:halt, socket}

      {:error, _changeset} ->
        socket = assign(socket, :settings_save_status, :error)
        {:halt, socket}
    end
  end

  defp save_system_page_url_slug(socket, page, url_slug) do
    case Pages.update_page_url_slug(page.slug, url_slug) do
      {:ok, _updated} ->
        # Reload page from cache to get updated data
        updated_page = Pages.get_page(page.slug)

        socket =
          socket
          |> assign(:page, updated_page)
          |> assign(:settings_form, nil)
          |> assign(:settings_dirty, false)
          |> assign(:settings_save_status, :saved)

        socket = init_settings_form(socket)
        {:halt, socket}

      {:error, _} ->
        socket = assign(socket, :settings_save_status, :error)
        {:halt, socket}
    end
  end

  # Save URL prefix for prefixed pages (collection, pdp, orders)
  defp save_system_page_url_prefix(socket, page, new_prefix) do
    # Determine the prefix type based on the page slug
    prefix_type =
      case page.slug do
        "collection" -> :collections
        "pdp" -> :products
        "orders" -> :orders
        "order_detail" -> :orders
        _ -> nil
      end

    if prefix_type do
      case Settings.update_url_prefix(prefix_type, new_prefix) do
        {:ok, _} ->
          socket =
            socket
            |> assign(:settings_form, nil)
            |> assign(:settings_dirty, false)
            |> assign(:settings_save_status, :saved)

          {:halt, socket}

        {:error, _reason} ->
          socket = assign(socket, :settings_save_status, :error)
          {:halt, socket}
      end
    else
      {:halt, socket}
    end
  end

  # --- Site tab event handlers ---

  defp handle_site_action("update", %{"site" => site_params}, socket) do
    socket = handle_site_update(socket, site_params)
    {:halt, socket}
  end

  defp handle_site_action("update", _params, socket), do: {:halt, socket}

  # Social link CRUD operations (using SiteEditorState)
  defp handle_site_action("add_social_link", _params, socket) do
    socket = update_site_state(socket, &SiteEditorState.add_social_link/1)
    {:halt, socket}
  end

  defp handle_site_action("update_social_link", %{"id" => id} = params, socket) do
    state = socket.assigns.site_state
    link = Enum.find(state.social_links, &(&1.id == id))

    if link do
      link_params = params["social_link"][id] || %{}
      target = params["_target"] || []
      changed_field = List.last(target)

      attrs =
        case changed_field do
          "url" ->
            url = link_params["url"] |> Berrypod.Site.SocialLink.normalize_url()
            detected = Berrypod.Site.SocialLink.detect_platform(url)

            base = %{url: url}

            should_update_platform? =
              detected &&
                detected != "custom" &&
                (link.platform == "custom" || detected != link.platform)

            if should_update_platform?, do: Map.put(base, :platform, detected), else: base

          "platform" ->
            %{platform: link_params["platform"]}

          _ ->
            %{}
        end

      if attrs != %{} do
        socket = update_site_state(socket, &SiteEditorState.update_social_link(&1, id, attrs))
        {:halt, socket}
      else
        {:halt, socket}
      end
    else
      {:halt, socket}
    end
  end

  defp handle_site_action("remove_social_link", %{"id" => id}, socket) do
    socket = update_site_state(socket, &SiteEditorState.remove_social_link(&1, id))
    {:halt, socket}
  end

  defp handle_site_action("move_social_link", %{"id" => id, "dir" => dir}, socket) do
    direction = if dir == "up", do: :up, else: :down
    socket = update_site_state(socket, &SiteEditorState.move_social_link(&1, id, direction))
    {:halt, socket}
  end

  # Navigation item CRUD operations (using SiteEditorState)
  defp handle_site_action("add_nav_item", %{"location" => location}, socket) do
    socket = update_site_state(socket, &SiteEditorState.add_nav_item(&1, location))
    {:halt, socket}
  end

  defp handle_site_action(
         "update_nav_item",
         %{"id" => id, "location" => location} = params,
         socket
       ) do
    state = socket.assigns.site_state
    items = if location == "header", do: state.header_nav, else: state.footer_nav
    item = Enum.find(items, &(&1.id == id))

    if item do
      item_params = params["nav_item"][id] || %{}
      target = params["_target"] || []
      changed_field = List.last(target)

      attrs =
        case changed_field do
          "label" ->
            %{label: item_params["label"]}

          "url" ->
            %{url: item_params["url"]}

          "link_type" ->
            if item_params["link_type"] == "page", do: %{url: ""}, else: %{}

          "page_id" ->
            page_slug = item_params["page_id"]

            if page_slug && page_slug != "" do
              url = if String.starts_with?(page_slug, "/"), do: page_slug, else: "/#{page_slug}"
              %{url: url}
            else
              %{}
            end

          _ ->
            %{}
        end

      if attrs != %{} do
        socket =
          update_site_state(socket, &SiteEditorState.update_nav_item(&1, location, id, attrs))

        {:halt, socket}
      else
        {:halt, socket}
      end
    else
      {:halt, socket}
    end
  end

  defp handle_site_action("remove_nav_item", %{"id" => id, "location" => location}, socket) do
    socket = update_site_state(socket, &SiteEditorState.remove_nav_item(&1, location, id))
    {:halt, socket}
  end

  defp handle_site_action(
         "move_nav_item",
         %{"id" => id, "location" => location, "dir" => dir},
         socket
       ) do
    direction = if dir == "up", do: :up, else: :down

    socket =
      update_site_state(socket, &SiteEditorState.move_nav_item(&1, location, id, direction))

    {:halt, socket}
  end

  # ── Site branding events (route to theme state) ─────────────────────

  # Handle shop name and theme branding settings from Site tab
  defp handle_site_action("update_branding", %{"field" => "site_name"} = params, socket) do
    # Site name updates Settings immediately (same as theme tab)
    value = params["site_name"]

    if value do
      Settings.put_setting("site_name", value, "string")
      {:halt, assign(socket, :site_name, value)}
    else
      {:halt, socket}
    end
  end

  defp handle_site_action("update_branding", %{"field" => field} = params, socket) do
    # Route branding setting changes to theme state
    value = params[field] || params["setting_value"]

    if value do
      field_atom = String.to_existing_atom(field)
      update_theme_setting(socket, %{field_atom => value}, field)
    else
      {:halt, socket}
    end
  end

  defp handle_site_action("toggle_branding", %{"field" => field}, socket) do
    # Route toggle to theme state (same logic as theme_toggle_setting)
    field_atom = String.to_existing_atom(field)
    current_value = Map.get(socket.assigns.theme_editor_settings, field_atom)
    new_value = !current_value

    # Prevent turning off show_site_name when there's no logo
    if field_atom == :show_site_name && new_value == false && !has_valid_logo?(socket) do
      {:halt, socket}
    else
      update_theme_setting(socket, %{field_atom => new_value}, field)
    end
  end

  defp handle_site_action("update_color", %{"field" => field, "value" => value}, socket) do
    # Route color updates to theme state
    field_atom = String.to_existing_atom(field)
    update_theme_setting(socket, %{field_atom => value}, field)
  end

  defp handle_site_action("remove_logo", _params, socket) do
    # Delegate to theme action handler
    handle_theme_action("remove_logo", %{}, socket)
  end

  defp handle_site_action("remove_header", _params, socket) do
    # Delegate to theme action handler
    handle_theme_action("remove_header", %{}, socket)
  end

  # Catch-all for unknown site actions
  defp handle_site_action(_action, _params, socket), do: {:halt, socket}

  defp handle_site_update(socket, params) do
    state = socket.assigns.site_state

    # Handle announcement bar fields
    state =
      if Map.has_key?(params, "announcement_text") or
           Map.has_key?(params, "announcement_link") or
           Map.has_key?(params, "announcement_style") do
        text = params["announcement_text"] || state.announcement_text
        link = params["announcement_link"] || state.announcement_link
        style = params["announcement_style"] || state.announcement_style

        state
        |> SiteEditorState.put(:announcement_text, text)
        |> SiteEditorState.put(:announcement_link, link)
        |> SiteEditorState.put(:announcement_style, style)
      else
        state
      end

    # Handle footer fields
    state =
      if Map.has_key?(params, "footer_about") or
           Map.has_key?(params, "footer_copyright") or
           Map.has_key?(params, "show_newsletter") do
        about = params["footer_about"] || state.footer_about
        copyright = params["footer_copyright"] || state.footer_copyright

        show_newsletter =
          if Map.has_key?(params, "show_newsletter"),
            do: params["show_newsletter"] == "true",
            else: false

        state
        |> SiteEditorState.put(:footer_about, about)
        |> SiteEditorState.put(:footer_copyright, copyright)
        |> SiteEditorState.put(:show_newsletter, show_newsletter)
      else
        state
      end

    socket
    |> assign(:site_state, state)
    |> assign(:site_dirty, SiteEditorState.dirty?(state))
  end

  # Helper to update site state and compute dirty flag
  defp update_site_state(socket, update_fn) do
    state = socket.assigns.site_state
    new_state = update_fn.(state)

    socket
    |> assign(:site_state, new_state)
    |> assign(:site_dirty, SiteEditorState.dirty?(new_state))
  end

  # Check if settings have changed from current page values
  defp has_settings_changed?(page, params) do
    page.title != (params["title"] || "") or
      page.slug != (params["slug"] || "") or
      (page[:url_slug] || "") != (params["url_slug"] || "") or
      (page.meta_description || "") != (params["meta_description"] || "") or
      to_string(page.published) != (params["published"] || "false") or
      to_string(page.show_in_nav) != (params["show_in_nav"] || "false") or
      (page.nav_label || "") != (params["nav_label"] || "") or
      to_string(page.nav_position || 0) != (params["nav_position"] || "0") or
      has_prefix_changed?(page, params)
  end

  # Check if URL prefix changed for prefixed pages
  defp has_prefix_changed?(page, params) do
    case params["url_prefix"] do
      nil ->
        false

      new_prefix ->
        prefix_type = prefix_type_for_page(page.slug)
        prefix_type != nil and BerrypodWeb.R.prefix(prefix_type) != new_prefix
    end
  end

  defp prefix_type_for_page("collection"), do: :collections
  defp prefix_type_for_page("pdp"), do: :products
  defp prefix_type_for_page("orders"), do: :orders
  defp prefix_type_for_page("order_detail"), do: :orders
  defp prefix_type_for_page(_), do: nil

  # Initialize settings form from page values
  defp init_settings_form(socket) do
    page = socket.assigns.page

    form = %{
      "title" => page.title || "",
      "slug" => page.slug || "",
      "url_slug" => page[:url_slug] || "",
      "meta_description" => page.meta_description || "",
      "published" => to_string(page.published),
      "show_in_nav" => to_string(page.show_in_nav),
      "nav_label" => page.nav_label || "",
      "nav_position" => to_string(page.nav_position || 0)
    }

    assign(socket, :settings_form, form)
  end

  # Helper to update a theme setting in-memory (preview only, no persistence)
  defp update_theme_setting(socket, attrs, field) do
    current = socket.assigns.theme_editor_settings

    # Merge attrs into current settings (convert string keys to atoms)
    theme_settings =
      Enum.reduce(attrs, current, fn {key, value}, acc ->
        atom_key = if is_binary(key), do: String.to_existing_atom(key), else: key
        Map.put(acc, atom_key, value)
      end)

    generated_css =
      CSSGenerator.generate(theme_settings, &BerrypodWeb.Endpoint.static_path/1)

    active_preset = Presets.detect_preset(theme_settings)

    socket =
      socket
      # Update editor state
      |> assign(:theme_editor_settings, theme_settings)
      |> assign(:theme_editor_active_preset, active_preset)
      |> assign(:theme_dirty, true)
      |> maybe_recompute_contrast(field)
      # Update shop state so layout reflects changes live
      |> assign(:theme_settings, theme_settings)
      |> assign(:generated_css, generated_css)

    {:halt, socket}
  end

  defp maybe_recompute_contrast(socket, field)
       when field in ["mood", "header_background_enabled"] do
    header_image = socket.assigns.theme_editor_header_image
    theme_settings = socket.assigns.theme_editor_settings
    contrast_warning = compute_header_contrast(theme_settings, header_image)
    assign(socket, :theme_editor_contrast_warning, contrast_warning)
  end

  defp maybe_recompute_contrast(socket, _field), do: socket

  defp has_valid_logo?(socket) do
    socket.assigns.theme_editor_settings.show_logo &&
      socket.assigns.theme_editor_logo_image != nil
  end

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

  defp enter_edit_mode(socket) do
    page = socket.assigns.page
    allowed = BlockTypes.allowed_for(page.slug)
    at_defaults = Defaults.matches_defaults?(page.slug, page.blocks)

    socket
    |> assign(:editing, true)
    |> assign(:editing_blocks, page.blocks)
    |> assign(:editor_page_slug, page.slug)
    |> assign(:editor_dirty, false)
    |> assign(:editor_at_defaults, at_defaults)
    |> assign(:editor_history, [])
    |> assign(:editor_future, [])
    |> assign(:editor_expanded, MapSet.new())
    |> assign(:editor_show_picker, false)
    |> assign(:editor_picker_filter, "")
    |> assign(:editor_allowed_blocks, allowed)
    |> assign(:editor_live_region_message, nil)
    |> assign(:editor_sidebar_open, true)
    |> assign(:editor_sheet_state, :open)
    |> assign(:editor_image_picker_block_id, nil)
    |> assign(:editor_image_picker_field_key, nil)
    |> assign(:editor_image_picker_images, [])
    |> assign(:editor_image_picker_search, "")
    |> assign(:editor_save_status, :idle)
  end

  defp exit_edit_mode(socket) do
    socket
    |> assign(:editing, false)
    |> assign(:editing_blocks, nil)
    |> assign(:editor_page_slug, nil)
    |> assign(:editor_dirty, false)
    |> assign(:editor_history, [])
    |> assign(:editor_future, [])
    |> assign(:editor_expanded, MapSet.new())
    |> assign(:editor_show_picker, false)
    |> assign(:editor_picker_filter, "")
    |> assign(:editor_allowed_blocks, nil)
    |> assign(:editor_live_region_message, nil)
    |> assign(:editor_sidebar_open, true)
    |> assign(:editor_image_picker_block_id, nil)
    |> assign(:editor_image_picker_field_key, nil)
    |> assign(:editor_image_picker_images, [])
    |> assign(:editor_image_picker_search, "")
    |> assign(:editor_save_status, :idle)
  end

  defp apply_mutation(socket, new_blocks, message, type) do
    history =
      [socket.assigns.editing_blocks | socket.assigns.editor_history] |> Enum.take(50)

    at_defaults = Defaults.matches_defaults?(socket.assigns.page.slug, new_blocks)

    socket =
      socket
      |> assign(:editing_blocks, new_blocks)
      |> assign(:editor_history, history)
      |> assign(:editor_future, [])
      |> assign(:editor_dirty, true)
      |> assign(:editor_at_defaults, at_defaults)
      |> assign(:editor_live_region_message, message)

    case type do
      :content -> reload_block_data(socket, new_blocks)
      :reorder -> socket
    end
  end

  defp reload_block_data(socket, blocks) do
    extra = Pages.load_block_data(blocks, socket.assigns)
    Enum.reduce(extra, socket, fn {key, value}, acc -> assign(acc, key, value) end)
  end

  # ── Theme editing helpers ───────────────────────────────────────────

  # Load theme state without changing tabs (for settings tab that needs theme data)
  defp load_theme_state(socket) do
    theme_settings = Settings.get_theme_settings()

    generated_css =
      CSSGenerator.generate(theme_settings, &BerrypodWeb.Endpoint.static_path/1)

    active_preset = Presets.detect_preset(theme_settings)

    logo_image = Media.get_logo()
    header_image = Media.get_header()
    icon_image = Media.get_icon()

    contrast_warning = compute_header_contrast(theme_settings, header_image)

    socket
    |> assign(:theme_editing, true)
    |> assign(:theme_dirty, false)
    |> assign(:theme_editor_original, theme_settings)
    |> assign(:theme_editor_settings, theme_settings)
    |> assign(:theme_editor_active_preset, active_preset)
    |> assign(:theme_editor_logo_image, logo_image)
    |> assign(:theme_editor_header_image, header_image)
    |> assign(:theme_editor_icon_image, icon_image)
    |> assign(:theme_editor_contrast_warning, contrast_warning)
    |> assign(:theme_editor_customise_open, false)
    # Update both editor and shop state
    |> assign(:theme_settings, theme_settings)
    |> assign(:generated_css, generated_css)
    |> assign(:editor_sheet_state, :open)
  end

  defp enter_theme_edit_mode(socket) do
    socket
    |> load_theme_state()
    |> assign(:editor_active_tab, :theme)
  end

  defp exit_theme_edit_mode(socket) do
    socket
    |> assign(:theme_editing, false)
    |> assign(:theme_editor_settings, nil)
    |> assign(:theme_editor_active_preset, nil)
    |> assign(:theme_editor_contrast_warning, :ok)
    |> assign(:theme_editor_customise_open, false)
  end

  defp compute_header_contrast(theme_settings, header_image) do
    if theme_settings.header_background_enabled && header_image do
      text_color = Contrast.text_color_for_mood(theme_settings.mood)
      colors = Contrast.parse_dominant_colors(header_image.dominant_colors)
      Contrast.analyze_header_contrast(colors, text_color)
    else
      :ok
    end
  end

  # ── Site editing helpers ───────────────────────────────────────────

  defp load_site_state(socket) do
    # Load pages for the page picker (system pages that are linkable + custom pages)
    nav_pages = load_nav_pages()

    socket
    |> assign(:site_editing, true)
    |> assign(:site_state, SiteEditorState.load())
    |> assign(:site_nav_pages, nav_pages)
    |> assign(:editor_sheet_state, :open)
  end

  # Load pages suitable for navigation links
  # Excludes non-linkable system pages like cart, checkout_success, error, etc.
  defp load_nav_pages do
    linkable_system_slugs = ~w(home about delivery privacy terms contact)

    system_pages =
      Pages.list_system_pages()
      |> Enum.filter(&(&1.slug in linkable_system_slugs))
      |> Enum.map(&%{slug: &1.slug, title: &1.title})

    custom_pages =
      Pages.list_custom_pages()
      |> Enum.filter(& &1.published)
      |> Enum.map(&%{slug: &1.slug, title: &1.title})

    system_pages ++ custom_pages
  end

  # ── Unified save helpers ───────────────────────────────────────────

  defp save_all_tabs(socket) do
    socket
    |> maybe_save_page()
    |> maybe_save_settings()
    |> maybe_finish_save()
  end

  # If settings save triggered a navigation, don't overwrite the socket
  defp maybe_finish_save(%{redirected: {:live, _, _}} = socket), do: socket

  defp maybe_finish_save(socket) do
    socket
    |> maybe_save_theme()
    |> maybe_save_site()
    |> assign(:editor_save_status, :saved)
    |> schedule_save_status_clear()
  end

  defp maybe_save_page(socket) do
    if socket.assigns[:editor_dirty] do
      %{page: page, editing_blocks: blocks} = socket.assigns

      case Pages.save_page(page.slug, %{title: page.title, blocks: blocks}) do
        {:ok, _saved_page} ->
          updated_page = Pages.get_page(page.slug)
          at_defaults = Defaults.matches_defaults?(page.slug, updated_page.blocks)

          socket
          |> assign(:page, updated_page)
          |> assign(:editing_blocks, updated_page.blocks)
          |> assign(:editor_dirty, false)
          |> assign(:editor_at_defaults, at_defaults)
          |> assign(:editor_history, [])
          |> assign(:editor_future, [])

        {:error, _changeset} ->
          socket
      end
    else
      socket
    end
  end

  defp maybe_save_settings(socket) do
    if socket.assigns[:settings_dirty] do
      page = socket.assigns.page
      params = socket.assigns[:settings_form] || %{}

      cond do
        # Custom pages: save all settings including url_slug
        page && page.type == "custom" ->
          do_save_custom_page_settings(socket, page, params)

        # System pages with url_prefix (collections, products, orders)
        page && page.type == "system" && params["url_prefix"] ->
          do_save_system_page_url_prefix(socket, page, params["url_prefix"])

        # System pages: only save url_slug
        page && page.type == "system" && params["url_slug"] ->
          do_save_system_page_url_slug(socket, page, params["url_slug"])

        true ->
          socket
      end
    else
      socket
    end
  end

  defp do_save_custom_page_settings(socket, page, params) do
    params =
      params
      |> Map.put_new("published", "false")
      |> Map.put_new("show_in_nav", "false")

    page_struct = Pages.get_page_struct(page.slug)
    url_slug = params["url_slug"]
    old_url_slug = page[:url_slug]

    case Pages.update_custom_page(page_struct, params) do
      {:ok, updated_page} ->
        if url_slug != old_url_slug do
          Pages.update_page_url_slug(updated_page, url_slug)
        end

        updated_page = Pages.get_page(updated_page.slug)

        socket
        |> assign(:page, updated_page)
        |> assign(:settings_form, nil)
        |> assign(:settings_dirty, false)
        |> init_settings_form()

      {:error, _changeset} ->
        socket
    end
  end

  defp do_save_system_page_url_slug(socket, page, url_slug) do
    old_url = Pages.Page.effective_url(page)

    case Pages.update_page_url_slug(page.slug, url_slug) do
      {:ok, _updated} ->
        updated_page = Pages.get_page(page.slug)
        new_url = Pages.Page.effective_url(updated_page)

        socket =
          socket
          |> assign(:page, updated_page)
          |> assign(:settings_form, nil)
          |> assign(:settings_dirty, false)
          |> init_settings_form()

        # If URL changed, navigate to the new URL
        if old_url != new_url do
          push_navigate(socket, to: "/#{new_url}?edit=page")
        else
          socket
        end

      {:error, _reason} ->
        socket
    end
  end

  defp do_save_system_page_url_prefix(socket, page, new_prefix) do
    prefix_type = prefix_type_for_page(page.slug)

    if prefix_type do
      old_prefix = BerrypodWeb.R.prefix(prefix_type)

      case Settings.update_url_prefix(prefix_type, new_prefix) do
        {:ok, _} ->
          socket =
            socket
            |> assign(:settings_form, nil)
            |> assign(:settings_dirty, false)

          # If prefix changed, navigate to the new URL
          if old_prefix != new_prefix do
            # For collection page, navigate to /new-prefix/all
            new_path =
              case page.slug do
                "collection" -> "/#{new_prefix}/all?edit=page"
                "pdp" -> "/#{new_prefix}?edit=page"
                _ -> "/#{new_prefix}?edit=page"
              end

            push_navigate(socket, to: new_path)
          else
            socket
          end

        {:error, _reason} ->
          socket
      end
    else
      socket
    end
  end

  defp maybe_save_theme(socket) do
    if socket.assigns[:theme_dirty] do
      case Settings.update_theme_settings(Map.from_struct(socket.assigns.theme_editor_settings)) do
        {:ok, theme_settings} ->
          socket
          |> assign(:theme_editor_original, theme_settings)
          |> assign(:theme_dirty, false)

        {:error, _} ->
          socket
      end
    else
      socket
    end
  end

  defp maybe_save_site(socket) do
    state = socket.assigns[:site_state]

    if state && SiteEditorState.dirty?(state) do
      saved_state = SiteEditorState.save(state)

      socket
      |> assign(:site_state, saved_state)
      |> assign(:site_dirty, false)
    else
      socket
    end
  end

  defp schedule_save_status_clear(socket) do
    Process.send_after(self(), :editor_clear_save_status, 2500)
    socket
  end

  # ── Revert helpers ─────────────────────────────────────────────────

  defp revert_all_tabs(socket) do
    socket
    |> maybe_revert_page()
    |> maybe_revert_theme()
    |> maybe_revert_site()
  end

  defp maybe_revert_page(socket) do
    if socket.assigns[:editor_dirty] do
      # Reload the page from database
      page = Pages.get_page(socket.assigns.page.slug)
      at_defaults = Defaults.matches_defaults?(page.slug, page.blocks)

      socket
      |> assign(:page, page)
      |> assign(:editing_blocks, page.blocks)
      |> assign(:editor_dirty, false)
      |> assign(:editor_at_defaults, at_defaults)
      |> assign(:editor_history, [])
      |> assign(:editor_future, [])
    else
      socket
    end
  end

  defp maybe_revert_theme(socket) do
    if socket.assigns[:theme_dirty] do
      original = socket.assigns.theme_editor_original
      generated_css = CSSGenerator.generate(original, &BerrypodWeb.Endpoint.static_path/1)

      socket
      |> assign(:theme_editor_settings, original)
      |> assign(:theme_settings, original)
      |> assign(:generated_css, generated_css)
      |> assign(:theme_dirty, false)
    else
      socket
    end
  end

  defp maybe_revert_site(socket) do
    state = socket.assigns[:site_state]

    if state && SiteEditorState.dirty?(state) do
      reverted_state = SiteEditorState.revert(state)

      socket
      |> assign(:site_state, reverted_state)
      |> assign(:site_dirty, false)
    else
      socket
    end
  end
end
