# Copyright (c) HashiCorp, Inc.
# SPDX-License-Identifier: BUSL-1.1

module VagrantPlugins
  module CloudCommand
    module Util
      # @return [String] Vagrant Cloud server URL
      def api_server_url
        if Vagrant.server_url == Vagrant::DEFAULT_SERVER_URL
          return "#{Vagrant.server_url}/api/v1"
        end
        begin
          addr = URI.parse(Vagrant.server_url)
          if addr.path.empty? || addr.path.to_s == "/"
            addr.path = "/api/v1"
          end

          addr.to_s
        rescue URI::Error
          Vagrant.server_url
        end
      end

      # @param [Vagrant::Environment] env
      # @param [Hash] options
      # @option options [String] :login Username or email
      # @option options [String] :description Description of login usage for token
      # @option options [String] :code 2FA code for login
      # @option options [Boolean] :quiet Do not prompt user
      # @returns [VagrantPlugins::CloudCommand::Client, nil]
      def client_login(env, options={})
        return @_client if defined?(@_client)
        @_client = Client.new(env)
        return @_client if @_client.logged_in?

        # If directed to be quiet, do not continue and
        # just return nil
        return if options[:quiet]

        # Let the user know what is going on.
        env.ui.output(I18n.t("cloud_command.command_header") + "\n")

        # If it is a private cloud installation, show that
        if Vagrant.server_url != Vagrant::DEFAULT_SERVER_URL
          env.ui.output("Vagrant Cloud URL: #{Vagrant.server_url}")
        end

        options = {} if !options
        # Ask for the username
        if options[:login]
          @_client.username_or_email = options[:login]
          env.ui.output("Vagrant Cloud username or email: #{@_client.username_or_email}")
        else
          @_client.username_or_email = env.ui.ask("Vagrant Cloud username or email: ")
        end

        @_client.password = env.ui.ask("Password (will be hidden): ", echo: false)

        description_default = "Vagrant login from #{Socket.gethostname}"
        if !options[:description]
          description = env.ui.ask("Token description (Defaults to #{description_default.inspect}): ")
        else
          description = options[:description]
          env.ui.output("Token description: #{description}")
        end

        description = description_default if description.empty?

        code = nil

        begin
          token = @_client.login(description: description, code: code)
        rescue Errors::TwoFactorRequired
          until code
            code = env.ui.ask("2FA code: ")

            if @_client.two_factor_delivery_methods.include?(code.downcase)
              delivery_method, code = code, nil
              @_client.request_code delivery_method
            end
          end

          retry
        end

        @_client.store_token(token)
        Vagrant::Util::CredentialScrubber.sensitive(token)
        env.ui.success(I18n.t("cloud_command.logged_in"))
        @_client
      end

      # Print search results from Vagrant Cloud to the console
      #
      # @param [Array<VagrantCloud::Box>] search_results Box search results from Vagrant Cloud
      # @param [Boolean] short Print short summary
      # @param [Boolean] json Print output in JSON format
      # @param [Vagrant::Environment] env Current Vagrant environment
      # @return [nil]
      def format_search_results(search_results, short, json, env)
        result = search_results.map do |b|
          {
            name: b.tag,
            version: b.current_version.version,
            downloads: format_downloads(b.downloads.to_s),
            providers: b.current_version.providers.map(&:name).uniq.join(", "),
            architectures: b.current_version.providers.map(&:architecture).join(", ")
          }
        end

        if short
          result.map { |b| env.ui.info(b[:name]) }
        elsif json
          env.ui.info(result.to_json)
        else
          column_labels = {}
          columns = result.first.keys
          columns.each do |c|
            column_labels[c] = c.to_s.upcase
          end
          print_search_table(env, column_labels, result, [:downloads])
        end
        nil
      end

      # Output box details result from Vagrant Cloud
      #
      # @param [VagrantCloud::Box, VagrantCloud::Box::Version] box Box or box version to display
      # @param [Vagrant::Environment] env Current Vagrant environment
      # @return [nil]
      def format_box_results(box, env, options={})
        if box.is_a?(VagrantCloud::Box)
          info = box_info(box, options)
        elsif box.is_a?(VagrantCloud::Box::Version)
          info = version_info(box)
        else
          info = provider_info(box)
        end

        width = info.keys.map(&:size).max
        info.each do |k, v|
          v.to_s.split("\n").each_with_index do |line, idx|
            whitespace = width - k.size + line.to_s.size
            if idx == 0
              env.ui.info "#{k}: #{line.rjust(whitespace)}"
            else
              whitespace += k.size + 2
              env.ui.info line.rjust(whitespace)
            end
          end
        end
        nil
      end

      # Load box and yield
      #
      # @param [VagrantCloud::Account] account Vagrant Cloud account
      # @param [String] org Organization name
      # @param [String] box Box name
      # @yieldparam [VagrantCloud::Box] box Requested Vagrant Cloud box
      # @yieldreturn [Integer]
      # @return [Integer]
      def with_box(account:, org:, box:)
        org = account.organization(name: org)
        b = org.boxes.detect { |b| b.name == box }
        if !b
          @env.ui.error(I18n.t("cloud_command.box.not_found",
            org: org.username, box_name: box))
          return 1
        end
        yield b
      end

      # Load box version and yield
      #
      # @param [VagrantCloud::Account] account Vagrant Cloud account
      # @param [String] org Organization name
      # @param [String] box Box name
      # @param [String] version Box version
      # @yieldparam [VagrantCloud::Box::Version] version Requested Vagrant Cloud box version
      # @yieldreturn [Integer]
      # @return [Integer]
      def with_version(account:, org:, box:, version:)
        with_box(account: account, org: org, box: box) do |b|
          v = b.versions.detect { |v| v.version == version }
          if !v
            @env.ui.error(I18n.t("cloud_command.version.not_found",
              box_name: box, org: org, version: version))
            return 1
          end
          yield v
        end
      end

      # Load box version and yield
      #
      # @param [VagrantCloud::Account] account Vagrant Cloud account
      # @param [String] org Organization name
      # @param [String] box Box name
      # @param [String] version Box version
      # @param [String] provider Box version provider name
      # @yieldparam [VagrantCloud::Box::Provider] provider Requested Vagrant Cloud box version provider
      # @yieldreturn [Integer]
      # @return [Integer]
      def with_provider(account:, org:, box:, version:, provider:, architecture:)
        with_version(account: account, org: org, box: box, version: version) do |v|
          p = v.providers.detect { |p|
            p.name == provider &&
              p.architecture == architecture
          }
          if !p
            @env.ui.error(I18n.t("cloud_command.provider.not_found",
              org: org, box_name: box, version: version, provider_name: provider))
            return 1
          end
          yield p
        end
      end

      protected

      # Extract box information for display
      #
      # @param [VagrantCloud::Box] box Box for extracting information
      # @return [Hash<String,String>]
      def box_info(box, options={})
        current_version = box.current_version
        if current_version
          current_version = nil if !Array(options[:providers]).empty? &&
                                   current_version.providers.none? { |p| options[:providers].include?(p.name) }
          current_version = nil if !Array(options[:architectures]).empty? &&
                                   current_version.providers.none? { |p| options[:architectures].include?(p.architecture) }
        end
        versions = box.versions
        # Apply provider filter if defined
        versions = versions.find_all { |v|
          v.providers.any? { |p|
            options[:providers].include?(p.name)
          }
        } if !Array(options[:providers]).empty?
        # Apply architecture filter if defined
        versions = versions.find_all { |v|
          v.providers.any? { |p|
            options[:architectures].include?(p.architecture)
          }
        } if !Array(options[:architectures]).empty?

        raise "no matches" if current_version.nil? && versions.empty?

        Hash.new.tap do |i|
          i["Box"] = box.tag
          i["Description"] = box.description
          i["Private"] = box.private ? "yes" : "no"
          i["Created"] = box.created_at
          i["Updated"] = box.updated_at
          if !current_version.nil?
            i["Current Version"] = box.current_version.version
          else
            i["Current Version"] = "N/A"
          end
          i["Versions"] = versions.slice(0, 5).map(&:version).join(", ")
          if box.versions.size > 5
            i["Versions"] += " ..."
          end
          i["Downloads"] = format_downloads(box.downloads)
        end
      end

      # Extract version information for display
      #
      # @param [VagrantCloud::Box::Version] version Box version for extracting information
      # @return [Hash<String,String>]
      def version_info(version)
        provider_arches = version.providers.group_by(&:name).map { |provider_name, info|
          "#{provider_name} (#{info.map(&:architecture).sort.join(", ")})"
        }.sort.join("\n")
        Hash.new.tap do |i|
          i["Box"] = version.box.tag
          i["Version"] = version.version
          i["Description"] = version.description
          i["Status"] = version.status
          i["Providers"] = provider_arches
          i["Created"] = version.created_at
          i["Updated"] = version.updated_at
        end
      end

      # Extract provider information for display
      #
      # @param [VagrantCloud::Box::Provider] provider Box provider for extracting information
      # @return [Hash<String,String>]
      def provider_info(provider)
        {
          "Box" => provider.version.box.tag,
          "Private" => provider.version.box.private ? "yes" : "no",
          "Version" => provider.version.version,
          "Provider" => provider.name,
          "Architecture" => provider.architecture,
          "Default Architecture" => provider.default_architecture ? "yes" : "no",
        }
      end

      # Print table results from search request
      #
      # @param [Vagrant::Environment] env Current Vagrant environment
      # @param [Hash] column_labels A hash of key/value pairs for table labels (i.e. {col1: "COL1"})
      # @param [Array] results An array of hashes representing search resuls
      # @param [Array] to_jrust_keys - List of columns keys to right justify (left justify is defualt)
      # @return [nil]
      # @note Modified from https://stackoverflow.com/a/28685559
      def print_search_table(env, column_labels, results, to_rjust_keys)
        columns = column_labels.each_with_object({}) do |(col,label),h|
          h[col] = {
            label: label,
            width: [results.map { |g| g[col].size }.max, label.size].max
          }
        end

        write_header(env, columns)
        write_divider(env, columns)
        results.each { |h| write_line(env, columns, h, to_rjust_keys) }
        write_divider(env, columns)
      end

      # Write the header for a table
      #
      # @param [Vagrant::Environment] env Current Vagrant environment
      # @param [Array<Hash>] columns List of columns in Hash format with `:label` and `:width` keys
      # @return [nil]
      def write_header(env, columns)
        env.ui.info "| #{ columns.map { |_,g| g[:label].ljust(g[:width]) }.join(' | ') } |"
        nil
      end

      # Write a row divider for a table
      #
      # @param [Vagrant::Environment] env Current Vagrant environment
      # @param [Array<Hash>] columns List of columns in Hash format with `:label` and `:width` keys
      # @return [nil]
      def write_divider(env, columns)
        env.ui.info "+-#{ columns.map { |_,g| "-"*g[:width] }.join("-+-") }-+"
        nil
      end

      # Write a line of content for a table
      #
      # @param [Vagrant::Environment] env Current Vagrant environment
      # @param [Array<Hash>] columns List of columns in Hash format with `:label` and `:width` keys
      # @param [Hash] h Values to print in row
      # @param [Array<String>] to_rjust_keys List of columns to right justify
      # @return [nil]
      def write_line(env, columns, h, to_rjust_keys)
        str = h.keys.map { |k|
          if to_rjust_keys.include?(k)
            h[k].rjust(columns[k][:width])
          else
            h[k].ljust(columns[k][:width])
          end
        }.join(" | ")
        env.ui.info "| #{str} |"
        nil
      end

      # Converts a string of numbers into a formatted number
      #
      # 1234 -> 1,234
      #
      # @param [String] number Numer to format
      def format_downloads(number)
        number.to_s.chars.reverse.each_slice(3).map(&:join).join(",").reverse
      end
    end
  end
end
