# frozen_string_literal: true

require_relative 'values'
require_relative 'path_set'

module RubyTerraform
  module Models
    # rubocop:disable Metrics/ModuleLength
    module Objects
      # rubocop:disable Metrics/ClassLength
      class << self
        # rubocop:disable Style/RedundantAssignment
        # rubocop:disable Metrics/MethodLength
        def box(object, unknown: nil, sensitive: nil)
          initial = boxed_empty_by_value(object)
          object = symbolise(object)
          unknown = symbolised_or_native_empty(unknown, object)
          sensitive = symbolised_or_native_empty(sensitive, object)

          return Values.unknown(sensitive:) if unknown == true

          unless object.is_a?(Hash) || object.is_a?(Array)
            return Values.known(object, sensitive:)
          end

          boxed_unknown =
            box_unknown(unknown, sensitive:, initial:)

          boxed_object =
            box_known(object, sensitive:, initial: boxed_unknown)

          boxed_object
        end
        # rubocop:enable Metrics/MethodLength
        # rubocop:enable Style/RedundantAssignment

        def paths(object)
          PathSet.extract_from(object)
        end

        def known_values(path_set, object: {}, sensitive: {})
          path_set.paths.map do |path|
            resolved = path.read(object)
            resolved_sensitive = path.read(sensitive) == true

            Values.known(resolved, sensitive: resolved_sensitive)
          end
        end

        def unknown_values(path_set, unknown: {}, sensitive: {})
          path_set.paths.map do |path|
            resolved = path.read(unknown)
            resolved_sensitive = path.read(sensitive) == true

            resolved ? Values.unknown(sensitive: resolved_sensitive) : nil
          end
        end

        def object(path_set, values,
                   sensitive: {},
                   initial: Values.empty_map,
                   filler: Values.omitted)
          gaps = path_set.gaps
          extra_values = gaps.paths.collect { |p| [p, filler] }

          path_values = path_set.paths.zip(values) + extra_values
          path_values = sort_by_path(path_values)

          update_all(initial, path_values, sensitive)
        end

        private

        def box_unknown(unknown, sensitive: {}, initial: Values.empty_map)
          path_set = paths(unknown)
          unknown_values = unknown_values(
            path_set, unknown:, sensitive:
          )
          object(
            path_set, unknown_values, sensitive:, initial:
          )
        end

        def box_known(object, sensitive: {}, initial: Values.empty_map)
          path_set = paths(object)
          object_values = known_values(
            path_set, object:, sensitive:
          )
          object(
            path_set, object_values, sensitive:, initial:
          )
        end

        def update_all(object, path_values, sensitive = {})
          path_values.each_with_object(object) do |path_value, obj|
            path, value = path_value
            update_in(obj, path, value, sensitive:)
          end
        end

        def update_in(object, path, value, sensitive: {})
          path.traverse(object) do |obj, step|
            update_object_for_step(
              obj, step, value, sensitive:
            )
          end
        end

        # rubocop:disable Metrics/MethodLength
        def update_object_for_step(object, step, value, sensitive: {})
          parent = step.seen.read(object, default: object)
          upcoming = step.remaining.first

          found_sensitive = step.seen.append(step.element).read(sensitive)
          resolved_sensitive = found_sensitive == true
          resolved =
            if step.remaining.empty?
              value
            else
              boxed_empty_by_key(upcoming, sensitive: resolved_sensitive)
            end

          parent[step.element] ||= resolved

          object
        end
        # rubocop:enable Metrics/MethodLength

        def boxed_empty_by_key(key, sensitive: false)
          if key.is_a?(Numeric)
            Values.empty_list(sensitive:)
          else
            Values.empty_map(sensitive:)
          end
        end

        def boxed_empty_by_value(value, sensitive: false)
          case value
          when Array then Values.empty_list(sensitive:)
          when Hash then Values.empty_map(sensitive:)
          end
        end

        def native_empty_by_value(value)
          case value
          when Array then []
          when Hash then {}
          else false
          end
        end

        def symbolise(object)
          case object
          when Hash
            object.to_h { |key, value| [key.to_sym, symbolise(value)] }
          else
            object
          end
        end

        def symbolised_or_native_empty(object, target)
          object ? symbolise(object) : native_empty_by_value(target)
        end

        def sort_by_path(path_values)
          path_values.sort { |a, b| a[0] <=> b[0] }
        end
      end
      # rubocop:enable Metrics/ClassLength
    end
    # rubocop:enable Metrics/ModuleLength
  end
end
