lib/selecto/sql/functions.ex

defmodule Selecto.SQL.Functions do
  @moduledoc """
  Advanced SQL function support for Selecto.

  This module extends the existing function support in Selecto.Builder.Sql.Select
  with additional advanced SQL functions including window functions, array operations,
  string manipulation, and mathematical functions.

  ## Function Categories

  ### String Functions
  - `substr/3` - Extract substring
  - `trim/1`, `ltrim/1`, `rtrim/1` - String trimming
  - `upper/1`, `lower/1` - Case conversion
  - `length/1` - String length
  - `position/2` - Find substring position
  - `replace/3` - String replacement
  - `split_part/3` - Split and extract part

  ### Mathematical Functions
  - `abs/1` - Absolute value
  - `ceil/1`, `floor/1` - Rounding functions
  - `round/1`, `round/2` - Rounding with precision
  - `power/2` - Exponentiation
  - `sqrt/1` - Square root
  - `mod/2` - Modulo operation
  - `random/0` - Random number generation

  ### Date/Time Functions
  - `now/0` - Current timestamp
  - `date_trunc/2` - Truncate to date part
  - `interval/1` - Time intervals
  - `age/1`, `age/2` - Date arithmetic
  - `date_part/2` - Enhanced extract functionality

  ### Array Functions
  - `array_agg/1` - Array aggregation
  - `array_length/1` - Array length
  - `array_to_string/2` - Array to string conversion
  - `string_to_array/2` - String to array conversion
  - `unnest/1` - Array expansion
  - `array_cat/2` - Array concatenation

  ### Window Functions
  - `row_number/0` - Row numbering
  - `rank/0` - Ranking with gaps
  - `dense_rank/0` - Dense ranking
  - `lag/1`, `lag/2` - Previous row values
  - `lead/1`, `lead/2` - Next row values
  - `first_value/1`, `last_value/1` - Window boundaries
  - `ntile/1` - Percentile groups

  ### Conditional Functions
  - Enhanced `case` expressions
  - `decode/3+` - Oracle-style conditional
  - `iif/3` - Simple if-then-else

  ## Usage Examples

      # String functions
      {:substr, "description", 1, 50}
      {:trim, "name"}
      {:upper, "category"}

      # Math functions
      {:round, "price", 2}
      {:power, "base", 2}

      # Window functions
      {:window, {:row_number}, over: [partition_by: ["category"], order_by: ["price"]]}
      {:window, {:lag, "price"}, over: [partition_by: ["product_id"], order_by: ["date"]]}

      # Array functions
      {:array_agg, "tag_name", over: [partition_by: ["product_id"]]}
      {:unnest, "tags"}
  """

  @doc """
  Process advanced SQL functions that extend beyond the basic set.

  This integrates with the existing prep_selector in Selecto.Builder.Sql.Select
  to provide comprehensive function support.
  """
  def prep_advanced_selector(selecto, selector) do
    case selector do
      # String Functions
      {:substr, field, start, length} ->
        prep_string_function(selecto, "substr", [field, {:literal, start}, {:literal, length}])

      {:substr, field, start} ->
        prep_string_function(selecto, "substr", [field, {:literal, start}])

      {:trim, field} ->
        prep_string_function(selecto, "trim", [field])

      {:ltrim, field} ->
        prep_string_function(selecto, "ltrim", [field])

      {:rtrim, field} ->
        prep_string_function(selecto, "rtrim", [field])

      {:upper, field} ->
        prep_string_function(selecto, "upper", [field])

      {:lower, field} ->
        prep_string_function(selecto, "lower", [field])

      {:length, field} ->
        prep_string_function(selecto, "length", [field])

      {:position, substring, string} ->
        prep_position_function(selecto, substring, string)

      {:replace, field, old, new} ->
        prep_string_function(selecto, "replace", [field, old, new])

      {:split_part, field, delimiter, position} ->
        prep_string_function(selecto, "split_part", [field, delimiter, {:literal, position}])

      # Mathematical Functions
      {:abs, field} ->
        prep_math_function(selecto, "abs", [field])

      {:ceil, field} ->
        prep_math_function(selecto, "ceil", [field])

      {:floor, field} ->
        prep_math_function(selecto, "floor", [field])

      {:round, field} ->
        prep_math_function(selecto, "round", [field])

      {:round, field, precision} ->
        prep_math_function(selecto, "round", [field, {:literal, precision}])

      {:power, base, exponent} ->
        prep_math_function(selecto, "power", [base, exponent])

      {:sqrt, field} ->
        prep_math_function(selecto, "sqrt", [field])

      {:mod, dividend, divisor} ->
        prep_math_function(selecto, "mod", [dividend, divisor])

      {:random} ->
        prep_math_function(selecto, "random", [])

      # Date/Time Functions
      {:now} ->
        prep_datetime_function(selecto, "now", [])

      {:date_trunc, part, field} ->
        prep_datetime_function(selecto, "date_trunc", [part, field])

      {:interval, spec} ->
        prep_interval(selecto, spec)

      {:age, field} ->
        prep_datetime_function(selecto, "age", [field])

      {:age, field1, field2} ->
        prep_datetime_function(selecto, "age", [field1, field2])

      {:date_part, part, field} ->
        prep_datetime_function(selecto, "date_part", [part, field])

      # Array Functions
      {:array_agg, field} ->
        prep_array_function(selecto, "array_agg", [field])

      {:array_agg, field, opts} when is_list(opts) ->
        prep_array_agg_with_opts(selecto, field, opts)

      # String aggregation - 2 argument version
      {:string_agg, field, delimiter} ->
        prep_string_agg(selecto, field, delimiter)

      {:string_agg, field, delimiter, opts} when is_list(opts) ->
        prep_string_agg_with_opts(selecto, field, delimiter, opts)

      # JSON/JSONB object aggregation - 2 argument versions
      {:json_object_agg, key_field, value_field} ->
        prep_object_agg(selecto, "json_object_agg", key_field, value_field)

      {:jsonb_object_agg, key_field, value_field} ->
        prep_object_agg(selecto, "jsonb_object_agg", key_field, value_field)

      # JSON array aggregation
      {:json_agg, field} ->
        prep_array_function(selecto, "json_agg", [field])

      {:jsonb_agg, field} ->
        prep_array_function(selecto, "jsonb_agg", [field])

      # GROUPING function for ROLLUP/CUBE queries
      {:grouping, fields} when is_list(fields) ->
        prep_grouping_function(selecto, fields)

      {:grouping, field} ->
        prep_grouping_function(selecto, [field])

      {:array_length, field} ->
        prep_array_function(selecto, "array_length", [field, {:literal, 1}])

      {:array_to_string, field, delimiter} ->
        prep_array_function(selecto, "array_to_string", [field, {:literal, delimiter}])

      {:string_to_array, field, delimiter} ->
        prep_array_function(selecto, "string_to_array", [field, {:literal, delimiter}])

      {:unnest, field} ->
        prep_array_function(selecto, "unnest", [field])

      {:array_cat, array1, array2} ->
        prep_array_function(selecto, "array_cat", [array1, array2])

      # Additional array functions
      {:cardinality, field} ->
        prep_array_function(selecto, "cardinality", [field])

      {:array_ndims, field} ->
        prep_array_function(selecto, "array_ndims", [field])

      {:array_dims, field} ->
        prep_array_function(selecto, "array_dims", [field])

      {:array_append, array, element} ->
        prep_array_function(selecto, "array_append", [array, {:literal, element}])

      {:array_prepend, element, array} ->
        prep_array_function(selecto, "array_prepend", [{:literal, element}, array])

      {:array_fill, value, dimensions} ->
        prep_array_function(selecto, "array_fill", [{:literal, value}, {:literal, dimensions}])

      {:array_remove, array, element} ->
        prep_array_function(selecto, "array_remove", [array, {:literal, element}])

      {:array_replace, array, from_elem, to_elem} ->
        prep_array_function(selecto, "array_replace", [
          array,
          {:literal, from_elem},
          {:literal, to_elem}
        ])

      {:array_position, array, element} ->
        prep_array_function(selecto, "array_position", [array, {:literal, element}])

      {:array_position, array, element, start} ->
        prep_array_function(selecto, "array_position", [
          array,
          {:literal, element},
          {:literal, start}
        ])

      {:array_positions, array, element} ->
        prep_array_function(selecto, "array_positions", [array, {:literal, element}])

      # Window Functions
      {:window, func, opts} ->
        prep_window_function(selecto, func, opts)

      # Enhanced Conditional Functions
      {:decode, field, mappings} ->
        prep_decode_function(selecto, field, mappings)

      {:iif, condition, true_value, false_value} ->
        prep_iif_function(selecto, condition, true_value, false_value)

      # Fallback to existing prep_selector for standard functions
      _ ->
        nil
    end
  end

  # String function helpers
  defp prep_string_function(selecto, func_name, args) do
    {sel_parts, joins, params} = prep_function_args(selecto, args)
    func_iodata = [func_name, "(", Enum.intersperse(sel_parts, ", "), ")"]
    {func_iodata, joins, params}
  end

  defp prep_position_function(selecto, substring, string) do
    {substring_iodata, substring_joins, substring_params} = prep_single_arg(selecto, substring)
    {string_iodata, string_joins, string_params} = prep_single_arg(selecto, string)

    {[
       "position(",
       substring_iodata,
       " in ",
       string_iodata,
       ")"
     ], List.wrap(substring_joins) ++ List.wrap(string_joins), substring_params ++ string_params}
  end

  # Math function helpers
  defp prep_math_function(selecto, func_name, args) do
    {sel_parts, joins, params} = prep_function_args(selecto, args)
    func_iodata = [func_name, "(", Enum.intersperse(sel_parts, ", "), ")"]
    {func_iodata, joins, params}
  end

  # Date/time function helpers
  defp prep_datetime_function(selecto, func_name, args) do
    {sel_parts, joins, params} = prep_function_args(selecto, args)
    func_iodata = [func_name, "(", Enum.intersperse(sel_parts, ", "), ")"]
    {func_iodata, joins, params}
  end

  # Array function helpers
  defp prep_array_function(selecto, func_name, args) do
    {sel_parts, joins, params} = prep_function_args(selecto, args)
    func_iodata = [func_name, "(", Enum.intersperse(sel_parts, ", "), ")"]
    {func_iodata, joins, params}
  end

  # Window function helpers
  defp prep_window_function(selecto, func, opts) do
    # Process the base function
    {func_iodata, func_joins, func_params} =
      case func do
        {:row_number} ->
          {["row_number()"], [], []}

        {:rank} ->
          {["rank()"], [], []}

        {:dense_rank} ->
          {["dense_rank()"], [], []}

        {:lag, field} ->
          {field_iodata, joins, params} = prep_function_args(selecto, [field])
          {["lag(", field_iodata, ")"], joins, params}

        {:lag, field, offset} ->
          {args_iodata, joins, params} = prep_function_args(selecto, [field, {:literal, offset}])
          {["lag(", Enum.intersperse(args_iodata, ", "), ")"], joins, params}

        {:lead, field} ->
          {field_iodata, joins, params} = prep_function_args(selecto, [field])
          {["lead(", field_iodata, ")"], joins, params}

        {:lead, field, offset} ->
          {args_iodata, joins, params} = prep_function_args(selecto, [field, {:literal, offset}])
          {["lead(", Enum.intersperse(args_iodata, ", "), ")"], joins, params}

        {:first_value, field} ->
          {field_iodata, joins, params} = prep_function_args(selecto, [field])
          {["first_value(", field_iodata, ")"], joins, params}

        {:last_value, field} ->
          {field_iodata, joins, params} = prep_function_args(selecto, [field])
          {["last_value(", field_iodata, ")"], joins, params}

        {:ntile, buckets} ->
          {["ntile(", Integer.to_string(buckets), ")"], [], []}

        {:nth_value, field, n} ->
          {field_iodata, joins, params} = prep_function_args(selecto, [field])
          {["nth_value(", field_iodata, ", ", Integer.to_string(n), ")"], joins, params}

        {:cume_dist} ->
          {["cume_dist()"], [], []}

        {:percent_rank} ->
          {["percent_rank()"], [], []}

        {agg_func, field} when agg_func in [:sum, :count, :avg, :min, :max] ->
          {field_iodata, joins, params} = prep_function_args(selecto, [field])
          func_name = Atom.to_string(agg_func)
          {[func_name, "(", field_iodata, ")"], joins, params}
      end

    # Build OVER clause
    {over_iodata, over_joins, over_params} = build_over_clause(selecto, opts)

    # Combine function with OVER clause
    window_iodata = [func_iodata, " over (", over_iodata, ")"]
    all_joins = List.flatten([func_joins | over_joins])
    all_params = func_params ++ over_params

    {window_iodata, all_joins, all_params}
  end

  # Build OVER clause for window functions
  defp build_over_clause(selecto, opts) do
    partition_clause =
      case Keyword.get(opts, :partition_by) do
        nil ->
          []

        fields when is_list(fields) ->
          {part_args, part_joins, part_params} = prep_function_args(selecto, fields)
          {["partition by ", Enum.intersperse(part_args, ", ")], part_joins, part_params}

        field ->
          {part_args, part_joins, part_params} = prep_function_args(selecto, [field])
          {["partition by ", part_args], part_joins, part_params}
      end

    order_clause =
      case Keyword.get(opts, :order_by) do
        nil ->
          []

        fields when is_list(fields) ->
          {order_args, order_joins, order_params} = prep_function_args(selecto, fields)
          {["order by ", Enum.intersperse(order_args, ", ")], order_joins, order_params}

        field ->
          {order_args, order_joins, order_params} = prep_function_args(selecto, [field])
          {["order by ", order_args], order_joins, order_params}
      end

    # Combine clauses
    case {partition_clause, order_clause} do
      {[], []} ->
        {[], [], []}

      {{part_iodata, part_joins, part_params}, []} ->
        {part_iodata, part_joins, part_params}

      {[], {order_iodata, order_joins, order_params}} ->
        {order_iodata, order_joins, order_params}

      {{part_iodata, part_joins, part_params}, {order_iodata, order_joins, order_params}} ->
        combined_iodata = [part_iodata, " ", order_iodata]
        combined_joins = part_joins ++ order_joins
        combined_params = part_params ++ order_params
        {combined_iodata, combined_joins, combined_params}
    end
  end

  # Interval function helper
  defp prep_interval(_selecto, spec) when is_binary(spec) do
    # Handle PostgreSQL interval syntax: "1 day", "2 hours", etc.
    interval_iodata = ["interval '", spec, "'"]
    {interval_iodata, [], []}
  end

  defp prep_interval(_selecto, {amount, unit}) do
    # Handle tuple format: {1, "day"}, {2, "hour"}, etc.
    interval_iodata = ["interval '", Integer.to_string(amount), " ", unit, "'"]
    {interval_iodata, [], []}
  end

  # Decode function (Oracle-style conditional)
  defp prep_decode_function(selecto, field, mappings) do
    {field_iodata, field_joins, field_params} = prep_function_args(selecto, [field])

    {mapping_parts, mapping_joins, mapping_params} =
      Enum.reduce(mappings, {[], [], []}, fn {match_value, return_value},
                                             {parts, joins, params} ->
        {match_iodata, match_joins, match_params} = prep_function_args(selecto, [match_value])
        {return_iodata, return_joins, return_params} = prep_function_args(selecto, [return_value])

        new_parts = parts ++ [match_iodata, ", ", return_iodata]
        new_joins = joins ++ match_joins ++ return_joins
        new_params = params ++ match_params ++ return_params

        {new_parts, new_joins, new_params}
      end)

    decode_iodata = ["decode(", field_iodata, ", ", mapping_parts, ")"]
    all_joins = field_joins ++ mapping_joins
    all_params = field_params ++ mapping_params

    {decode_iodata, all_joins, all_params}
  end

  # Simple if-then-else function
  defp prep_iif_function(selecto, condition, true_value, false_value) do
    # Convert to CASE expression
    _case_selector = {:case, [{condition, true_value}], false_value}

    # Delegate to existing case handling (would need to call back to main prep_selector)
    # For now, build manually
    {cond_iodata, cond_joins, cond_params} = build_condition_iodata(selecto, condition)
    {true_iodata, true_joins, true_params} = prep_function_args(selecto, [true_value])
    {false_iodata, false_joins, false_params} = prep_function_args(selecto, [false_value])

    case_iodata = [
      "case when ",
      cond_iodata,
      " then ",
      true_iodata,
      " else ",
      false_iodata,
      " end"
    ]

    all_joins = cond_joins ++ true_joins ++ false_joins
    all_params = cond_params ++ true_params ++ false_params

    {case_iodata, all_joins, all_params}
  end

  # Helper to build condition iodata (simplified for now)
  defp build_condition_iodata(selecto, condition) do
    # This would need to integrate with the WHERE clause builder
    # For now, handle simple field comparisons
    case condition do
      {field, :eq, value} ->
        {field_iodata, field_joins, field_params} = prep_function_args(selecto, [field])
        {value_iodata, value_joins, value_params} = prep_function_args(selecto, [value])
        condition_iodata = [field_iodata, " = ", value_iodata]
        {condition_iodata, field_joins ++ value_joins, field_params ++ value_params}

      {field, :gt, value} ->
        {field_iodata, field_joins, field_params} = prep_function_args(selecto, [field])
        {value_iodata, value_joins, value_params} = prep_function_args(selecto, [value])
        condition_iodata = [field_iodata, " > ", value_iodata]
        {condition_iodata, field_joins ++ value_joins, field_params ++ value_params}

      # Add more condition types as needed
      _ ->
        # Fallback
        {["true"], [], []}
    end
  end

  # Generic function argument processor
  defp prep_function_args(selecto, args) do
    Enum.reduce(args, {[], [], []}, fn arg, {sel_parts, joins, params} ->
      {arg_iodata, arg_joins, arg_params} = prep_single_arg(selecto, arg)

      {
        sel_parts ++ [arg_iodata],
        joins ++ List.wrap(arg_joins),
        params ++ arg_params
      }
    end)
  end

  # Process individual function arguments
  defp prep_single_arg(selecto, arg) do
    case arg do
      {:literal, value} ->
        {{:param, value}, :selecto_root, [value]}

      field when is_binary(field) ->
        # Handle field references by calling back to main prep_selector
        Selecto.Builder.Sql.Select.prep_selector(selecto, field)

      value when is_integer(value) or is_float(value) or is_boolean(value) ->
        {{:param, value}, :selecto_root, [value]}

      # For complex expressions, call back to main prep_selector
      complex_expr ->
        case prep_advanced_selector(selecto, complex_expr) do
          nil ->
            # Try the main prep_selector for standard expressions
            try do
              Selecto.Builder.Sql.Select.prep_selector(selecto, complex_expr)
            rescue
              _ -> {[inspect(complex_expr)], :selecto_root, []}
            end

          result ->
            result
        end
    end
  end

  # String aggregation helper - string_agg(field, delimiter)
  defp prep_string_agg(selecto, field, delimiter) do
    {field_iodata, field_joins, field_params} = prep_single_arg(selecto, field)

    # Delimiter is typically a literal string
    delimiter_str = if is_binary(delimiter), do: delimiter, else: to_string(delimiter)

    func_iodata = ["string_agg(", field_iodata, ", ", {:param, delimiter_str}, ")"]
    {func_iodata, field_joins, field_params ++ [delimiter_str]}
  end

  # String aggregation with options (ORDER BY, DISTINCT)
  defp prep_string_agg_with_opts(selecto, field, delimiter, opts) do
    {field_iodata, field_joins, field_params} = prep_single_arg(selecto, field)

    delimiter_str = if is_binary(delimiter), do: delimiter, else: to_string(delimiter)

    # Handle DISTINCT
    distinct = if Keyword.get(opts, :distinct, false), do: "DISTINCT ", else: ""

    # Handle ORDER BY within the aggregate
    {order_clause, order_joins, order_params} =
      case Keyword.get(opts, :order_by) do
        nil ->
          {[], [], []}

        fields when is_list(fields) ->
          {order_parts, o_joins, o_params} = prep_function_args(selecto, fields)
          {[" ORDER BY ", Enum.intersperse(order_parts, ", ")], o_joins, o_params}

        field_spec ->
          {order_parts, o_joins, o_params} = prep_function_args(selecto, [field_spec])
          {[" ORDER BY ", order_parts], o_joins, o_params}
      end

    func_iodata = [
      "string_agg(",
      distinct,
      field_iodata,
      ", ",
      {:param, delimiter_str},
      order_clause,
      ")"
    ]

    all_joins = field_joins ++ order_joins
    all_params = field_params ++ [delimiter_str] ++ order_params

    {func_iodata, all_joins, all_params}
  end

  # Array aggregation with options (ORDER BY, DISTINCT)
  defp prep_array_agg_with_opts(selecto, field, opts) do
    {field_iodata, field_joins, field_params} = prep_single_arg(selecto, field)

    # Handle DISTINCT
    distinct = if Keyword.get(opts, :distinct, false), do: "DISTINCT ", else: ""

    # Handle ORDER BY within the aggregate
    {order_clause, order_joins, order_params} =
      case Keyword.get(opts, :order_by) do
        nil ->
          {[], [], []}

        fields when is_list(fields) ->
          {order_parts, o_joins, o_params} = prep_function_args(selecto, fields)
          {[" ORDER BY ", Enum.intersperse(order_parts, ", ")], o_joins, o_params}

        field_spec ->
          {order_parts, o_joins, o_params} = prep_function_args(selecto, [field_spec])
          {[" ORDER BY ", order_parts], o_joins, o_params}
      end

    func_iodata = ["array_agg(", distinct, field_iodata, order_clause, ")"]

    all_joins = field_joins ++ order_joins
    all_params = field_params ++ order_params

    {func_iodata, all_joins, all_params}
  end

  # JSON/JSONB object aggregation helper - takes key and value fields
  defp prep_object_agg(selecto, func_name, key_field, value_field) do
    {key_iodata, key_joins, key_params} = prep_single_arg(selecto, key_field)
    {value_iodata, value_joins, value_params} = prep_single_arg(selecto, value_field)

    func_iodata = [func_name, "(", key_iodata, ", ", value_iodata, ")"]

    all_joins = key_joins ++ value_joins
    all_params = key_params ++ value_params

    {func_iodata, all_joins, all_params}
  end

  # GROUPING function for ROLLUP/CUBE - indicates whether a column is aggregated
  defp prep_grouping_function(selecto, fields) do
    {field_parts, all_joins, all_params} = prep_function_args(selecto, fields)

    func_iodata = ["GROUPING(", Enum.intersperse(field_parts, ", "), ")"]

    {func_iodata, all_joins, all_params}
  end
end