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

        UI.important("Fetched #{bots.count} Bots from Xcode Server at #{host}.")

        # 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 }
        UI.user_error!("Failed to find a Bot with name #{bot_name} on server #{host}, only available Bots: #{bot_names}") if found_bots.count == 0

        bot = found_bots[0]

        UI.success("Found Bot with name #{bot_name} with id #{bot['_id']}.")

        # we have our bot, get finished integrations, sorted from newest to oldest
        integrations = xcs.fetch_integrations(bot['_id']).select { |i| i['currentStep'] == 'completed' }
        UI.user_error!("Failed to find any completed integration for Bot \"#{bot_name}\"") 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.find { |i| i['number'] == integration_number_override }
          UI.user_error!("Specified integration number #{integration_number_override} does not exist.") unless integration
        else
          integration = integrations.first
        end

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

        UI.important("Using integration #{integration['number']}.")

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

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

        UI.success("Successfully downloaded #{asset_entries.count} assets to file #{assets_path}!")

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

        if zipped_archive_path

          UI.important("Found an archive in the assets folder...")

          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.exist?(archive_path)
            # we already have the archive, skip
            UI.important("Archive #{archive_path} already exists, not unzipping again...")
          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
          unless 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')
          UI.user_error!("You are unauthorized to access data on #{@host}, please check that you're passing in a correct username and password.") if response.status == 401
          UI.user_error!("Failed to fetch Bots from Xcode Server at #{@host}, response: #{response.status}: #{response.body}.") if response.status != 200
          JSON.parse(response.body)['results']
        end

        def fetch_integrations(bot_id)
          response = get_endpoint("/bots/#{bot_id}/integrations?last=10")
          UI.user_error!("Failed to fetch Integrations for Bot #{bot_id} from Xcode Server at #{@host}, response: #{response.status}: #{response.body}") if response.status != 200
          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(1_000_000)}")
            f = open(temp_file, 'w')
            streamer = lambda do |chunk, remaining_bytes, total_bytes|
              if remaining_bytes && total_bytes
                UI.important("Downloading: #{100 - (100 * remaining_bytes.to_f / total_bytes.to_f).to_i}%")
              else
                UI.error(chunk.to_s)
              end
              f.write(chunk)
            end

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

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

            # unzip it, it's a .tar.gz file
            out_folder = File.join(dir, "out_#{rand(1_000_000)}")
            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].delete('"')
            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]

            UI.user_error!("Internal error, couldn't find unzipped folder") 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."
        ].join("\n")
      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",
                                       type: Integer,
                                       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",
                                       sensitive: true,
                                       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,
                                       type: Boolean,
                                       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,
                                       type: Boolean,
                                       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.return_type
        :array_of_strings
      end

      def self.authors
        ["czechboy0"]
      end

      def self.is_supported?(platform)
        [:ios, :mac].include?(platform)
      end

      def self.example_code
        [
          'xcode_server_get_assets(
            host: "10.99.0.59", # Specify Xcode Server\'s Host or IP Address
            bot_name: "release-1.3.4" # Specify the particular Bot
          )'
        ]
      end

      def self.category
        :testing
      end
    end
  end
end
