# The MIT License (MIT)

# Copyright (c) 2023 Mike DeAngelo Google, Inc.

# Permission is hereby granted, free of charge, to any person obtaining a copy of
# this software and associated documentation files (the "Software"), to deal in
# the Software without restriction, including without limitation the rights to
# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
# the Software, and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:

# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.

# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

# frozen_string_literal: true

require 'json'
require 'pastel'
require 'tty-reader'

require_relative '../../gzr'

module Gzr
  module Session

    def pastel
      @pastel ||= Pastel.new
    end

    def say_ok(data, output: $stdout)
      output.puts pastel.green data
    end

    def say_warning(data, output: $stderr)
      output.puts pastel.yellow data
    end

    def say_error(data, output: $stderr)
      output.puts pastel.red data
    end

    @versions = []
    @current_version = nil

    def sufficient_version?(given_version, minimum_version)
      return true unless (given_version && minimum_version)
      versions = @versions.sort
      !versions.drop_while {|v| v < minimum_version}.reverse.drop_while {|v| v > given_version}.empty?
    end

    def token_file
      "#{ENV["HOME"]}/.gzr_auth"
    end

    def read_token_data
      return nil unless File.exist?(token_file)
      s = File.stat(token_file)
      if !(s.mode.to_s(8)[3..5] == "600")
        say_error "#{token_file} mode is #{s.mode.to_s(8)[3..5]}. It must be 600. Ignoring."
        return nil
      end
      token_data = nil
      file = nil
      begin
        file = File.open(token_file)
        token_data = JSON.parse(file.read,{:symbolize_names => true})
      ensure
        file.close if file
      end
      token_data
    end

    def write_token_data(token_data)
      file = nil
      begin
        file = File.new(token_file, "wt")
        file.chmod(0600)
        file.write JSON.pretty_generate(token_data)
      ensure
        file.close if file
      end
    end

    def build_connection_hash(api_version=nil)
      conn_hash = Hash.new
      conn_hash[:api_endpoint] = "http#{@options[:ssl] ? "s" : ""}://#{@options[:host]}:#{@options[:port]}/api/#{api_version||@current_version||""}"
      if @options[:http_proxy]
        conn_hash[:connection_options] ||= {}
        conn_hash[:connection_options][:proxy] = {
          :uri => @options[:http_proxy]
        }
      end
      if @options[:ssl]
        conn_hash[:connection_options] ||= {}
        if @options[:verify_ssl] then
          conn_hash[:connection_options][:ssl] = {
            :verify => true,
            :verify_mode => (OpenSSL::SSL::VERIFY_PEER | OpenSSL::SSL::VERIFY_FAIL_IF_NO_PEER_CERT)
          }
        else
          conn_hash[:connection_options][:ssl] = {
            :verify => false,
            :verify_mode => (OpenSSL::SSL::VERIFY_NONE)
          }
        end
      end
      if @options[:timeout]
        conn_hash[:connection_options] ||= {}
        conn_hash[:connection_options][:request] = {
          :timeout => @options[:timeout]
        }
      end
      conn_hash[:user_agent] = "Gazer #{Gzr::VERSION}"

      return conn_hash if @options[:token] || @options[:token_file]

      if @options[:client_id] then
        conn_hash[:client_id] = @options[:client_id]
        if @options[:client_secret] then
          conn_hash[:client_secret] = @options[:client_secret]
        else
          reader = TTY::Reader.new
          @secret ||= reader.read_line("Enter your client_secret:", echo: false)
          conn_hash[:client_secret] = @secret
        end
      else
        conn_hash[:netrc] = true
        conn_hash[:netrc_file] = "~/.netrc"
      end
      conn_hash
    end

    def login(min_api_version="4.0")
      if !@options[:token] && !@options[:token_file]
        if (@options[:client_id].nil? && ENV["LOOKERSDK_CLIENT_ID"])
          @options[:client_id] = ENV["LOOKERSDK_CLIENT_ID"]
        end

        if (@options[:client_secret].nil? && ENV["LOOKERSDK_CLIENT_SECRET"])
          @options[:client_secret] = ENV["LOOKERSDK_CLIENT_SECRET"]
        end
      end

      if (@options[:verify_ssl] && ENV["LOOKERSDK_VERIFY_SSL"])
        @options[:verify_ssl] = !(/^f(alse)?$/i =~ ENV["LOOKERSDK_VERIFY_SSL"])
      end

      if ((@options[:host] == 'localhost') && ENV["LOOKERSDK_BASE_URL"])
        base_url = ENV["LOOKERSDK_BASE_URL"]
        @options[:ssl] = !!(/^https/ =~ base_url)
        @options[:host] = /^https?:\/\/([^:\/]+)/.match(base_url)[1]
        md = /:([0-9]+)\/?$/.match(base_url)
        @options[:port] = md[1] if md
      end

      say_ok("using options #{@options.select { |k,v| k != 'client_secret' }.map { |k,v| "#{k}=>#{v}" }}") if @options[:debug]

      @secret = nil
      begin
        conn_hash = build_connection_hash

        sawyer_options = {
          :links_parser => Sawyer::LinkParsers::Simple.new,
          :serializer  => LookerSDK::Client::Serializer.new(JSON),
          :faraday => Faraday.new(conn_hash[:connection_options]) do |conn|
            conn.use LookerSDK::Response::RaiseError
            if @options[:persistent]
              conn.adapter :net_http_persistent
            end
          end
        }

        endpoint = conn_hash[:api_endpoint]
        endpoint_uri = URI.parse(endpoint)
        root = endpoint.slice(0..-endpoint_uri.path.length)

        agent = Sawyer::Agent.new(root, sawyer_options) do |http|
          http.headers[:accept] = 'application/json'
          http.headers[:user_agent] = conn_hash[:user_agent]
        end

        begin
          versions_response = agent.call(:get,"/versions")
          @versions = versions_response.data.supported_versions.map {|v| v.version}
          @current_version = versions_response.data.current_version.version || "4.0"
        rescue Faraday::SSLError => e
          raise Gzr::CLI::Error, "SSL Certificate could not be verified\nDo you need the --no-verify-ssl option or the --no-ssl option?"
        rescue Faraday::ConnectionFailed => cf
          raise Gzr::CLI::Error, "Connection Failed.\nDid you specify the --no-ssl option for an ssl secured server?\nYou may need to use --port=443 in some cases as well."
        rescue LookerSDK::NotFound => nf
          say_warning "endpoint #{root}/versions was not found"
        end
      end

      say_warning "API current_version #{@current_version}" if @options[:debug]
      say_warning "Supported API versions #{@versions}" if @options[:debug]

      api_version = [min_api_version, @current_version].max
      raise Gzr::CLI::Error, "Operation requires API v#{api_version}, which is not available from this host" if api_version && !@versions.any? {|v| v == api_version}

      conn_hash = build_connection_hash(api_version)
      @secret = nil

      say_ok("connecting to #{conn_hash.map { |k,v| "#{k}=>#{(k == :client_secret) ? '*********' : v}" }}") if @options[:debug]

      begin
        faraday = Faraday.new(conn_hash[:connection_options]) do |conn|
          conn.use LookerSDK::Response::RaiseError
          if @options[:persistent]
            conn.adapter :net_http_persistent
          end
        end
        @sdk = LookerSDK::Client.new(conn_hash.merge(faraday: faraday)) unless @sdk

        say_ok "check for connectivity: #{@sdk.alive?}" if @options[:debug]
        if @options[:token_file]
          entry = read_token_data&.fetch(@options[:host].to_sym,nil)&.fetch(@options[:su]&.to_sym || :default,nil)
          if entry.nil?
            say_error "No token found for host #{@options[:host]} and user #{@options[:su] || "default"}"
            say_error "login with `gzr session login --host #{@options[:host]}` to set a token"
            raise LookerSDK::Unauthorized.new
          end
          (day, time, tz) = entry[:expiration].split(' ')
          day_parts = day.split('-')
          time_parts = time.split(':')
          date_time_parts = day_parts + time_parts + [tz]
          expiration = Time.new(*date_time_parts)
          if expiration < (Time.now + 300)
            if expiration < Time.now
              say_error "token expired at #{expiration}"
            else
              say_error "token expires at #{expiration}, which is in the next 5 minutes"
            end
            say_error "login again with `gzr session login --host #{@options[:host]}`"
            raise LookerSDK::Unauthorized.new
          end
          @sdk.access_token = entry[:token]
        elsif @options[:token]
          @sdk.access_token = @options[:token]
        end
        say_ok "verify authentication: #{@sdk.authenticated?}" if @options[:debug]
      rescue LookerSDK::Unauthorized => e
        say_error "Unauthorized - credentials are not valid"
        raise
      rescue LookerSDK::Error => e
        say_error "Unable to connect"
        say_error e
        say_error e.errors if e.respond_to?(:errors) && e.errors
        raise
      end
      raise Gzr::CLI::Error, "Invalid credentials" unless @sdk.authenticated?


      if @options[:su] && !(@options[:token] || @options[:token_file])then
        say_ok "su to user #{@options[:su]}" if @options[:debug]
        @access_token_stack.push(@sdk.access_token)
        begin
          @sdk.access_token = @sdk.login_user(@options[:su]).access_token
          say_warning "verify authentication: #{@sdk.authenticated?}" if @options[:debug]
        rescue LookerSDK::Error => e
          say_error "Unable to su to user #{@options[:su]}"
          say_error e
          say_error e.errors if e.respond_to?(:errors) && e.errors
          raise
        end
      end
      @sdk
    end

    def logout_all
      pastel = Pastel.new(enabled: true)
      say_ok "logout" if @options[:debug]
      begin
        @sdk.logout
      rescue LookerSDK::Error => e
        say_error "Unable to logout"
        say_error e
        say_error e.errors if e.respond_to?(:errors) && e.errors
      end if @sdk
      loop do
        token = @access_token_stack.pop
        break unless token
        say_ok "logout the parent session" if @options[:debug]
        @sdk.access_token = token
        begin
          @sdk.logout
        rescue LookerSDK::Error => e
          say_error "Unable to logout"
          say_error e
          say_error e.errors if e.respond_to?(:errors) && e.errors
        end
      end
    end

    def with_session(min_api_version="4.0")
      return nil unless block_given?
      begin
        login(min_api_version) unless @sdk
        yield
      rescue LookerSDK::Error => e
        say_error e.errors if e.respond_to?(:errors) && e.errors
        e.backtrace.each { |b| say_error b } if @options[:debug]
        raise Gzr::CLI::Error, e.message
      ensure
        logout_all unless @options[:token] || @options[:token_file]
      end
    end

    def update_auth(workspace_id)
      body = {}
      body[:workspace_id] = workspace_id
      begin
        @sdk.update_session(body)&.to_attrs
      rescue LookerSDK::Error => e
        say_error "Unable to run update_session(#{JSON.pretty_generate(body)})"
        say_error e
      end
    end

  end
end
