defmodule Berrypod.Products.Product do
  @moduledoc """
  Schema for products synced from POD providers.

  Products are uniquely identified by the combination of
  provider_connection_id and provider_product_id.
  """

  use Ecto.Schema
  import Ecto.Changeset

  @primary_key {:id, :binary_id, autogenerate: true}
  @foreign_key_type :binary_id

  @statuses ~w(active draft archived discontinued)

  schema "products" do
    field :provider_product_id, :string
    field :title, :string
    field :description, :string
    field :slug, :string
    field :status, :string, default: "active"
    field :visible, :boolean, default: true
    field :category, :string
    field :provider_data, :map, default: %{}
    field :checksum, :string

    # Denormalized from variants — recomputed by Products.recompute_cached_fields/1
    field :cheapest_price, :integer, default: 0
    field :compare_at_price, :integer
    field :in_stock, :boolean, default: true
    field :on_sale, :boolean, default: false

    # Denormalized from reviews — recomputed by Reviews.update_product_rating_cache/1
    field :rating_avg, :decimal
    field :rating_count, :integer, default: 0

    belongs_to :provider_connection, Berrypod.Products.ProviderConnection
    has_many :images, Berrypod.Products.ProductImage
    has_many :variants, Berrypod.Products.ProductVariant

    timestamps(type: :utc_datetime)
  end

  @doc """
  Returns the list of valid product statuses.
  """
  def statuses, do: @statuses

  @doc """
  Returns true if the product is discontinued (no longer available from provider).
  """
  def discontinued?(%__MODULE__{status: "discontinued"}), do: true
  def discontinued?(_), do: false

  @doc """
  Returns true if the product can be purchased.
  """
  def purchasable?(%__MODULE__{status: status, visible: visible}) do
    status == "active" and visible == true
  end

  def purchasable?(_), do: false

  @doc """
  Changeset for creating or updating a product.
  """
  def changeset(product, attrs) do
    product
    |> cast(attrs, [
      :provider_connection_id,
      :provider_product_id,
      :title,
      :description,
      :slug,
      :status,
      :visible,
      :category,
      :provider_data,
      :checksum
    ])
    |> generate_slug_if_missing()
    |> validate_required([:provider_connection_id, :provider_product_id, :title, :slug])
    |> validate_inclusion(:status, @statuses)
    |> unique_constraint(:slug)
    |> unique_constraint([:provider_connection_id, :provider_product_id])
  end

  @doc """
  Changeset for admin storefront controls (visibility and category only).
  """
  def storefront_changeset(product, attrs) do
    product
    |> cast(attrs, [:visible, :category])
  end

  @doc """
  Changeset for recomputing denormalized fields from variants.
  """
  def recompute_changeset(product, attrs) do
    product
    |> cast(attrs, [:cheapest_price, :compare_at_price, :in_stock, :on_sale])
  end

  # ---------------------------------------------------------------------------
  # Display helpers
  # ---------------------------------------------------------------------------

  @doc """
  Returns the primary (first by position) image, or nil.
  Works with preloaded images association or plain maps.
  """
  def primary_image(%{images: images}) when is_list(images) do
    Enum.min_by(images, & &1.position, fn -> nil end)
  end

  def primary_image(_), do: nil

  @doc """
  Returns the second image by position (hover image), or nil.
  """
  def hover_image(%{images: images}) when is_list(images) and length(images) >= 2 do
    images
    |> Enum.sort_by(& &1.position)
    |> Enum.at(1)
  end

  def hover_image(_), do: nil

  @doc """
  Extracts option types from provider_data.
  Returns a list of %{name: "Size", type: :size, values: [%{title: "S"}, ...]}.
  Color options include :hex from the provider's color data.
  """
  def option_types(%{provider_data: %{"options" => options}}) when is_list(options) do
    Enum.map(options, fn opt ->
      type = option_type_atom(opt["type"])

      values =
        Enum.map(opt["values"] || [], fn val ->
          base = %{title: val["title"]}

          case val["colors"] do
            [hex | _] -> Map.put(base, :hex, hex)
            _ -> base
          end
        end)

      %{name: singularize_option_name(opt["name"]), type: type, values: values}
    end)
  end

  def option_types(%{option_types: option_types}) when is_list(option_types), do: option_types
  def option_types(_), do: []

  defp option_type_atom("color"), do: :color
  defp option_type_atom(_), do: :size

  # Printify sends plural names ("Colors", "Sizes") but variant options
  # use singular — keep them consistent so gallery filtering works.
  defp singularize_option_name("Colors"), do: "Color"
  defp singularize_option_name("Sizes"), do: "Size"
  defp singularize_option_name(name), do: name

  @doc """
  Generates a checksum from provider data for detecting changes.
  """
  def compute_checksum(provider_data) when is_map(provider_data) do
    provider_data
    |> Jason.encode!()
    |> then(&:crypto.hash(:sha256, &1))
    |> Base.encode16(case: :lower)
    |> binary_part(0, 16)
  end

  def compute_checksum(_), do: nil

  defp generate_slug_if_missing(changeset) do
    slug_change = get_change(changeset, :slug)
    title_change = get_change(changeset, :title)
    current_slug = get_field(changeset, :slug)

    cond do
      # Explicit slug provided — use it as-is
      slug_change != nil ->
        changeset

      # Title changed — regenerate slug to match
      title_change != nil ->
        put_change(changeset, :slug, Slug.slugify(title_change))

      # No slug yet — generate from title
      current_slug == nil ->
        title = get_field(changeset, :title)

        if title do
          put_change(changeset, :slug, Slug.slugify(title))
        else
          changeset
        end

      # Slug exists and title didn't change — keep it
      true ->
        changeset
    end
  end
end

defmodule Slug do
  @moduledoc false

  def slugify(nil), do: nil

  def slugify(string) when is_binary(string) do
    string
    |> String.downcase()
    |> String.replace(~r/[^\w\s-]/, "")
    |> String.replace(~r/\s+/, "-")
    |> String.replace(~r/-+/, "-")
    |> String.trim("-")
  end
end
