# frozen_string_literal: true

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

module Litany
  class S3Bucket < Resource
    include ServiceRoot
    include Stack
    include Taggable

    cfn_type 'AWS::S3::Bucket'

    output :Arn
    output :DomainName
    output

    flag :extended_metrics, true
    flag :basic_naming, false # TODO: autodetect based on number of possible stagings...
    flag :futures_unique_naming, false

    flag :encrypt, false
    flag :versioned, false

    property :access_logs_prefix, nil, [String, Symbol, nil]
    property :custom_name, nil, [nil, String]
    property :error_page, nil, [nil, String]
    property :expire_days, nil, [nil, Integer]
    property :expire_noncurrent_days, nil, [nil, Integer]
    property :index_page, nil, [nil, String]
    property :redirect_to, nil, [nil, /^http(?:s)?:\/\/[a-z0-9.-]+$/]

    property_collection :transition_rule, [], [Hash], required: false

    child_resource S3BucketPolicy, :policy, automatic: :on_access

    resource_collection S3CorsConfiguration, :cors_rule, required: false
    resource_reference S3Bucket, :access_logs_bucket, required: false

    validator(:redirection) do
      raise 'If you set `redirect_to` you may not set `error_page` or `index_page`.' if set_redirect_to? && (set_error_page? || set_index_page?)
    end

    # :public_read_write is also possible, but not allowed
    property :access, :private, [nil, :authenticated_read, :aws_exec_read, :bucket_owner_read, :bucket_owner_full_control, :log_delivery_write, :private, :public_read]

    resource_references Environment, :environment

    validator(:bucket_policy) do
      self.access = nil if set_policy?
    end

    def allow(permission = :read, path: '*', account: { Ref: 'AWS::AccountId' }, role: nil, user: nil, beanstalk: nil, environment: nil, mssql: nil, ecs: nil, ecs_task: nil, redshift: nil)
      valid_permissions = [:full, :read, :read_write, :write, :log]
      raise "Permissions must be one of the following: #{valid_permissions.inspect}. Received `#{permission.inspect}`." unless valid_permissions.include?(permission)

      permission = :read_write if !mssql.nil?

      actions = case permission
        when :full
          's3:*'
        when :read
          ['s3:GetObject*']
        when :write
          ['s3:PutObject*', 's3:DeleteObject*', 's3:RestoreObject', 's3:AbortMultipartUpload', 's3:ListMultipartUploadParts']
        when :log
          # permissions for alb access logs
          ['s3:PutObject*']
        when :read_write
          ['s3:GetObject*', 's3:PutObject*', 's3:DeleteObject*', 's3:RestoreObject', 's3:AbortMultipartUpload', 's3:ListMultipartUploadParts']
      end

      unless mssql.nil?
        RDSMssql.new(mssql).native_backup_bucket(self)
        return
      end

      # grab the aws account used for elbs in the given region
      account = AWS_ELB_ACCOUNT_IDS[active_environment_name] if permission == :log

      statement = policy.document.next_statement
      statement.actions ['s3:ListBucket*', 's3:GetBucketLocation']
      statement.on_resource ref(self, 'Arn')
      statement.allow_principal account: account, role: role, user: user, beanstalk: beanstalk, environment: environment, mssql: mssql, ecs: ecs, ecs_task: ecs_task, redshift: redshift

      statement = policy.document.next_statement
      statement.actions actions
      statement.allow_principal account: account, role: role, user: user, beanstalk: beanstalk, environment: environment, mssql: mssql, ecs: ecs, ecs_task: ecs_task, redshift: redshift
      statement.on_resource join([ref(self, 'Arn'), path], '/')
    end

    def allow_public_read(from_ips: nil)
      statement = policy.document.next_statement
      statement.actions ['s3:GetObject']
      statement.principal(AWS: '*')
      statement.on_resource arn(path: '*')

      statement.condition({ IpAddress: { 'aws:SourceIp': from_ips } }) unless from_ips.nil?
    end

    def allow_ses_access
      statement = policy.document.next_statement
      statement.actions ['s3:PutObject']
      statement.allow_principal service: 'ses.amazonaws.com'
      statement.on_resource join([ref(self, 'Arn'), '*'], '/')
      statement.condition({ StringEquals: { 'aws:Referer': { Ref: 'AWS::AccountId' } } })
    end

    # Used to allow for cross-account replication from a specified IAM role
    # @param account: [string] the account ID were the IAM role is, default: the current account
    # @param role: [string] the name of the IAM role to allow replication by
    def allow_replication_from(account:, role:)
      statement = policy.document.next_statement
      statement.actions ['s3:GetBucketVersioning', 's3:PutBucketVersioning']
      statement.on_resource ref(self, 'Arn')
      statement.allow_principal account: account, role: role


      statement = policy.document.next_statement
      statement.actions ['s3:ReplicateObject', 's3:ReplicateDelete', 's3:ObjectOwnerOverrideToBucketOwner']
      statement.on_resource join([ref(self, 'Arn'), '*'], '/')
      statement.allow_principal account: account, role: role
    end

    def arn(path: nil)
      arn_builder service: :s3, region: nil, account: nil, resource: ref(self), path: path
    end

    def bucket_name
      return service_id.tr('_', '-')[0..62].downcase if futures_unique_naming?

      base_name = basic_naming? ? "#{name}-#{active_environment_pretty_region}" : "#{name}-#{project.stage}-#{active_environment_pretty_region}"
      "#{base_name}-#{Digest::SHA256.hexdigest(base_name)}".tr('_', '-')[0..62].downcase
    end

    def deny_public_read(path)
      statement = policy.document.next_statement
      statement.actions ['s3:GetObject']
      statement.principal(AWS: '*')
      statement.on_resource arn(path: path)
      statement.effect :deny
    end

    def hosted_zone_id
      raise 'Cannot request `hosted_zone_id` before `environment` is set.' unless set_environments?

      {
        'ap-northeast-1': :Z2M4EHUR26P7ZW,
        'ap-northeast-2': :Z3W03O7B5YMIYP,
        'ap-southeast-1': :Z3O0J2DXBE1FTB,
        'ap-southeast-2': :Z1WCIGYICN2BYD,
        'ap-south-1': :Z11RGJOFQNVJUP,
        'ca-central-1': :Z1QDHH18159H29,
        'eu-central-1': :Z21DNDUVLTQW6Q,
        'eu-west-1': :Z1BKCTXD74EZPE,
        'eu-west-2': :Z3GKZC51ZF0DB4,
        'sa-east-1': :Z7KQH4QJS55SO,
        'us-east-1': :Z3AQBSTGFYJSTF,
        'us-east-2': :Z2O1EMRO9K5GLX,
        'us-west-1': :Z2F56UZL2M1ACD,
        'us-west-2': :Z3BJ6K6RIION7M
      }[active_environment.region]
    end

    def properties
      props = {
        BucketName: set_custom_name? ? custom_name : bucket_name
      }

      # Bad pattern here that can cause the set_access? helper return true even when it actually evaluates to nil...
      props[:AccessControl] = access.pascal_case unless access.nil?

      if set_access_logs_bucket?
        props[:LoggingConfiguration] = {
          DestinationBucketName: ref(access_logs_bucket),
          LogFilePrefix: set_access_logs_prefix? ? access_logs_prefix : bucket_name
        }
      end

      if set_cors_rules?
        props[:CorsConfiguration] = {
          CorsRules: cors_rules
        }
      end

      if encrypt?
        props[:BucketEncryption] = {
          ServerSideEncryptionConfiguration: [
            {
              ServerSideEncryptionByDefault: {
                SSEAlgorithm: 'AES256'
              }
            }
          ]
        }
      end

      if set_error_page?
        props[:WebsiteConfiguration] ||= {}
        props[:WebsiteConfiguration][:ErrorDocument] = error_page
      end

      if set_expire_days?
        props[:LifecycleConfiguration] ||= { Rules: [] }
        props[:LifecycleConfiguration][:Rules] <<
          {
            ExpirationInDays: expire_days,
            Status: 'Enabled'
          }
      end

      if set_expire_noncurrent_days?
        props[:LifecycleConfiguration] ||= { Rules: [] }
        props[:LifecycleConfiguration][:Rules] <<
          {
            NoncurrentVersionExpiration: expire_noncurrent_days,
            Status: 'Enabled'
          }
      end

      if extended_metrics?
        props[:MetricsConfigurations] = [
          { Id: 'EntireBucket' }
        ]
      end

      if set_index_page?
        props[:WebsiteConfiguration] ||= {}
        props[:WebsiteConfiguration][:IndexDocument] = index_page
      end

      if set_redirect_to?
        protocol, hostname = redirect_to.split('://')
        props[:WebsiteConfiguration] = {
          RedirectAllRequestsTo: {
            HostName: hostname,
            Protocol: protocol
          }
        }
      end

      if set_transition_rules?
        props[:LifecycleConfiguration] ||= { Rules: [] }
        props[:LifecycleConfiguration][:Rules] << {
          Transitions: transition_rules,
          Status: 'Enabled'
        }
      end

      if versioned?
        props[:VersioningConfiguration] = {
          Status: 'Enabled'
        }
      end

      props
    end

    def restricted
      deferred do
        encrypt
        access_logs_bucket project.security_logging_bucket unless set_access_logs_bucket?
      end
    end

    def transition(after:, to:)
      classes = [:deep_archive, :glacier, :intelligent_tiering, :onezone_ia, :standard_ia]
      raise "Invalid storage class `#{to} given. Must be one of the following: #{classes}" unless self.class._valid_value?(classes, to)
      raise "Invalid `after_days` value `#{after}`.  Must be between 1 and 36,500." unless self.class._valid_value?(1..36_500, after)

      rule = {
        StorageClass: to.upcase,
        TransitionInDays: after
      }

      transition_rule(rule)
    end

    def website_domain
      raise 'Cannot request `website_domain` before `environment` is set.' unless set_environments?

      return 'unknown active_environment, therefor website_domain returned this placeholder' if active_environment.nil?

      region_endpoint = {
        'ap-northeast-1': 's3-website-ap-northeast-1.amazonaws.com',
        'ap-northeast-2': 's3-website.ap-northeast-2.amazonaws.com',
        'ap-southeast-1': 's3-website-ap-southeast-1.amazonaws.com',
        'ap-southeast-2': 's3-website-ap-southeast-2.amazonaws.com',
        'ap-south-1': 's3-website.ap-south-1.amazonaws.com',
        'ca-central-1': 's3-website.ca-central-1.amazonaws.com',
        'eu-central-1': 's3-website.eu-central-1.amazonaws.com',
        'eu-west-1': 's3-website-eu-west-1.amazonaws.com',
        'eu-west-2': 's3-website.eu-west-2.amazonaws.com',
        'sa-east-1': 's3-website-sa-east-1.amazonaws.com',
        'us-east-1': 's3-website-us-east-1.amazonaws.com',
        'us-east-2': 's3-website.us-east-2.amazonaws.com',
        'us-west-1': 's3-website-us-west-1.amazonaws.com',
        'us-west-2': 's3-website-us-west-2.amazonaws.com'
      }[active_environment.region]

      "#{bucket_name}.#{region_endpoint}"
    end

    def default_tags
      {
        LitanyS3BucketName: name
      }
    end
  end
end
