# frozen_string_literal: true

module Gitlab
  module Database
    module QueryAnalyzers
      class RestrictAllowedSchemas < Base
        UnsupportedSchemaError = Class.new(QueryAnalyzerError)
        DDLNotAllowedError = Class.new(UnsupportedSchemaError)
        DMLNotAllowedError = Class.new(UnsupportedSchemaError)
        DMLAccessDeniedError = Class.new(UnsupportedSchemaError)

        # Re-map schemas observed schemas to a single cluster mode
        # - symbol:
        #     The mapped schema indicates that it contains all data in a single-cluster mode
        # - nil:
        #     Inidicates that changes made to this schema are ignored and always allowed
        SCHEMA_MAPPING = {
          gitlab_shared: nil,
          gitlab_internal: nil,

          # Pods specific changes
          gitlab_main_clusterwide: :gitlab_main
        }.freeze

        class << self
          def enabled?
            true
          end

          def allowed_gitlab_schemas
            self.context[:allowed_gitlab_schemas]
          end

          def allowed_gitlab_schemas=(value)
            self.context[:allowed_gitlab_schemas] = value
          end

          def analyze(parsed)
            # If list of schemas is empty, we allow only DDL changes
            if self.dml_mode?
              self.restrict_to_dml_only(parsed)
            else
              self.restrict_to_ddl_only(parsed)
            end
          end

          def require_ddl_mode!(message = "")
            return unless self.context

            self.raise_dml_not_allowed_error(message) if self.dml_mode?
          end

          def require_dml_mode!(message = "")
            return unless self.context

            self.raise_ddl_not_allowed_error(message) if self.ddl_mode?
          end

          private

          def restrict_to_ddl_only(parsed)
            tables = self.dml_tables(parsed)
            schemas = self.dml_schemas(tables)

            if schemas.any?
              self.raise_dml_not_allowed_error("Modifying of '#{tables}' (#{schemas.to_a}) with '#{parsed.sql}'")
            end
          end

          def restrict_to_dml_only(parsed)
            if parsed.pg.ddl_tables.any?
              self.raise_ddl_not_allowed_error("Modifying of '#{parsed.pg.ddl_tables}' with '#{parsed.sql}'")
            end

            if parsed.pg.ddl_functions.any?
              self.raise_ddl_not_allowed_error("Modifying of '#{parsed.pg.ddl_functions}' with '#{parsed.sql}'")
            end

            tables = self.dml_tables(parsed)
            schemas = self.dml_schemas(tables)

            if (schemas - self.allowed_gitlab_schemas).any?
              raise DMLAccessDeniedError, \
                "Select/DML queries (SELECT/UPDATE/DELETE) do access '#{tables}' (#{schemas.to_a}) " \
                "which is outside of list of allowed schemas: '#{self.allowed_gitlab_schemas}'. " \
                "#{documentation_url}"
            end
          end

          def dml_mode?
            self.allowed_gitlab_schemas&.any?
          end

          def ddl_mode?
            !self.dml_mode?
          end

          def dml_tables(parsed)
            parsed.pg.select_tables + parsed.pg.dml_tables
          end

          def dml_schemas(tables)
            extra_schemas = ::Gitlab::Database::GitlabSchema.table_schemas(tables)

            SCHEMA_MAPPING.each do |schema, mapped_schema|
              next unless extra_schemas.delete?(schema)

              extra_schemas.add(mapped_schema) if mapped_schema
            end

            extra_schemas
          end

          def raise_dml_not_allowed_error(message)
            raise DMLNotAllowedError, \
              "Select/DML queries (SELECT/UPDATE/DELETE) are disallowed in the DDL (structure) mode. " \
              "#{message}. #{documentation_url}" \
          end

          def raise_ddl_not_allowed_error(message)
            raise DDLNotAllowedError, \
              "DDL queries (structure) are disallowed in the Select/DML (SELECT/UPDATE/DELETE) mode. " \
              "#{message}. #{documentation_url}"
          end

          def documentation_url
            "For more information visit: https://docs.gitlab.com/ee/development/database/migrations_for_multiple_databases.html"
          end
        end
      end
    end
  end
end