require 'aws-sdk-core'
require 'retriable'
require 'aws-sdk-elasticloadbalancing'
require 'aws-sdk-autoscaling'

module AutoCanary24
  class CanaryStack
    attr_reader :stack_name

    def initialize(stack_name, wait_timeout, sleep_during_wait = 5)
      raise "ERR: stack_name is missing" if stack_name.nil?
      raise "ERR: wait_timeout is missing" if wait_timeout.nil?
      @stack_name = stack_name
      @wait_timeout = wait_timeout
      @sleep_during_wait = sleep_during_wait
    end

    def get_desired_capacity
      asg = get_autoscaling_group
      asg_client = Aws::AutoScaling::Client.new
      describe_asg(asg).desired_capacity
    end

    def set_desired_capacity_and_wait(desired_capacity)
      asg = get_autoscaling_group
      asg_client = Aws::AutoScaling::Client.new
      resp = asg_client.set_desired_capacity({
        auto_scaling_group_name: asg,
        desired_capacity: desired_capacity,
        honor_cooldown: false,
      })
      wait_for_instances_in_asg(asg, desired_capacity)
    end

    def is_attached_to(elb)
      asg = get_autoscaling_group
      elbs = get_attached_loadbalancers(asg) unless asg.nil?
      (!elbs.nil? && elbs.any? { |e| e.load_balancer_name == elb && e.state != "Removing" && e.state != "Removed" })
    end

    def attach_instances_to_elb_and_wait(elb, instances)
      elb_client = Aws::ElasticLoadBalancing::Client.new
      elb_client.register_instances_with_load_balancer({ load_balancer_name: elb, instances: instances })
      wait_for_instances_attached_to_elb(instances, elb)
    end

    def detach_instances_from_elb(elb, instances)
      elb_client = Aws::ElasticLoadBalancing::Client.new
      elb_client.deregister_instances_from_load_balancer({ load_balancer_name: elb, instances: instances })
    end

    def detach_asg_from_elb_and_wait(elb)
      asg = get_autoscaling_group
      asg_client = Aws::AutoScaling::Client.new
      asg_client.detach_load_balancers({auto_scaling_group_name: asg, load_balancer_names: [elb]})
      wait_for_asg_detached_from_elb(asg, elb)
    end

    def attach_asg_to_elb_and_wait(elb)
      asg = get_autoscaling_group
      asg_client = Aws::AutoScaling::Client.new
      asg_client.attach_load_balancers({auto_scaling_group_name: asg, load_balancer_names: [elb]})
      wait_for_asg_on_elb(asg, elb)
    end

    def suspend_asg_processes
      processes = ['Launch', 'Terminate', 'AddToLoadBalancer', 'AlarmNotification', 'AZRebalance']
      asg = get_autoscaling_group
      asg_client = Aws::AutoScaling::Client.new
      asg_client.suspend_processes({auto_scaling_group_name: asg, scaling_processes: processes})
    end

    def resume_asg_processes
      processes = ['Launch', 'Terminate', 'AddToLoadBalancer', 'AlarmNotification', 'AZRebalance']
      asg = get_autoscaling_group
      asg_client = Aws::AutoScaling::Client.new
      asg_client.resume_processes({auto_scaling_group_name: asg, scaling_processes: processes})
    end

    def get_instance_ids
      asg = get_autoscaling_group
      asg_client = Aws::AutoScaling::Client.new
      describe_asg(asg)[:instances] \
        .select { |i| i[:lifecycle_state]=="InService" } \
        .map{ |i| { instance_id: i[:instance_id] } }
    end

    def is_stack_created?
      begin
        get_instance_ids
      rescue
        return false
      end
      true
    end

    private
    def describe_asg(asg)
      asg_client = Aws::AutoScaling::Client.new
      asg_client.describe_auto_scaling_groups({auto_scaling_group_names: [asg], max_records: 1})[:auto_scaling_groups][0]
    end

    def wait_for_asg_detached_from_elb(asg, elb)
      auto_scaling_group = describe_asg(asg)

      if auto_scaling_group[:load_balancer_names].select{|l| l == elb}.length == 1
        puts "WARNING: ASG still on the ELB!"
      end

      asg_instances = auto_scaling_group[:instances] \
        .select { |i| i[:lifecycle_state]=="InService" } \
        .map{ |i| { instance_id: i[:instance_id] } }

      elb_client = Aws::ElasticLoadBalancing::Client.new
      elb_instances = elb_client.describe_instance_health({load_balancer_name: elb})[:instance_states] \
        .map{ |i| { instance_id: i[:instance_id] } }

      # Remove instances that are not registered at the ELB
      instances = elb_instances & asg_instances

      if instances.length > 0
        wait_for_instances_detached_from_elb(instances, elb)
      end
    end

    def wait_for_instances_detached_from_elb(instances, elb)
      elb_client = Aws::ElasticLoadBalancing::Client.new
      retries = (@wait_timeout / @sleep_during_wait).round
      while retries > 0
        begin
          elb_instances = {}
          Retriable.retriable(on: Aws::CloudFormation::Errors::Throttling, tries: 5, base_interval: 1) do
            elb_instances = elb_client.describe_instance_health({load_balancer_name: elb, instances: instances})
          end
          break if elb_instances[:instance_states].select{ |s| s.state == 'InService' }.length == 0
        rescue Aws::ElasticLoadBalancing::Errors::InvalidInstance
        end
        sleep @sleep_during_wait
        retries -= 1
      end

      raise "Timeout. Couldn't wait for instances '#{instances}' to get detached from ELB '#{elb}'." if retries == 0
    end

    def wait_for_asg_on_elb(asg, elb)
      auto_scaling_group = describe_asg(asg)

      if auto_scaling_group[:load_balancer_names].select{|l| l == elb}.length == 0
        puts "WARNING: ASG not on the ELB yet!"
      end

      instances = auto_scaling_group[:instances] \
        .select { |i| i[:lifecycle_state]=="InService" } \
        .map{ |i| { instance_id: i[:instance_id] } }

      wait_for_instances_attached_to_elb(instances, elb)
    end

    def wait_for_instances_attached_to_elb(instances, elb)
      elb_client = Aws::ElasticLoadBalancing::Client.new
      retries = (@wait_timeout / @sleep_during_wait).round
      while retries > 0
        begin
          elb_instances = elb_client.describe_instance_health({load_balancer_name: elb, instances: instances})
          break if elb_instances[:instance_states].select{ |s| s.state != 'InService' }.length == 0
        rescue Aws::ElasticLoadBalancing::Errors::InvalidInstance
        end
        sleep @sleep_during_wait
        retries -= 1
      end

      raise "Timeout. Couldn't wait for instances '#{instances}' to get attached to ELB '#{elb}'." if retries == 0
    end

    def wait_for_instances_in_asg(asg, expected_number_of_instances)
      asg_client = Aws::AutoScaling::Client.new
      retries = (@wait_timeout / @sleep_during_wait).round
      while retries > 0
        instances = asg_client.describe_auto_scaling_groups({auto_scaling_group_names: [asg]})[:auto_scaling_groups][0].instances
        healthy_instances = instances.select{ |i| i[:health_status] == "Healthy" && i[:lifecycle_state]=="InService"}.length
        break if healthy_instances == expected_number_of_instances
        sleep @sleep_during_wait
        retries -= 1
      end

      raise "Timeout. Only #{healthy_instances} of #{expected_number_of_instances} instances got healthy in ASG '#{asg}'." if retries == 0
    end

    def get_attached_loadbalancers(asg)
      asg_client = Aws::AutoScaling::Client.new
      asg_client.describe_load_balancers({ auto_scaling_group_name: asg }).load_balancers
    end

    def get_autoscaling_group
      get_first_resource_id('AWS::AutoScaling::AutoScalingGroup')
    end

    def get_first_resource_id(resource_type)
      client = Aws::CloudFormation::Client.new
      begin
        response = client.list_stack_resources({ stack_name: @stack_name })
      rescue Exception
        return nil
      end
      resources = response.data.stack_resource_summaries.select{|x| x[:resource_type] == resource_type }
      resources.map{ |e| e.physical_resource_id }[0]
    end
  end
end
