defmodule Berrypod.Reviews do
  @moduledoc """
  Product review management.

  Reviews require verified purchase (via email session) and are moderated
  before public display. Each email can only review a product once.
  """

  import Ecto.Query

  alias Berrypod.Media
  alias Berrypod.Orders
  alias Berrypod.Repo
  alias Berrypod.Reviews.Review

  # ── Queries ────────────────────────────────────────────────────────

  @doc """
  Gets a review by ID.
  """
  def get_review(id) do
    Repo.get(Review, id)
  end

  @doc """
  Gets a review by ID, raising if not found.
  """
  def get_review!(id) do
    Repo.get!(Review, id)
  end

  @doc """
  Gets the review for a specific email and product combination.
  Returns nil if no review exists.
  """
  def get_review_by_email_and_product(email, product_id) do
    email = normalise_email(email)

    Repo.one(
      from r in Review,
        where: r.email == ^email and r.product_id == ^product_id
    )
  end

  @doc """
  Lists approved reviews for a product, newest first.

  Options:
    * `:status` - filter by status (default "approved")
    * `:limit` - max number of reviews to return
    * `:offset` - number of reviews to skip (for pagination)
  """
  def list_reviews_for_product(product_id, opts \\ []) do
    status = Keyword.get(opts, :status, "approved")
    limit = Keyword.get(opts, :limit)
    offset = Keyword.get(opts, :offset, 0)

    query =
      from r in Review,
        where: r.product_id == ^product_id and r.status == ^status,
        order_by: [desc: r.inserted_at]

    query = if limit, do: limit(query, ^limit), else: query
    query = if offset > 0, do: offset(query, ^offset), else: query

    Repo.all(query)
  end

  @doc """
  Lists pending reviews for admin moderation queue.
  """
  def list_pending_reviews do
    Repo.all(
      from r in Review,
        where: r.status == "pending",
        order_by: [asc: r.inserted_at],
        preload: [:product]
    )
  end

  @doc """
  Lists all reviews with optional status filter, for admin.
  """
  def list_reviews(opts \\ []) do
    status = Keyword.get(opts, :status)

    query =
      from r in Review,
        order_by: [desc: r.inserted_at],
        preload: [:product]

    query = if status, do: where(query, [r], r.status == ^status), else: query

    Repo.all(query)
  end

  @doc """
  Counts pending reviews for admin nav badge.
  """
  def count_pending_reviews do
    Repo.one(from r in Review, where: r.status == "pending", select: count(r.id))
  end

  @doc """
  Returns counts of reviews by status for admin tabs.
  """
  def count_reviews_by_status do
    Repo.all(
      from r in Review,
        group_by: r.status,
        select: {r.status, count(r.id)}
    )
    |> Map.new()
  end

  @doc """
  Lists reviews with pagination for admin.

  Options:
    * `:status` - filter by status (nil for all)
    * `:search` - search email or product title
    * `:page` - page number
    * `:per_page` - items per page (default 20)
  """
  def list_reviews_paginated(opts \\ []) do
    status = Keyword.get(opts, :status)
    search = Keyword.get(opts, :search)

    query =
      from r in Review,
        join: p in assoc(r, :product),
        order_by: [desc: r.inserted_at],
        preload: [product: p]

    query = if status && status != "", do: where(query, [r], r.status == ^status), else: query

    query =
      if search && search != "" do
        pattern = "%#{String.downcase(search)}%"

        where(
          query,
          [r, p],
          like(fragment("lower(?)", r.email), ^pattern) or
            like(fragment("lower(?)", r.author_name), ^pattern) or
            like(fragment("lower(?)", p.title), ^pattern)
        )
      else
        query
      end

    Berrypod.Pagination.paginate(query, page: opts[:page], per_page: opts[:per_page] || 20)
  end

  # ── Aggregates ─────────────────────────────────────────────────────

  @doc """
  Returns the average rating and count for a product's approved reviews.
  Returns `{nil, 0}` if no approved reviews exist.
  """
  def average_rating_for_product(product_id) do
    result =
      Repo.one(
        from r in Review,
          where: r.product_id == ^product_id and r.status == "approved",
          select: {avg(r.rating), count(r.id)}
      )

    case result do
      {nil, 0} -> {nil, 0}
      {avg, count} when is_float(avg) -> {Decimal.from_float(avg) |> Decimal.round(1), count}
      {avg, count} -> {Decimal.round(avg, 1), count}
    end
  end

  # ── Purchase verification ──────────────────────────────────────────

  @doc """
  Checks if an email has purchased a specific product (via any paid order).
  Returns true if they have a paid order containing the product.
  """
  def can_review?(email, product_id) do
    email = normalise_email(email)
    orders = Orders.list_orders_by_email(email)

    Enum.any?(orders, fn order ->
      Enum.any?(order.items, fn item ->
        item_matches_product?(item, product_id)
      end)
    end)
  end

  @doc """
  Finds the first order where this email purchased this product.
  Used to link the review to an order for "verified purchase" badge.
  """
  def find_matching_order(email, product_id) do
    email = normalise_email(email)
    orders = Orders.list_orders_by_email(email)

    Enum.find(orders, fn order ->
      Enum.any?(order.items, fn item ->
        item_matches_product?(item, product_id)
      end)
    end)
  end

  # Check if an order item matches a product by looking up the variant
  defp item_matches_product?(item, product_id) do
    case Berrypod.Products.get_variant(item.variant_id) do
      %{product_id: ^product_id} -> true
      _ -> false
    end
  end

  # ── CRUD ───────────────────────────────────────────────────────────

  @doc """
  Creates a new review.

  The review starts in "pending" status. If the email has a matching order
  for this product, the order_id is automatically linked.
  """
  def create_review(attrs) do
    email = attrs[:email] || attrs["email"]
    product_id = attrs[:product_id] || attrs["product_id"]

    # Auto-link to matching order for "verified purchase" badge
    order = if email && product_id, do: find_matching_order(email, product_id), else: nil

    attrs =
      if order do
        # Use same key type as the input map
        key = if Map.has_key?(attrs, :email), do: :order_id, else: "order_id"
        Map.put(attrs, key, order.id)
      else
        attrs
      end

    %Review{}
    |> Review.changeset(attrs)
    |> Repo.insert()
  end

  @doc """
  Updates an existing review.
  Resets status to pending for re-moderation.
  """
  def update_review(%Review{} = review, attrs) do
    review
    |> Review.update_changeset(attrs)
    |> Repo.update()
  end

  @doc """
  Approves a review for public display.
  Updates the product's rating cache.
  """
  def approve_review(%Review{} = review) do
    result =
      review
      |> Review.moderation_changeset("approved")
      |> Repo.update()

    case result do
      {:ok, updated} ->
        update_product_rating_cache(updated.product_id)
        {:ok, updated}

      error ->
        error
    end
  end

  @doc """
  Rejects a review (won't be displayed).
  Updates the product's rating cache if the review was previously approved.
  """
  def reject_review(%Review{} = review) do
    was_approved = review.status == "approved"

    result =
      review
      |> Review.moderation_changeset("rejected")
      |> Repo.update()

    case result do
      {:ok, updated} ->
        if was_approved, do: update_product_rating_cache(updated.product_id)
        {:ok, updated}

      error ->
        error
    end
  end

  @doc """
  Deletes a review.
  Updates the product's rating cache if the review was approved.
  """
  def delete_review(%Review{} = review) do
    was_approved = review.status == "approved"
    product_id = review.product_id

    result = Repo.delete(review)

    case result do
      {:ok, _} ->
        if was_approved, do: update_product_rating_cache(product_id)
        result

      error ->
        error
    end
  end

  # ── Images ─────────────────────────────────────────────────────────

  @doc """
  Gets the image records for a review's image_ids.
  Returns a list of Image structs.
  """
  def get_review_images(%Review{image_ids: ids}) when is_list(ids) and ids != [] do
    Media.get_images(ids)
  end

  def get_review_images(_), do: []

  @doc """
  Batch loads images for multiple reviews, returning a map of review_id => [images].
  More efficient than calling get_review_images/1 for each review.
  """
  def preload_review_images(reviews) when is_list(reviews) do
    # Collect all image IDs across all reviews
    all_image_ids =
      reviews
      |> Enum.flat_map(fn r -> r.image_ids || [] end)
      |> Enum.uniq()

    if all_image_ids == [] do
      Map.new(reviews, fn r -> {r.id, []} end)
    else
      # Fetch all images in one query
      images_by_id = Media.get_images(all_image_ids) |> Map.new(&{&1.id, &1})

      # Build map of review_id => [images]
      Map.new(reviews, fn review ->
        images =
          (review.image_ids || [])
          |> Enum.map(&Map.get(images_by_id, &1))
          |> Enum.reject(&is_nil/1)

        {review.id, images}
      end)
    end
  end

  # ── Review tokens ──────────────────────────────────────────────────

  @review_salt "review_verification_v1"
  # 1 hour
  @review_token_max_age 3600

  @doc """
  Generates a signed token for review verification.

  Token encodes email and product_id, valid for 1 hour.
  """
  def generate_review_token(email, product_id) do
    Phoenix.Token.sign(
      BerrypodWeb.Endpoint,
      @review_salt,
      %{email: normalise_email(email), product_id: product_id}
    )
  end

  @doc """
  Verifies a review token and returns the email and product_id.

  Returns `{:ok, %{email: email, product_id: product_id}}` or `{:error, :invalid}`.
  """
  def verify_review_token(token) do
    case Phoenix.Token.verify(BerrypodWeb.Endpoint, @review_salt, token,
           max_age: @review_token_max_age
         ) do
      {:ok, %{email: email, product_id: product_id}} ->
        {:ok, %{email: email, product_id: product_id}}

      _ ->
        {:error, :invalid}
    end
  end

  @doc """
  Sends a review verification email if the email has purchased the product.

  Returns:
  - `{:ok, :sent}` if email was sent
  - `{:error, :no_purchase}` if email hasn't purchased the product
  - `{:error, :already_reviewed}` if they've already reviewed
  - `{:error, reason}` if email sending failed
  """
  def request_review_verification(email, product_id, product_title) do
    email = normalise_email(email)

    cond do
      get_review_by_email_and_product(email, product_id) ->
        {:error, :already_reviewed}

      not can_review?(email, product_id) ->
        {:error, :no_purchase}

      true ->
        token = generate_review_token(email, product_id)
        link = BerrypodWeb.Endpoint.url() <> "/reviews/new?token=#{token}"

        case Berrypod.Reviews.ReviewNotifier.deliver_review_verification(
               email,
               product_title,
               link
             ) do
          {:ok, _} -> {:ok, :sent}
          {:error, reason} -> {:error, reason}
        end
    end
  end

  # ── Rating cache ──────────────────────────────────────────────────

  @doc """
  Updates the cached rating_avg and rating_count on a product.

  Called after review approval, rejection, or deletion to keep the
  denormalized values in sync. Uses approved reviews only.
  """
  def update_product_rating_cache(product_id) do
    {avg, count} = average_rating_for_product(product_id)

    Repo.update_all(
      from(p in Berrypod.Products.Product, where: p.id == ^product_id),
      set: [rating_avg: avg, rating_count: count]
    )

    :ok
  end

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

  defp normalise_email(email) when is_binary(email) do
    email |> String.trim() |> String.downcase()
  end

  defp normalise_email(_), do: ""
end
