# Copyright 2024 - 2025 Block, Inc.
#
# Use of this source code is governed by an MIT-style
# license that can be found in the LICENSE file or at
# https://opensource.org/licenses/MIT.
#
# frozen_string_literal: true

require "rake/tasklib"
require "elastic_graph/schema_artifacts/runtime_metadata/schema_element_names"

module ElasticGraph
  module SchemaDefinition
    # Defines rake tasks for managing artifacts generated from a schema definition.
    #
    # @note {ElasticGraph::Local::RakeTasks} wraps this and provides additional functionality. Most users will not need to interact with
    #   this class directly.
    class RakeTasks < ::Rake::TaskLib
      # @private
      attr_reader :output

      # @param index_document_sizes [Boolean] When enabled, ElasticGraph will configure the index mappings so that the datastore indexes a
      #   `_size` field in each document. ElasticGraph itself does not do anything with this field, but it will be available for your use
      #   in any direct queries (e.g. via Kibana). Important note: this requires the [mapper-size
      #   plugin](https://www.elastic.co/guide/en/elasticsearch/plugins/8.15/mapper-size.html) to be installed on your datastore cluster.
      #   You are responsible for ensuring that is installed if you enable this feature. If you enable this and the plugin is not
      #   installed, you will get errors!
      # @param path_to_schema [String, Pathname] path to the main (or only) schema definition file
      # @param schema_artifacts_directory [String, Pathname] Directory to dump the schema artifacts in
      # @param schema_element_name_form [:camelCase, :snake_case] the form of names for schema elements (fields, arguments, directives)
      #   generated by ElasticGraph.
      # @param schema_element_name_overrides [Hash<Symbol, String>] overrides for specific names of schema elements (fields, arguments,
      #   directives) generated by ElasticGraph. For example, to rename the `gt` filter field to `greaterThan`, pass `{gt: "greaterThan"}`.
      # @param derived_type_name_formats [Hash<Symbol, String>] overrides for the naming formats used by ElasticGraph for derived GraphQL
      #   type names. For example, to use `Metrics` instead of `AggregatedValues` as the suffix for the generated types supporting
      #   getting aggregated metrid values, pass `{AggregatedValues: "%{base}Metrics"}`. See {SchemaElements::TypeNamer::DEFAULT_FORMATS}
      #   for the available formats.
      # @param type_name_overrides [Hash<Symbol, String>] overrides for the names of specific GraphQL types. For example, to rename the
      #   `DateTime` scalar to `Timestamp`, pass `{DateTime: "Timestamp}`.
      # @param enum_value_overrides_by_type [Hash<Symbol, Hash<Symbol, String>>] overrides for the names of specific enum values for
      #   specific enum types. For example, to rename the `DayOfWeek.MONDAY` enum to `DayOfWeek.MON`, pass `{DayOfWeek: {MONDAY: "MON"}}`.
      # @param extension_modules [Array<Module>] List of Ruby modules to extend onto the `SchemaDefinition::API` instance. Designed to
      #   support ElasticGraph extension gems (such as `elasticgraph-apollo`).
      # @param enforce_json_schema_version [Boolean] Whether or not to enforce the requirement that the JSON schema version is incremented
      #   every time dumping the JSON schemas results in a changed artifact. Generally speaking, you will want this to be `true` for any
      #   ElasticGraph application that is in production as the versioning of JSON schemas is what supports safe schema evolution as it
      #   allows ElasticGraph to identify which version of the JSON schema the publishing system was operating on when it published an
      #   event. It can be useful to set it to `false` before your application is in production, as you do not want to be forced to bump
      #   the version after every single schema change while you are building an initial prototype.
      # @param output [IO] used for printing task output
      #
      # @example Minimal setup with defaults
      #   ElasticGraph::SchemaDefinition::RakeTasks.new(
      #     index_document_sizes: false,
      #     path_to_schema: "config/schema.rb",
      #     schema_artifacts_directory: "config/schema/artifacts",
      #     schema_element_name_form: :camelCase
      #   )
      #
      # @example Spell out the full names of the `gt`/`gte`/`lt`/`lte` filter operators
      #   ElasticGraph::SchemaDefinition::RakeTasks.new(
      #     index_document_sizes: false,
      #     path_to_schema: "config/schema.rb",
      #     schema_artifacts_directory: "config/schema/artifacts",
      #     schema_element_name_form: :camelCase,
      #     schema_element_name_overrides: {
      #       gt: "greaterThan",
      #       gte: "greaterThanOrEqualTo",
      #       lt: "lessThan",
      #       lte: "lessThanOrEqualTo"
      #     }
      #   )
      #
      # @example Change the `AggregatedValues` type suffix to `Metrics`
      #   ElasticGraph::SchemaDefinition::RakeTasks.new(
      #     index_document_sizes: false,
      #     path_to_schema: "config/schema.rb",
      #     schema_artifacts_directory: "config/schema/artifacts",
      #     schema_element_name_form: :camelCase,
      #     derived_type_name_formats: {AggregatedValues: "Metrics"}
      #   )
      #
      # @example Rename `JsonSafeLong` to `BigInt`
      #   ElasticGraph::SchemaDefinition::RakeTasks.new(
      #     index_document_sizes: false,
      #     path_to_schema: "config/schema.rb",
      #     schema_artifacts_directory: "config/schema/artifacts",
      #     schema_element_name_form: :camelCase,
      #     type_name_overrides: {JsonSafeLong: "BigInt"}
      #   )
      #
      # @example Shorten the names of the `DayOfWeek` enum values
      #   ElasticGraph::SchemaDefinition::RakeTasks.new(
      #     index_document_sizes: false,
      #     path_to_schema: "config/schema.rb",
      #     schema_artifacts_directory: "config/schema/artifacts",
      #     schema_element_name_form: :camelCase,
      #     enum_value_overrides_by_type: {
      #       DayOfWeek: {
      #         MONDAY: "MON",
      #         TUESDAY: "TUE",
      #         WEDNESDAY: "WED",
      #         THURSDAY: "THU",
      #         FRIDAY: "FRI",
      #         SATURDAY: "SAT",
      #         SUNDAY: "SUN"
      #       }
      #     }
      #   )
      def initialize(
        index_document_sizes:,
        path_to_schema:,
        schema_artifacts_directory:,
        schema_element_name_form:,
        schema_element_name_overrides: {},
        derived_type_name_formats: {},
        type_name_overrides: {},
        enum_value_overrides_by_type: {},
        extension_modules: [],
        enforce_json_schema_version: true,
        output: $stdout
      )
        @schema_element_names = SchemaArtifacts::RuntimeMetadata::SchemaElementNames.new(
          form: schema_element_name_form,
          overrides: schema_element_name_overrides
        )

        @derived_type_name_formats = derived_type_name_formats
        @type_name_overrides = type_name_overrides
        @enum_value_overrides_by_type = enum_value_overrides_by_type
        @index_document_sizes = index_document_sizes
        @path_to_schema = path_to_schema
        @schema_artifacts_directory = schema_artifacts_directory
        @enforce_json_schema_version = enforce_json_schema_version
        @extension_modules = extension_modules
        @output = output

        define_tasks
      end

      private

      def define_tasks
        namespace :schema_artifacts do
          desc "Dumps all schema artifacts based on the current ElasticGraph schema definition"
          task :dump do
            schema_artifact_manager.dump_artifacts
          end

          desc "Checks the artifacts to make sure they are up-to-date, raising an exception if not"
          task :check do
            schema_artifact_manager.check_artifacts
          end
        end
      end

      def schema_artifact_manager
        require "elastic_graph/schema_definition/schema_artifact_manager"

        # :nocov: -- tests don't cover the `VERBOSE` side
        max_diff_lines = ENV["VERBOSE"] ? 999999999 : 50
        # :nocov:

        SchemaArtifactManager.new(
          schema_definition_results: schema_definition_results,
          schema_artifacts_directory: @schema_artifacts_directory.to_s,
          enforce_json_schema_version: @enforce_json_schema_version,
          output: @output,
          max_diff_lines: max_diff_lines
        )
      end

      def schema_definition_results
        require "elastic_graph/schema_definition/api"

        API.new(
          @schema_element_names,
          @index_document_sizes,
          extension_modules: @extension_modules,
          derived_type_name_formats: @derived_type_name_formats,
          type_name_overrides: @type_name_overrides,
          enum_value_overrides_by_type: @enum_value_overrides_by_type,
          output: @output
        ).tap do |api|
          api.as_active_instance { load @path_to_schema.to_s }
        end.results
      end
    end
  end
end
