lib/ex_catalog.ex

defmodule ExCatalog do
  @moduledoc """
  Documentation for `ExCatalog`.
  """
  @repo ExCatalog.Config.repo()

  alias ExCatalog.Category
  alias ExCatalog.Product
  alias ExCatalog.Manufacturer

  @doc """
  List version.
  """
  @version Mix.Project.config()[:version]
  def version, do: @version

  @doc """
  List all the categories (the index).

  ## Examples

      iex> ExCatalog.index(25)


  """
  def index(limit \\ 25, metadata \\ nil, cursor \\ nil, deleted \\ false) do
    import Ecto.Query
    import Ecto.SoftDelete.Query

    query =
      case(deleted) do
        false ->
          from(ExCatalog.Category,
            preload: [:parent_category],
            preload: [:image]
          )

        true ->
          from(ExCatalog.Category,
            preload: [:parent_category],
            preload: [:image]
          )
          |> with_undeleted
      end

    case cursor do
      :before ->
        @repo.paginate(
          query,
          before: metadata.before,
          include_total_count: true,
          cursor_fields: [:inserted_at, :id],
          limit: limit
        )

      :after ->
        @repo.paginate(
          query,
          after: metadata.after,
          include_total_count: true,
          cursor_fields: [:inserted_at, :id],
          limit: limit
        )

      _ ->
        @repo.paginate(
          query,
          include_total_count: true,
          cursor_fields: [:inserted_at, :id],
          limit: limit
        )
    end
  end

  @doc """
  List all products with preloads and optional currency conversion..

  ## Examples

      iex> ExCatalog.products(25)
      iex> ExCatalog.products(25,:USD)


  """
  def products(limit \\ 25, currency \\ :USD, deleted \\ false, owner_id \\ nil) do
    products(limit, nil, nil, currency, deleted, owner_id)
  end

  def products(limit \\ 25, metadata, cursor, currency, deleted, owner_id) do
    import Ecto.Query
    import Ecto.SoftDelete.Query

    query =
      case(owner_id) do
        nil ->
          case(deleted) do
            false ->
              from(ExCatalog.Product,
                preload: [:variations],
                preload: [:categories],
                preload: [:metas],
                preload: [:primary_image],
                preload: [:images],
                preload: [:manufacturer],
                preload: [:videos]
              )

            true ->
              from(ExCatalog.Product,
                preload: [:variations],
                preload: [:categories],
                preload: [:metas],
                preload: [:primary_image],
                preload: [:images],
                preload: [:manufacturer],
                preload: [:videos]
              )
              |> with_undeleted
          end

        _ ->
          case(deleted) do
            false ->
              from(ExCatalog.Product,
                where: [owner_id: ^owner_id],
                preload: [:variations],
                preload: [:categories],
                preload: [:metas],
                preload: [:primary_image],
                preload: [:manufacturer],
                preload: [:images],
                preload: [:videos]
              )

            true ->
              from(ExCatalog.Product,
                where: [owner_id: ^owner_id],
                preload: [:variations],
                preload: [:categories],
                preload: [:metas],
                preload: [:primary_image],
                preload: [:manufacturer],
                preload: [:images],
                preload: [:videos]
              )
              |> with_undeleted
          end
      end

    reply =
      case cursor do
        :before ->
          @repo.paginate(
            query,
            before: metadata.before,
            include_total_count: true,
            cursor_fields: [:inserted_at, :id],
            limit: limit
          )

        :after ->
          @repo.paginate(
            query,
            after: metadata.after,
            include_total_count: true,
            cursor_fields: [:inserted_at, :id],
            limit: limit
          )

        _ ->
          @repo.paginate(
            query,
            include_total_count: true,
            cursor_fields: [:inserted_at, :id],
            limit: limit
          )
      end

    case currency do
      nil ->
        reply

      _ ->
        modified =
          Enum.map(reply.entries, fn x ->
            {:ok, price} = ExCatalog.Currencies.convert(x.price, currency)
            %{x | price: price}
          end)

        {modified, reply.metadata}
    end
  end

  @doc """
  List product with preloads by sku and optional currency conversion.

  ## Examples

      iex> price = Money.new(:USD, 100)
      iex>  product = %{sku: "12345", price: price, title: "test product", sub_title: "test product", description: "test product"}
      iex> ExCatalog.product(product.sku)


  """
  def product(sku, currency \\ nil, deleted \\ false) do
    import Ecto.Query
    import Ecto.SoftDelete.Query

    query =
      case(deleted) do
        false ->
          from(ExCatalog.Product,
            where: [sku: ^sku],
            preload: [:variations],
            preload: [:categories],
            preload: [:metas],
            preload: [:primary_image],
            preload: [:manufacturer],
            preload: [:images],
            preload: [:videos]
          )

        true ->
          from(ExCatalog.Product,
            where: [sku: ^sku],
            preload: [:variations],
            preload: [:categories],
            preload: [:metas],
            preload: [:primary_image],
            preload: [:manufacturer],
            preload: [:images],
            preload: [:videos]
          )
          |> with_undeleted
      end

    reply = @repo.all(query)

    case List.first(reply) do
      nil ->
        reply

      record ->
        case currency do
          nil ->
            record

          currency ->
            {:ok, price} = ExCatalog.Currencies.convert(record.price, currency)
            %{record | price: price}
        end
    end
  end

  @doc """
  List product with preloads by category and optional currency conversion.

  ## Examples
      iex> ExCatalog.products_by_category("test_category", 1, :USD, false, order_by: :sku)


  """
  def products_by_category(slug, limit \\ 25, currency \\ :USD, deleted \\ false, opts \\ []) do
    products_by_category(slug, limit, nil, nil, currency, deleted, opts)
  end

  def products_by_category(slug, limit, metadata, cursor, currency, deleted, opts) do
    category = @repo.get_by(Category, slug: slug)

    case category do
      nil ->
        {:error, "Invalid Category"}

      _ ->
        import Ecto.Query
        import Ecto.SoftDelete.Query

        order = Keyword.get(opts, :order) || :desc
        by = Keyword.get(opts, :order_by) || :updated_at
        order_by = [{order, by}]
        origins = Keyword.get(opts, :origins) || []

        query =
          case(deleted) do
            false ->
              from(p in ExCatalog.Product,
                where: p.category_id == ^category.id and p.origin not in ^origins,
                order_by: ^order_by,
                preload: [:variations],
                preload: [:categories],
                preload: [:metas],
                preload: [:primary_image],
                preload: [:manufacturer],
                preload: [:images],
                preload: [:videos]
              )

            true ->
              from(p in ExCatalog.Product,
                where: p.category_id == ^category.id and p.origin not in ^origins,
                preload: [:variations],
                preload: [:categories],
                preload: [:metas],
                preload: [:primary_image],
                preload: [:manufacturer],
                preload: [:images],
                preload: [:videos]
              )
              |> with_undeleted
          end

        reply =
          case cursor do
            :before ->
              @repo.paginate(
                query,
                before: metadata.before,
                include_total_count: true,
                cursor_fields: [:inserted_at, :id],
                limit: limit
              )

            :after ->
              @repo.paginate(
                query,
                after: metadata.after,
                include_total_count: true,
                cursor_fields: [:inserted_at, :id],
                limit: limit
              )

            _ ->
              @repo.paginate(
                query,
                include_total_count: true,
                cursor_fields: [:inserted_at, :id],
                limit: limit
              )
          end

        case currency do
          nil ->
            reply

          _ ->
            modified =
              Enum.map(reply.entries, fn x ->
                {:ok, price} = ExCatalog.Currencies.convert(x.price, currency)
                %{x | price: price}
              end)

            {modified, reply.metadata}
        end
    end
  end

  ###
  @doc """
  List product with preloads by country and optional currency conversion.

  ## Examples

      iex> ExCatalog.products_by_country(["US"], 2, :USD, false, order_by: :sku)


  """
  def products_by_country(origin, limit \\ 25, currency \\ :USD, deleted \\ false, opts \\ [])
      when is_list(origin) do
    products_by_country(origin, limit, nil, nil, currency, deleted, opts)
  end

  def products_by_country(origin, limit, metadata, cursor, currency, deleted, opts) do
    import Ecto.Query
    import Ecto.SoftDelete.Query

    order = Keyword.get(opts, :order) || :desc
    by = Keyword.get(opts, :order_by) || :updated_at
    order_by = [{order, by}]

    query =
      case(deleted) do
        false ->
          from(p in ExCatalog.Product,
            where: p.origin not in ^origin,
            order_by: ^order_by,
            preload: [:variations],
            preload: [:categories],
            preload: [:metas],
            preload: [:primary_image],
            preload: [:manufacturer],
            preload: [:images],
            preload: [:videos]
          )

        true ->
          from(p in ExCatalog.Product,
            where: p.origin not in ^origin,
            preload: [:variations],
            preload: [:categories],
            preload: [:metas],
            preload: [:primary_image],
            preload: [:manufacturer],
            preload: [:images],
            preload: [:videos]
          )
          |> with_undeleted
      end

    reply =
      case cursor do
        :before ->
          @repo.paginate(
            query,
            before: metadata.before,
            include_total_count: true,
            cursor_fields: [:inserted_at, :id],
            limit: limit
          )

        :after ->
          @repo.paginate(
            query,
            after: metadata.after,
            include_total_count: true,
            cursor_fields: [:inserted_at, :id],
            limit: limit
          )

        _ ->
          @repo.paginate(
            query,
            include_total_count: true,
            cursor_fields: [:inserted_at, :id],
            limit: limit
          )
      end

    case currency do
      nil ->
        reply

      _ ->
        modified =
          Enum.map(reply.entries, fn x ->
            {:ok, price} = ExCatalog.Currencies.convert(x.price, currency)
            %{x | price: price}
          end)

        {modified, reply.metadata}
    end
  end

  @doc """
  List product with preloads by manufacturer and optional currency conversion.

  ## Examples

      iex> ExCatalog.products_by_manufacturer("test_manufacturer", 2, :USD, false, order_by: :sku)


  """

  def products_by_manufacturer(slug, limit \\ 25, currency \\ :USD, deleted \\ false, opts \\ []) do
    products_by_manufacturer(slug, limit, nil, nil, currency, deleted, opts)
  end

  def products_by_manufacturer(slug, limit, metadata, cursor, currency, deleted, opts) do
    manufacturer = @repo.get_by(Manufacturer, slug: slug)

    case manufacturer do
      nil ->
        {:error, "Invalid Manufacturer"}

      _ ->
        import Ecto.Query
        import Ecto.SoftDelete.Query

        order = Keyword.get(opts, :order) || :desc
        by = Keyword.get(opts, :order_by) || :updated_at
        order_by = [{order, by}]

        query =
          case(deleted) do
            false ->
              from(ExCatalog.Product,
                where: [manufacturer: ^manufacturer],
                order_by: ^order_by,
                preload: [:variations],
                preload: [:categories],
                preload: [:metas],
                preload: [:primary_image],
                preload: [:manufacturer],
                preload: [:images],
                preload: [:videos]
              )

            true ->
              from(ExCatalog.Product,
                where: [manufacturer: ^manufacturer],
                preload: [:variations],
                preload: [:categories],
                preload: [:metas],
                preload: [:primary_image],
                preload: [:manufacturer],
                preload: [:images],
                preload: [:videos]
              )
              |> with_undeleted
          end

        reply =
          case cursor do
            :before ->
              @repo.paginate(
                query,
                before: metadata.before,
                include_total_count: true,
                cursor_fields: [:inserted_at, :id],
                limit: limit
              )

            :after ->
              @repo.paginate(
                query,
                after: metadata.after,
                include_total_count: true,
                cursor_fields: [:inserted_at, :id],
                limit: limit
              )

            _ ->
              @repo.paginate(
                query,
                include_total_count: true,
                cursor_fields: [:inserted_at, :id],
                limit: limit
              )
          end

        case currency do
          nil ->
            reply

          _ ->
            modified =
              Enum.map(reply.entries, fn x ->
                {:ok, price} = ExCatalog.Currencies.convert(x.price, currency)
                %{x | price: price}
              end)

            {modified, reply.metadata}
        end
    end
  end

  ###
  @doc """
  Change the status active or disabled, this controls which products the user can see, (by default only the active products are displayed)

  ## Examples

      iex>  id = Ecto.UUID.generate
      iex> ExCatalog.active(:category, id)
      iex> ExCatalog.active(:product, id)


  """
  def active(type, id) do
    import Ecto.Query
    import Ecto.SoftDelete.Query

    query =
      case type do
        :category ->
          from(c in Category, where: c.id == ^id, select: c)
          |> with_undeleted

        _ ->
          from(p in Product, where: p.id == ^id, select: p)
          |> with_undeleted
      end

    case Keyword.has_key?(@repo.__info__(:functions), :soft_restore!) do
      true ->
        try do
          @repo.one!(query)
          |> @repo.soft_restore!()
        rescue
          _ -> {:error, "Not Available"}
        end

      false ->
        {:error, "Not Available"}
    end
  end

  def active(type, id, false) do
    import Ecto.Query

    case type do
      :category ->
        @repo.get_by!(Category, id: id)

        from(c in Category, where: c.id == ^id, select: c)
        |> @repo.soft_delete!()

      _ ->
        from(p in Product, where: p.id == ^id, select: p)
        |> @repo.soft_delete!()
    end
  end
end