defmodule BerrypodWeb.Admin.Pages.Editor do
  use BerrypodWeb, :live_view

  alias Berrypod.{LegalPages, Media, Pages, Products, Site}
  alias Berrypod.Pages.{BlockEditor, BlockTypes, Page}
  alias Berrypod.Products.ProductImage
  alias Berrypod.Theme.{Fonts, PreviewData}

  import BerrypodWeb.BlockEditorComponents

  @impl true
  def mount(%{"slug" => slug}, _session, socket) do
    page = Pages.get_page(slug)
    allowed_blocks = BlockTypes.allowed_for(slug)

    real_products = Products.list_visible_products(limit: 8)
    real_categories = Products.list_categories()

    preview_data = %{
      products: if(real_products != [], do: real_products, else: PreviewData.products()),
      categories: if(real_categories != [], do: real_categories, else: PreviewData.categories()),
      cart_items: PreviewData.cart_items()
    }

    {:ok,
     socket
     |> assign(:page_title, page.title)
     |> assign(:slug, slug)
     |> assign(:page_data, page)
     |> assign(:blocks, page.blocks)
     |> assign(:allowed_blocks, allowed_blocks)
     |> assign(:history, [])
     |> assign(:future, [])
     |> assign(:dirty, false)
     |> assign(:save_status, :idle)
     |> assign(:show_picker, false)
     |> assign(:picker_filter, "")
     |> assign(:expanded, MapSet.new())
     |> assign(:live_region_message, nil)
     |> assign(:show_preview, false)
     |> assign(:preview_data, preview_data)
     |> assign(:logo_image, Media.get_logo())
     |> assign(:header_image, Media.get_header())
     |> assign(:image_picker_block_id, nil)
     |> assign(:image_picker_field_key, nil)
     |> assign(:image_picker_images, [])
     |> assign(:image_picker_search, "")
     |> assign(:is_custom_page, !Page.system_slug?(slug))
     |> assign(:is_legal_page, LegalPages.legal_slug?(slug))
     |> assign_settings_form(slug)
     |> allow_upload(:image_picker_upload,
       accept: ~w(.png .jpg .jpeg .webp .svg),
       max_entries: 1,
       max_file_size: 5_000_000,
       auto_upload: true,
       progress: &handle_image_picker_upload/3
     )
     |> assign_content_source(slug, page.blocks)}
  end

  defp handle_image_picker_upload(:image_picker_upload, entry, socket) do
    if entry.done? do
      consume_uploaded_entries(socket, :image_picker_upload, fn %{path: path}, entry ->
        case Media.upload_from_entry(path, entry, "media") do
          {:ok, image} -> {:ok, image}
          {:error, _} = error -> error
        end
      end)
      |> case do
        [{:ok, image}] ->
          block_id = socket.assigns.image_picker_block_id
          field_key = socket.assigns.image_picker_field_key

          case BlockEditor.update_settings(socket.assigns.blocks, block_id, %{
                 field_key => image.id
               }) do
            {:ok, new_blocks} ->
              {:noreply,
               socket
               |> apply_mutation(new_blocks, "Image uploaded and selected")
               |> assign(:image_picker_block_id, nil)
               |> assign(:image_picker_field_key, nil)}

            :noop ->
              {:noreply, socket}
          end

        _ ->
          {:noreply, put_flash(socket, :error, "Upload failed")}
      end
    else
      {:noreply, socket}
    end
  end

  # Determines whether legal page content is auto-generated or customised.
  # For non-legal pages, content_source is nil (not shown).
  # On mount, auto-populates empty legal pages with generated content.
  defp assign_content_source(socket, slug, blocks) do
    if LegalPages.legal_slug?(slug) do
      current_content = get_content_body_text(blocks)
      fresh_content = LegalPages.regenerate_legal_content(slug)

      cond do
        # Empty content — auto-populate on mount
        current_content == "" ->
          blocks = populate_content_body(blocks, fresh_content)

          socket
          |> assign(:blocks, blocks)
          |> assign(:content_source, :auto)

        # Content matches generated — it's auto
        current_content == fresh_content ->
          assign(socket, :content_source, :auto)

        # Content differs — it's been customised
        true ->
          assign(socket, :content_source, :custom)
      end
    else
      assign(socket, :content_source, nil)
    end
  end

  defp populate_content_body(blocks, content) do
    Enum.map(blocks, fn
      %{"type" => "content_body", "settings" => settings} = block ->
        %{block | "settings" => Map.put(settings || %{}, "content", content)}

      block ->
        block
    end)
  end

  defp get_content_body_text(blocks) do
    case Enum.find(blocks, &(&1["type"] == "content_body")) do
      %{"settings" => %{"content" => content}} when is_binary(content) -> content
      _ -> ""
    end
  end

  # ── Block manipulation events ────────────────────────────────────

  @impl true
  def handle_event("move_up", %{"id" => block_id}, socket) do
    case BlockEditor.move_up(socket.assigns.blocks, block_id) do
      {:ok, new_blocks, message} ->
        {:noreply, apply_mutation(socket, new_blocks, message)}

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

  def handle_event("move_down", %{"id" => block_id}, socket) do
    case BlockEditor.move_down(socket.assigns.blocks, block_id) do
      {:ok, new_blocks, message} ->
        {:noreply, apply_mutation(socket, new_blocks, message)}

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

  def handle_event("remove_block", %{"id" => block_id}, socket) do
    {:ok, new_blocks, message} = BlockEditor.remove_block(socket.assigns.blocks, block_id)
    {:noreply, apply_mutation(socket, new_blocks, message)}
  end

  def handle_event("duplicate_block", %{"id" => block_id}, socket) do
    case BlockEditor.duplicate_block(socket.assigns.blocks, block_id) do
      {:ok, new_blocks, message} ->
        {:noreply, apply_mutation(socket, new_blocks, message)}

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

  def handle_event("add_block", %{"type" => type}, socket) do
    case BlockEditor.add_block(socket.assigns.blocks, type) do
      {:ok, new_blocks, message} ->
        {:noreply,
         socket
         |> apply_mutation(new_blocks, message)
         |> assign(:show_picker, false)}

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

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

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

        # Update content source badge if a legal page content block was edited
        socket =
          if socket.assigns.is_legal_page do
            assign_content_source(socket, socket.assigns.slug, new_blocks)
          else
            socket
          end

        {:noreply, socket}

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

  # ── Repeater events ──────────────────────────────────────────────

  def handle_event("repeater_add", %{"block-id" => block_id, "field" => field_key}, socket) do
    case BlockEditor.repeater_add(socket.assigns.blocks, block_id, field_key) do
      {:ok, new_blocks, message} ->
        {:noreply, apply_mutation(socket, new_blocks, message)}

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

  def handle_event(
        "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.blocks, block_id, field_key, index) do
      {:ok, new_blocks, message} ->
        {:noreply, apply_mutation(socket, new_blocks, message)}

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

  def handle_event(
        "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.blocks, block_id, field_key, index, dir) do
      {:ok, new_blocks, message} ->
        {:noreply, apply_mutation(socket, new_blocks, message)}

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

  # ── Image picker events ──────────────────────────────────────────

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

    {:noreply,
     socket
     |> assign(:image_picker_block_id, block_id)
     |> assign(:image_picker_field_key, field_key)
     |> assign(:image_picker_images, images)
     |> assign(:image_picker_search, "")}
  end

  def handle_event("hide_image_picker", _params, socket) do
    {:noreply,
     socket
     |> assign(:image_picker_block_id, nil)
     |> assign(:image_picker_field_key, nil)}
  end

  def handle_event("image_picker_search", %{"value" => value}, socket) do
    {:noreply, assign(socket, :image_picker_search, value)}
  end

  def handle_event("pick_image", %{"image-id" => image_id}, socket) do
    block_id = socket.assigns.image_picker_block_id
    field_key = socket.assigns.image_picker_field_key

    case BlockEditor.update_settings(socket.assigns.blocks, block_id, %{field_key => image_id}) do
      {:ok, new_blocks} ->
        {:noreply,
         socket
         |> apply_mutation(new_blocks, "Image selected")
         |> assign(:image_picker_block_id, nil)
         |> assign(:image_picker_field_key, nil)}

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

  def handle_event(
        "clear_image",
        %{"block-id" => block_id, "field" => field_key},
        socket
      ) do
    # Grab the old image_id before clearing
    old_image_id =
      case Enum.find(socket.assigns.blocks, &(&1["id"] == block_id)) do
        %{"settings" => settings} -> settings[field_key]
        _ -> nil
      end

    case BlockEditor.update_settings(socket.assigns.blocks, block_id, %{field_key => ""}) do
      {:ok, new_blocks} ->
        socket = apply_mutation(socket, new_blocks, "Image cleared")

        socket =
          if is_binary(old_image_id) && old_image_id != "" &&
               Media.find_usages(old_image_id) == [] do
            put_flash(
              socket,
              :info,
              "Image removed — it's no longer used anywhere and can be deleted from the media library"
            )
          else
            socket
          end

        {:noreply, socket}

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

  # ── UI events ────────────────────────────────────────────────────

  def handle_event("toggle_expand", %{"id" => block_id}, socket) do
    expanded = socket.assigns.expanded
    block = Enum.find(socket.assigns.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

    {:noreply,
     socket
     |> assign(:expanded, new_expanded)
     |> assign(:live_region_message, "#{block_name} settings #{action}")}
  end

  def handle_event("show_picker", _params, socket) do
    {:noreply,
     socket
     |> assign(:show_picker, true)
     |> assign(:picker_filter, "")}
  end

  def handle_event("hide_picker", _params, socket) do
    {:noreply, assign(socket, :show_picker, false)}
  end

  def handle_event("filter_picker", %{"value" => value}, socket) do
    {:noreply, assign(socket, :picker_filter, value)}
  end

  def handle_event("toggle_preview", _params, socket) do
    {:noreply, assign(socket, :show_preview, !socket.assigns.show_preview)}
  end

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

  def handle_event("save", _params, socket) do
    %{slug: slug, blocks: blocks, page_data: page_data} = socket.assigns

    case Pages.save_page(slug, %{title: page_data.title, blocks: blocks}) do
      {:ok, _page} ->
        {:noreply,
         socket
         |> assign(:dirty, false)
         |> assign(:save_status, :saved)}

      {:error, _changeset} ->
        {:noreply,
         socket
         |> assign(:save_status, :error)
         |> put_flash(:error, "Failed to save page")}
    end
  end

  def handle_event("regenerate_legal", _params, socket) do
    slug = socket.assigns.slug
    content = LegalPages.regenerate_legal_content(slug)

    case Enum.find(socket.assigns.blocks, &(&1["type"] == "content_body")) do
      %{"id" => block_id} ->
        case BlockEditor.update_settings(socket.assigns.blocks, block_id, %{"content" => content}) do
          {:ok, new_blocks} ->
            {:noreply,
             socket
             |> apply_mutation(new_blocks, "Legal content regenerated")
             |> assign(:content_source, :auto)
             |> put_flash(:info, "Content regenerated from current settings")}

          :noop ->
            {:noreply, socket}
        end

      nil ->
        {:noreply, put_flash(socket, :error, "No content block found on this page")}
    end
  end

  def handle_event("reset_defaults", _params, socket) do
    slug = socket.assigns.slug
    :ok = Pages.reset_page(slug)
    page = Pages.get_page(slug)

    {:noreply,
     socket
     |> assign(:blocks, page.blocks)
     |> assign(:history, [])
     |> assign(:future, [])
     |> assign(:dirty, false)
     |> put_flash(:info, "Page reset to defaults")}
  end

  def handle_event("undo", _params, socket) do
    case socket.assigns.history do
      [prev | rest] ->
        future = [socket.assigns.blocks | socket.assigns.future]

        {:noreply,
         socket
         |> assign(:blocks, prev)
         |> assign(:history, rest)
         |> assign(:future, future)
         |> assign(:dirty, prev != socket.assigns.page_data.blocks)
         |> assign(:live_region_message, "Undone")}

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

  def handle_event("redo", _params, socket) do
    case socket.assigns.future do
      [next | rest] ->
        history = [socket.assigns.blocks | socket.assigns.history]

        {:noreply,
         socket
         |> assign(:blocks, next)
         |> assign(:history, history)
         |> assign(:future, rest)
         |> assign(:dirty, true)
         |> assign(:live_region_message, "Redone")}

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

  # ── Page settings (custom pages only) ─────────────────────────────

  def handle_event("toggle_settings", _params, socket) do
    {:noreply, assign(socket, :show_settings, !socket.assigns.show_settings)}
  end

  def handle_event("validate_settings", %{"page" => params}, socket) do
    form =
      socket.assigns.page_struct
      |> Page.custom_changeset(params)
      |> Map.put(:action, :validate)
      |> to_form()

    {:noreply, assign(socket, :settings_form, form)}
  end

  def handle_event("save_settings", %{"page" => params}, socket) do
    case Pages.update_custom_page(socket.assigns.page_struct, params) do
      {:ok, page} ->
        {:noreply,
         socket
         |> assign(:page_struct, page)
         |> assign(:page_data, %{socket.assigns.page_data | title: page.title})
         |> assign(:page_title, page.title)
         |> assign(:settings_form, to_form(Page.custom_changeset(page, %{})))
         |> assign(:show_settings, false)
         |> put_flash(:info, "Page settings saved")}

      {:error, changeset} ->
        {:noreply, assign(socket, :settings_form, to_form(changeset))}
    end
  end

  defp assign_settings_form(socket, slug) do
    if Page.system_slug?(slug) do
      socket
      |> assign(:show_settings, false)
      |> assign(:page_struct, nil)
      |> assign(:settings_form, nil)
    else
      page_struct = Pages.get_page_struct(slug)

      socket
      |> assign(:show_settings, false)
      |> assign(:page_struct, page_struct)
      |> assign(:settings_form, to_form(Page.custom_changeset(page_struct, %{})))
    end
  end

  # ── Handle info ──────────────────────────────────────────────────

  # ── Render ───────────────────────────────────────────────────────

  @impl true
  def render(assigns) do
    ~H"""
    <div id="page-editor" phx-hook="EditorKeyboard" data-dirty={to_string(@dirty)}>
      <.link navigate={~p"/admin/pages"} class="admin-back-link">
        &larr; Pages
      </.link>
      <.header>
        {@page_data.title}
        <:actions>
          <button
            phx-click="toggle_preview"
            class="admin-btn admin-btn-sm admin-btn-ghost page-editor-toggle-preview"
          >
            <.icon
              name={if @show_preview, do: "hero-pencil-square", else: "hero-eye"}
              class="size-4"
            />
            {if @show_preview, do: "Edit", else: "Preview"}
          </button>
          <button
            :if={@is_custom_page}
            phx-click="toggle_settings"
            class={[
              "admin-btn admin-btn-sm admin-btn-ghost",
              @show_settings && "admin-btn-active"
            ]}
          >
            <.icon name="hero-cog-6-tooth" class="size-4" /> Settings
          </button>
          <button
            phx-click="undo"
            class="admin-btn admin-btn-sm admin-btn-ghost"
            disabled={@history == []}
            aria-label="Undo"
          >
            <.icon name="hero-arrow-uturn-left" class="size-4" />
          </button>
          <button
            phx-click="redo"
            class="admin-btn admin-btn-sm admin-btn-ghost"
            disabled={@future == []}
            aria-label="Redo"
          >
            <.icon name="hero-arrow-uturn-right" class="size-4" />
          </button>
          <button
            :if={@is_legal_page}
            phx-click="regenerate_legal"
            data-confirm="Regenerate this page's content from your current shop settings? Any manual edits will be replaced."
            class="admin-btn admin-btn-sm admin-btn-ghost"
          >
            <.icon name="hero-arrow-path" class="size-4" /> Regenerate
          </button>
          <button
            :if={!@is_custom_page}
            phx-click="reset_defaults"
            data-confirm="Reset this page to its default layout? Your changes will be lost."
            class="admin-btn admin-btn-sm admin-btn-ghost"
          >
            Reset to defaults
          </button>
          <button
            phx-click="save"
            class="admin-btn admin-btn-sm admin-btn-primary"
            disabled={!@dirty}
          >
            Save
          </button>
          <.inline_feedback status={@save_status} />
        </:actions>
      </.header>

      <%!-- ARIA live region for screen reader announcements --%>
      <div aria-live="polite" aria-atomic="true" class="sr-only">
        {if @live_region_message, do: @live_region_message}
      </div>

      <%!-- Status badges --%>
      <div class="admin-editor-badges">
        <p :if={@dirty} class="admin-badge admin-badge-warning">
          Unsaved changes
        </p>
        <p :if={@content_source == :auto} class="admin-badge admin-badge-info">
          Auto-generated from settings
        </p>
        <p :if={@content_source == :custom} class="admin-badge">
          Customised
        </p>
      </div>

      <%!-- Inline page settings (custom pages) --%>
      <div :if={@show_settings && @settings_form} class="page-settings-panel">
        <.form
          for={@settings_form}
          id="inline-page-settings"
          phx-change="validate_settings"
          phx-submit="save_settings"
        >
          <div class="page-settings-fields">
            <.input field={@settings_form[:title]} label="Title" />
            <.input field={@settings_form[:slug]} label="URL slug" />
            <.input
              field={@settings_form[:meta_description]}
              type="textarea"
              label="Meta description"
              phx-no-feedback
            />
            <div class="page-settings-row">
              <.input field={@settings_form[:published]} type="checkbox" label="Published" />
              <.input
                field={@settings_form[:show_in_nav]}
                type="checkbox"
                label="Show in navigation"
              />
            </div>
          </div>
          <div class="page-settings-actions">
            <.button type="submit" phx-disable-with="Saving..." class="admin-btn-sm">
              Save settings
            </.button>
            <button
              type="button"
              phx-click="toggle_settings"
              class="admin-btn admin-btn-ghost admin-btn-sm"
            >
              Cancel
            </button>
          </div>
        </.form>
      </div>

      <div class="page-editor-container">
        <%!-- Editor pane --%>
        <div class={[
          "page-editor-pane",
          @show_preview && "page-editor-pane-hidden-mobile"
        ]}>
          <%!-- Block list --%>
          <div class="block-list" role="list" aria-label="Page blocks">
            <.block_card
              :for={{block, idx} <- Enum.with_index(@blocks)}
              block={block}
              idx={idx}
              total={length(@blocks)}
              expanded={@expanded}
            />

            <div :if={@blocks == []} class="block-list-empty">
              <p>No blocks on this page yet.</p>
            </div>
          </div>

          <%!-- Add block button --%>
          <div class="block-actions">
            <button phx-click="show_picker" class="admin-btn admin-btn-outline block-add-btn">
              <.icon name="hero-plus" class="size-4" /> Add block
            </button>
          </div>
        </div>

        <%!-- Preview pane --%>
        <div class={[
          "page-editor-preview-pane",
          !@show_preview && "page-editor-preview-hidden-mobile"
        ]}>
          <.preview_pane
            slug={@slug}
            blocks={@blocks}
            page_data={@page_data}
            preview_data={@preview_data}
            theme_settings={@theme_settings}
            site_name={@site_name}
            generated_css={@generated_css}
            logo_image={@logo_image}
            header_image={@header_image}
          />
        </div>
      </div>

      <%!-- Block picker modal --%>
      <.block_picker
        :if={@show_picker}
        allowed_blocks={@allowed_blocks}
        filter={@picker_filter}
      />

      <%!-- Image picker modal --%>
      <.image_picker
        :if={@image_picker_block_id}
        images={@image_picker_images}
        search={@image_picker_search}
        upload={@uploads.image_picker_upload}
      />
    </div>
    """
  end

  # ── Preview ───────────────────────────────────────────────────────

  defp preview_pane(assigns) do
    page = %{slug: assigns.slug, title: assigns.page_data.title, blocks: assigns.blocks}

    preview =
      assigns
      |> assign(:page, page)
      |> assign(:mode, :preview)
      |> assign(:products, assigns.preview_data.products)
      |> assign(:categories, assigns.preview_data.categories)
      |> assign(:cart_items, PreviewData.cart_drawer_items())
      |> assign(:cart_count, 2)
      |> assign(:cart_subtotal, "£72.00")
      |> assign(:cart_drawer_open, false)
      |> assign(:header_nav_items, load_nav_items("header"))
      |> assign(:footer_nav_items, load_nav_items("footer"))
      |> preview_page_context(assigns.slug)

    extra = Pages.load_block_data(page.blocks, preview)
    assigns = assign(assigns, :preview, assign(preview, extra))

    ~H"""
    <div
      class="page-editor-preview themed"
      data-mood={@theme_settings.mood}
      data-typography={@theme_settings.typography}
      data-shape={@theme_settings.shape}
      data-density={@theme_settings.density}
      data-grid={@theme_settings.grid_columns}
      data-header={@theme_settings.header_layout}
      data-sticky={to_string(@theme_settings.sticky_header)}
      data-layout={@theme_settings.layout_width}
      data-shadow={@theme_settings.card_shadow}
      data-button-style={@theme_settings.button_style}
    >
      <style>
        <%= Phoenix.HTML.raw(Fonts.generate_all_font_faces(&BerrypodWeb.Endpoint.static_path/1)) %>
        <%= Phoenix.HTML.raw(@generated_css) %>
      </style>
      <BerrypodWeb.PageRenderer.render_page {@preview} />
    </div>
    """
  end

  defp preview_page_context(assigns, "pdp") do
    product = List.first(assigns.preview_data.products)
    option_types = Map.get(product, :option_types) || []
    variants = Map.get(product, :variants) || []

    {selected_options, selected_variant} =
      case variants do
        [first | _] -> {first.options, first}
        [] -> {%{}, nil}
      end

    available_options =
      Enum.reduce(option_types, %{}, fn opt, acc ->
        values = Enum.map(opt.values, & &1.title)
        Map.put(acc, opt.name, values)
      end)

    display_price =
      if selected_variant, do: selected_variant.price, else: product.cheapest_price

    assigns
    |> assign(:product, product)
    |> assign(:gallery_images, build_gallery_images(product))
    |> assign(:option_types, option_types)
    |> assign(:selected_options, selected_options)
    |> assign(:available_options, available_options)
    |> assign(:display_price, display_price)
    |> assign(:quantity, 1)
    |> assign(:option_urls, %{})
  end

  defp preview_page_context(assigns, "cart") do
    cart_items = assigns.preview_data.cart_items

    subtotal =
      Enum.reduce(cart_items, 0, fn item, acc ->
        acc + item.product.cheapest_price * item.quantity
      end)

    assigns
    |> assign(:cart_page_items, cart_items)
    |> assign(:cart_page_subtotal, subtotal)
  end

  defp preview_page_context(assigns, "about"),
    do: assign(assigns, :content_blocks, PreviewData.about_content())

  defp preview_page_context(assigns, "delivery"),
    do: assign(assigns, :content_blocks, PreviewData.delivery_content())

  defp preview_page_context(assigns, "privacy"),
    do: assign(assigns, :content_blocks, PreviewData.privacy_content())

  defp preview_page_context(assigns, "terms"),
    do: assign(assigns, :content_blocks, PreviewData.terms_content())

  defp preview_page_context(assigns, "error") do
    assign(assigns, %{
      error_code: "404",
      error_title: "Page Not Found",
      error_description:
        "Sorry, we couldn't find the page you're looking for. Perhaps you've mistyped the URL or the page has been moved."
    })
  end

  defp preview_page_context(assigns, _slug), do: assigns

  defp build_gallery_images(product) do
    (Map.get(product, :images) || [])
    |> Enum.sort_by(& &1.position)
    |> Enum.map(fn img -> ProductImage.url(img, 1200) end)
    |> Enum.reject(&is_nil/1)
  end

  # ── Helpers ──────────────────────────────────────────────────────

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

    socket
    |> assign(:blocks, new_blocks)
    |> assign(:history, history)
    |> assign(:future, [])
    |> assign(:dirty, true)
    |> assign(:save_status, :idle)
    |> assign(:live_region_message, message)
  end

  # Load nav items from database for preview, falling back to defaults if empty
  defp load_nav_items(location) do
    case Site.nav_items_for_shop(location) do
      [] -> default_nav_items(location)
      items -> items
    end
  end

  defp default_nav_items("header"), do: Site.default_header_nav()
  defp default_nav_items("footer"), do: Site.default_footer_nav()
end
