lib/selecto/subfilter/registry.ex

defmodule Selecto.Subfilter.Registry do
  @moduledoc """
  Registry system for managing multiple subfilters with strategy selection and optimization.

  The Registry handles:
  - Multiple subfilter registration and management
  - Strategy selection (EXISTS, IN, ANY, ALL) based on query patterns
  - Performance optimization through join analysis
  - Conflict detection and resolution
  - SQL generation coordination

  ## Examples

      iex> registry = Selecto.Subfilter.Registry.new(:film_domain)
      iex> registry = Registry.add_subfilter(registry, "film.rating", "R")
      iex> registry = Registry.add_subfilter(registry, "film.category.name", "Action")
      iex> Registry.generate_sql(registry, base_query)
      {:ok, optimized_query_with_subfilters}
  """

  # alias Selecto.Subfilter
  alias Selecto.Subfilter.{Spec, Parser, JoinPathResolver, SQL, Error}

  # Registry structure to manage multiple subfilters
  defstruct [
    # Domain configuration to use
    :domain_name,
    # Base table for the query
    :base_table,
    # Map of subfilter_id => Spec
    :subfilters,
    # Map of subfilter_id => JoinResolution
    :join_resolutions,
    # Manual strategy overrides
    :strategy_overrides,
    # Performance optimization hints
    :optimization_hints,
    # Compound operations (AND/OR between subfilters)
    :compound_ops
  ]

  @type t :: %__MODULE__{
          domain_name: atom() | map(),
          base_table: atom() | nil,
          subfilters: %{String.t() => Spec.t()},
          join_resolutions: %{String.t() => JoinPathResolver.JoinResolution.t()},
          strategy_overrides: %{String.t() => atom()},
          optimization_hints: keyword(),
          compound_ops: [compound_operation()]
        }

  @type compound_operation :: %{
          type: :and | :or,
          subfilter_ids: [String.t()]
        }

  @doc """
  Create a new subfilter registry for the specified domain.

  ## Parameters

  - `domain_name` - Domain configuration to use (e.g., :film_domain)
  - `opts` - Options including :base_table, :optimization_hints
  """
  @spec new(atom() | map(), keyword()) :: t()
  def new(domain_name, opts \\ []) do
    %__MODULE__{
      domain_name: domain_name,
      base_table: Keyword.get(opts, :base_table),
      subfilters: %{},
      join_resolutions: %{},
      strategy_overrides: %{},
      optimization_hints: Keyword.get(opts, :optimization_hints, []),
      compound_ops: []
    }
  end

  @doc """
  Add a subfilter to the registry.

  ## Examples

      add_subfilter(registry, "film.rating", "R")
      add_subfilter(registry, "film.category.name", ["Action", "Drama"], strategy: :in)
      add_subfilter(registry, "film", {:count, ">", 5}, id: "film_count_filter")
  """
  @spec add_subfilter(t(), String.t(), any(), keyword()) ::
          {:ok, t()} | {:error, Error.t()}
  def add_subfilter(%__MODULE__{} = registry, relationship_path, filter_spec, opts \\ []) do
    with {:ok, parsed_spec} <- Parser.parse(relationship_path, filter_spec, opts),
         {:ok, resolved_joins} <-
           JoinPathResolver.resolve(
             parsed_spec.relationship_path,
             registry.domain_name,
             registry.base_table
           ) do
      subfilter_id = Keyword.get(opts, :id, parsed_spec.id)

      # Check for conflicts
      case check_for_conflicts(registry, subfilter_id, parsed_spec) do
        :ok ->
          # Update the spec with the final ID
          final_spec = %{parsed_spec | id: subfilter_id}

          updated_registry = %{
            registry
            | subfilters: Map.put(registry.subfilters, subfilter_id, final_spec),
              join_resolutions: Map.put(registry.join_resolutions, subfilter_id, resolved_joins)
          }

          # Apply strategy optimization
          optimized_registry = optimize_strategies(updated_registry)

          {:ok, optimized_registry}

        {:error, reason} ->
          {:error, reason}
      end
    else
      {:error, reason} -> {:error, reason}
    end
  end

  @doc """
  Add compound subfilter operations (AND/OR).

  ## Examples

      add_compound(registry, :and, [
        {"film.rating", "R"},
        {"film.release_year", {">", 2000}}
      ])
  """
  @spec add_compound(
          t(),
          :and | :or,
          [{String.t(), any()}] | [{String.t(), any(), keyword()}],
          keyword()
        ) ::
          {:ok, t()} | {:error, Error.t()}
  def add_compound(%__MODULE__{} = registry, compound_type, subfilter_specs, opts \\ []) do
    with {:ok, compound_spec} <- Parser.parse_compound(compound_type, subfilter_specs, opts) do
      # Add each individual subfilter first
      case add_compound_subfilters(registry, compound_spec.subfilters) do
        {:ok, updated_registry, subfilter_ids} ->
          compound_op = %{
            type: compound_type,
            subfilter_ids: subfilter_ids
          }

          final_registry = %{
            updated_registry
            | compound_ops: [compound_op | updated_registry.compound_ops]
          }

          {:ok, final_registry}

        {:error, reason} ->
          {:error, reason}
      end
    else
      {:error, reason} -> {:error, reason}
    end
  end

  @doc """
  Remove a subfilter from the registry.
  """
  @spec remove_subfilter(t(), String.t()) :: t()
  def remove_subfilter(%__MODULE__{} = registry, subfilter_id) do
    %{
      registry
      | subfilters: Map.delete(registry.subfilters, subfilter_id),
        join_resolutions: Map.delete(registry.join_resolutions, subfilter_id),
        strategy_overrides: Map.delete(registry.strategy_overrides, subfilter_id),
        compound_ops: remove_from_compound_ops(registry.compound_ops, subfilter_id)
    }
  end

  @doc """
  Override the strategy for a specific subfilter.

  Useful for performance tuning when the automatic strategy selection
  doesn't produce optimal results.
  """
  @spec override_strategy(t(), String.t(), atom()) :: {:ok, t()} | {:error, Error.t()}
  def override_strategy(%__MODULE__{} = registry, subfilter_id, strategy)
      when strategy in [:exists, :in, :any, :all] do
    case Map.has_key?(registry.subfilters, subfilter_id) do
      true ->
        updated_registry = %{
          registry
          | strategy_overrides: Map.put(registry.strategy_overrides, subfilter_id, strategy)
        }

        {:ok, updated_registry}

      false ->
        {:error,
         %Error{
           type: :subfilter_not_found,
           message: "Subfilter not found in registry",
           details: %{subfilter_id: subfilter_id, available_ids: Map.keys(registry.subfilters)}
         }}
    end
  end

  def override_strategy(_registry, _subfilter_id, invalid_strategy) do
    {:error,
     %Error{
       type: :invalid_strategy,
       message: "Invalid strategy for override",
       details: %{strategy: invalid_strategy, valid_strategies: [:exists, :in, :any, :all]}
     }}
  end

  @doc """
  Get comprehensive analysis of all subfilters in the registry.

  Returns information about join patterns, strategy selections,
  performance implications, and optimization opportunities.
  """
  @spec analyze(t()) :: %{
          subfilter_count: non_neg_integer(),
          join_complexity: atom(),
          strategy_distribution: %{atom() => non_neg_integer()},
          performance_score: float(),
          optimization_suggestions: [String.t()]
        }
  def analyze(%__MODULE__{} = registry) do
    subfilter_count = map_size(registry.subfilters)

    %{
      subfilter_count: subfilter_count,
      join_complexity: assess_join_complexity(registry),
      strategy_distribution: calculate_strategy_distribution(registry),
      performance_score: calculate_performance_score(registry),
      optimization_suggestions: generate_optimization_suggestions(registry)
    }
  end

  @doc """
  Generate SQL for all subfilters in the registry.

  This coordinates with the SQL generation system to produce optimized
  subquery SQL that integrates with the main query.
  """
  @spec generate_sql(t(), String.t()) :: {:ok, String.t(), [any()]} | {:error, Error.t()}
  def generate_sql(%__MODULE__{} = registry, base_query) when is_binary(base_query) do
    if map_size(registry.subfilters) == 0 do
      {:ok, base_query, []}
    else
      case SQL.generate(registry) do
        {:ok, where_clause, params} ->
          {:ok, merge_where_clause(base_query, where_clause), params}

        {:error, reason} ->
          {:error, reason}
      end
    end
  end

  # Private implementation functions

  defp check_for_conflicts(%__MODULE__{} = registry, subfilter_id, _spec) do
    case Map.has_key?(registry.subfilters, subfilter_id) do
      true ->
        {:error,
         %Error{
           type: :duplicate_subfilter_id,
           message: "Subfilter ID already exists in registry",
           details: %{subfilter_id: subfilter_id}
         }}

      false ->
        :ok
    end
  end

  defp optimize_strategies(%__MODULE__{} = registry) do
    # Apply automatic strategy optimization based on subfilter patterns
    # This would analyze join complexity, filter selectivity, etc.
    # For now, return as-is
    registry
  end

  defp add_compound_subfilters(registry, subfilters) do
    prepared_subfilters =
      Enum.map(subfilters, fn spec ->
        subfilter_id = generate_compound_subfilter_id(spec)
        final_spec = %{spec | id: subfilter_id}
        {subfilter_id, final_spec}
      end)

    subfilter_ids = Enum.map(prepared_subfilters, fn {subfilter_id, _spec} -> subfilter_id end)

    with :ok <- ensure_unique_subfilter_ids(subfilter_ids),
         :ok <- ensure_registry_conflicts(registry, prepared_subfilters),
         {:ok, resolutions} <- resolve_compound_paths(prepared_subfilters, registry) do
      updated_registry =
        Enum.zip(prepared_subfilters, resolutions)
        |> Enum.reduce(registry, fn {{subfilter_id, spec}, resolved_joins}, acc ->
          %{
            acc
            | subfilters: Map.put(acc.subfilters, subfilter_id, spec),
              join_resolutions: Map.put(acc.join_resolutions, subfilter_id, resolved_joins)
          }
        end)

      {:ok, optimize_strategies(updated_registry), subfilter_ids}
    else
      {:error, reason} ->
        {:error, reason}
    end
  end

  defp ensure_unique_subfilter_ids(subfilter_ids) do
    duplicate_id =
      subfilter_ids
      |> Enum.frequencies()
      |> Enum.find_value(fn
        {subfilter_id, count} when count > 1 -> subfilter_id
        _ -> nil
      end)

    case duplicate_id do
      nil ->
        :ok

      subfilter_id ->
        {:error,
         %Error{
           type: :duplicate_subfilter_id,
           message: "Subfilter ID already exists in registry",
           details: %{subfilter_id: subfilter_id}
         }}
    end
  end

  defp ensure_registry_conflicts(registry, prepared_subfilters) do
    Enum.reduce_while(prepared_subfilters, :ok, fn {subfilter_id, spec}, _acc ->
      case check_for_conflicts(registry, subfilter_id, spec) do
        :ok -> {:cont, :ok}
        {:error, reason} -> {:halt, {:error, reason}}
      end
    end)
  end

  defp resolve_compound_paths(prepared_subfilters, registry) do
    relationship_paths =
      Enum.map(prepared_subfilters, fn {_subfilter_id, spec} -> spec.relationship_path end)

    JoinPathResolver.resolve_multiple(
      relationship_paths,
      registry.domain_name,
      registry.base_table
    )
  end

  defp merge_where_clause(base_query, where_clause) do
    trimmed_where = String.replace_prefix(where_clause, "WHERE ", "")

    if Regex.match?(~r/\bwhere\b/i, base_query) do
      base_query <> " AND (" <> trimmed_where <> ")"
    else
      base_query <> " " <> where_clause
    end
  end

  defp generate_compound_subfilter_id(spec) do
    path_segments = spec.relationship_path.path_segments
    path_str = Enum.join(path_segments, "_")
    field = spec.relationship_path.target_field || "agg"

    "compound_#{path_str}_#{field}"
  end

  defp remove_from_compound_ops(compound_ops, subfilter_id) do
    Enum.map(compound_ops, fn op ->
      %{op | subfilter_ids: List.delete(op.subfilter_ids, subfilter_id)}
    end)
    |> Enum.reject(fn op -> Enum.empty?(op.subfilter_ids) end)
  end

  defp assess_join_complexity(%__MODULE__{join_resolutions: join_resolutions}) do
    total_joins =
      join_resolutions
      |> Map.values()
      |> Enum.map(fn resolution -> length(resolution.joins) end)
      |> Enum.sum()

    cond do
      total_joins == 0 -> :none
      total_joins <= 3 -> :low
      total_joins <= 8 -> :medium
      total_joins <= 15 -> :high
      true -> :very_high
    end
  end

  defp calculate_strategy_distribution(%__MODULE__{
         subfilters: subfilters,
         strategy_overrides: overrides
       }) do
    subfilters
    |> Map.keys()
    |> Enum.reduce(%{}, fn subfilter_id, acc ->
      strategy = Map.get(overrides, subfilter_id, Map.get(subfilters, subfilter_id).strategy)
      Map.update(acc, strategy, 1, &(&1 + 1))
    end)
  end

  defp calculate_performance_score(%__MODULE__{} = registry) do
    # Simple scoring based on join complexity and subfilter count
    complexity_penalty =
      case assess_join_complexity(registry) do
        :none -> 0.0
        :low -> 0.1
        :medium -> 0.3
        :high -> 0.5
        :very_high -> 0.7
      end

    subfilter_count_penalty = map_size(registry.subfilters) * 0.05

    max(0.0, 1.0 - complexity_penalty - subfilter_count_penalty)
  end

  defp generate_optimization_suggestions(%__MODULE__{} = registry) do
    suggestions = []

    # Check for high join complexity
    suggestions =
      case assess_join_complexity(registry) do
        complexity when complexity in [:high, :very_high] ->
          [
            "Consider reducing join complexity by using IN strategy for some subfilters"
            | suggestions
          ]

        _ ->
          suggestions
      end

    # Check for too many EXISTS subfilters
    strategy_dist = calculate_strategy_distribution(registry)
    exists_count = Map.get(strategy_dist, :exists, 0)

    suggestions =
      if exists_count > 3 do
        [
          "Consider using IN strategy for some EXISTS subfilters to improve performance"
          | suggestions
        ]
      else
        suggestions
      end

    # Check for compound operations optimization
    suggestions =
      if length(registry.compound_ops) > 2 do
        ["Complex compound operations may benefit from query restructuring" | suggestions]
      else
        suggestions
      end

    suggestions
  end
end