# frozen_string_literal: true

require "active_support/concern"

module Togglefy
  # The Assignable module provides functionality for models to relate with features.
  # It includes methods to add, remove, and query features, as well as ActiveRecord scopes.
  module Assignable
    extend ActiveSupport::Concern

    included do
      # Establishes a many-to-many relationship with features through feature assignments.
      has_many :feature_assignments, as: :assignable, class_name: "Togglefy::FeatureAssignment"
      has_many :features, through: :feature_assignments, class_name: "Togglefy::Feature"

      # Scope to retrieve assignables with specific features.
      #
      # @param feature_ids [Array<Integer>] The IDs of the features to filter by.
      scope :with_features, lambda { |feature_ids|
        joins(:feature_assignments)
          .where(feature_assignments: {
                   feature_id: feature_ids
                 })
          .distinct
      }

      # Scope to retrieve assignables without specific features.
      #
      # @param feature_ids [Array<Integer>] The IDs of the features to filter by.
      scope :without_features, lambda { |feature_ids|
        joins(left_join_on_features(feature_ids))
          .where("fa.id IS NULL")
          .distinct
      }
    end

    # Checks if the assignable has a specific feature.
    #
    # @param identifier [Symbol, String] The identifier of the feature.
    # @return [Boolean] True if the feature exists, false otherwise.
    def feature?(identifier)
      features.active.exists?(identifier: identifier.to_s)
    end
    alias has_feature? feature?

    # Adds a feature to the assignable.
    #
    # @param feature [Togglefy::Feature, String] The feature or its identifier.
    def add_feature(feature)
      feature = find_feature!(feature)
      features << feature unless has_feature?(feature.identifier)
    end

    # Removes a feature from the assignable.
    #
    # @param feature [Togglefy::Feature, String] The feature or its identifier.
    def remove_feature(feature)
      feature = find_feature!(feature)
      features.destroy(feature) if has_feature?(feature.identifier)
    end

    # Clears all features from the assignable.
    def clear_features
      features.destroy_all
    end

    private

    # Finds a feature by its identifier or returns the feature if already provided.
    #
    # @param feature [Togglefy::Feature, String] The feature or its identifier.
    # @return [Togglefy::Feature] The found feature.
    def find_feature!(feature)
      return feature if feature.is_a?(Togglefy::Feature)

      Togglefy::Feature.find_by!(identifier: feature.to_s)
    end

    class_methods do
      # Generates a SQL LEFT JOIN clause for features.
      #
      # @param feature_ids [Array<Integer>] The IDs of the features to join on.
      # @return [String] The SQL LEFT JOIN clause.
      def left_join_on_features(feature_ids)
        table = table_name
        type = name

        <<~SQL.squish
          LEFT JOIN togglefy_feature_assignments fa
            ON fa.assignable_id = #{table}.id
            AND fa.assignable_type = '#{type}'
            AND fa.feature_id IN (#{Array(feature_ids).join(",")})
        SQL
      end
    end
  end
end