#!/usr/bin/env ruby

require 'thor'
require 'awesome_print'
require 'inifile'
require 'rubygems'
require 'aws-sdk-core'
require 'json'

# =====================================================================================================================
# A little include path tom-foolery ...
path = File.dirname(__FILE__)
# Borrowing from "whiches" gem ...
cmd  = File.basename(__FILE__, '.rb')
exes = []
exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : ['']
ENV['PATH'].split(File::PATH_SEPARATOR).each do |pth|
  exts.each { |ext|
    exe = File.join(pth, "#{cmd}#{ext}")
    exes << exe if File.executable? exe
  }
end
if exes.size > 0
  path = File.dirname(exes[0])
end

# add_path = File.expand_path(File.join(path, '../../lib'))
# $:.unshift(add_path)
# add_path = File.expand_path(File.join(path, 'lib'))
add_path = path
$:.unshift(add_path)

# =====================================================================================================================
class UpdateBucketPolicy < Thor
  KNOWN = %w(38.117.159.162/32 70.151.98.131/32 )
  class_option :verbose,      :type => :boolean
  class_option :debug,        :type => :boolean
  class_option :log_level,    :type => :string, :banner => 'Log level ([:trace, :debug, :info, :step, :warn, :error, :fatal, :todo])', :default => :step
  class_option :bucket,       :type => :string, :default => 'wgen-sto-artifacts'
  class_option :bucketini,    :type => :string
  class_option :bucketprofile,:type => :string
  class_option :basepolicy,   :type => :string
  class_option :policy,       :type => :string
  class_option :iniglob,      :type => :string
  class_option :inifile,      :type => :string
  class_option :profiles,     :type => :array
  class_option :color,        :type => :boolean, :default => false
  class_option :yes,          :type => :boolean
  attr_reader   :accounts, :environments
  attr_accessor :logger

  no_commands do

    require 'mixins/no_commands'
    include Amplify::AWS::MixIns::NoCommands

    def get_environments
      logger.step 'Get environments ...'

      @environments = {}
      @nats         = {}
      @accounts.each do |acc|
        @options.merge!(acc.get_options())
        parse_options
        get_environments_for_account
      end
      logger.info "Environments:\n"+@environments.ai
      logger.info "NATs:\n"+@nats.ai

    end

    def get_environments_for_account
      logger.step "Get environments for #{@options[:account]} ..."

      config = @options

      # noinspection RubyArgCount
      cfn = Aws::CloudFormation::Client.new(retry_limit: 10)

      stacks = []
      resp = cfn.list_stacks()

      stacks << resp[:stack_summaries]
      while resp[:next_token]
        resp = cfn.list_stacks(next_token: resp[:next_token])
        logger.debug resp.size
        stacks << resp[:stack_summaries]
      end
      stacks.flatten!
      stacks = stacks.select{ |stack| stack[:stack_status].match(%r'^(UPDATE|CREATE).*?_COMPLETE$')}
      logger.debug stacks.ai

      stacks.each do |stack|
        env = nil
        resp = cfn.describe_stacks(stack_name: stack[:stack_id])
        stck = resp[:stacks].shift
        tags = stck[:tags]
        tags.each do |tag|
          if tag[:key] == 'EnvironmentName'
            env = tag[:value]
            break
          end
        end
        unless env
          env = stack[:stack_name]
        end
        @environments[env] ||= []
        @environments[env] << stack[:stack_name]

        resources = []
        resp = cfn.describe_stack_resources(stack_name: stack[:stack_id], logical_resource_id: 'NATIPAddress')
        resources << resp[:stack_resources]
        resp = cfn.describe_stack_resources(stack_name: stack[:stack_id], logical_resource_id: 'BastionIPAddress')
        resources << resp[:stack_resources]
        resources.flatten!
        logger.debug resources.ai

        _nats = resources.map{ |r|
          r.to_h[:physical_resource_id]
        }

        @nats[env] = @nats[env] ? [ @nats[env], _nats ].flatten : _nats
      end

    end

    def get_eips_for_account
      logger.step "Get EIPs for #{@options[:inifile]} ..."

      # noinspection RubyArgCount
      ec2 = Aws::EC2::Client.new(retry_limit: 10)

      resp = ec2.describe_addresses()

      @eips << resp[:addresses].map{ |a| a[:public_ip] }
      @eips.flatten!
      logger.debug @eips.ai

    end

    def get_eips
      logger.step 'Get EIPs ...'

      @eips         = []
      @accounts.each do |acc|
        @options.merge!(acc.get_options())
        parse_options
        get_eips_for_account
      end
      @eips.uniq!
      logger.info "EIPs:\n"+@eips.sort_by { |ip| ip.split(%r'[\./]').map{ |octet| octet.to_i} }.ai

    end

    def read_bucket_policy

      parse_bucket_options(__method__, [ :policy, [:bucketini, :bucketprofile], :bucket ])

      logger.step "Read the current policy for #{@options[:bucket]} ..."

      # noinspection RubyArgCount
      s3 = Aws::S3::Client.new()

      resp = s3.get_bucket_policy(bucket: options[:bucket])
      logger.debug resp.size
      @policies = []
      resp.each do |item|
        json = item.policy.read
        @policies << JSON.load(json)

        logger.debug JSON.pretty_generate(@policies[-1], { indent: "\t", space: ' '})
      end

      logger.debug @policies.ai

      json = ''
      @policies.each do |pol|
        json += JSON.pretty_generate(pol, { indent: "\t", space: ' '})
      end
      IO.write("#{options[:policy]}", json)

    end

    def inspect_bucket_policy

      parse_bucket_options(__method__, [ :policy, [:bucketini, :bucketprofile], :bucket ])

      logger.step "Inspect the policy for #{@options[:bucket]} ..."

      config = @options.dup
      tempfile =
      @options[:policy] = Tempfile.new('policy').path+'.json'
      read_bucket_policy()
      @options = config

      get_eips()

      get_environments()

      ips = [ @nats.values, @eips ].flatten.map{ |ip| "#{ip}/32"}
      ips = [ ips, KNOWN ].flatten.uniq.select{ |ip| not ip.nil? }
      ips = ips.sort_by { |ip| ip.split(%r'[\./]').map{ |octet| octet.to_i} }.uniq
      logger.info "Valid IPs: \n"+ips.ai

      json = ''
      @policies.each do |pol|
        unknown = 0
        missing = 0
        unadded = 0
        if pol['Statement']
          pol['Statement'].each do |smt|
            logger.debug smt.ai
            if smt['Condition'] and smt['Condition']['IpAddress'] and smt['Condition']['IpAddress']['aws:SourceIp']
              smt['Condition']['IpAddress']['aws:SourceIp'].sort_by { |ip| ip.split(%r'[\./]').map{ |octet| octet.to_i} }.each do |entry|
                unless ips.include?(entry)
                  logger.warn "#{entry} unknown"
                  unknown += 1
                end
              end
              ips.each do |entry|
                unless smt['Condition']['IpAddress']['aws:SourceIp'].include?(entry)
                  logger.info "#{entry} missing"
                  missing += 1
                  smt['Condition']['IpAddress']['aws:SourceIp'] << entry
                end
              end
              if missing > 0
                ips.each do |entry|
                  unless smt['Condition']['IpAddress']['aws:SourceIp'].include?(entry)
                    logger.warn "#{entry} missing"
                    unadded += 1
                  end
                end
              end
              smt['Condition']['IpAddress']['aws:SourceIp'] = smt['Condition']['IpAddress']['aws:SourceIp'].sort_by { |ip| ip.split(%r'[\./]').map{ |octet| octet.to_i} }
            end
          end
        else
          logger.warn "Policy has no Statement: #{pol.ai}"
        end
        if missing+unknown > 0
          logger.warn "Policy needs to be updated for #{unknown} unknown IPs, #{missing} missing and #{unadded} IPs which failed to add"
        end
        # require 'date_time'
        logger.debug JSON.pretty_generate(pol, { indent: "\t", space: ' '})
        pol['Id'] = "Policy-#{DateTime.now.strftime('%Y%m%dT%H%M%S')}"
        json += JSON.pretty_generate(pol, { indent: "\t", space: ' '})
      end
      IO.write(options[:policy], json)

    end

    def update_bucket_policy

      parse_bucket_options(__method__, [ :policy, [:bucketini, :bucketprofile], :bucket ])

      logger.step "Update the current policy for #{@options[:bucket]} ..."

      inspect_bucket_policy

      write_bucket_policy

    end

    def write_bucket_policy

      parse_bucket_options(__method__, [ :policy, [:bucketini, :bucketprofile], :bucket ])

      logger.step "Write the new policy for #{@options[:bucket]} ..."

      @options[:inifile] = @options[:bucketini]
      parse_options
      @logger.info options.ai

      json = IO.read(options[:policy])

      # noinspection RubyArgCount
      s3 = Aws::S3::Client.new()

      resp = s3.put_bucket_policy(
                                    bucket: options[:bucket],
                                    policy: json
                                  )
      logger.debug resp.ai
      if resp.error
        logger.error resp.error
      end
    end

    def parse_bucket_options(method,optionset)
      check_missing_options(method, optionset)

      if @options[:bucketini]
        @options[:inifile] = @options[:bucketini]
      else
        @options[:profile] = @options[:bucketprofile]
      end
      parse_options
      @logger.info options.ai
    end
  end

  def initialize(args = [], local_options = {}, config = {})
    super(args,local_options,config)
  end

  desc 'read', 'read the policy for BUCKET'
  def read()
    prepare_accounts

    read_bucket_policy

    n = 1
    @policies.each do |pol|
      logger.step "#{n+=1}\n"+JSON.pretty_generate(pol, { indent: "\t", space: ' '})
    end

    0
  end

  desc 'inspect', 'inspect the policy for BUCKET'
  def inspect()
    prepare_accounts()

    inspect_bucket_policy()

    @policies.each do |pol|
      logger.debug JSON.pretty_generate(pol, { indent: "\t", space: ' '})
    end
    0
  end

  desc 'update', 'update the policy for BUCKET'
  def update()
    prepare_accounts()

    update_bucket_policy()

    0
  end

  desc 'write', 'write the new POLICY to BUCKET'
  def write()
    prepare_accounts
    @options[:inifile] = @options[:bucketini]
    parse_options
    @logger.info options.ai

    write_bucket_policy

    0
  end

