# frozen_string_literal: true

require 'litany/resource_dsl'
require 'litany/mixins/metable'

module Litany
  # Methods that are used to help define resources
  # MetaResources behave like resources in most aspects, but are not tied back to CFN resources.
  class MetaResource < Base
    extend ResourceDSL
    include Metable

    class << self
      # In this function we essentially turn all resources and meta-resources into singleton factories.
      def new(name, *args, &block)
        raise "Resource names must be Tokens or Strings. Received: #{name.inspect}" unless name.is_a?(String) || name.is_a?(Symbol)

        name = :"#{name}".downcase
        debug "#{inspect.split('::').last}, accessing: #{name.inspect}, pre-existing: #{instances.values.collect(&:name)}"
        instances[name] ||= super(name, *args, &block)
      end

      def instances
        @instances ||= {}
      end

      def reset_instances
        @instances = {}
      end

    end

    validator(:parent_resource) do
      raise "`parent_resource` may not be nil on #{self}." if parent_resource.nil? && !is_a?(Project)
    end

    attr_reader :name, :parent_resource, :child_resources

    def initialize(name, parent = nil, &block)
      @name = name
      @parent_resource = parent unless parent.nil?
      @child_resources = Set.new

      instance_eval(&block) if block_given?
    end

    def active_environment
      project.active_environment
    end

    def active_environment_name
      active_environment&.name || :unset
    end

    def active_environment_pretty_region
      active_environment&.pretty_region || :unset
    end

    def arn_builder(partition: 'aws', service:, region: { Ref: 'AWS::Region' }, account: { Ref: 'AWS::AccountId' }, resource:, path: nil, sub_resource: nil)
      raise 'You may not request an arn with both a `path` and a `sub_resource`.' unless path.nil? || sub_resource.nil?

      region = '' if service.to_sym == :iam

      arn_parts = ['arn', partition, service, region, account, resource]
      arn_parts << sub_resource unless sub_resource.nil?

      arn = []
      arn_segment = []

      arn_parts.each do |part|
        case part
          when String, Integer, Symbol
            arn_segment << part
          when NilClass
            arn_segment << ''
          else
            arn << arn_segment.join(':') unless arn_segment.empty?
            arn << part
            arn_segment.clear
        end
      end

      arn << arn_segment.join(':') unless arn_segment.empty?

      arn = arn.count == 1 ? arn.first : { 'Fn::Join': [':', arn] }

      unless path.nil?
        arn = case arn
          when String
            "#{arn}/#{path}"
          when Hash
            { 'Fn::Join': ['/', [arn, path]] }
        end
      end

      arn
    end

    def chef_attributes
      (service_root || project).chef_attributes
    end

    def child_resource_hook(resource)
      raise 'Child resources must subclass from MetaResources.' unless resource.subclass_of?(MetaResource)
      child_resources << resource
    end

    def config(parent = nil, &block)
      raise "Attempting to reconfigure `@parent_resource` on #{self}. Old Value: #{@parent_resource}. New Value: #{parent}" unless parent.nil? || @parent_resource.nil? || @parent_resource == parent
      @parent_resource = parent unless parent.nil?
      instance_eval &block if block_given?
      self
    end

    def default_environment
      project.default_environment
    end

    def deferred(step: :pre_finalization, &block)
      if (step == :pre_finalization && @running_pre_finalization_blocks) || (step == :post_finalization && @running_post_finalization_blocks)
        raise 'A pre_finalization deferred call has been attempted after finalization!' if step == :pre_finalization && @finalized

        block.call
      else
        deferred_blocks[step] << block
      end
    end

    def deferred_blocks
      @deferred_blocks ||= Hash.new { |hash, key| hash[key] = [] }
    end

    def join(values, separator)
      case values
        when Array
          return { 'Fn::Join': [separator, values] } if values.length > 1
          values[0]
        else
          values
      end
    end

    def multi_region?
      stack.multi_region?
    end

    def output_name(variant = nil)
      parts = [project.name.pascal_case, stack.resource_name, resource_name]
      parts << variant unless variant.nil? || variant == :__ref__
      parts.join('-')
    end

    def project
      resource_tree.find { |resource| resource.is_a? Project }
    end

    def run_finalization
      self.class.automatic_children_resources.each { |resource_name| send(resource_name) }

      @running_pre_finalization_blocks = true
      deferred_blocks[:pre_finalization].each(&:call) unless @ran_pre_finalization_blocks
      @ran_pre_finalization_blocks = true

      finalize_resource if respond_to?(:finalize_resource) && !@finalized
      @finalized = true

      @running_post_finalization_blocks = true
      deferred_blocks[:post_finalization].each(&:call) unless @ran_post_finalization_blocks
      @ran_post_finalization_blocks = true
    end

    def run_validation
      valid = self.class.validators.all? { |validator| send(validator) }
      fatal "Unable to validate all properties on `#{self}`." unless valid
      valid
    end

    def ref(target, attribute = nil)
      case target
        when Array
          raise 'Ref target Arrays may only contain subclasses of Resource or valid resource ids.' unless target.all? { |t| t.is_a?(Resource) || (t.is_a?(String) && id_validator.match(t)) }

          target.collect { |t| ResourceReference.new(self, t, attribute) }
        else
          ResourceReference.new(self, target, attribute)
      end
    end

    def resource_name(suffix = nil)
      parts = [sanitized_name, self.class.name.rpartition('::')[-1]]
      parts << suffix unless suffix.nil? || suffix == :__ref__
      parts.join('_').pascal_case.to_sym
    end

    def resource_tree
      parents = parent_resource.respond_to?(:resource_tree) ? parent_resource.resource_tree : [parent_resource]
      parents = parents.reject(&:nil?) + [self]

      project_count = parents.select { |p| p.is_a?(Project) }.count
      stack_count = parents.select { |p| p.is_a?(Stack) }.count

      error "Invalid resource tree for #{self}. Found #{project_count} Projects and #{stack_count} Stacks." if (project_count.zero? || stack_count.zero?) && !(parents[1].is_a?(Environment) || parents[1].is_a?(Image))

      parents
    end

    def sanitized_name
      name
    end

    def secret(material)
      project.secret(material)
    end

    def select(index, list)
      { 'Fn::Select': [index.to_s, list] }
    end

    def service_root
      # Finds the closest service_root to this one, thus transversing the tree in reverse order.
      resource_tree.reverse.find(&:service?)
    end

    def service?
      false
    end

    def service_id
      parts = [
        project.stage,
        project.name,
        active_environment_name
      ]

      suffix = parts.collect { |p| p.camel_case.downcase }.join('-')
      "#{service_name}-#{suffix}".dash_case.to_sym
    end

    def service_name
      service_root.service_name
    end

    def split(deliminator, string)
      { 'Fn::Split': [deliminator, string] }
    end

    def sub(pattern)
      { 'Fn::Sub': pattern }
    end

    def stack
      resource_tree.find { |resource| resource.is_a? Stack }
    end

    def taggable?
      false
    end

    def to_s
      "#<#{self.class}:0x#{__id__.to_s(16)} resource_name=`#{resource_name}`>"
    end

    def inspect
      "#<#{self.class}:0x#{__id__.to_s(16)} resource_name=`#{resource_name}`>"
    end
  end
end
