require 'faraday'
require 'launchy'
require 'cgi'
require 'json'
require 'google/api_client'
require 'google/api_client/client_secrets'
require 'google/api_client/auth/installed_app'
require 'socket'

#Google::Apis.logger.level = Logger::DEBUG

YOUTUBE_API_SERVICE_NAME = 'youtube'
YOUTUBE_API_VERSION = 'v3'

# Youtube OAuth and API functionality:
module JamRuby
  class GoogleClient
    attr_accessor :client
    attr_accessor :api
    attr_accessor :request
    attr_accessor :server
    attr_accessor :socket
    attr_accessor :config
    attr_accessor :redirect_uri

    def initialize()
      self.config = Rails.application.config
      self.redirect_uri='http://localhost:2112/auth/google_login/callback'
      self.client = Google::APIClient.new(
          :application_name => 'JamKazam',
          :application_version => '1.0.0'
      )

    end

    def youtube
      @youtube ||= client.discovered_api('youtube', 'v3')
    end

    def create_authorization(user_auth, scope, autorefresh)

      authorization = Signet::OAuth2::Client.new(
          :authorization_uri => "https://accounts.google.com/o/oauth2/auth",
          :token_credential_uri => "https://accounts.google.com/o/oauth2/token",
          :client_id => @config.google_client_id,
          :client_secret => @config.google_secret,
          #:redirect_uri => credentials.redirect_uris.first,
          :scope => scope
      )
      authorization.access_token  = user_auth.token
      authorization.refresh_token = user_auth.refresh_token
      authorization.expires_at    = user_auth.token_expiration

      if autorefresh && (user_auth.token_expiration < (Time.now - 15)) # add 15 second buffer to this time, because OAUth server does not respond with timestamp, but 'expires_in' which is just offset seconds

        # XXX: what to do when this fails?
        authorization.refresh!
        user_auth.token = authorization.access_token
        user_auth.token_expiration = authorization.issued_at + authorization.expires_in
        user_auth.save
      end
      authorization
    end

    def create_client
      Google::APIClient.new(
          :application_name => 'JamKazam',
          :application_version => '1.0.0',
      )
    end

    # Return a login URL that will show a web page with 
    def get_login_url(username=nil)
      puts "GET LOGIN URL"
      uri = "https://accounts.google.com/o/oauth2/auth"
      uri << "?scope=#{CGI.escape('https://www.googleapis.com/auth/youtube https://www.googleapis.com/auth/youtube.upload https://www.googleapis.com/auth/youtube https://gdata.youtube.com  email profile ')}" # # https://www.googleapis.com/auth/youtube https://www.googleapis.com/auth/youtube.upload
      uri << "&client_id=#{CGI.escape(self.config.google_email)}"
      uri << "&response_type=code"
      uri << "&access_type=online"
      uri << "&prompt=consent"
      uri << "&state=4242"
      uri << "&redirect_uri=#{redirect_uri}"
      if username.present?
        uri << "&login_hint=#{(username)}"
      end
      uri
    end

    # create youtube broadcast
    def create_broadcast(user, broadcast_options)
      auth = UserAuthorization.google_auth(user).first
      if auth.nil? || auth.token.nil?
        raise JamPermissionError, "No current google token found for user #{user}"
      end

      broadcast_data = {
          "snippet" => broadcast_options[:snippet],
          "status" => broadcast_options[:status],
          "contentDetails" => broadcast_options[:contentDetails]
      }

      begin
        #secrets = Google::APIClient::ClientSecrets.new({"web" => {"access_token" => auth.token, "refresh_token" => auth.refresh_token, "client_id" => @config.google_client_id, "client_secret" => @config.google_secret}})


        my_client = create_client

        my_client.authorization = create_authorization(auth, 'https://www.googleapis.com/auth/youtube https://www.googleapis.com/auth/youtube.force-ssl', true)

        puts "BROADCAST DATA: #{broadcast_data}"
        #y = my_client.discovered_api('youtube', 'v3')
        response = my_client.execute!(:api_method => youtube.live_broadcasts.insert,
                                    :parameters => {:part => 'contentDetails,status,snippet'},
                                    :body_object => broadcast_data)

        body = JSON.parse(response.body)
        puts "CREATE BROADCAST RESPONSE: #{body}"
        return body
      rescue Google::APIClient::ClientError => e
        # ex:
