defmodule BerrypodWeb.Shop.Pages.Product do
  @moduledoc """
  Product detail page handler for the unified Shop.Page LiveView.
  """

  import Phoenix.Component, only: [assign: 2, assign: 3]
  import Phoenix.LiveView, only: [connected?: 1, push_navigate: 2]

  alias Berrypod.{Analytics, Cart, Pages, Reviews}
  alias BerrypodWeb.R
  alias Berrypod.Images.Optimizer
  alias Berrypod.Products
  alias Berrypod.Products.{Product, ProductImage}

  def init(socket, %{"id" => slug}, _uri) do
    # Try to get product by slug, including discontinued products
    case Products.get_product_by_slug(slug, preload: [:variants, :images]) do
      nil ->
        {:noreply, push_navigate(socket, to: R.collection("all"))}

      %{visible: false, status: status} when status != "discontinued" ->
        # Hidden but not discontinued - redirect away
        {:noreply, push_navigate(socket, to: R.collection("all"))}

      product ->
        all_images =
          (product.images || [])
          |> Enum.sort_by(& &1.position)
          |> Enum.map(fn img ->
            width =
              case ProductImage.source_width(img) do
                nil -> 1200
                sw -> Enum.max(Optimizer.applicable_widths(sw))
              end

            %{url: ProductImage.url(img, width), color: img.color}
          end)
          |> Enum.reject(fn img -> is_nil(img.url) end)

        option_types = Product.option_types(product)
        variants = product.variants || []

        if connected?(socket) and socket.assigns[:analytics_visitor_hash] do
          Analytics.track_event(
            "product_view",
            Map.put(BerrypodWeb.AnalyticsHook.attrs(socket), :pathname, R.product(slug))
          )
        end

        og_url = R.url(R.product(slug))
        og_image = og_image_url(all_images)

        # Load review aggregates for JSON-LD (SEO)
        {avg_rating, review_count} = Reviews.average_rating_for_product(product.id)
        seo_reviews = Reviews.list_reviews_for_product(product.id, limit: 5)

        page = Pages.get_page("pdp")
        is_discontinued = product.status == "discontinued"

        socket =
          socket
          |> assign(:page_title, product.title)
          |> assign(:page_description, meta_description(product.description))
          |> assign(:og_type, "product")
          |> assign(:og_url, og_url)
          |> assign(:og_image, og_image)
          |> assign(
            :json_ld,
            product_json_ld(product, og_url, og_image, avg_rating, review_count, seo_reviews)
          )
          |> assign(:product, product)
          |> assign(:all_images, all_images)
          |> assign(:quantity, 1)
          |> assign(:option_types, option_types)
          |> assign(:variants, variants)
          |> assign(:page, page)
          |> assign(:product_discontinued, is_discontinued)
          |> assign(:review_form, nil)
          |> assign(:review_status, nil)
          |> assign(:existing_review, nil)

        # Check if user has an existing review for this product
        socket = load_existing_review(socket)

        # Block data loaders (related_products, reviews) run after product is assigned
        extra = Pages.load_block_data(page.blocks, socket.assigns)

        {:noreply, assign(socket, extra)}
    end
  end

  def handle_params(params, _uri, socket) do
    if socket.assigns[:product] do
      {:noreply, apply_variant_params(params, socket)}
    else
      {:noreply, socket}
    end
  end

  def handle_event("increment_quantity", _params, socket) do
    quantity = min(socket.assigns.quantity + 1, 99)
    {:noreply, assign(socket, :quantity, quantity)}
  end

  def handle_event("decrement_quantity", _params, socket) do
    quantity = max(socket.assigns.quantity - 1, 1)
    {:noreply, assign(socket, :quantity, quantity)}
  end

  def handle_event("add_to_cart", _params, socket) do
    variant = socket.assigns.selected_variant
    is_discontinued = socket.assigns[:product_discontinued] || false

    if variant && variant.is_available && not is_discontinued do
      cart = Cart.add_item(socket.assigns.raw_cart, variant.id, socket.assigns.quantity)

      if socket.assigns[:analytics_visitor_hash] do
        Analytics.track_event(
          "add_to_cart",
          Map.put(
            BerrypodWeb.AnalyticsHook.attrs(socket),
            :pathname,
            R.product(socket.assigns.product.slug)
          )
        )
      end

      socket =
        socket
        |> BerrypodWeb.CartHook.broadcast_and_update(cart)
        |> assign(:quantity, 1)
        |> assign(:cart_drawer_open, true)
        |> assign(:cart_status, "#{socket.assigns.product.title} added to cart")

      {:noreply, socket}
    else
      {:noreply, socket}
    end
  end

  # ── Review events ──────────────────────────────────────────────────────

  def handle_event("request_review", %{"email" => email}, socket) do
    product = socket.assigns.product

    # Always show the same message to prevent email enumeration attacks.
    # Attacker shouldn't be able to confirm whether an email has purchased a product.
    generic_success =
      {:info, "If you've purchased this product, we've sent a verification link to your email."}

    case Reviews.request_review_verification(email, product.id, product.title) do
      {:ok, :sent} ->
        {:noreply, assign(socket, :review_status, generic_success)}

      {:error, :no_purchase} ->
        # Don't reveal that this email hasn't purchased
        {:noreply, assign(socket, :review_status, generic_success)}

      {:error, :already_reviewed} ->
        # This one is safe to reveal - they already have a public review
        {:noreply,
         assign(socket, :review_status, {:info, "You've already reviewed this product."})}

      {:error, _reason} ->
        {:noreply,
         assign(socket, :review_status, {:error, "Something went wrong. Please try again later."})}
    end
  end

  def handle_event("load_more_reviews", _params, socket) do
    product = socket.assigns.product
    current_reviews = socket.assigns[:reviews] || []
    offset = length(current_reviews)

    # Load the next batch
    more_reviews =
      Reviews.list_reviews_for_product(product.id, limit: 10, offset: offset)
      |> Enum.map(&format_review_for_display/1)

    all_reviews = current_reviews ++ more_reviews

    {:noreply, assign(socket, :reviews, all_reviews)}
  end

  def handle_event(_event, _params, _socket), do: :cont

  # ── Review helpers ───────────────────────────────────────────────────

  defp load_existing_review(socket) do
    email = socket.assigns[:email_session]
    product = socket.assigns.product

    if email do
      case Reviews.get_review_by_email_and_product(email, product.id) do
        nil -> socket
        review -> assign(socket, :existing_review, review)
      end
    else
      socket
    end
  end

  # ── Variant selection logic ──────────────────────────────────────────

  defp apply_variant_params(params, socket) do
    %{option_types: option_types, variants: variants, product: product, all_images: all_images} =
      socket.assigns

    option_names = Enum.map(option_types, & &1.name)

    default_options =
      case variants do
        [first | _] -> first.options
        [] -> %{}
      end

    # Extract option params from the URL, ignoring unknown keys
    param_options =
      Enum.reduce(option_names, %{}, fn name, acc ->
        case params[name] do
          nil -> acc
          value -> Map.put(acc, name, value)
        end
      end)

    {selected_options, changed_option} =
      if map_size(param_options) == 0 do
        {default_options, nil}
      else
        merged = Map.merge(default_options, param_options)

        # Identify which option was changed from the default
        changed =
          Enum.find(option_names, fn name ->
            Map.has_key?(param_options, name) and param_options[name] != default_options[name]
          end)

        {merged, changed}
      end

    selected_options =
      if changed_option do
        resolve_valid_combo(variants, option_types, selected_options, changed_option)
      else
        selected_options
      end

    selected_variant = find_variant(variants, selected_options)

    # If params produced an invalid combo with no matching variant, fall back to defaults
    {selected_options, selected_variant} =
      if is_nil(selected_variant) and variants != [] do
        opts = List.first(variants).options
        {opts, List.first(variants)}
      else
        {selected_options, selected_variant}
      end

    available_options = compute_available_options(option_types, variants, selected_options)
    display_price = variant_price(selected_variant, product)
    gallery_images = filter_gallery_images(all_images, selected_options["Color"])

    option_urls = build_option_urls(option_types, selected_options, product.slug)

    socket
    |> assign(:selected_options, selected_options)
    |> assign(:selected_variant, selected_variant)
    |> assign(:available_options, available_options)
    |> assign(:display_price, display_price)
    |> assign(:gallery_images, gallery_images)
    |> assign(:option_urls, option_urls)
  end

  defp build_option_urls(option_types, selected_options, slug) do
    Enum.reduce(option_types, %{}, fn opt_type, acc ->
      urls =
        opt_type.values
        |> Enum.map(fn value ->
          params = Map.put(selected_options, opt_type.name, value.title)
          {value.title, R.product(slug) <> "?#{URI.encode_query(params)}"}
        end)
        |> Map.new()

      Map.put(acc, opt_type.name, urls)
    end)
  end

  defp compute_available_options(option_types, variants, selected_options) do
    Enum.reduce(option_types, %{}, fn opt_type, acc ->
      other_options = Map.delete(selected_options, opt_type.name)

      available_values =
        variants
        |> Enum.filter(fn v ->
          v.is_available &&
            Enum.all?(other_options, fn {k, selected_val} ->
              v.options[k] == selected_val
            end)
        end)
        |> Enum.map(fn v -> v.options[opt_type.name] end)
        |> Enum.uniq()

      Map.put(acc, opt_type.name, available_values)
    end)
  end

  defp variant_price(%{price: price}, _product) when is_integer(price), do: price
  defp variant_price(_, %{cheapest_price: price}), do: price
  defp variant_price(_, _), do: 0

  # If the current combo doesn't match any variant, auto-adjust other options
  # to find a valid one. Keeps the just-changed option fixed, adjusts the rest.
  defp resolve_valid_combo(variants, option_types, selected_options, changed_option) do
    if Enum.any?(variants, fn v -> v.options == selected_options end) do
      selected_options
    else
      matching =
        Enum.filter(variants, fn v ->
          v.is_available && v.options[changed_option] == selected_options[changed_option]
        end)

      case matching do
        [first | _] ->
          Enum.reduce(option_types, selected_options, fn opt_type, acc ->
            if opt_type.name == changed_option do
              acc
            else
              Map.put(acc, opt_type.name, first.options[opt_type.name])
            end
          end)

        [] ->
          selected_options
      end
    end
  end

  defp find_variant(variants, selected_options) do
    Enum.find(variants, fn v -> v.options == selected_options end)
  end

  defp filter_gallery_images(all_images, selected_color) do
    if selected_color do
      color_images = Enum.filter(all_images, &(&1.color == selected_color))
      if color_images == [], do: all_images, else: color_images
    else
      all_images
    end
    |> Enum.map(& &1.url)
  end

  # ── JSON-LD and meta helpers ─────────────────────────────────────────

  defp product_json_ld(product, url, image, avg_rating, review_count, reviews) do
    category_slug =
      if product.category,
        do: product.category |> String.downcase() |> String.replace(" ", "-"),
        else: "all"

    breadcrumbs =
      [
        %{"@type" => "ListItem", "position" => 1, "name" => "Home", "item" => R.url(R.home())},
        product.category &&
          %{
            "@type" => "ListItem",
            "position" => 2,
            "name" => product.category,
            "item" => R.url(R.collection(category_slug))
          },
        %{
          "@type" => "ListItem",
          "position" => if(product.category, do: 3, else: 2),
          "name" => product.title,
          "item" => url
        }
      ]
      |> Enum.reject(&is_nil/1)

    product_data =
      %{
        "@type" => "Product",
        "name" => product.title,
        "description" => plain_text(product.description),
        "image" => Enum.reject([image], &is_nil/1),
        "url" => url,
        "offers" => %{
          "@type" => "Offer",
          "price" => format_price(product.cheapest_price),
          "priceCurrency" => "GBP",
          "availability" =>
            if(product.in_stock,
              do: "https://schema.org/InStock",
              else: "https://schema.org/OutOfStock"
            ),
          "url" => url
        }
      }
      |> maybe_add_rating(avg_rating, review_count)
      |> maybe_add_reviews(reviews)

    data = %{
      "@context" => "https://schema.org",
      "@graph" => [
        product_data,
        %{
          "@type" => "BreadcrumbList",
          "itemListElement" => breadcrumbs
        }
      ]
    }

    Jason.encode!(data, escape: :html_safe)
  end

  defp maybe_add_rating(product_data, nil, _count), do: product_data
  defp maybe_add_rating(product_data, _avg, 0), do: product_data

  defp maybe_add_rating(product_data, avg_rating, review_count) do
    Map.put(product_data, "aggregateRating", %{
      "@type" => "AggregateRating",
      "ratingValue" => Decimal.to_string(avg_rating),
      "reviewCount" => to_string(review_count),
      "bestRating" => "5",
      "worstRating" => "1"
    })
  end

  defp maybe_add_reviews(product_data, []), do: product_data

  defp maybe_add_reviews(product_data, reviews) do
    review_data =
      Enum.map(reviews, fn review ->
        review_item = %{
          "@type" => "Review",
          "author" => %{"@type" => "Person", "name" => review.author_name},
          "datePublished" => Date.to_iso8601(DateTime.to_date(review.inserted_at)),
          "reviewRating" => %{
            "@type" => "Rating",
            "ratingValue" => to_string(review.rating),
            "bestRating" => "5",
            "worstRating" => "1"
          }
        }

        review_item =
          if review.body && review.body != "" do
            Map.put(review_item, "reviewBody", review.body)
          else
            review_item
          end

        if review.title && review.title != "" do
          Map.put(review_item, "name", review.title)
        else
          review_item
        end
      end)

    Map.put(product_data, "review", review_data)
  end

  defp format_price(pence) when is_integer(pence) do
    "#{div(pence, 100)}.#{String.pad_leading(to_string(rem(pence, 100)), 2, "0")}"
  end

  defp format_price(_), do: "0.00"

  defp plain_text(nil), do: nil
  defp plain_text(text), do: String.replace(text, ~r/<[^>]+>/, "")

  defp og_image_url([%{url: "/" <> _ = path} | _]), do: BerrypodWeb.Endpoint.url() <> path
  defp og_image_url(_), do: nil

  defp meta_description(nil), do: nil

  defp meta_description(text) do
    plain = String.replace(text, ~r/<[^>]+>/, "")

    if String.length(plain) <= 155 do
      plain
    else
      plain
      |> String.slice(0, 155)
      |> String.split(~r/\s+/)
      |> Enum.drop(-1)
      |> Enum.join(" ")
      |> Kernel.<>("…")
    end
  end

  defp format_review_for_display(review) do
    %{
      id: review.id,
      rating: review.rating,
      title: review.title,
      body: review.body,
      author: review.author_name,
      date: relative_time(review.inserted_at),
      verified: not is_nil(review.order_id),
      images: Reviews.get_review_images(review)
    }
  end

  defp relative_time(datetime) do
    now = DateTime.utc_now()
    diff = DateTime.diff(now, datetime, :second)

    cond do
      diff < 60 -> "just now"
      diff < 3600 -> "#{div(diff, 60)} minutes ago"
      diff < 86400 -> "#{div(diff, 3600)} hours ago"
      diff < 604_800 -> "#{div(diff, 86400)} days ago"
      diff < 2_592_000 -> "#{div(diff, 604_800)} weeks ago"
      true -> "#{div(diff, 2_592_000)} months ago"
    end
  end
end
