# frozen_string_literal: true

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

module Litany
  class ElasticsearchDomain < Resource
    include Stack
    include Taggable

    cfn_type 'AWS::Elasticsearch::Domain'
    parent_resource :service

    child_resource IAMPolicyDocument, :policy_document, automatic: :on_access

    flag :encrypt, true
    flag :zone_aware, true

    property :allow_explicit_references, true, [false, true]
    property :fielddata_cache_size, nil, [nil, 1..100]
    property :master_nodes, 0, 0..9
    property :master_node_type, nil, [nil, String] # TODO: Replace with a proper enum
    property :nodes, 4, 0..100
    property :node_type, 'm4.large', String # TODO: Replace with a proper enum
    property :snapshot_hour, 7, 0..23 # In UTC, so defaulting to midnight PST.
    property :version, 5.3, [1.5, 2.3, 5.1, 5.3, 5.5, 5.6, 6.0, 6.2, 6.3, 6.4, 6.5, 6.7]
    property :volume_iops, 0, [0, 100..20_000]
    property :volume_size, 100, 4..16_384
    property :volume_type, :ebs, [:ebs, :instance]


    resource_reference VPC, :vpc, required: false

    resource_references Environment, :environment, required: false
    resource_references Environment, :only_in, required: false

    resource_collection CloudWatchAlarm, :cw_alarm, required: false
    resource_collection SecurityGroup, :security_group, required: false

    validator :security do
      raise 'We do not permit signing to be disabled unless you are using a vpc end point.' if @without_signing && !set_vpc?
    end

    validator :overall_node_count do
      raise "You can only have a combined node count of 100. Current: #{nodes} data nodes; #{master_nodes} master nodes." if (nodes + master_nodes) > 100
    end

    def alarm(alarm_name, &block)
      cw_alarm(:"#{name}_elasticsearch_domain_#{alarm_name}", &block)
    end

    def arn(path:)
      arn = join(['arn:aws:es', { Ref: 'AWS::Region' }, { Ref: 'AWS::AccountId' }, 'domain'], ':')
      join([arn, name.tr('_', '-'), path], '/')
    end

    def allow_v2(permission = :read, path: '*', ips: nil, account: { Ref: 'AWS::AccountId' }, role: nil, user: nil, beanstalk: nil, environment: nil, ecs: nil, ecs_task: nil, without_signing: false)
      # TODO: replace all usages of allow with this function and rename.
      valid_permissions = [:full, :read, :read_write, :write]
      raise "Permissions must be one of the following: #{valid_permissions.inspect}. Received `#{permission.inspect}`." unless valid_permissions.include?(permission)
      raise "You may not specify both `ips` and a `beanstalk` and `environment` combination." unless ips.nil? || beanstalk.nil?
      raise "If you specify `beanstalk` or `environment` you must specify both." if beanstalk.nil? ^ environment.nil?
      raise 'You may not provide both a `role` and a `ecs` or `ecs_task` value.' if !role.nil? && (!ecs.nil? || !ecs_task.nil?)

      actions = case permission
        when :full
          'es:*'
        when :read
          ['es:ESHttpGet', 'es:ESHttpHead']
        when :write
          ['es:ESHttpDelete', 'es:ESHttpPost', 'es:ESHttpPut']
        when :read_write
          ['es:ESHttpGet', 'es:ESHttpHead', 'es:ESHttpDelete', 'es:ESHttpPost', 'es:ESHttpPut']
      end

      statement = policy_document.next_statement
      statement.actions actions
      statement.on_resource arn(path: path) # Cannot use a ref here because it creates a circular reference inside the CFN template.

      if without_signing
        statement.principal({ AWS: '*' })
        @without_signing = true
      else
        statement.allow_principal account: account, role: role, user: user, beanstalk: beanstalk, environment: environment, ecs: ecs, ecs_task: ecs_task
      end

      deferred do
        raise 'You must provide `ips`, `beanstalk`, or `ecs` when using `allow_v2` on a vpc attached elasticsearch domain.' if set_vpc? && ips.nil? && beanstalk.nil? && ecs.nil?

        source = ips unless ips.nil?
        source = Beanstalk.new(beanstalk).beanstalk_environment(environment).reference_sg unless beanstalk.nil?
        source = ECSService.new(ecs).reference_sg unless ecs.nil?
        reference_sg_sources << source unless source.nil?
      end
    end

    def allow(principal, permission = :read, path: '*', ips: nil)
      valid_permissions = [:full, :read, :read_write, :write]
      raise "Permissions must be one of the following: #{valid_permissions.inspect}. Received `#{permission.inspect}`." unless valid_permissions.include?(permission)

      actions = case permission
        when :full
          'es:*'
        when :read
          ['es:ESHttpGet', 'es:ESHttpHead']
        when :write
          ['es:ESHttpDelete', 'es:ESHttpPost', 'es:ESHttpPut']
        when :read_write
          ['es:ESHttpGet', 'es:ESHttpHead', 'es:ESHttpDelete', 'es:ESHttpPost', 'es:ESHttpPut']
      end

      resource_ref = {
        'Fn::Join': [
          '/',
          [
            {
              'Fn::Join': [
                ':',
                [
                  'arn:aws:es', { Ref: 'AWS::Region' }, { Ref: 'AWS::AccountId' }, 'domain'
                ]
              ]
            },
            name.tr('_', '-'),
            path
          ]
        ]
      }

      principal = {
        AWS: principal
      }

      condition = {
        IpAddress: {
          'aws:SourceIp': ips
        }
      }

      statement = policy_document.next_statement
      statement.actions(actions)
      statement.principal(principal)
      statement.on_resource(resource_ref)
      statement.condition(condition) unless ips.nil?

    end

    def finalize_resource
      return unless set_vpc?

      reference_sg.description("Used to reference to the #{name} Elasticsearch Domain.")
      reference_sg_sources.each { |source| reference_sg.ingress port: 443, source: source }
    end

    def properties
      props = {
        AdvancedOptions: {
          'rest.action.multi.allow_explicit_index': allow_explicit_references
        },
        DomainName: name.tr('_', '-'),
        ElasticsearchClusterConfig: {
          InstanceCount: nodes,
          InstanceType: "#{node_type}.elasticsearch",
          ZoneAwarenessEnabled: zone_aware?
        },
        ElasticsearchVersion: version.to_s,
        SnapshotOptions: {
          AutomatedSnapshotStartHour: snapshot_hour
        }
      }

      props[:AccessPolicies] = policy_document if set_policy_document?

      if master_nodes.positive?
        props[:ElasticsearchClusterConfig][:DedicatedMasterCount] = master_nodes
        props[:ElasticsearchClusterConfig][:DedicatedMasterEnabled] = true
        props[:ElasticsearchClusterConfig][:DedicatedMasterType] = "#{master_node_type || node_type}.elasticsearch"
      end

      unless volume_type == :instance
        props[:EBSOptions] = {
          EBSEnabled: true,
          VolumeSize: volume_size,
          VolumeType: :gp2
        }

        if volume_iops.positive?
          props[:EBSOptions][:Iops] = volume_iops
          props[:EBSOptions][:VolumeType] = :io1
        end
      end

      if encrypt?
        props[:EncryptionAtRestOptions] = { Enabled: true }
        props[:NodeToNodeEncryptionOptions] = { Enabled: true }
      end

      props[:AdvancedOptions][:'indices.fielddata.cache.size'] = "#{fielddata_cache_size}%" if set_fielddata_cache_size?
      props[:VPCOptions] = { SecurityGroupIds: [ref(reference_sg)], SubnetIds: ref(vpc.subnets[0..1]) } if set_vpc?

      props
    end

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

    def reference_sg_sources
      @reference_sg_sources ||= []
    end

    def default_tags
      {
        LitanyESDomainName: self.name
      }
    end
  end
end
