defmodule BerrypodWeb.Shop.Page do
  @moduledoc """
  Unified shop LiveView that handles all shop pages.

  Using a single LiveView enables `patch` navigation between pages,
  preserving socket state (including editor state) across transitions.
  """

  use BerrypodWeb, :live_view

  alias BerrypodWeb.Shop.Pages
  alias BerrypodWeb.R
  alias Berrypod.{Media, Settings}
  alias Berrypod.Workers.FaviconGeneratorWorker

  # Map page type atoms to handler modules
  @page_modules %{
    home: Pages.Home,
    product: Pages.Product,
    collection: Pages.Collection,
    cart: Pages.Cart,
    contact: Pages.Contact,
    search: Pages.Search,
    orders: Pages.Orders,
    order_detail: Pages.OrderDetail,
    checkout_success: Pages.CheckoutSuccess,
    custom_page: Pages.CustomPage,
    # Content pages all use the same module
    about: Pages.Content,
    delivery: Pages.Content,
    privacy: Pages.Content,
    terms: Pages.Content
  }

  # Pages that need session data passed to init
  @session_pages [:orders, :order_detail]

  @impl true
  def mount(_params, session, socket) do
    # Store session for pages that need it (orders, order_detail)
    socket = assign(socket, :_session, session)

    # Configure uploads only for admin users (theme editor image uploads)
    socket =
      if socket.assigns[:is_admin] do
        socket
        |> allow_upload(:theme_logo_upload,
          accept: ~w(.png .jpg .jpeg .webp .svg),
          max_entries: 1,
          max_file_size: 2_000_000,
          auto_upload: true,
          progress: &handle_theme_upload_progress/3
        )
        |> allow_upload(:theme_header_upload,
          accept: ~w(.png .jpg .jpeg .webp),
          max_entries: 1,
          max_file_size: 5_000_000,
          auto_upload: true,
          progress: &handle_theme_upload_progress/3
        )
        |> allow_upload(:theme_icon_upload,
          accept: ~w(.png .jpg .jpeg .webp .svg),
          max_entries: 1,
          max_file_size: 5_000_000,
          auto_upload: true,
          progress: &handle_theme_upload_progress/3
        )
      else
        socket
      end

    {:ok, socket}
  end

  # Handle theme image upload progress (logo, header, icon)
  defp handle_theme_upload_progress(:theme_logo_upload, entry, socket) do
    if entry.done? do
      consume_uploaded_entries(socket, :theme_logo_upload, fn %{path: path}, entry ->
        case Media.upload_from_entry(path, entry, "logo") do
          {:ok, image} ->
            Settings.update_theme_settings(%{logo_image_id: image.id})
            {:ok, image}

          {:error, _} = error ->
            error
        end
      end)
      |> case do
        [image | _] ->
          # Trigger favicon generation if using logo as icon
          if socket.assigns[:theme_editor_settings] &&
               socket.assigns.theme_editor_settings.use_logo_as_icon do
            enqueue_favicon_generation(image.id)
          end

          {:noreply,
           assign(socket, :theme_editor_logo_image, image) |> assign(:logo_image, image)}

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

  defp handle_theme_upload_progress(:theme_header_upload, entry, socket) do
    if entry.done? do
      consume_uploaded_entries(socket, :theme_header_upload, fn %{path: path}, entry ->
        case Media.upload_from_entry(path, entry, "header") do
          {:ok, image} ->
            Settings.update_theme_settings(%{header_image_id: image.id})
            {:ok, image}

          {:error, _} = error ->
            error
        end
      end)
      |> case do
        [image | _] ->
          socket =
            socket
            |> assign(:theme_editor_header_image, image)
            |> assign(:header_image, image)
            |> recompute_header_contrast()

          {:noreply, socket}

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

  defp handle_theme_upload_progress(:theme_icon_upload, entry, socket) do
    if entry.done? do
      consume_uploaded_entries(socket, :theme_icon_upload, fn %{path: path}, entry ->
        case Media.upload_from_entry(path, entry, "icon") do
          {:ok, image} ->
            Settings.update_theme_settings(%{icon_image_id: image.id})
            {:ok, image}

          {:error, _} = error ->
            error
        end
      end)
      |> case do
        [image | _] ->
          enqueue_favicon_generation(image.id)

          {:noreply,
           assign(socket, :theme_editor_icon_image, image) |> assign(:icon_image, image)}

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

  defp enqueue_favicon_generation(source_image_id) do
    %{source_image_id: source_image_id}
    |> FaviconGeneratorWorker.new()
    |> Oban.insert()
  end

  defp recompute_header_contrast(socket) do
    header_image = socket.assigns[:theme_editor_header_image]
    theme_settings = socket.assigns[:theme_editor_settings]

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

    assign(socket, :theme_editor_contrast_warning, warning)
  end

  @impl true
  def handle_params(params, uri, socket) do
    action = socket.assigns.live_action
    prev_action = socket.assigns[:_current_page_action]
    prev_path = socket.assigns[:_current_path]
    parsed_uri = URI.parse(uri)
    current_path = parsed_uri.path

    # For custom_page action, check if slug is a custom URL for a system page
    {action, params} = resolve_custom_slug(action, params)

    # Handle unrecognised routes (e.g., /shoppppp/thing)
    if action == :not_found do
      record_broken_url(current_path)
      raise BerrypodWeb.NotFoundError
    end

    # Update live_action if we resolved to a different page type
    socket =
      if action != socket.assigns.live_action do
        assign(socket, :live_action, action)
      else
        socket
      end

    module = @page_modules[action]

    # Clean up previous page if needed (e.g., unsubscribe from PubSub)
    socket = maybe_cleanup_previous_page(socket, prev_action)

    socket =
      if action != prev_action do
        # Page type changed - clear previous page's assigns and call init
        socket =
          socket
          |> assign(:_current_page_action, action)
          |> clear_page_specific_assigns(prev_action)

        result =
          if action in @session_pages do
            module.init(socket, params, uri, socket.assigns._session)
          else
            module.init(socket, params, uri)
          end

        case result do
          {:noreply, socket} -> socket
          {:redirect, socket} -> socket
        end
      else
        socket
      end

    # Scroll to top on navigation (different path, not just query param changes)
    socket =
      if prev_path && prev_path != current_path do
        Phoenix.LiveView.push_event(socket, "scroll-top", %{})
      else
        socket
      end

    socket = assign(socket, :_current_path, current_path)

    # Always call handle_params for URL changes - some pages (Content) set @page here
    socket =
      case module.handle_params(params, uri, socket) do
        {:noreply, socket} -> socket
      end

    # After module.handle_params, sync editor state if editing and page changed.
    # Must run after handle_params since Content pages set @page there.
    socket = maybe_sync_editing_blocks(socket)

    # If edit mode was requested via URL but @page wasn't available yet, enter it now
    socket = BerrypodWeb.PageEditorHook.maybe_enter_pending_page_mode(socket)

    {:noreply, socket}
  end

  # If editing and we navigated to a different page, reload editing_blocks
  defp maybe_sync_editing_blocks(socket) do
    page = socket.assigns[:page]
    editing = socket.assigns[:editing]
    editor_page_slug = socket.assigns[:editor_page_slug]

    if editing && page && page.slug != editor_page_slug do
      # Page changed while editing - reload editing state for the new page
      allowed = Berrypod.Pages.BlockTypes.allowed_for(page.slug)
      at_defaults = Berrypod.Pages.Defaults.matches_defaults?(page.slug, page.blocks)

      socket
      |> 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_allowed_blocks, allowed)
    else
      socket
    end
  end

  @impl true
  def handle_event(event, params, socket) do
    module = @page_modules[socket.assigns.live_action]

    case module.handle_event(event, params, socket) do
      :cont ->
        # Event not handled by page module, let hooks handle it
        {:noreply, socket}

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

  @impl true
  def handle_info(msg, socket) do
    module = @page_modules[socket.assigns.live_action]

    # Check if the module defines handle_info
    if function_exported?(module, :handle_info, 2) do
      case module.handle_info(msg, socket) do
        :cont -> {:noreply, socket}
        {:noreply, socket} -> {:noreply, socket}
      end
    else
      {:noreply, socket}
    end
  end

  @impl true
  def render(assigns) do
    # Cart page needs extra assigns computed at render time
    assigns =
      if assigns.live_action == :cart do
        Pages.Cart.compute_assigns(assigns)
      else
        assigns
      end

    ~H"""
    <BerrypodWeb.PageRenderer.render_page {assigns} />
    """
  end

  # Clean up previous page state when transitioning
  defp maybe_cleanup_previous_page(socket, :checkout_success) do
    Pages.CheckoutSuccess.cleanup(socket)
  end

  defp maybe_cleanup_previous_page(socket, _), do: socket

  # Clear assigns that are specific to each page type to prevent stale data
  # from bleeding into the next page's render
  defp clear_page_specific_assigns(socket, :product) do
    socket
    |> assign(:product, nil)
    |> assign(:all_images, nil)
    |> assign(:variants, nil)
    |> assign(:option_types, nil)
    |> assign(:selected_variant, nil)
    |> assign(:selected_options, nil)
    |> assign(:available_options, nil)
    |> assign(:display_price, nil)
    |> assign(:gallery_images, nil)
    |> assign(:option_urls, nil)
    |> assign(:related_products, nil)
    |> assign(:json_ld, nil)
  end

  defp clear_page_specific_assigns(socket, :collection) do
    socket
    |> assign(:products, nil)
    |> assign(:pagination, nil)
    |> assign(:current_sort, nil)
    |> assign(:collection_title, nil)
    |> assign(:collection_slug, nil)
  end

  defp clear_page_specific_assigns(socket, :contact) do
    socket
    |> assign(:contact_form, nil)
    |> assign(:lookup_form, nil)
    |> assign(:looked_up_orders, nil)
  end

  defp clear_page_specific_assigns(socket, :search) do
    assign(socket, :search_results, nil)
  end

  defp clear_page_specific_assigns(socket, :orders) do
    assign(socket, :orders, nil)
  end

  defp clear_page_specific_assigns(socket, :order_detail) do
    socket
    |> assign(:order, nil)
    |> assign(:email_form, nil)
  end

  defp clear_page_specific_assigns(socket, :checkout_success) do
    socket
    |> assign(:order, nil)
    |> assign(:js_purchase_tracked, nil)
    |> assign(:css_purchase_tracked, nil)
  end

  defp clear_page_specific_assigns(socket, _), do: socket

  # Resolve custom URL slugs to system pages
  # E.g., if cart page has url_slug: "basket", visiting /basket should load the cart page
  defp resolve_custom_slug(:custom_page, %{"slug" => slug} = params) do
    case R.page_type_from_slug(slug) do
      {:page, page_type} ->
        # Custom URL for a system page - dispatch to that page type
        # Preserve other params (like query params) but remove slug
        {page_type, Map.delete(params, "slug")}

      {:custom, _page} ->
        # Regular custom CMS page - let CustomPage handle it
        {:custom_page, params}

      nil ->
        # Unknown slug - let CustomPage handle the 404
        {:custom_page, params}
    end
  end

  # Handle dynamic prefix routes for products, collections, and orders.
  # Works with both default prefixes (/products/123) and custom prefixes (/p/123).
  # R.prefix_type_from_segment checks both the ETS cache (custom) and defaults.
  defp resolve_custom_slug(:dynamic_prefix, %{"prefix" => prefix} = params) do
    # The second segment param name is "id_or_slug" from the route definition
    id_or_slug = params["id_or_slug"]
    # Preserve query params by keeping all params except route-specific ones
    other_params = Map.drop(params, ["prefix", "id_or_slug"])

    case R.prefix_type_from_segment(prefix) do
      :collections ->
        {:collection, Map.put(other_params, "slug", id_or_slug)}

      :products ->
        {:product, Map.put(other_params, "id", id_or_slug)}

      :orders ->
        {:order_detail, Map.put(other_params, "order_number", id_or_slug)}

      nil ->
        # Not a known prefix - check if combined path is a system page (e.g. checkout/success)
        combined_slug = prefix <> "/" <> id_or_slug

        case R.page_type_from_slug(combined_slug) do
          {:page, page_type} ->
            {page_type, other_params}

          {:custom, _page} ->
            {:custom_page, Map.put(other_params, "slug", combined_slug)}

          nil ->
            # Unknown prefix and not a valid page - 404
            {:not_found, other_params}
        end
    end
  end

  defp resolve_custom_slug(action, params), do: {action, params}

  # Record broken URL for 404 tracking (mirrors CustomPage.record_broken_url)
  defp record_broken_url(path) do
    unless static_path?(path) do
      prior_hits = Berrypod.Analytics.count_pageviews_for_path(path)
      Berrypod.Redirects.record_broken_url(path, prior_hits)

      if prior_hits > 0 do
        Berrypod.Redirects.attempt_auto_resolve(path)
      end
    end
  rescue
    _ -> :ok
  end

  defp static_path?(path) do
    String.starts_with?(path, "/assets/") or
      String.starts_with?(path, "/images/") or
      String.starts_with?(path, "/image_cache/") or
      String.starts_with?(path, "/mockups/") or
      String.starts_with?(path, "/favicon")
  end
end
