defmodule BerrypodWeb.Shop.ReviewForm do
  @moduledoc """
  LiveView for submitting and editing product reviews.

  Accessed via /reviews/new?token=xxx for new reviews or
  /reviews/:id/edit for editing existing reviews.
  """

  use BerrypodWeb, :live_view

  alias Berrypod.{Media, Products, Reviews}
  alias Berrypod.Reviews.Review

  @max_photos 3
  @max_file_size 10_000_000

  @impl true
  def mount(_params, session, socket) do
    email_session = session["email_session"]

    socket =
      socket
      |> assign(:email_session, email_session)
      |> assign(:page_title, "Write a review")
      |> assign(:product, nil)
      |> assign(:review, nil)
      |> assign(:form, nil)
      |> assign(:submitted, false)
      |> assign(:error, nil)
      |> assign(:existing_images, [])
      |> assign(:removed_image_ids, [])
      |> allow_upload(:photos,
        accept: ~w(.jpg .jpeg .png .webp .heic),
        max_entries: @max_photos,
        max_file_size: @max_file_size
      )

    {:ok, socket}
  end

  @impl true
  def handle_params(params, _uri, socket) do
    {:noreply, apply_action(socket, socket.assigns.live_action, params)}
  end

  defp apply_action(socket, :new, %{"token" => token}) do
    case Reviews.verify_review_token(token) do
      {:ok, %{email: email, product_id: product_id}} ->
        product = Products.get_product(product_id)

        if product do
          # Check if they've already reviewed
          existing = Reviews.get_review_by_email_and_product(email, product_id)

          if existing do
            socket
            |> assign(:error, "You've already reviewed this product.")
            |> assign(:review, existing)
            |> assign(:product, product)
          else
            # Get matching order for pre-filling author name
            order = Reviews.find_matching_order(email, product_id)
            author_name = if order, do: order.shipping_address["name"], else: nil

            changeset =
              Review.changeset(%Review{}, %{
                product_id: product_id,
                email: email,
                author_name: author_name || "",
                rating: nil
              })

            socket
            |> assign(:product, product)
            |> assign(:email, email)
            |> assign(:form, to_form(changeset))
            |> assign(:page_title, "Review #{product.title}")
          end
        else
          assign(socket, :error, "Product not found.")
        end

      {:error, :invalid} ->
        assign(
          socket,
          :error,
          "This link has expired or is invalid. Please request a new one from the product page."
        )
    end
  end

  defp apply_action(socket, :new, _params) do
    assign(socket, :error, "Invalid review link.")
  end

  defp apply_action(socket, :edit, %{"id" => id} = params) do
    email_session = socket.assigns.email_session

    case Reviews.get_review(id) do
      nil ->
        assign(socket, :error, "Review not found.")

      review ->
        # Verify ownership via email session
        if email_session && String.downcase(email_session) == String.downcase(review.email) do
          product = params["product"] && Products.get_product_by_slug(params["product"])
          product = product || Products.get_product(review.product_id)

          changeset = Review.update_changeset(review, %{})

          # Load existing images for display
          existing_images = Reviews.get_review_images(review)

          socket
          |> assign(:review, review)
          |> assign(:product, product)
          |> assign(:form, to_form(changeset))
          |> assign(:page_title, "Edit your review")
          |> assign(:existing_images, existing_images)
        else
          assign(socket, :error, "You don't have permission to edit this review.")
        end
    end
  end

  @impl true
  def handle_event("validate", %{"review" => params}, socket) do
    changeset =
      if socket.assigns.review do
        Review.update_changeset(socket.assigns.review, params)
      else
        Review.changeset(%Review{}, Map.put(params, "product_id", socket.assigns.product.id))
      end
      |> Map.put(:action, :validate)

    {:noreply, assign(socket, :form, to_form(changeset))}
  end

  def handle_event("save", %{"review" => params}, socket) do
    if socket.assigns.review do
      update_review(socket, params)
    else
      create_review(socket, params)
    end
  end

  def handle_event("set_rating", %{"rating" => rating}, socket) do
    form = socket.assigns.form
    params = Map.put(form.params || %{}, "rating", rating)

    changeset =
      if socket.assigns.review do
        Review.update_changeset(socket.assigns.review, params)
      else
        Review.changeset(%Review{}, Map.put(params, "product_id", socket.assigns.product.id))
      end

    {:noreply, assign(socket, :form, to_form(changeset))}
  end

  def handle_event("cancel_upload", %{"ref" => ref}, socket) do
    {:noreply, cancel_upload(socket, :photos, ref)}
  end

  def handle_event("remove_image", %{"id" => image_id}, socket) do
    removed = [image_id | socket.assigns.removed_image_ids]
    existing = Enum.reject(socket.assigns.existing_images, &(&1.id == image_id))

    {:noreply,
     socket
     |> assign(:removed_image_ids, removed)
     |> assign(:existing_images, existing)}
  end

  defp create_review(socket, params) do
    # Consume uploads at submit time - they're ready since auto_upload: true
    # means files are uploaded as soon as selected
    image_ids = consume_photo_uploads(socket)

    params =
      params
      |> Map.put("product_id", socket.assigns.product.id)
      |> Map.put("email", socket.assigns.email)
      |> Map.put("image_ids", image_ids)

    case Reviews.create_review(params) do
      {:ok, _review} ->
        {:noreply,
         socket
         |> assign(:submitted, true)
         |> put_flash(:info, "Thanks! Your review will appear once approved.")}

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

  defp update_review(socket, params) do
    # Consume uploads at submit time
    new_image_ids = consume_photo_uploads(socket)

    # Keep existing images that weren't removed, plus new uploads
    kept_ids =
      socket.assigns.existing_images
      |> Enum.map(& &1.id)
      |> Enum.reject(&(&1 in socket.assigns.removed_image_ids))

    all_image_ids = kept_ids ++ new_image_ids

    params = Map.put(params, "image_ids", all_image_ids)

    case Reviews.update_review(socket.assigns.review, params) do
      {:ok, _review} ->
        {:noreply,
         socket
         |> assign(:submitted, true)
         |> put_flash(:info, "Your updated review will appear once approved.")}

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

  # Consume uploaded photos and save them to the database.
  # Returns a list of image IDs for the successfully saved images.
  # Uses async variant to avoid blocking - image processing happens in Oban.
  defp consume_photo_uploads(socket) do
    consume_uploaded_entries(socket, :photos, fn %{path: path}, entry ->
      case Media.upload_from_entry_async(path, entry, "review") do
        {:ok, image} -> {:ok, image.id}
        {:error, _reason} -> {:ok, nil}
      end
    end)
    |> Enum.reject(&is_nil/1)
  end

  @impl true
  def render(assigns) do
    ~H"""
    <div class="review-form-page">
      <%= cond do %>
        <% @submitted -> %>
          <.success_message product={@product} />
        <% @error -> %>
          <.error_message error={@error} product={@product} review={@review} />
        <% @form -> %>
          <.review_form
            form={@form}
            product={@product}
            review={@review}
            uploads={@uploads}
            existing_images={@existing_images}
          />
        <% true -> %>
          <p>Loading...</p>
      <% end %>
    </div>
    """
  end

  defp success_message(assigns) do
    ~H"""
    <div class="review-success">
      <svg
        class="review-success-icon"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
      >
        <path
          stroke-linecap="round"
          stroke-linejoin="round"
          d="M9 12.75 11.25 15 15 9.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"
        />
      </svg>
      <h1>Thanks for your review!</h1>
      <p>Your review will appear on the product page once it's been approved.</p>
      <.link href={BerrypodWeb.R.product(@product.slug)} class="review-back-link">
        Back to {@product.title}
      </.link>
    </div>
    """
  end

  defp error_message(assigns) do
    ~H"""
    <div class="review-error">
      <h1>Unable to leave review</h1>
      <p>{@error}</p>
      <.link :if={@product} href={BerrypodWeb.R.product(@product.slug)} class="review-back-link">
        Back to {@product.title}
      </.link>
      <.link
        :if={@product && @review}
        href={"/reviews/#{@review.id}/edit?product=#{@product.slug}"}
        class="review-edit-link"
      >
        Edit your existing review
      </.link>
      <.link :if={!@product} href={BerrypodWeb.R.home()} class="review-back-link">
        Back to shop
      </.link>
    </div>
    """
  end

  defp review_form(assigns) do
    ~H"""
    <div class="review-form-container">
      <h1 class="review-form-heading">
        {if @review, do: "Edit your review", else: "Write a review"}
      </h1>
      <p class="review-form-product">{@product.title}</p>

      <.form for={@form} phx-change="validate" phx-submit="save" class="review-form">
        <div class="review-form-field">
          <label class="review-form-label">Rating</label>
          <input type="hidden" name="review[rating]" value={@form[:rating].value} />
          <.star_rating_input rating={@form[:rating].value} />
          <p
            :for={msg <- Enum.map(@form[:rating].errors, &translate_error/1)}
            class="review-form-error"
          >
            {msg}
          </p>
        </div>

        <div class="review-form-field">
          <label for="review_author_name" class="review-form-label">Your name</label>
          <input
            type="text"
            id="review_author_name"
            name="review[author_name]"
            value={@form[:author_name].value}
            required
            maxlength="50"
            class="review-form-input"
            placeholder="How should we display your name?"
          />
          <p
            :for={msg <- Enum.map(@form[:author_name].errors, &translate_error/1)}
            class="review-form-error"
          >
            {msg}
          </p>
        </div>

        <div class="review-form-field">
          <label for="review_title" class="review-form-label">
            Title <span class="review-form-optional">(optional)</span>
          </label>
          <input
            type="text"
            id="review_title"
            name="review[title]"
            value={@form[:title].value}
            maxlength="100"
            class="review-form-input"
            placeholder="Summarise your experience"
          />
          <p
            :for={msg <- Enum.map(@form[:title].errors, &translate_error/1)}
            class="review-form-error"
          >
            {msg}
          </p>
        </div>

        <div class="review-form-field">
          <label for="review_body" class="review-form-label">
            Review <span class="review-form-optional">(optional)</span>
          </label>
          <textarea
            id="review_body"
            name="review[body]"
            maxlength="2000"
            rows="5"
            class="review-form-textarea"
            placeholder="Share the details of your experience"
          >{@form[:body].value}</textarea>
          <p :for={msg <- Enum.map(@form[:body].errors, &translate_error/1)} class="review-form-error">
            {msg}
          </p>
        </div>

        <.photo_upload_section uploads={@uploads} existing_images={@existing_images} />

        <button type="submit" class="review-form-submit">
          {if @review, do: "Update review", else: "Submit review"}
        </button>
      </.form>
    </div>
    """
  end

  defp photo_upload_section(assigns) do
    # Count total photos (existing + pending uploads)
    total_photos = length(assigns.existing_images) + length(assigns.uploads.photos.entries)
    can_add_more = total_photos < 3

    assigns = assign(assigns, :can_add_more, can_add_more)

    ~H"""
    <div class="review-form-field">
      <label class="review-form-label">
        Photos <span class="review-form-optional">(optional)</span>
      </label>

      <div class="review-photo-previews">
        <%!-- Existing images (from editing a review) --%>
        <div :for={image <- @existing_images} class="review-photo-preview">
          <img src={image_thumb_url(image)} alt="Review photo" />
          <button
            type="button"
            class="review-photo-remove"
            phx-click="remove_image"
            phx-value-id={image.id}
            aria-label="Remove photo"
          >
            <svg
              width="16"
              height="16"
              viewBox="0 0 24 24"
              fill="none"
              stroke="currentColor"
              stroke-width="2"
            >
              <line x1="18" y1="6" x2="6" y2="18"></line>
              <line x1="6" y1="6" x2="18" y2="18"></line>
            </svg>
          </button>
        </div>

        <%!-- Pending upload entries (uploaded to server, consumed on submit) --%>
        <div :for={entry <- @uploads.photos.entries} class="review-photo-preview">
          <.live_img_preview entry={entry} />
          <button
            type="button"
            class="review-photo-remove"
            phx-click="cancel_upload"
            phx-value-ref={entry.ref}
            aria-label="Remove photo"
          >
            <svg
              width="16"
              height="16"
              viewBox="0 0 24 24"
              fill="none"
              stroke="currentColor"
              stroke-width="2"
            >
              <line x1="18" y1="6" x2="6" y2="18"></line>
              <line x1="6" y1="6" x2="18" y2="18"></line>
            </svg>
          </button>
          <div :if={entry.progress > 0 and entry.progress < 100} class="review-photo-progress">
            <div class="review-photo-progress-bar" style={"width: #{entry.progress}%"}></div>
          </div>
        </div>

        <.live_file_input upload={@uploads.photos} class="sr-only" />
        <label :if={@can_add_more} class="review-photo-add" for={@uploads.photos.ref}>
          <svg
            width="24"
            height="24"
            viewBox="0 0 24 24"
            fill="none"
            stroke="currentColor"
            stroke-width="2"
          >
            <rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
            <circle cx="8.5" cy="8.5" r="1.5"></circle>
            <polyline points="21 15 16 10 5 21"></polyline>
          </svg>
          <span>Add photo</span>
        </label>
      </div>

      <p
        :for={entry <- @uploads.photos.entries}
        :if={entry.client_type not in ~w(image/jpeg image/png image/webp image/heic)}
        class="review-form-error"
      >
        {entry.client_name}: not a supported image type
      </p>
      <p :for={err <- upload_errors(@uploads.photos)} class="review-form-error">
        {upload_error_to_string(err)}
      </p>

      <p class="review-form-hint">You can add up to 3 photos (max 10MB each)</p>
    </div>
    """
  end

  defp upload_error_to_string(:too_large), do: "File is too large (max 10MB)"
  defp upload_error_to_string(:too_many_files), do: "Too many files (max 3)"
  defp upload_error_to_string(:not_accepted), do: "File type not supported"
  defp upload_error_to_string(_), do: "Upload error"

  defp image_thumb_url(image) do
    if image.variants_status == "complete" do
      "/image_cache/#{image.id}-thumb.jpg"
    else
      "/images/#{image.id}"
    end
  end

  defp star_rating_input(assigns) do
    assigns = assign(assigns, :rating, parse_rating(assigns.rating))

    ~H"""
    <div class="star-rating-input">
      <button
        :for={i <- 1..5}
        type="button"
        phx-click="set_rating"
        phx-value-rating={i}
        class={["star-rating-btn", if(@rating && @rating >= i, do: "star-filled", else: "star-empty")]}
        aria-label={"Rate #{i} stars"}
      >
        <svg viewBox="0 0 24 24" fill="currentColor" class="star-icon">
          <path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" />
        </svg>
      </button>
    </div>
    """
  end

  defp parse_rating(""), do: nil
  defp parse_rating(s) when is_binary(s), do: String.to_integer(s)
  defp parse_rating(rating), do: rating
end
