# frozen_string_literal: true

module Gitlab
  module Schema
    module Validation
      module Sources
        class StructureSql
          DEFAULT_SCHEMA = 'public'

          def initialize(structure_file_path, schema_name = DEFAULT_SCHEMA)
            @structure_file_path = structure_file_path
            @schema_name = schema_name
            @table_map = to_map(tables)
            @index_map = to_map(indexes)
            @trigger_map = to_map(triggers)
            @foreign_key_map = to_map(foreign_keys)
          end

          def fetch_index_by_name(index_name)
            index_map[index_name]
          end

          def fetch_trigger_by_name(trigger_name)
            trigger_map[trigger_name]
          end

          def fetch_foreign_key_by_name(foreign_key_name)
            foreign_key_map[foreign_key_name]
          end

          def fetch_table_by_name(table_name)
            table_map[table_name]
          end

          def fetch_sequence_by_name(sequence_name)
            sequence_map[sequence_name]
          end

          def index_exists?(index_name)
            !!fetch_index_by_name(index_name)
          end

          def trigger_exists?(trigger_name)
            !!fetch_trigger_by_name(trigger_name)
          end

          def foreign_key_exists?(foreign_key_name)
            !!fetch_foreign_key_by_name(foreign_key_name)
          end

          def table_exists?(table_name)
            !!fetch_table_by_name(table_name)
          end

          def sequence_exists?(sequence_name)
            !!fetch_sequence_by_name(sequence_name)
          end

          def indexes
            @indexes ||= map_with_default_schema(index_statements, SchemaObjects::Index)
          end

          def triggers
            @triggers ||= map_with_default_schema(trigger_statements, SchemaObjects::Trigger)
          end

          def sequences
            sequence_map.values
          end

          def sequence_map
            @sequences ||= begin
              parser = Gitlab::Schema::Validation::Sources::SequenceStructureSqlParser.new(
                parsed_structure_file, schema_name)
              parser.execute.transform_values! { |sequence| SchemaObjects::Sequence.new(sequence) }
            end
          end

          def foreign_keys
            @foreign_keys ||= foreign_key_statements.map do |stmt|
              stmt.relation.schemaname = schema_name if stmt.relation.schemaname == ''

              SchemaObjects::ForeignKey.new(Adapters::ForeignKeyStructureSqlAdapter.new(stmt))
            end
          end

          def tables
            @tables ||= table_statements.map do |stmt|
              table_name = stmt.relation.relname
              partition_stmt = stmt.partspec

              columns = stmt.table_elts.select { |n| n.node == :column_def }.map do |column|
                adapter = Adapters::ColumnStructureSqlAdapter.new(table_name, column.column_def, partition_stmt)
                SchemaObjects::Column.new(adapter)
              end

              SchemaObjects::Table.new(table_name, columns)
            end
          end

          private

          attr_reader :structure_file_path, :schema_name, :table_map, :index_map, :trigger_map, :foreign_key_map

          def to_map(array)
            array.each_with_object({}) do |entry, hash| # rubocop:disable Rails/IndexBy -- This gem does not depend on ActiveSupport.
              hash[entry.name] = entry
            end
          end

          def index_statements
            statements.filter_map { |s| s.stmt.index_stmt }
          end

          def trigger_statements
            statements.filter_map { |s| s.stmt.create_trig_stmt }
          end

          def table_statements
            statements.filter_map { |s| s.stmt.create_stmt }
          end

          def foreign_key_statements
            constraint_statements(:CONSTR_FOREIGN)
          end

          # Filter constraint statement nodes
          #
          # @param constraint_type [Symbol] node type. One of CONSTR_PRIMARY, CONSTR_CHECK, CONSTR_EXCLUSION,
          #        CONSTR_UNIQUE or CONSTR_FOREIGN.
          def constraint_statements(constraint_type)
            alter_table_statements(:AT_AddConstraint).filter do |stmt|
              stmt.cmds.first.alter_table_cmd.def.constraint.contype == constraint_type
            end
          end

          # Filter alter table statement nodes
          #
          # @param subtype [Symbol] node subtype +AT_AttachPartition+, +AT_ColumnDefault+ or +AT_AddConstraint+
          def alter_table_statements(subtype)
            statements.filter_map do |statement|
              node = statement.stmt.alter_table_stmt

              next unless node

              node if node.cmds.first.alter_table_cmd.subtype == subtype
            end
          end

          def statements
            @statements ||= parsed_structure_file.tree.stmts
          end

          def parsed_structure_file
            @parsed_structure_file ||= PgQuery.parse(File.read(structure_file_path))
          end

          def map_with_default_schema(statements, validation_class)
            statements.map do |statement|
              statement.relation.schemaname = schema_name if statement.relation.schemaname == ''

              validation_class.new(statement)
            end
          end
        end
      end
    end
  end
end
