# frozen_string_literal: true

require 'English'
require 'travis/tools/system'
require 'yaml'
require 'json'
require 'gh'

module Travis
  module Tools
    class Github
      TOKEN_SIZE  = 40
      GITHUB_API  = 'api.github.com'
      GITHUB_HOST = 'github.com'

      attr_accessor :api_url, :scopes, :github_token, :drop_token, :callback, :explode, :after_tokens,
                    :login_header, :auto_token, :note,
                    :hub_path, :oauth_paths, :composer_path, :git_config_keys, :debug, :no_token, :check_token

      def initialize(options = nil)

        @check_token     = true
        @ask_login       = proc { raise 'ask_login callback not set' }
        @after_tokens    = proc {}
        @debug           = proc { |_| }
        @hub_path        = ENV['HUB_CONFIG'] || '~/.config/hub'
        @oauth_paths     = ['~/.github-oauth-token']
        @composer_path   = '~/.composer/config.json'
        @note            = 'temporary token'
        @git_config_keys = %w[github.token github.oauth-token]
        @scopes          = ['user', 'user:email', 'repo'] # overridden by value from /config
        options&.each_pair { |k, v| send("#{k}=", v) if respond_to? "#{k}=" }
        yield self if block_given?
      end

      def with_token
        each_token { |t| break yield(t) }
      end

      def each_token
        require 'gh' unless defined? GH
        possible_tokens { |t| yield(t) if acceptable?(t) }
      ensure
        callback = self.callback
        self.callback = nil
        callback&.call
      end

      def with_session
        with_token { |t| GH.with(token: t) { yield(t) } }
      end

      def possible_tokens(&block)
        return block[github_token] if github_token

        if auto_token
          git_tokens(&block)
          hub_tokens(&block)
          oauth_file_tokens(&block)
          github_for_mac_token(&block)
          issuepost_token(&block)
          composer_token(&block)
        end

        if github_token || auto_token
          after_tokens.call
        elsif login_header
          login_header.call
        end
      end

      def git_tokens
        return unless System.has? 'git'

        git_config_keys.each do |key|
          `git config --get-all #{key}`.each_line do |line|
            token = line.strip
            yield token unless token.empty?
          end
        end
      end

      def composer_token
        file(composer_path) do |content|
          token = JSON.parse(content)['config'].fetch('github-oauth', {})[host]
          yield token if token
        end
      end

      def hub_tokens
        hub.fetch(host, []).each do |entry|
          yield entry['oauth_token'] if entry['oauth_token']
        end
      end

      def oauth_file_tokens
        oauth_paths.each do |path|
          file(path) do |content|
            token = content.strip
            yield token unless token.empty?
          end
        end
      end

      def hub
        file(hub_path, {}) do |contents|
          YAML.load(contents)
        end
      end

      def issuepost_token(&block)
        security(:generic, :w, '-l issuepost.github.access_token', 'issuepost token', &block) if host == 'github.com'
      end

      def github_for_mac_token(&block)
        command = '-s "github.com/mac"'
        security(:internet, :w, command, 'GitHub for Mac token', &block) if host == 'github.com'
      end

      def host
        api_host == GITHUB_API ? GITHUB_HOST : api_host
      end

      def api_host
        return GITHUB_API unless api_url

        api_url[%r{^(?:https?://)?([^/]+)}, 1]
      end

      def acceptable?(token)
        return true unless check_token

        gh = GH.with(token:)
        gh['user']

        true
      rescue GH::Error => e
        debug "token is not acceptable: #{gh_error(e)}"
        false
      end

      private

      def gh_error(error)
        raise error if explode

        if error.info.key? :response_body
          JSON.parse(error.info[:response_body])['message'].to_s
        else
          'Unknown error'
        end
      end

      def debug(line)
        return unless @debug

        @debug.call "Tools::Github: #{line}"
      end

      def security(type, key, arg, name)
        return false unless System.has? 'security'
        return false unless system "security find-#{type}-password #{arg} 2>/dev/null >/dev/null"

        debug "requesting to load #{name} from keychain"
        result = `security find-#{type}-password #{arg} -#{key} 2>&1`.chomp
        $CHILD_STATUS.success? ? yield(result) : debug('request denied')
      rescue StandardError => e
        raise e if explode
      end

      def file(path, default = nil)
        path        &&= File.expand_path(path)
        @file       ||= {}
        @file[path] ||= if path && File.readable?(path)
                          debug "reading #{path}"
                          yield File.read(path)
                        end
        @file[path] || default
      rescue StandardError => e
        raise e if explode
      end
    end
  end
end
