module Fastlane
  module Actions
    module SharedValues
      GITHUB_API_STATUS_CODE = :GITHUB_API_STATUS_CODE
      GITHUB_API_RESPONSE = :GITHUB_API_RESPONSE
      GITHUB_API_JSON = :GITHUB_API_JSON
    end

    class GithubApiAction < Action
      class << self
        def run(params)
          require 'json'

          http_method = (params[:http_method] || 'GET').to_s.upcase
          url = construct_url(params[:server_url], params[:path], params[:url])
          headers = construct_headers(params[:api_token], params[:headers])
          payload = construct_body(params[:body], params[:raw_body])
          error_handlers = params[:error_handlers] || {}
          secure = params[:secure]

          response = call_endpoint(
            url,
            http_method,
            headers,
            payload,
            secure
          )

          status_code = response[:status]
          result = {
            status: status_code,
            body: response.body || "",
            json: parse_json(response.body) || {}
          }

          if status_code.between?(200, 299)
            UI.verbose("Response:")
            UI.verbose(response.body)
            UI.verbose("---")
            yield(result) if block_given?
          else
            handled_error = error_handlers[status_code] || error_handlers['*']
            if handled_error
              handled_error.call(result)
            else
              UI.error("---")
              UI.error("Request failed:\n#{http_method}: #{url}")
              UI.error("Headers:\n#{headers}")
              UI.error("---")
              UI.error("Response:")
              UI.error(response.body)
              UI.user_error!("GitHub responded with #{status_code}\n---\n#{response.body}")
            end
          end

          Actions.lane_context[SharedValues::GITHUB_API_STATUS_CODE] = result[:status]
          Actions.lane_context[SharedValues::GITHUB_API_RESPONSE] = result[:body]
          Actions.lane_context[SharedValues::GITHUB_API_JSON] = result[:json]

          return result
        end

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

        def description
          "Call a GitHub API endpoint and get the resulting JSON response"
        end

        def details
          [
            "Calls any GitHub API endpoint. You must provide your GitHub Personal token (get one from [https://github.com/settings/tokens/new](https://github.com/settings/tokens/new)).",
            "Out parameters provide the status code and the full response JSON if valid, otherwise the raw response body.",
            "Documentation: [https://developer.github.com/v3](https://developer.github.com/v3)."
          ].join("\n")
        end

        def available_options
          [
            FastlaneCore::ConfigItem.new(key: :server_url,
                                         env_name: "FL_GITHUB_API_SERVER_URL",
                                         description: "The server url. e.g. 'https://your.internal.github.host/api/v3' (Default: 'https://api.github.com')",
                                         default_value: "https://api.github.com",
                                         optional: true,
                                         verify_block: proc do |value|
                                           UI.user_error!("Please include the protocol in the server url, e.g. https://your.github.server/api/v3") unless value.include?("//")
                                         end),
            FastlaneCore::ConfigItem.new(key: :api_token,
                                         env_name: "FL_GITHUB_API_TOKEN",
                                         description: "Personal API Token for GitHub - generate one at https://github.com/settings/tokens",
                                         sensitive: true,
                                         code_gen_sensitive: true,
                                         is_string: true,
                                         default_value: ENV["GITHUB_API_TOKEN"],
                                         default_value_dynamic: true,
                                         optional: false),
            FastlaneCore::ConfigItem.new(key: :http_method,
                                         env_name: "FL_GITHUB_API_HTTP_METHOD",
                                         description: "The HTTP method. e.g. GET / POST",
                                         default_value: "GET",
                                         optional: true,
                                         verify_block: proc do |value|
                                           unless %w(GET POST PUT DELETE HEAD CONNECT PATCH).include?(value.to_s.upcase)
                                             UI.user_error!("Unrecognised HTTP method")
                                           end
                                         end),
            FastlaneCore::ConfigItem.new(key: :body,
                                         env_name: "FL_GITHUB_API_REQUEST_BODY",
                                         description: "The request body in JSON or hash format",
                                         is_string: false,
                                         default_value: {},
                                         optional: true),
            FastlaneCore::ConfigItem.new(key: :raw_body,
                                         env_name: "FL_GITHUB_API_REQUEST_RAW_BODY",
                                         description: "The request body taken verbatim instead of as JSON, useful for file uploads",
                                         is_string: true,
                                         optional: true),
            FastlaneCore::ConfigItem.new(key: :path,
                                         env_name: "FL_GITHUB_API_PATH",
                                         description: "The endpoint path. e.g. '/repos/:owner/:repo/readme'",
                                         optional: true),
            FastlaneCore::ConfigItem.new(key: :url,
                                         env_name: "FL_GITHUB_API_URL",
                                         description: "The complete full url - used instead of path. e.g. 'https://uploads.github.com/repos/fastlane...'",
                                         optional: true,
                                         verify_block: proc do |value|
                                           UI.user_error!("Please include the protocol in the url, e.g. https://uploads.github.com") unless value.include?("//")
                                         end),
            FastlaneCore::ConfigItem.new(key: :error_handlers,
                                         description: "Optional error handling hash based on status code, or pass '*' to handle all errors",
                                         is_string: false,
                                         default_value: {},
                                         optional: true),
            FastlaneCore::ConfigItem.new(key: :headers,
                                         description: "Optional headers to apply",
                                         is_string: false,
                                         default_value: {},
                                         optional: true),
            FastlaneCore::ConfigItem.new(key: :secure,
                                         env_name: "FL_GITHUB_API_SECURE",
                                         description: "Optionally disable secure requests (ssl_verify_peer)",
                                         type: Boolean,
                                         default_value: true,
                                         optional: true)
          ]
        end

        def output
          [
            ['GITHUB_API_STATUS_CODE', 'The status code returned from the request'],
            ['GITHUB_API_RESPONSE', 'The full response body'],
            ['GITHUB_API_JSON', 'The parsed json returned from GitHub']
          ]
        end

        def return_value
          "A hash including the HTTP status code (:status), the response body (:body), and if valid JSON has been returned the parsed JSON (:json)."
        end

        def authors
          ["tommeier"]
        end

        def example_code
          [
            'result = github_api(
            server_url: "https://api.github.com",
            api_token: ENV["GITHUB_TOKEN"],
            http_method: "GET",
            path: "/repos/:owner/:repo/readme",
            body: { ref: "master" }
          )',
            '# Alternatively call directly with optional error handling or block usage
            GithubApiAction.run(
              server_url: "https://api.github.com",
              api_token: ENV["GITHUB_TOKEN"],
              http_method: "GET",
              path: "/repos/:owner/:repo/readme",
              error_handlers: {
                404 => proc do |result|
                  UI.message("Something went wrong - I couldn\'t find it...")
                end,
                \'*\' => proc do |result|
                  UI.message("Handle all error codes other than 404")
                end
              }
            ) do |result|
              UI.message("JSON returned: #{result[:json]}")
            end
          '
          ]
        end

        def is_supported?(platform)
          true
        end

        def category
          :source_control
        end

        private

        def construct_headers(api_token, overrides)
          require 'base64'
          headers = { 'User-Agent' => 'fastlane-github_api' }
          headers['Authorization'] = "Basic #{Base64.strict_encode64(api_token)}" if api_token
          headers.merge(overrides || {})
        end

        def construct_url(server_url, path, url)
          return_url = (server_url && path) ? File.join(server_url, path) : url

          UI.user_error!("Please provide either `server_url` (e.g. https://api.github.com) and 'path' or full 'url' for GitHub API endpoint") unless return_url

          return_url
        end

        def construct_body(body, raw_body)
          body ||= {}

          if raw_body
            raw_body
          elsif body.kind_of?(Hash)
            body.to_json
          elsif body.kind_of?(Array)
            body.to_json
          else
            UI.user_error!("Please provide valid JSON, or a hash as request body") unless parse_json(body)
            body
          end
        end

        def parse_json(value)
          JSON.parse(value)
        rescue JSON::ParserError
          nil
        end

        def call_endpoint(url, http_method, headers, body, secure)
          require 'excon'

          Excon.defaults[:ssl_verify_peer] = secure
          middlewares = Excon.defaults[:middlewares] + [Excon::Middleware::RedirectFollower] # allow redirect in case of repo renames

          UI.verbose("#{http_method} : #{url}")

          connection = Excon.new(url)
          connection.request(
            method: http_method,
            headers: headers,
            middlewares: middlewares,
            body: body,
            debug_request: FastlaneCore::Globals.verbose?,
            debug_response: FastlaneCore::Globals.verbose?
          )
        end
      end
    end
  end
end
