# frozen_string_literal: false

# @!parse
#   class ActiveRecord::Migration
#     # Create a procedure
#     #
#     # @param [#to_s] name (nil)
#     #   The qualified name of the procedure with arguments and returned value type
#     # @option options [Boolean] :replace_existing (false) If the procedure should overwrite an existing one
#     # @option options [#to_s] :language ("sql") The language (like "sql" or "plpgsql")
#     # @option options [#to_s] :body (nil) The body of the procedure
#     # @option options [Symbol] :security (:invoker) Define the role under which the procedure is invoked
#     #   Supported values: :invoker (default), :definer
#     # @option options [#to_s] :comment The description of the procedure
#     # @yield [p] the block with the procedure's definition
#     # @yieldparam Object receiver of methods specifying the procedure
#     # @return [void]
#     #
#     # The syntax of the operation is the same as for `create_function`,
#     # but with only `security` option available. Notice, that
#     # procedures cannot return values so you're expected not to
#     # define a returned value as well.
#     #
#     # The procedure can be created either using inline syntax
#     #
#     # ```ruby
#     # create_procedure "metadata.set_foo(a int)",
#     #                  language: :sql,
#     #                  body: "SET foo = a",
#     #                  comment: "Sets foo value"
#     # ```
#     #
#     # or using a block:
#     #
#     # ```ruby
#     # create_procedure "metadata.set_foo(a int)" do |p|
#     #   p.language "sql" # (default)
#     #   p.body <<~SQL
#     #     SET foo = a
#     #   SQL
#     #   p.security :invoker # (default), also :definer
#     #   p.comment "Multiplies 2 integers"
#     # SQL
#     # ```
#     #
#     # With a `replace_existing: true` option,
#     # it will be created using the `CREATE OR REPLACE` clause.
#     # In this case the migration is irreversible because we
#     # don't know if and how to restore its previous definition.
#     #
#     # ```ruby
#     # create_procedure "set_foo(a int)",
#     #                  body: "SET foo = a",
#     #                  replace_existing: true
#     # ```
#     #
#     # A procedure without arguments is supported as well
#     #
#     # ```ruby
#     # # the same as "do_something()"
#     # create_procedure "do_something" do |p|
#     #   # ...
#     # end
#     # ```
#     def create_procedure(name, **options, &block); end
#   end
module PGTrunk::Operations::Procedures
  # @private
  class CreateProcedure < Base
    validates :body, presence: true
    validates :if_exists, :new_name, absence: true

    from_sql do |server_version|
      # Procedures were added to PostgreSQL in v11
      next if server_version < "11"

      <<~SQL.squish
        SELECT
          p.oid,
          (
            p.pronamespace::regnamespace || '.' || p.proname || '(' ||
            regexp_replace(
              regexp_replace(
                pg_get_function_arguments(p.oid), '^\s*IN\s+', '', 'g'
              ), '[,]\s*IN\s+', ',', 'g'
            ) || ')'
          ) AS name,
          p.prosrc AS body,
          l.lanname AS language,
          (
            CASE
              WHEN p.prosecdef THEN 'definer'
              ELSE 'invoker'
            END
          ) AS security,
          d.description AS comment
        FROM pg_proc p
          JOIN pg_trunk e ON e.oid = p.oid
          JOIN pg_language l ON l.oid = p.prolang
          LEFT JOIN pg_description d ON d.objoid = p.oid
        WHERE e.classid = 'pg_proc'::regclass
          AND p.prokind = 'p';
      SQL
    end

    def to_sql(version)
      # Procedures were added to PostgreSQL in v11
      check_version!(version)

      [create_proc, *comment_proc, register_proc].join(" ")
    end

    def invert
      irreversible!("replace_existing: true") if replace_existing
      DropProcedure.new(**to_h)
    end

    private

    def create_proc
      sql = "CREATE"
      sql << " OR REPLACE" if replace_existing
      sql << " PROCEDURE #{name.to_sql(true)}"
      sql << " LANGUAGE #{language&.downcase || 'sql'}"
      sql << " SECURITY DEFINER" if security == :definer
      sql << " AS $$#{body}$$;"
    end

    def comment_proc
      <<~SQL
        COMMENT ON PROCEDURE #{name.to_sql(true)}
        IS $comment$#{comment}$comment$;
      SQL
    end

    # Register the most recent `oid` of procedures with this schema/name
    # There can be several overloaded definitions, but we're interested
    # in that one we created just now so we can skip checking its args.
    def register_proc
      <<~SQL.squish
        WITH latest AS (
          SELECT
            oid,
            (proname = #{name.quoted} AND pronamespace = #{name.namespace}) AS ok
          FROM pg_proc
          WHERE prokind = 'p'
          ORDER BY oid DESC LIMIT 1
        )
        INSERT INTO pg_trunk (oid, classid)
          SELECT oid, 'pg_proc'::regclass
          FROM latest
          WHERE ok
        ON CONFLICT DO NOTHING;
      SQL
    end
  end
end
