defmodule SimpleshopTheme.Products do
  @moduledoc """
  The Products context.

  Manages products synced from POD providers, including provider connections,
  products, images, and variants.
  """

  import Ecto.Query
  alias SimpleshopTheme.Repo
  alias SimpleshopTheme.Products.{ProviderConnection, Product, ProductImage, ProductVariant}

  # =============================================================================
  # Provider Connections
  # =============================================================================

  @doc """
  Returns the list of provider connections.
  """
  def list_provider_connections do
    Repo.all(ProviderConnection)
  end

  @doc """
  Gets a single provider connection.
  """
  def get_provider_connection(id) do
    Repo.get(ProviderConnection, id)
  end

  @doc """
  Gets a provider connection by type.
  """
  def get_provider_connection_by_type(provider_type) do
    Repo.get_by(ProviderConnection, provider_type: provider_type)
  end

  @doc """
  Creates a provider connection.
  """
  def create_provider_connection(attrs \\ %{}) do
    %ProviderConnection{}
    |> ProviderConnection.changeset(attrs)
    |> Repo.insert()
  end

  @doc """
  Updates a provider connection.
  """
  def update_provider_connection(%ProviderConnection{} = conn, attrs) do
    conn
    |> ProviderConnection.changeset(attrs)
    |> Repo.update()
  end

  @doc """
  Deletes a provider connection.
  """
  def delete_provider_connection(%ProviderConnection{} = conn) do
    Repo.delete(conn)
  end

  @doc """
  Updates the sync status of a provider connection.
  """
  def update_sync_status(%ProviderConnection{} = conn, status, synced_at \\ nil) do
    attrs = %{sync_status: status}
    attrs = if synced_at, do: Map.put(attrs, :last_synced_at, synced_at), else: attrs

    conn
    |> ProviderConnection.sync_changeset(attrs)
    |> Repo.update()
  end

  # =============================================================================
  # Products
  # =============================================================================

  @doc """
  Returns the list of products.

  ## Options

    * `:visible` - filter by visibility (boolean)
    * `:status` - filter by status (string)
    * `:category` - filter by category (string)
    * `:provider_connection_id` - filter by provider connection
    * `:preload` - list of associations to preload

  """
  def list_products(opts \\ []) do
    Product
    |> apply_product_filters(opts)
    |> order_by([p], desc: p.inserted_at)
    |> maybe_preload(opts[:preload])
    |> Repo.all()
  end

  @doc """
  Gets a single product by ID.
  """
  def get_product(id, opts \\ []) do
    Product
    |> maybe_preload(opts[:preload])
    |> Repo.get(id)
  end

  @doc """
  Gets a single product by slug.
  """
  def get_product_by_slug(slug, opts \\ []) do
    Product
    |> maybe_preload(opts[:preload])
    |> Repo.get_by(slug: slug)
  end

  @doc """
  Gets a product by provider connection and provider product ID.
  """
  def get_product_by_provider(provider_connection_id, provider_product_id) do
    Repo.get_by(Product,
      provider_connection_id: provider_connection_id,
      provider_product_id: provider_product_id
    )
  end

  @doc """
  Creates a product.
  """
  def create_product(attrs \\ %{}) do
    %Product{}
    |> Product.changeset(attrs)
    |> Repo.insert()
  end

  @doc """
  Updates a product.
  """
  def update_product(%Product{} = product, attrs) do
    product
    |> Product.changeset(attrs)
    |> Repo.update()
  end

  @doc """
  Deletes a product.
  """
  def delete_product(%Product{} = product) do
    Repo.delete(product)
  end

  @doc """
  Upserts a product from provider data.

  Creates a new product if one doesn't exist for the given provider connection
  and provider product ID. Updates the existing product if checksum differs.

  Returns `{:ok, product, :created | :updated | :unchanged}`.
  """
  def upsert_product(%ProviderConnection{id: conn_id}, attrs) do
    provider_product_id = attrs[:provider_product_id] || attrs["provider_product_id"]
    new_checksum = Product.compute_checksum(attrs[:provider_data] || attrs["provider_data"])
    attrs = Map.put(attrs, :checksum, new_checksum)

    case get_product_by_provider(conn_id, provider_product_id) do
      nil ->
        attrs = Map.put(attrs, :provider_connection_id, conn_id)

        case create_product(attrs) do
          {:ok, product} -> {:ok, product, :created}
          error -> error
        end

      %Product{checksum: ^new_checksum} = product ->
        {:ok, product, :unchanged}

      product ->
        case update_product(product, attrs) do
          {:ok, product} -> {:ok, product, :updated}
          error -> error
        end
    end
  end

  # =============================================================================
  # Product Images
  # =============================================================================

  @doc """
  Creates a product image.
  """
  def create_product_image(attrs \\ %{}) do
    %ProductImage{}
    |> ProductImage.changeset(attrs)
    |> Repo.insert()
  end

  @doc """
  Deletes all images for a product.
  """
  def delete_product_images(%Product{id: product_id}) do
    from(i in ProductImage, where: i.product_id == ^product_id)
    |> Repo.delete_all()
  end

  @doc """
  Syncs product images from a list of image data.

  Deletes existing images and inserts new ones.
  """
  def sync_product_images(%Product{id: product_id} = product, images) when is_list(images) do
    delete_product_images(product)

    images
    |> Enum.with_index()
    |> Enum.map(fn {image_data, index} ->
      attrs =
        image_data
        |> Map.put(:product_id, product_id)
        |> Map.put_new(:position, index)

      create_product_image(attrs)
    end)
  end

  # =============================================================================
  # Product Variants
  # =============================================================================

  @doc """
  Creates a product variant.
  """
  def create_product_variant(attrs \\ %{}) do
    %ProductVariant{}
    |> ProductVariant.changeset(attrs)
    |> Repo.insert()
  end

  @doc """
  Updates a product variant.
  """
  def update_product_variant(%ProductVariant{} = variant, attrs) do
    variant
    |> ProductVariant.changeset(attrs)
    |> Repo.update()
  end

  @doc """
  Deletes all variants for a product.
  """
  def delete_product_variants(%Product{id: product_id}) do
    from(v in ProductVariant, where: v.product_id == ^product_id)
    |> Repo.delete_all()
  end

  @doc """
  Gets a variant by product and provider variant ID.
  """
  def get_variant_by_provider(product_id, provider_variant_id) do
    Repo.get_by(ProductVariant,
      product_id: product_id,
      provider_variant_id: provider_variant_id
    )
  end

  @doc """
  Syncs product variants from a list of variant data.

  Upserts variants based on provider_variant_id.
  """
  def sync_product_variants(%Product{id: product_id}, variants) when is_list(variants) do
    existing_ids =
      from(v in ProductVariant,
        where: v.product_id == ^product_id,
        select: v.provider_variant_id
      )
      |> Repo.all()
      |> MapSet.new()

    incoming_ids =
      variants
      |> Enum.map(&(&1[:provider_variant_id] || &1["provider_variant_id"]))
      |> MapSet.new()

    # Delete variants that are no longer in the incoming list
    removed_ids = MapSet.difference(existing_ids, incoming_ids)

    if MapSet.size(removed_ids) > 0 do
      from(v in ProductVariant,
        where:
          v.product_id == ^product_id and v.provider_variant_id in ^MapSet.to_list(removed_ids)
      )
      |> Repo.delete_all()
    end

    # Upsert incoming variants
    Enum.map(variants, fn variant_data ->
      provider_variant_id =
        variant_data[:provider_variant_id] || variant_data["provider_variant_id"]

      attrs = Map.put(variant_data, :product_id, product_id)

      case get_variant_by_provider(product_id, provider_variant_id) do
        nil ->
          create_product_variant(attrs)

        existing ->
          update_product_variant(existing, attrs)
      end
    end)
  end

  # =============================================================================
  # Private Helpers
  # =============================================================================

  defp apply_product_filters(query, opts) do
    query
    |> filter_by_visible(opts[:visible])
    |> filter_by_status(opts[:status])
    |> filter_by_category(opts[:category])
    |> filter_by_provider_connection(opts[:provider_connection_id])
  end

  defp filter_by_visible(query, nil), do: query
  defp filter_by_visible(query, visible), do: where(query, [p], p.visible == ^visible)

  defp filter_by_status(query, nil), do: query
  defp filter_by_status(query, status), do: where(query, [p], p.status == ^status)

  defp filter_by_category(query, nil), do: query
  defp filter_by_category(query, category), do: where(query, [p], p.category == ^category)

  defp filter_by_provider_connection(query, nil), do: query

  defp filter_by_provider_connection(query, conn_id),
    do: where(query, [p], p.provider_connection_id == ^conn_id)

  defp maybe_preload(query, nil), do: query
  defp maybe_preload(query, preloads), do: preload(query, ^preloads)
end
