require 'openssl'
require 'instance_metadata'
require 'open-uri'

module InstanceAgent
  module Plugins
    module CodeDeployPlugin
      class DeploymentSpecification
        DEFAULT_FILE_EXISTS_BEHAVIOR = 'DISALLOW'

        attr_accessor :deployment_id, :deployment_group_id, :deployment_group_name, :revision, :revision_source, :application_name, :deployment_type, :deployment_creator
        attr_accessor :bucket, :key, :bundle_type, :version, :etag
        attr_accessor :external_account, :repository, :commit_id, :anonymous, :external_auth_token
        attr_accessor :file_exists_behavior
        attr_accessor :local_location, :all_possible_lifecycle_events
        attr_accessor :app_spec_path
        class << self
          attr_accessor :cert_store
        end

        def self.init_cert_store(ca_chain_path)
          @cert_store = OpenSSL::X509::Store.new
          begin
            @cert_store.add_file ca_chain_path
          rescue OpenSSL::X509::StoreError => e
            raise "Could not load certificate store '#{ca_chain_path}'.\nCaused by: #{e.inspect}"
          end
          return @cert_store
        end

        @cert_store = init_cert_store(File.expand_path('../../../../certs/host-agent-deployment-signer-ca-chain.pem', File.dirname(__FILE__)))

        def initialize(data)
          raise 'Deployment Spec has no DeploymentId' unless property_set?(data, "DeploymentId")
          raise 'Deployment Spec has no DeploymentGroupId' unless property_set?(data, "DeploymentGroupId")
          raise 'Deployment Spec has no DeploymentGroupName' unless property_set?(data, "DeploymentGroupName")
          raise 'Deployment Spec has no ApplicationName' unless property_set?(data, "ApplicationName")

          @application_name = data["ApplicationName"]
          @deployment_group_name = data["DeploymentGroupName"]

          if data["DeploymentId"].start_with?("arn:")
            @deployment_id = getDeploymentIdFromArn(data["DeploymentId"])
          else
            @deployment_id = data["DeploymentId"]
          end
          @deployment_group_id = data["DeploymentGroupId"]
          @deployment_creator = data["DeploymentCreator"] || "user"
          @deployment_type = data["DeploymentType"] || "IN_PLACE"

          if property_set?(data, "AppSpecFilename")
            @app_spec_path = data["AppSpecFilename"]
          else
            @app_spec_path = "appspec.yml"
          end

          raise 'Must specify a revison' unless data["Revision"]
          @revision_source = data["Revision"]["RevisionType"]
          raise 'Must specify a revision source' unless @revision_source

          @file_exists_behavior = DEFAULT_FILE_EXISTS_BEHAVIOR
          if property_set?(data, "AgentActionOverrides")
            agentActionsOverrides = data["AgentActionOverrides"]
            if property_set?(agentActionsOverrides,  "AgentOverrides")
              agentActionsOverridesMap = agentActionsOverrides["AgentOverrides"]
              if property_set?(agentActionsOverridesMap, "FileExistsBehavior")
                @file_exists_behavior = agentActionsOverridesMap["FileExistsBehavior"].upcase
              end
            end
          end

          if property_set?(data, 'AllPossibleLifecycleEvents')
            @all_possible_lifecycle_events = data['AllPossibleLifecycleEvents']
          end

          case @revision_source
          when 'S3'
            @revision = data["Revision"]["S3Revision"]
            raise 'S3Revision in Deployment Spec must specify Bucket, Key and BundleType' unless valid_s3_revision?(@revision)
            raise 'BundleType in S3Revision must be tar, tgz or zip' unless valid_bundle_type?(@revision)

            @bucket = @revision["Bucket"]
            @key = @revision["Key"]
            @bundle_type = @revision["BundleType"]
            @version = @revision["Version"]
            @etag = @revision["ETag"]
          when 'GitHub'
            @revision = data["Revision"]["GitHubRevision"]
            raise 'GitHubRevision in Deployment Spec must specify Account, Repository and CommitId' unless valid_github_revision?(revision)
            @external_account = revision["Account"]
            @repository = revision["Repository"]
            @commit_id = revision["CommitId"]
            @external_auth_token = data["GitHubAccessToken"]
            @anonymous = @external_auth_token.nil?
            @bundle_type = @revision["BundleType"]
          when 'Local File', 'Local Directory'
            @revision = data["Revision"]["LocalRevision"]
            raise 'LocalRevision in Deployment Spec must specify Location and BundleType' unless valid_local_revision?(revision)
            raise 'BundleType in LocalRevision must be tar, tgz, zip, or directory' unless valid_local_bundle_type?(@revision)

            @local_location = @revision["Location"]
            @bundle_type = @revision["BundleType"]
          else
            raise 'Exactly one of S3Revision, GitHubRevision, or LocalRevision must be specified'
          end
        end
        # Decrypts the envelope /deployment specs
        # Params:
        # envelope: deployment specification that's to be checked and decrypted
        def self.parse(envelope)
          raise 'Provided deployment spec was nil' if envelope.nil?

          case envelope.format
          when "PKCS7/JSON"
            pkcs7 = OpenSSL::PKCS7.new(envelope.payload)
            pkcs7.verify([], @cert_store, nil, OpenSSL::PKCS7::NOVERIFY)
            # NOTE: the pkcs7.data field is only populated AFTER pkcs7.verify() is called!
            parse_deployment_spec_data(pkcs7.data)
          when "TEXT/JSON"
            raise "Unsupported DeploymentSpecification format: #{envelope.format}" unless AWS::CodeDeploy::Local::Deployer.running_as_developer_utility?
            # We only allow json unsigned messages from the local developer utility (codedeploy-local cli)
            # This is because the local cli cannot actually sign messages since it doens't have the private key
            # that the CodeDeploy service has.
            parse_deployment_spec_data(envelope.payload)
          else
            raise "Unsupported DeploymentSpecification format: #{envelope.format}"
          end
        end

        private
        def self.parse_deployment_spec_data(deployment_spec_data)
            deployment_spec = JSON.parse(deployment_spec_data)

            sanitized_spec = deployment_spec.clone
            sanitized_spec["GitHubAccessToken"] &&= "REDACTED"
            InstanceAgent::Log.debug("#{self.to_s}: Parse: #{sanitized_spec}")

            new(deployment_spec)
        end

        def property_set?(propertyHash, property)
          propertyHash.has_key?(property) && !propertyHash[property].nil? && !propertyHash[property].empty?
        end

        def valid_s3_revision?(revision)
          revision.nil? || %w(Bucket Key BundleType).all? { |k| revision.has_key?(k) }
        end

        def valid_github_revision?(revision)
          required_fields = %w(Account Repository CommitId)
          if !(revision.nil? || revision['Anonymous'].nil? || revision['Anonymous'])
            required_fields << 'OAuthToken'
          end
          revision.nil? || required_fields.all? { |k| revision.has_key?(k) }
        end

        def valid_local_revision?(revision)
          revision.nil? || %w(Location BundleType).all? { |k| revision.has_key?(k) }
        end

        private
        def valid_bundle_type?(revision)
          revision.nil? || %w(tar zip tgz).any? { |k| revision["BundleType"] == k }
        end

        def valid_local_bundle_type?(revision)
          revision.nil? || %w(tar zip tgz directory).any? { |k| revision["BundleType"] == k }
        end

        private
        def getDeploymentIdFromArn(arn)
          # example arn format: "arn:aws:codedeploy:us-east-1:123412341234:deployment/12341234-1234-1234-1234-123412341234"
          arn.split(":", 6)[5].split("/",2)[1]
        end
      end
    end
  end
end