# frozen_string_literal: true

require 'litany/mixins'
require 'litany/resource'

module Litany
  class BeanstalkEnvironment < Resource
    include Taggable
    class << self
      attr_accessor :stack_mapping
    end

    self.stack_mapping = {
      # TODO: There's a lot I'm not pulling in up front.  This should really be an enum.
      packer_2_3_4: '64bit Amazon Linux 2017.09 v2.3.4 running Packer 1.0.3',
      docker_2_8_0: '64bit Amazon Linux 2017.09 v2.8.0 running Docker 17.06.2-ce',
      docker_multi_2_7_5: '64bit Amazon Linux 2017.03 v2.7.5 running Multi-container Docker 17.03.2-ce (Generic)',
      go_2_7_1: '64bit Amazon Linux 2017.09 v2.7.1 running Go 1.9',
      java8_2_5_5: '64bit Amazon Linux 2017.03 v2.5.5 running Java 8',
      java7_2_5_5: '64bit Amazon Linux 2017.03 v2.5.5 running Java 7',
      windows2016_1_2_0: '64bit Windows Server 2016 v1.2.0 running IIS 10.0',
      windows2016_core_1_2_0: '64bit Windows Server Core 2016 v1.2.0 running IIS 10.0',
      php_5_5: '64bit Amazon Linux 2017.09 v2.6.4 running PHP 5.5'
    }

    cfn_type 'AWS::ElasticBeanstalk::Environment'
    parent_resource :application

    child_resource InstanceProfile, :instance_profile

    # ASG
    property :cluster_size, 2, [Integer, Range], inherited: true

    # ASG Triggers
    property :breach_duration, 5, 1..600
    property :lower_threshold, 15, 0..20_000_000
    property :measure_name, :cpu_utilization, [:cpu_utilization, :network_in, :network_out, :disk_read_ops, :disk_write_ops, :disk_read_bytes, :disk_write_bytes, :latency, :request_count, :healthy_host_count, :unhealthy_host_count]
    property :period, 10, 1..60
    property :scale_increment, 1, 1..1000
    property :statistic, :average, [:average, :maximum, :minimum, :sum]
    property :unit, :percent, [:bits, :bits_second, :bytes, :bytes_second, :count, :count_second, :none, :percent, :seconds]
    property :upper_threshold, 40, 0..20_000_000

    # ASG LC
    property :device_prefix, 'sd', ['sd', 'xvd'], inherited: true
    property :ephemeral_volumes, 0, 0..26
    property :instance_type, nil, [nil, String], inherited: true
    property :key_pair, nil, [nil, String, Symbol], inherited: true
    property :monitoring_interval, 1, [1, 5]
    property :root_volume_size, 50, 4..16_384, inherited: true

    resource_references SecurityGroup, :instance_security_group, required: false
    resource_references AutoScalingVolume, :volume, inherited: true, required: false

    # VPC
    flag :internal, false, inherited: true
    flag :rolling_updates, true, inherited: true

    resource_reference VPC, :vpc, inherited: true

    # Cloud Watch
    property :log_retention, 365, [1, 3, 5, 7, 14, 30, 60, 90, 120, 150, 180, 365, 400, 545, 731, 1827, 3653], inherited: true

    # EB Deployment Policy
    flag :ignore_health_check, false
    property :deployment_policy, :rolling, [:rolling, :rolling_with_additional_batch, :all_at_once, :immutable]
    property :deployment_batch_size, 1, Integer

    # Healthchecks
    property :deregistration_delay, 10, [5..3600]
    property :health_check_interval, 5, [5..300], inherited: true
    property :health_check_path, '/', [String], inherited: true
    property :health_check_port, nil, [nil, Integer, String], inherited: true, &:to_s
    property :health_check_protocol, nil, [nil, :http, :https, :tcp, :HTTP, :HTTPS, :TCP], inherited: true, &:upcase
    property :health_check_timeout, 2, [2..60], inherited: true
    property :healthy_threshold, 3, [2..10], inherited: true
    property :healthy_statuses, '200', [String, Integer, Range, Array], inherited: true do |value|
      case value
        when Array
          raise 'Invalid value for `healthy_statuses` array. Only Integers and Ranges allowed.' unless value.all? { |v| v.is_a?(Range) || v.is_a?(Integer) }
          value.collect { |status| status.is_a?(Range) ? "#{status.first}-#{status.last}" : status.to_s }.join(',').to_s
        when Integer
          value.to_s
        when Range
          "#{value.first}-#{value.last}"
        else
          raise 'Invalid value for `healthy_statuses` string. Only comma separated Integers and Ranges allowed.' unless value.split(',').all? { |v| /(^\d+$)|(^\d+-\d+$)/.match(v) }
          value
      end
    end
    property :unhealthy_threshold, 2, [2..10]

    # ALBS
    flag :sticky_sessions, false
    flag :support_xp, false

    property :access_logs_prefix, nil, [String, Symbol, nil]
    property :idle_timeout, 60, [1..3600]

    resource_collection DNSAlias, :dns_alias, required: false
    resource_collection SecurityGroup, :security_group, required: false

    resource_reference Certificate, :certificate, required: false, inherited: true
    resource_reference S3Bucket, :access_logs_bucket, required: false
    resource_references SecurityGroup, :lb_security_group, required: false


    # Local Resource Settings
    # Not sure what to call these, but they're the general settings you'd see make it ways into properties

    flag :instance_crosstalk, false, inherited: true

    property :application_name, nil, [String, Symbol], inherited: true
    property :cname, nil, [nil, String]
    property :description, nil, [nil, String]
    property :environment_name, nil, [String, Symbol]
    property :stack_name, nil, stack_mapping.keys, inherited: true

    resource_collection BeanstalkOptionSetting, :option, required: false

    validator(:lb_jeopardy) do
      raise 'You cannot use both a NLB and a ALB for this service.  This is likely a Litany bug.' if listener_port_protocols.include?(:tcp) && (listener_port_protocols.include?(:http) || listener_port_protocols.include?(:https))

      if load_balancer_type == :network
        raise 'When using a NLB `health_check_interval` only accepts 10 and 30.' if set_health_check_interval? && ![10, 30].include?(health_check_interval)
        raise 'You may not set a distinct `unhealthy_threshold` when using NLBs.' if set_unhealthy_threshold?
        raise 'You may not set a `certificate` with a NLB.' if set_certificate?
        raise 'You may not set `access_logs_bucket` when using a NLB.' if set_access_logs_bucket?
      end
    end

    validator :ssl_configuration do
      raise 'You must set a `certificate` if you flag an ingress port for https.' unless set_certificate? || !listener_port_protocols.values.include?(:https)
    end

    def add_alias(name, zone = nil, latency: nil)
      # TODO: I'm not sure if we can setup a healthcheck on these or not
      record = dns_alias(name)
      record.target self
      record.zone zone || name

      latency = vpc.environments.length > 1 if latency.nil?
      record.latency_routing if latency
    end

    def composite_name
      "#{application_name}-#{environment_name}".tr('_', '-')
    end

    def depends_on
      [application.resource_name, application.service.service_role.resource_name]
    end

    def dns_name
      return '' if active_environment.nil?

      cname = set_cname? ? cname : composite_name
      "#{cname}.#{active_environment.region}.elasticbeanstalk.com"
    end

    def set_option(namespace, option, value)
      opt = option(:"#{composite_name.snake_case}_#{namespace.snake_case}_#{option.snake_case}")
      opt.namespace(namespace)
      opt.option(option)
      opt.value(value)
      opt
    end

    def finalize_resource
      instance_profile.role.managed_policy('arn:aws:iam::aws:policy/AWSElasticBeanstalkWebTier')
      instance_profile.role.managed_policy('arn:aws:iam::aws:policy/AWSElasticBeanstalkMulticontainerDocker')
      instance_profile.role.managed_policy('arn:aws:iam::aws:policy/AWSElasticBeanstalkWorkerTier')

      if instance_crosstalk?
        crosstalk_sg = security_group(:"#{composite_name.snake_case}_beanstalk_crosstalk")
        crosstalk_sg.description 'Allow nodes to have crosstalk between themselves.'
        crosstalk_sg.ingress port: 80, source: reference_sg

        instance_security_group crosstalk_sg
      end

      # ASG Settings
      set_option('aws:autoscaling:asg', 'MaxSize', cluster_size.is_a?(Range) ? cluster_size.max : cluster_size)
      set_option('aws:autoscaling:asg', 'MinSize', cluster_size.is_a?(Range) ? cluster_size.min : cluster_size)

      # ASG Triggers
      set_option('aws:autoscaling:trigger', 'BreachDuration', breach_duration)
      set_option('aws:autoscaling:trigger', 'LowerBreachScaleIncrement', scale_increment * -1)
      set_option('aws:autoscaling:trigger', 'LowerThreshold', lower_threshold)
      set_option('aws:autoscaling:trigger', 'MeasureName', measure_name.pascal_case)
      set_option('aws:autoscaling:trigger', 'Period', period)
      set_option('aws:autoscaling:trigger', 'Statistic', statistic.pascal_case)
      set_option('aws:autoscaling:trigger', 'Unit', unit.split('_').map(&:pascal_case).join('/'))
      set_option('aws:autoscaling:trigger', 'UpperBreachScaleIncrement', scale_increment)
      set_option('aws:autoscaling:trigger', 'UpperThreshold', upper_threshold)

      # ASG LC Settings
      set_option('aws:autoscaling:launchconfiguration', 'EC2KeyName', key_pair) if set_key_pair?
      set_option('aws:autoscaling:launchconfiguration', 'IamInstanceProfile', ref(instance_profile))
      set_option('aws:autoscaling:launchconfiguration', 'InstanceType', instance_type) if set_instance_type?
      set_option('aws:autoscaling:launchconfiguration', 'MonitoringInterval', "#{monitoring_interval} Minute")
      set_option('aws:autoscaling:launchconfiguration', 'RootVolumeSize', root_volume_size)
      set_option('aws:autoscaling:launchconfiguration', 'RootVolumeType', 'gp2')

      reference_sg.description "Used to reference instances of the #{name} Beanstalk Environment."
      set_option('aws:autoscaling:launchconfiguration', 'SecurityGroups', join(ref([reference_sg] + instance_security_groups), ','))

      # compiling these are kinda awkward
      devices = volumes.collect(&:to_eb_string)
      ephemeral_volumes.times { |index| devices << "/dev/#{device_prefix}#{('b'.ord + index).chr}=ephemeral#{index}}" }
      set_option('aws:autoscaling:launchconfiguration', 'BlockDeviceMappings', devices.join(',')) unless devices.empty?

      # VPC Settings
      set_option('aws:ec2:vpc', 'VPCId', ref(vpc))
      set_option('aws:ec2:vpc', 'Subnets', join(ref(vpc.subnets), ','))
      set_option('aws:ec2:vpc', 'ELBSubnets', join(ref(vpc.subnets), ','))
      set_option('aws:ec2:vpc', 'ELBScheme', 'internal') if internal?
      set_option('aws:ec2:vpc', 'AssociatePublicIpAddress', true)

      # EB Application Settings
      set_option('aws:elasticbeanstalk:application', 'Application Healthcheck URL', health_check_path)

      # CloudWatch Log Settings
      set_option('aws:elasticbeanstalk:cloudwatch:logs', 'StreamLogs', true)
      set_option('aws:elasticbeanstalk:cloudwatch:logs', 'RetentionInDays', log_retention)
      set_option('aws:elasticbeanstalk:hostmanager', 'LogPublicationControl', true)

      # EB Deployment Policy
      set_option('aws:elasticbeanstalk:command', 'DeploymentPolicy', deployment_policy.pascal_case)
      set_option('aws:elasticbeanstalk:command', 'BatchSizeType', 'Fixed')
      set_option('aws:elasticbeanstalk:command', 'BatchSize', deployment_batch_size)
      set_option('aws:elasticbeanstalk:command', 'IgnoreHealthCheck', ignore_health_check?)

      # Update Policy

      if rolling_updates?
        set_option('aws:autoscaling:updatepolicy:rollingupdate', 'MaxBatchSize', 1)
        set_option('aws:autoscaling:updatepolicy:rollingupdate', 'RollingUpdateEnabled', true)
        set_option('aws:autoscaling:updatepolicy:rollingupdate', 'RollingUpdateType', 'Health')
        set_option('aws:autoscaling:updatepolicy:rollingupdate', 'Timeout', 'PT45M')

        min_desired = cluster_size.is_a?(Range) ? cluster_size.min : cluster_size
        max_desired = cluster_size.is_a?(Range) ? cluster_size.max : cluster_size

        if min_desired == max_desired && min_desired > 1
          set_option('aws:autoscaling:updatepolicy:rollingupdate', 'MinInstancesInService', min_desired - 1)
        elsif min_desired < max_desired
          set_option('aws:autoscaling:updatepolicy:rollingupdate', 'MinInstancesInService', min_desired)
        end
      else
        set_option('aws:autoscaling:updatepolicy:rollingupdate', 'RollingUpdateEnabled', false)
      end

      # EB Environment Settings
      set_option('aws:elasticbeanstalk:environment', 'ServiceRole', ref(application.service.service_role))
      set_option('aws:elasticbeanstalk:environment', 'LoadBalancerType', load_balancer_type)

      # EB Default Process Healthchecks
      hc_protocol = health_check_protocol || (load_balancer_type == :application ? :http : :tcp)
      health_check_interval 10 if load_balancer_type == :network && !set_health_check_interval?

      set_option('aws:elasticbeanstalk:environment:process:default', 'DeregistrationDelay', deregistration_delay)
      set_option('aws:elasticbeanstalk:environment:process:default', 'HealthCheckInterval', health_check_interval)
      set_option('aws:elasticbeanstalk:environment:process:default', 'HealthyThresholdCount', healthy_threshold)
      set_option('aws:elasticbeanstalk:environment:process:default', 'Port', health_check_port) unless health_check_port.nil?
      set_option('aws:elasticbeanstalk:environment:process:default', 'Protocol', hc_protocol.upcase)
      set_option('aws:elasticbeanstalk:environment:process:default', 'StickinessEnabled', true) if sticky_sessions?
      set_option('aws:elasticbeanstalk:environment:process:default', 'UnhealthyThresholdCount', load_balancer_type == :network ? healthy_threshold : unhealthy_threshold)

      unless hc_protocol == :tcp
        set_option('aws:elasticbeanstalk:environment:process:default', 'HealthCheckTimeout', health_check_timeout)
        set_option('aws:elasticbeanstalk:environment:process:default', 'HealthCheckPath', health_check_path)
        set_option('aws:elasticbeanstalk:environment:process:default', 'MatcherHTTPCode', healthy_statuses)
      end

      # ALB Settings
      if set_access_logs_bucket?
        set_option('aws:elbv2:loadbalancer', 'AccessLogsS3Bucket', ref(access_logs_bucket))
        set_option('aws:elbv2:loadbalancer', 'AccessLogsS3Enabled', true)
        set_option('aws:elbv2:loadbalancer', 'AccessLogsS3Prefix', access_logs_prefix) if set_access_logs_prefix?
      end

      set_option('aws:elbv2:loadbalancer', 'IdleTimeout', idle_timeout) if load_balancer_type == :application

      # ALB Listener Settings
      listener_port_protocols.each do |port, protocol|
        next if port == 80
        set_option("aws:elbv2:listener:#{port}", 'Protocol', protocol.upcase)
        set_option("aws:elbv2:listener:#{port}", 'SSLCertificateArns', ref(certificate)) if protocol == :https && set_certificate?
        set_option("aws:elbv2:listener:#{port}", 'SSLPolicy', 'ELBSecurityPolicy-TLS-1-0-2015-04') if protocol == :https && set_certificate? && support_xp?
      end

      listener_port_sources.each do |port, sources|
        if load_balancer_type == :application
          lb_security_group :"#{name}_ebs_alb_port_#{port}_ingress"
          sg = security_group :"#{name}_ebs_alb_port_#{port}_ingress"
          sg.description "Ingress for the #{name} beanstalk environment."
        else
          sg = reference_sg
        end

        sg.ingress port: port, source: sources.flatten
      end

      # ALB Security
      if load_balancer_type == :application
        lb_reference_sg.description "Used to reference the load balancer for the #{name} Beanstalk Environment."
        lb_security_group lb_reference_sg

        set_option('aws:elbv2:loadbalancer', 'SecurityGroups', join(ref(lb_security_groups), ',')) if set_lb_security_groups?
        set_option('aws:elbv2:loadbalancer', 'ManagedSecurityGroup', ref(lb_reference_sg))
      end
    end

    def ingress(port:, source: nil, beanstalk: nil, environment: nil, protocol: nil)
      protocol ||= :http if port == 80
      protocol ||= :https if port == 443

      raise 'You must provide either a `source` or a beanstalk environment.' if source.nil? && beanstalk.nil?
      raise 'You may not provide both a `source` and a beanstalk environment in the same `ingress` call.' unless source.nil? || beanstalk.nil?
      raise 'You must specify both `beanstalk` and `environment` to allow ingress from a beanstalk environment' if !beanstalk.nil? && environment.nil?

      raise 'When setting ingress on a non standard port you must provide [:http, :https] for protocol:.' if protocol.nil?
      raise "Invalid protocol `#{protocol}` provided for port #{port}. Must be [:http, :https, :tcp]." unless [:http, :https, :tcp].include?(protocol)
      raise "Port `#{port}` was already registered as `#{listener_port_protocols[port]}`." unless listener_port_protocols[port].nil? || listener_port_protocols[port] == protocol
      raise 'Attempting to add ingress rule after load balancer has been resolved.' if @resolved

      source = Beanstalk.new(beanstalk).beanstalk_environment(environment).reference_sg unless beanstalk.nil?

      begin
        [source].flatten.each { |s| NetAddr::CIDR.create(s) } unless internal?
      rescue ArgumentError, NetAddr::ValidationError
        raise "You may not limit a public load balancer with security group as the source."
      end

      listener_port_sources[port] ||= []
      listener_port_sources[port] << source
      listener_port_protocols[port] = protocol
    end

    def listener_port_protocols
      @listener_port_protocols ||= {}
    end

    def listener_port_sources
      @listener_port_sources ||= {}
    end

    def load_balancer_type
      @resolved = true
      listener_port_protocols.values.include?(:tcp) ? :network : :application
    end

    def lb_reference_sg
      security_group(:"#{name}_ebs_lb_reference")
    end

    def properties
      # Not Implementing: PlatformArn, TemplateName, VersionLabel
      # TODO: Implement Tier
      {
        ApplicationName: application_name,
        CNAMEPrefix: set_cname? ? cname : composite_name,
        Description: description || "#{environment_name} environment for #{application_name}.",
        EnvironmentName: composite_name,
        OptionSettings: options,
        SolutionStackName: self.class.stack_mapping[stack_name]
      }
    end

    def reference_sg
      security_group(:"#{name}_ebs_instance_reference")
    end
  end
end
