# frozen_string_literal: true

require_relative "../ui"
require "net/http"
require "uri"
require "json"
require "launchy"
require "fileutils"

module Neetob
  class CLI
    module Github
      class Auth
        attr_accessor :client_id, :grant_type, :uris, :provider, :scope, :access_token, :ui

        def initialize(client_id:, grant_type:, auth_uris:, provider:, scope:)
          @client_id = client_id
          @grant_type = grant_type
          @uris = auth_uris
          @provider = provider
          @scope = scope
          @access_token = retrieve_persisted_token
          @ui = CLI::UI.new
        end

        def token_persisted?
          access_token_present?
        end

        def open_url_in_browser!(url)
          Launchy.open(url) do |exception|
            raise(StandardError, "Attempted to open #{url} in browser and failed because #{exception}.")
          end
        end

        def start_oauth2_device_flow
          auth_data = request_authorization
          show_user_code(auth_data[:user_code])
          open_url_in_browser!(auth_data[:verification_uri])
          poll_for_token(auth_data)
        end

        def request_authorization
          post(uris["auth_req"], params: { client_id: client_id, scope: scope })
        end

        private

          def show_user_code(code)
            ui.say("You'll be redirected to the browser in 5 secs. Enter the following code in the browser:")
            ui.success(code)
            sleep 5
          end

          def tmp_token_path(provider)
            "/tmp/neetob_#{provider}_token"
          end

          def provider_local_token_path
            @_provider_local_token_path ||= tmp_token_path(provider)
          end

          def retrieve_persisted_token
            File.read(provider_local_token_path) if File.file?(provider_local_token_path)
          end

          def parse_response(http_result)
            case http_result
            when Net::HTTPOK
              JSON.parse(http_result.body, { symbolize_names: true })
            else
              raise(StandardError, "Request failed with status code #{http_result.code}. #{http_result.body}")
              nil
            end
          end

          def post(url, params:, headers: { "Accept" => "application/json" })
            base_uri = URI(url)
            enc_params = URI.encode_www_form(params)
            http_result = Net::HTTP.post(base_uri, enc_params, headers)
            parse_response(http_result)
          end

          def poll_for_token(auth_data)
            interval = auth_data[:interval]
            loop do
              res = post(
                uris["token_req"],
                params: {
                  client_id: client_id,
                  device_code: auth_data[:device_code],
                  grant_type: grant_type
                }
              )

              if res.key?(:error)
                case res[:error]
                when "authorization_pending"
                  # dont hit the endpoint too fast such that we get rate limited
                  sleep interval
                  next
                when "slow_down"
                  # increase polling interval incase rate limited
                  interval = res["interval"]
                  sleep interval
                  next
                when "access_denied"
                  # user clicks cancel button
                  raise(StandardError, "Access denied while authorizing #{provider} access.")
                  break
                else
                  raise(StandardError, res)
                  break
                end
              end

              # We won't be running this script regularly. Thus storing in /tmp/
              # for temporary reuse of token till machine is rebooted
              File.write(provider_local_token_path, res[:access_token])
              FileUtils.chmod(0600, provider_local_token_path)

              self.access_token = res[:access_token]
              ui.info("You've been authenticated!")
              break
            end
          end

          def access_token_present?
            !(access_token.nil? || access_token.empty?)
          end
      end
    end
  end
end