=begin
        ex = {
            "error": {
                "errors": [
                    {
                        "domain": "youtube.liveBroadcast",
                        "reason": "liveStreamingNotEnabled",
                        "message": "The user is not enabled for live streaming.",
                        "extendedHelp": "https://www.youtube.com/features"
                    }
                ],
                "code": 403,
                "message": "The user is not enabled for live streaming."
            }
        }

        ex = {
            "error": {
                "errors": [
                    {
                        "domain": "youtube.liveBroadcast",
                        "reason": "insufficientLivePermissions",
                        "message": "Request is not authorized",
                        "extendedHelp": "https://developers.google.com/youtube/v3/live/docs/liveBroadcasts/insert#auth_required"
                    }
                ],
                "code": 403,
                "message": "Request is not authorized"
            }
        }
=end

        puts e.result.body
          raise e
      end
    end

    def bind_broadcast(user, broadcast_id, stream_id)
      auth = UserAuthorization.google_auth(user).first
      if auth.nil? || auth.token.nil?
        raise JamPermissionError, "No current google token found for user #{user}"
      end

      begin
        my_client = create_client

        my_client.authorization = create_authorization(auth, 'https://www.googleapis.com/auth/youtube https://www.googleapis.com/auth/youtube.force-ssl', true)
        #y = my_client.discovered_api('youtube', 'v3')
        response = my_client.execute!(:api_method => youtube.live_broadcasts.bind,
                                      :parameters => {:part => 'id,contentDetails,status,snippet', :id => broadcast_id, :streamId => stream_id })

        body = JSON.parse(response.body)
        puts "BIND RESPONSE: #{body}"
        return body
      rescue Google::APIClient::ClientError => e
        puts e.result.body
        raise e
      end
    end

    def transition_broadcast(user, broadcast_id, broadcastStatus)
      auth = UserAuthorization.google_auth(user).first
      if auth.nil? || auth.token.nil?
        raise JamPermissionError, "No current google token found for user #{user}"
      end

      begin
        my_client = create_client

        my_client.authorization = create_authorization(auth, 'https://www.googleapis.com/auth/youtube https://www.googleapis.com/auth/youtube.force-ssl', true)
        #y = my_client.discovered_api('youtube', 'v3')
        response = my_client.execute!(:api_method => youtube.live_broadcasts.transition,
                                      :parameters => {:part => 'id,contentDetails,status,snippet', :id => broadcast_id, :broadcastStatus => broadcastStatus })

        body = JSON.parse(response.body)
        puts "TRANSITION RESPONSE: #{body}"
        return body
      rescue Google::APIClient::ClientError => e
        puts e.result.body
        raise e
      end
    end

    def get_broadcast(user, broadcast_id)
      auth = UserAuthorization.google_auth(user).first
      if auth.nil? || auth.token.nil?
        raise JamPermissionError, "No current google token found for user #{user}"
      end

      begin
        my_client = create_client

        my_client.authorization = create_authorization(auth, 'https://www.googleapis.com/auth/youtube https://www.googleapis.com/auth/youtube.force-ssl', true)
        #y = my_client.discovered_api('youtube', 'v3')
        response = my_client.execute!(:api_method => youtube.live_broadcasts.list,
                                      :parameters => {:part => 'id,contentDetails,status,snippet', :id => broadcast_id })

        body = JSON.parse(response.body)
        puts "GET BROADCAST RESPONSE: #{body}"
        if body["items"].length == 0
          nil
        else
          body["items"][0] # returns array of items. meh
        end
      rescue Google::APIClient::ClientError => e
        puts e.result.body
        raise e
      end
    end

    def get_livestream(user, stream_id)
      auth = UserAuthorization.google_auth(user).first
      if auth.nil? || auth.token.nil?
        raise JamPermissionError, "No current google token found for user #{user}"
      end

      begin
        my_client = create_client

        my_client.authorization = create_authorization(auth, 'https://www.googleapis.com/auth/youtube https://www.googleapis.com/auth/youtube.force-ssl', true)
        #y = my_client.discovered_api('youtube', 'v3')
        response = my_client.execute!(:api_method => youtube.live_streams.list,
                                      :parameters => {:part => 'id,snippet,cdn,status', :id => stream_id })

        body = JSON.parse(response.body)
        puts "GET LIVE STREAM RESPONSE: #{body}"
        if body["items"].length == 0
          nil
        else
          body["items"][0] # returns array of items. meh
        end
      rescue Google::APIClient::ClientError => e
        puts e.result.body
        raise e
      end
    end

    def create_stream(user, stream_options)
      auth = UserAuthorization.google_auth(user).first
      if auth.nil? || auth.token.nil?
        raise JamPermissionError, "No current google token found for user #{user}"
      end

      broadcast_data = {
          "snippet" => stream_options[:snippet],
          "cdn" => stream_options[:cdn],
          "contentDetails" => stream_options[:contentDetails]
      }

      begin
        my_client = create_client

        my_client.authorization = create_authorization(auth, 'https://www.googleapis.com/auth/youtube https://www.googleapis.com/auth/youtube.force-ssl', true)

        puts "STREAM DATA: #{broadcast_data}"
        #y = my_client.discovered_api('youtube', 'v3')
        response = my_client.execute!(:api_method => youtube.live_streams.insert,
                                      :parameters => {:part => 'id,contentDetails,cdn,status,snippet'},
                                      :body_object => broadcast_data)

        body = JSON.parse(response.body)
        puts "CREATE STREAM RESPONSE: #{body}"
        return body
      rescue Google::APIClient::ClientError => e
        puts e.result.body
        raise e
      end
    end

    # create youtube broadcast
    def update_broadcast(user, broadcast_options)

    end


    # Contacts youtube and prepares an upload to youtube.  This 
    # process is somewhat painful, even in ruby, so we do the preparation
    # and the client does the actual upload using the URL returned:

    # https://developers.google.com/youtube/v3/docs/videos/insert
    # https://developers.google.com/youtube/v3/guides/using_resumable_upload_protocol
    def sign_youtube_upload(user, filename, length)
      raise ArgumentError, "Length is required and should be > 0" if length.to_i.zero?

      # Something like this:
      # POST /upload/youtube/v3/videos?uploadType=resumable&part=snippet,status,contentDetails HTTP/1.1
      # Host: www.googleapis.com
      # Authorization: Bearer AUTH_TOKEN
      # Content-Length: 278
      # Content-Type: application/json; charset=UTF-8
      # X-Upload-Content-Length: 3000000
      # X-Upload-Content-Type: video/*

      # {
      #   "snippet": {
      #     "title": "My video title",
      #     "description": "This is a description of my video",
      #     "tags": ["cool", "video", "more keywords"],
      #     "categoryId": 22
      #   },
      #   "status": {
      #     "privacyStatus": "public",
      #     "embeddable": True,
      #     "license": "youtube"
      #   }
      # }
      auth = UserAuthorization.google_auth(user).first
      if auth.nil? || auth.token.nil?
        raise SecurityError, "No current google token found for user #{user}"
      end

      video_data = {
          "snippet" => {
              "title" => filename,
              "description" => filename,
              "tags" => ["cool", "video", "more keywords"],
              "categoryId" => 1
          },
          "status" => {
              "privacyStatus" => "public",
              "embeddable" => true,
              "license" => "youtube"
          }
      }

      conn = Faraday.new(:url => "https://www.googleapis.com", :ssl => {:verify => false}) do |faraday|
        faraday.request :url_encoded
        faraday.adapter Faraday.default_adapter
      end

      video_json=video_data.to_json
      result = conn.post("/upload/youtube/v3/videos?access_token=#{CGI.escape(auth.token)}&uploadType=resumable&part=snippet,status,contentDetails",
                         video_json,
                         {
                             'content-type' => 'application/json;charset=utf-8',
                             'x-Upload-Content-Length' => "#{length}",
                             'x-upload-content-type' => "video/*"
                         }
      )

      # Response should something look like:
      #   HTTP/1.1 200 OK
      #   Location: https://www.googleapis.com/upload/youtube/v3/videos?uploadType=resumable&upload_id=xa298sd_f&part=snippet,status,contentDetails
      #   Content-Length: 0        

      if (result.nil? || result.status!=200 || result.headers['location'].blank?)
        msg = "Failed signing with status=#{result.status} #{result.inspect}: "
        if result.body.present? && result.body.length > 2
          msg << result.body.inspect #   JSON.parse(result.body).inspect
        end

        # TODO: how to test for this:
        # If reason is "youtubeSignupRequired"
        # If the user's youtube account is unlinked, they'll have to go here.        
        # http://m.youtube.com/create_channel.  With v3, there is no automated way to do this.        
        raise msg
      else
        # This has everything one needs to start the upload to youtube:      
        {
            "method" => "PUT",
            "url" => result.headers['location'],
            "Authorization" => "Bearer #{auth.token}",
            "Content-Length" => length,
            "Content-Type" => "video/*"
        }
      end
    end

    # https://developers.google.com/youtube/v3/guides/using_resumable_upload_protocol#Check_Upload_Status
    def youtube_upload_status(user, upload_url, length)
      auth = UserAuthorization.google_auth(user).first
      if auth.nil? || auth.token.nil?
        raise SecurityError, "No current google token found for user #{user}"
      end

      # PUT UPLOAD_URL HTTP/1.1
      # Authorization: Bearer AUTH_TOKEN
      # Content-Length: 0
      # Content-Range: bytes */CONTENT_LENGTH
      RestClient.put(upload_url, nil, {
                                   'Authorization' => "Bearer #{auth.token}",
                                   'Content-Length' => "0",
                                   'Content-Range' => "bytes */#{length}"
                               }) do |response, request, result|
        # Result looks like this:
        # 308 Resume Incomplete
        # Content-Length: 0
        # Range: bytes=0-999999
        case (response.code)
          when 200..207
            result_hash = {
                "offset" => 0,
                "length" => length,
                "status" => response.code
            }
          when 308
            range_str = response.headers['Range']
            if range_str.nil?
              range = 0..length
            else
              range = range_str.split("-")
            end
            result_hash = {
                "offset" => range.first.to_i,
                "length" => range.last.to_i,
                "status" => response.code
            }
          else
            raise "Unexpected status from youtube: [#{response.code}] with headers: #{response.headers.inspect}"
        end

        result_hash
      end
    end

    # @return true if file specified by URL uploaded, false otherwise
    def verify_youtube_upload(user, upload_url, length)
      status_hash=youtube_upload_status(user, upload_url, length)
      (status_hash['status']>=200 && status_hash['status']<300)
    end

    # Set fully_uploaded if the upload can be verified.
    # @return true if verified; false otherwise:
    def complete_upload(recorded_video)
      if (verify_youtube_upload(recorded_video.user, recorded_video.url, recorded_video.length))
        recorded_video.update_attribute(:fully_uploaded, true)
      else
        false
      end
    end

    def verify_recaptcha(recaptcha_response)
      success = false
      if !Rails.application.config.recaptcha_enable
        success = true
      else
        Rails.logger.info "Login with: #{recaptcha_response}"
        RestClient.get("https://www.google.com/recaptcha/api/siteverify",
                       params: {
                           secret: Rails.application.config.recaptcha_private_key,
                           response: recaptcha_response
                       }
        ) do |response, request, result|
          Rails.logger.info "response: #{response.inspect}"
          case (response.code)
            when 200..207
              json = JSON.parse(response.to_str)
              if json['success']
                success = true
              else
                Rails.logger.info("Error verifying recaptcha: #{json['error-codes'].inspect}")
              end
            else
              Rails.logger.info("Unexpected status from google_recaptcha: [#{response.code}] with headers: #{response.headers.inspect}")
          end #case
        end #do
      end # if
      success
    end

    #def

    # This will also sign in and prompt for login as necessary; 
    # currently requires the server to be running at localhost:3000
    def signin_flow()
      config = Rails.application.config

      self.client = Google::APIClient.new(
          :application_name => 'JamKazam',
          :application_version => '1.0.0'
      )

      raise "SIGNIN FLOW!!"
      flow = Google::APIClient::InstalledAppFlow.new(
          :client_id => config.google_client_id,
          :client_secret => config.google_secret,
          :redirect_uri => redirect_uri,
          :scope => 'email profile https://www.googleapis.com/auth/youtube https://www.googleapis.com/auth/youtube.upload'
      )

      self.client.authorization = flow.authorize
    end

    # Must manually confirm to obtain refresh token:
    def get_refresh_token
      config = Rails.application.config
      conn = Faraday.new(:url => 'https://accounts.google.com', :ssl => {:verify => false}) do |faraday|
        faraday.request :url_encoded
        faraday.adapter Faraday.default_adapter
      end

      wait_for_callback do |refresh_token|
        Rails.logger.info("The refresh_token is #{refresh_token}")
      end

      result = conn.get '/o/oauth2/auth', {
                                            'scope' => 'email profile https://www.googleapis.com/auth/youtube https://www.googleapis.com/auth/youtube.upload',
                                            'client_id' => config.google_client_id,
                                            'response_type' => "code",
                                            'access_type' => "offline",
                                            'redirect_uri' => redirect_uri
                                        }
    end

    def get_access_token(refresh_token)
      refresh_token = "4/g9uZ8S4lq2Bj1J8PPIkgOFKhTKmCHSmRe68iHA75hRg.gj8Nt5bpVYQdPm8kb2vw2M23tnRnkgI"

      config = Rails.application.config
      conn = Faraday.new(:url => 'https://accounts.google.com', :ssl => {:verify => false}) do |faraday|
        faraday.request :url_encoded
        faraday.adapter Faraday.default_adapter
      end

      wait_for_callback do |access_token|
        Rails.logger.info("The access_token is #{access_token}")
      end

      result = conn.post '/o/oauth2/token', nil, {
                                              'scope' => 'email profile https://www.googleapis.com/auth/youtube https://www.googleapis.com/auth/youtube.upload',
                                              'client_id' => config.google_client_id,
                                              'client_secret' => config.google_secret,
                                              'refresh_token' => refresh_token,
                                              'grant_type' => "refresh_token",
                                              'redirect_uri' => redirect_uri
                                          }

      Rails.logger.info("REsult: #{result.inspect}\n\n")
    end

    def wait_for_callback(port=3000)
      shutdown()
      self.server = Thread.new {
        Rails.logger.info("STARTING SERVER THREAD...")
        tcp_server = TCPServer.new('localhost', port)

        self.socket = tcp_server.accept
        if self.socket
          request = self.socket.gets
          Rails.logger.info("REQUEST: #{request}")

          params=CGI.parse(request)
          code = params['code'].first
          # Whack the end part:
          access_code = code ? code.split(" ").first : ""

          status = (access_code.present?) ? 'OK' : 'EMPTY'
          Rails.logger.info("access_code is #{status}")
          token=exchange_for_token(access_code)
          yield(token)

          response = "#{status}\n"
          self.socket.print "HTTP/1.1 200 OK\r\n" +
                                "Content-Type: text/plain\r\n" +
                                "Content-Length: #{response.bytesize}\r\n" +
                                "Connection: close\r\n"

          self.socket.print "\r\n"
          self.socket.print response
          self.socket.close
          self.socket=nil
        else
          puts "WHY WOULD THIS EVER HAPPEN?"
          raise "WHY WOULD THIS EVER HAPPEN?"
        end

      }
    end


    def exchange_for_token(access_code)
      Rails.logger.info("Exchanging token for code: [#{access_code}]")
      conn = Faraday.new(:url => "https://accounts.google.com", :ssl => {:verify => false}) do |faraday|
        faraday.request :url_encoded
        faraday.adapter Faraday.default_adapter
      end

      exchange_parms={
          'grant_type' => 'authorization_code',
          'code' => (access_code),
          'client_id' => (config.google_email),
          'client_secret' => (config.google_secret),
          'redirect_uri' => (redirect_uri),
      }

      result = conn.post('/o/oauth2/token', exchange_parms)
      if result.body.nil? || result.body.blank?
        raise "Result not in correct form: [#{result.body}]"
      end

      body_hash = JSON.parse(result.body)
      body_hash['access_token']
    end

    # shutdown
    def shutdown()
      Rails.logger.info("Stopping oauth server...")
      if (self.socket)
        begin
          self.socket.close
        rescue IOError
          # Expected for most cases:
          Rails.logger.info("Socket already closed.")
        end
        self.socket = nil
      end
    end
  end # class
end # module
  