end

# =====================================================================================================================
# UpdateBucketPolicy.start(ARGV)


__END__
					"aws:SourceIp": [
						"54.84.38.255/32",
						"54.86.79.230/32",
						"54.84.206.23/32",
						"54.172.63.6/32",
						"54.86.33.222/32",
						"38.117.159.162/32",
						"70.151.98.131/32",
						"54.208.252.192/32",
						"54.86.6.21/32",
						"54.86.156.100/32",
						"54.209.155.115/32",
						"107.23.90.79/32",
						"54.172.109.59/32",
						"54.209.93.210/32",
						"54.88.205.140/32",
						"54.209.196.100/32",
						"54.84.6.106/32",
						"54.165.122.109/32",
						"107.21.36.141/32",
						"54.86.150.196/32",
						"54.209.17.121/32",
						"54.84.31.228/32",
						"54.88.2.227/32",
						"54.85.80.166/32",
						"54.86.31.209/32",
						"50.19.114.108/32",
						"54.85.159.92/32",
						"54.85.129.109/32",
						"54.165.86.129/32",
						"54.165.114.103/32",
						"54.172.8.125/32",
						"54.86.144.175/32",
						"107.23.57.237/32",
						"54.165.231.248/32",
						"54.165.228.106/32"
					]

{
	"Version": "2008-10-17",
	"Id": "Policy13947375644752",
	"Statement": [
		{
			"Sid": "Stmt13947375624982",
			"Effect": "Allow",
			"Principal": {
				"AWS": "*"
			},
			"Action": "s3:*",
			"Resource": "arn:aws:s3:::wgen-sto-artifacts/*",
			"Condition": {
				"IpAddress": {
					"aws:SourceIp": [
						"54.86.79.230/32",
						"54.84.206.23/32",
						"54.86.33.222/32",
						"38.117.159.162/32",
						"70.151.98.131/32",
						"54.209.155.115/32",
						"107.23.90.79/32",
						"54.209.93.210/32",
						"54.88.205.140/32",
						"54.84.6.106/32",
						"107.21.36.141/32",
						"54.86.150.196/32",
						"54.209.17.121/32",
						"54.84.31.228/32",
						"54.86.31.209/32",
						"50.19.114.108/32",
						"54.85.129.109/32",
						"54.165.86.129/32",
						"54.165.114.103/32",
						"54.172.8.125/32",
						"54.86.144.175/32",
						"107.23.57.237/32",
						"54.165.231.248/32",
						"54.165.228.106/32"
					]
				}
			}
		},
		{
			"Sid": "2",
			"Effect": "Allow",
			"Principal": {
				"AWS": "arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity EXU2IEU8NKUSP"
			},
			"Action": "s3:GetObject",
			"Resource": "arn:aws:s3:::wgen-sto-artifacts/*"
		}
	]
}