module Fastlane
  module Actions
    module SharedValues
      XCODE_SERVER_GET_ASSETS_PATH = :XCODE_SERVER_GET_ASSETS_PATH
      XCODE_SERVER_GET_ASSETS_ARCHIVE_PATH = :XCODE_SERVER_GET_ASSETS_ARCHIVE_PATH
    end

    class XcodeServerGetAssetsAction < Action

      require 'excon'
      require 'json'
      require 'fileutils'

      def self.run(params)

        host = params[:host]
        bot_name = params[:bot_name]
        integration_number_override = params[:integration_number]
        target_folder = params[:target_folder]
        keep_all_assets = params[:keep_all_assets]
        username = params[:username]
        password = params[:password]
        trust_self_signed_certs = params[:trust_self_signed_certs]

        # setup (not)trusting self signed certificates.
        # it's normal to have a self signed certificate on your Xcode Server
        Excon.defaults[:ssl_verify_peer] = !trust_self_signed_certs # for self-signed certificates

        # create Xcode Server config
        xcs = XcodeServer.new(host, username, password)
        bots = xcs.fetch_all_bots

        Helper.log.info "Fetched #{bots.count} Bots from Xcode Server at #{host}.".yellow

        # pull out names
        bot_names = bots.map { |bot| bot['name'] }

        # match the bot name with a found bot, otherwise fail
        found_bots = bots.select { |bot| bot['name'] == bot_name }
        raise "Failed to find a Bot with name #{bot_name} on server #{host}, only available Bots: #{bot_names}".red if found_bots.count == 0

        bot = found_bots[0]

        Helper.log.info "Found Bot with name #{bot_name} with id #{bot['_id']}.".green

        # we have our bot, get finished integrations, sorted from newest to oldest
        integrations = xcs.fetch_integrations(bot['_id']).select { |i| i['currentStep'] == 'completed' }
        raise "Failed to find any completed integration for Bot \"#{bot_name}\"".red if (integrations || []).count == 0
        
        # if no integration number is specified, pick the newest one (this is sorted from newest to oldest)
        if integration_number_override
          integration = integrations.select { |i| i['number'] == integration_number_override }.first
          raise "Specified integration number #{integration_number_override} does not exist.".red unless integration
        else
          integration = integrations.first
        end

        # consider: only taking the last successful one? or allow failing tests? warnings?

        Helper.log.info "Using integration #{integration['number']}.".yellow

        # fetch assets for this integration
        assets_path = xcs.fetch_assets(integration['_id'], target_folder, self)
        raise "Failed to fetch assets for integration #{integration['number']}." unless assets_path

        asset_entries = Dir.entries(assets_path).map { |i| File.join(assets_path, i) }

        Helper.log.info "Successfully downloaded #{asset_entries.count} assets to file #{assets_path}!".green

        # now find the archive and unzip it
        zipped_archive_path = asset_entries.select { |i| i.end_with?('xcarchive.zip') }.first

        if zipped_archive_path

          Helper.log.info "Found an archive in the assets folder...".yellow

          archive_file_path = File.basename(zipped_archive_path, File.extname(zipped_archive_path))
          archive_dir_path = File.dirname(zipped_archive_path)
          archive_path = File.join(archive_dir_path, archive_file_path)
          if File.exists?(archive_path)
            # we already have the archive, skip
            Helper.log.info "Archive #{archive_path} already exists, not unzipping again...".yellow
          else
            # unzip the archive
            sh "unzip -q \"#{zipped_archive_path}\" -d \"#{archive_dir_path}\""
          end

          # reload asset entries to also contain the xcarchive file
          asset_entries = Dir.entries(assets_path).map { |i| File.join(assets_path, i) }

          # optionally delete everything except for the archive
          if !keep_all_assets
            files_to_delete = asset_entries.select do |i| 
              File.extname(i) != '.xcarchive' && ![".", ".."].include?(File.basename(i))
            end

            files_to_delete.each do |i|
              FileUtils.rm_rf(i)
            end
          end

          Actions.lane_context[SharedValues::XCODE_SERVER_GET_ASSETS_ARCHIVE_PATH] = archive_path
        end
        
        Actions.lane_context[SharedValues::XCODE_SERVER_GET_ASSETS_PATH] = assets_path

        return assets_path
      end

      class XcodeServer

        def initialize(host, username, password)
          @host = host.start_with?('https://') ? host : "https://#{host}"
          @username = username
          @password = password
        end

        def fetch_all_bots
          response = get_endpoint('/bots')
          raise "You are unauthorized to access data on #{@host}, please check that you're passing in a correct username and password.".red if response.status == 401 
          raise "Failed to fetch Bots from Xcode Server at #{@host}, response: #{response.status}: #{response.body}.".red if response.status != 200
          bots = JSON.parse(response.body)['results']
        end

        def fetch_integrations(bot_id)
          response = get_endpoint("/bots/#{bot_id}/integrations?limit=10")
          raise "Failed to fetch Integrations for Bot #{bot_id} from Xcode Server at #{@host}, response: #{response.status}: #{response.body}".red if response.status != 200
          integrations = JSON.parse(response.body)['results']
        end

        def fetch_assets(integration_id, target_folder, action)
          
          # create a temp folder and a file, stream the download into it
          Dir.mktmpdir do |dir|

            temp_file = File.join(dir, "tmp_download.#{rand(1000000)}")
            f = open(temp_file, 'w')
            streamer = lambda do |chunk, remaining_bytes, total_bytes|
              if remaining_bytes && total_bytes
                Helper.log.info "Downloading: #{100 - (100 * remaining_bytes.to_f / total_bytes.to_f).to_i}%".yellow
              else
                Helper.log.error "#{chunk}".red
              end
              f.write(chunk)
            end

            response = self.get_endpoint("/integrations/#{integration_id}/assets", streamer)
            f.close()

            raise "Integration doesn't have any assets (it probably never ran).".red if response.status == 500
            raise "Failed to fetch Assets zip for Integration #{integration_id} from Xcode Server at #{@host}, response: #{response.status}: #{response.body}".red if response.status != 200

            # unzip it, it's a .tar.gz file
            out_folder = File.join(dir, "out_#{rand(1000000)}")
            FileUtils.mkdir_p(out_folder)

            action.sh "cd \"#{out_folder}\"; cat \"#{temp_file}\" | gzip -d | tar -x"

            # then pull the real name from headers
            asset_filename = response.headers['Content-Disposition'].split(';')[1].split('=')[1].gsub('"', '')
            asset_foldername = asset_filename.split('.')[0]

            # rename the folder in out_folder to asset_foldername
            found_folder = Dir.entries(out_folder).select { |item| item != '.' && item != '..' }[0]

            raise "Internal error, couldn't find unzipped folder".red if found_folder.nil?

            unzipped_folder_temp_name = File.join(out_folder, found_folder)
            unzipped_folder = File.join(out_folder, asset_foldername)

            # rename to destination name
            FileUtils.mv(unzipped_folder_temp_name, unzipped_folder)

            target_folder = File.absolute_path(target_folder)

            # create target folder if it doesn't exist
            FileUtils.mkdir_p(target_folder)

            # and move+rename it to the destination place
            FileUtils.cp_r(unzipped_folder, target_folder)
            out = File.join(target_folder, asset_foldername)
            return out
          end
          return nil
        end

        def headers
          require 'base64'
          headers = { 
            'User-Agent' => 'fastlane-xcode_server_get_assets', # XCS wants user agent. for some API calls. not for others. sigh.
            'X-XCSAPIVersion' => 1 # XCS API version with this API, Xcode needs this otherwise it explodes in a 500 error fire. Currently Xcode 7 Beta 5 is on Version 5.
          }

          if @username && @password 
            userpass = "#{@username}:#{@password}"
            headers['Authorization'] = "Basic #{Base64.strict_encode64(userpass)}"
          end
          
          return headers
        end

        def get_endpoint(endpoint, response_block=nil)
          url = url_for_endpoint(endpoint)
          headers = self.headers || {}

          if response_block
            response = Excon.get(url, :response_block => response_block, :headers => headers)
          else
            response = Excon.get(url, :headers => headers)
          end

          return response
        end

        private 

        def url_for_endpoint(endpoint)
          "#{@host}:20343/api#{endpoint}"
        end
      end

      #####################################################
      # @!group Documentation
      #####################################################

      def self.description
        "Downloads Xcode Bot assets like the `.xcarchive` and logs"
      end

      def self.details
        "This action downloads assets from your Xcode Server Bot (works with Xcode Server
          using Xcode 6 and 7. By default this action downloads all assets, unzips them and
          deletes everything except for the `.xcarchive`. If you'd like to keep all downloaded
          assets, pass `:keep_all_assets: true`. This action returns the path to the downloaded
          assets folder and puts into shared values the paths to the asset folder and to the `.xcarchive` inside it"
      end

      def self.available_options        
        [
          FastlaneCore::ConfigItem.new(key: :host,
                                       env_name: "FL_XCODE_SERVER_GET_ASSETS_HOST",
                                       description: "IP Address/Hostname of Xcode Server",
                                       optional: false),
          FastlaneCore::ConfigItem.new(key: :bot_name,
                                       env_name: "FL_XCODE_SERVER_GET_ASSETS_BOT_NAME",
                                       description: "Name of the Bot to pull assets from",
                                       optional: false),
          FastlaneCore::ConfigItem.new(key: :integration_number,
                                       env_name: "FL_XCODE_SERVER_GET_ASSETS_INTEGRATION_NUMBER",
                                       description: "Optionally you can override which integration's assets should be downloaded. If not provided, the latest integration is used",
                                       is_string: false,
                                       optional: true),
          FastlaneCore::ConfigItem.new(key: :username,
                                       env_name: "FL_XCODE_SERVER_GET_ASSETS_USERNAME",
                                       description: "Username for your Xcode Server",
                                       optional: true,
                                       default_value: ""),
          FastlaneCore::ConfigItem.new(key: :password,
                                       env_name: "FL_XCODE_SERVER_GET_ASSETS_PASSWORD",
                                       description: "Password for your Xcode Server",
                                       optional: true,
                                       default_value: ""),
          FastlaneCore::ConfigItem.new(key: :target_folder,
                                       env_name: "FL_XCODE_SERVER_GET_ASSETS_TARGET_FOLDER",
                                       description: "Relative path to a folder into which to download assets",
                                       optional: true,
                                       default_value: './xcs_assets'),
          FastlaneCore::ConfigItem.new(key: :keep_all_assets,
                                       env_name: "FL_XCODE_SERVER_GET_ASSETS_KEEP_ALL_ASSETS",
                                       description: "Whether to keep all assets or let the script delete everything except for the .xcarchive",
                                       optional: true,
                                       is_string: false,
                                       default_value: false),
          FastlaneCore::ConfigItem.new(key: :trust_self_signed_certs,
                                       env_name: "FL_XCODE_SERVER_GET_ASSETS_TRUST_SELF_SIGNED_CERTS",
                                       description: "Whether to trust self-signed certs on your Xcode Server",
                                       optional: true,
                                       is_string: false,
                                       default_value: true)
        ]
      end

      def self.output
        [
          ['XCODE_SERVER_GET_ASSETS_PATH', 'Absolute path to the downloaded assets folder'],
          ['XCODE_SERVER_GET_ASSETS_ARCHIVE_PATH', 'Absolute path to the downloaded xcarchive file']
        ]
      end

      def self.authors
        ["czechboy0"]
      end

      def self.is_supported?(platform)
        true
      end
    end
  end
end