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

require 'optparse'
require "vagrant/util/uploader"

module VagrantPlugins
  module CloudCommand
    module Command
      class Publish < Vagrant.plugin("2", :command)
        include Util

        def execute
          options = {
            architecture: Vagrant::Util::Platform.architecture,
            direct_upload: true,
          }

          opts = OptionParser.new do |o|
            o.banner = "Usage: vagrant cloud publish [options] organization/box-name version provider-name [provider-file]"
            o.separator ""
            o.separator "Create and release a new Vagrant Box on Vagrant Cloud"
            o.separator ""
            o.separator "Options:"
            o.separator ""

            o.on("-a", "--architecture ARCH", String, "Architecture of guest box (defaults to current host architecture)") do |a|
              options[:architecture] = a
            end
            o.on("--url URL", String, "Remote URL to download this provider (cannot be used with provider-file)") do |u|
              options[:url] = u
            end
            o.on("-d", "--description DESCRIPTION", String, "Full description of box") do |d|
              options[:description] = d
            end
            o.on("--version-description DESCRIPTION", String, "Description of the version to create") do |v|
              options[:version_description] = v
            end
            o.on("-f", "--[no-]force", "Disables confirmation to create or update box") do |f|
              options[:force] = f
            end
            o.on("-p", "--[no-]private", "Makes box private") do |p|
              options[:private] = p
            end
            o.on("-r", "--[no-]release", "Releases box") do |p|
              options[:release] = p
            end
            o.on("-s", "--short-description DESCRIPTION", String, "Short description of the box") do |s|
              options[:short_description] = s
            end
            o.on("-c", "--checksum CHECKSUM_VALUE", String, "Checksum of the box for this provider. --checksum-type option is required.") do |c|
              options[:checksum] = c
            end
            o.on("-C", "--checksum-type TYPE", String, "Type of checksum used (md5, sha1, sha256, sha384, sha512). --checksum option is required.") do |c|
              options[:checksum_type] = c
            end
            o.on("--[no-]direct-upload", "Upload asset directly to backend storage") do |d|
              options[:direct_upload] = d
            end
            o.on("--[no-]default-architecture", "Mark as default architecture for specific provider") do |d|
              options[:default_architecture] = d
            end
          end

          # Parse the options
          argv = parse_options(opts)
          return if !argv

          if argv.length < 3 || # missing required arguments
              argv.length > 4 || # too many arguments
              (argv.length < 4 && !options.key?(:url)) || # file argument required if url is not provided
              (argv.length > 3 && options.key?(:url)) # cannot provide url and file argument
            raise Vagrant::Errors::CLIInvalidUsage,
              help: opts.help.chomp
          end

          org, box_name = argv.first.split('/', 2)
          _, version, provider_name, box_file = argv

          if box_file && !File.file?(box_file)
            raise Vagrant::Errors::BoxFileNotExist,
              file: box_file
          end

          @client = client_login(@env)
          params = options.slice(:private, :release, :url, :short_description,
            :description, :version_description, :checksum, :checksum_type,
            :architecture, :default_architecture)

          # Display output to user describing action to be taken
          display_preamble(org, box_name, version, provider_name, params)

          if !options[:force]
            cont = @env.ui.ask(I18n.t("cloud_command.continue"))
            return 1 if cont.strip.downcase != "y"
          end

          # Load up all the models we'll need to publish the asset
          box = load_box(org, box_name, @client.token)
          box_v = load_box_version(box, version)
          box_p = load_version_provider(box_v, provider_name, params[:architecture])

          # Update all the data
          set_box_info(box, params.slice(:private, :short_description, :description))
          set_version_info(box_v, params.slice(:version_description))
          set_provider_info(box_p, params.slice(
            :architecture,
            :checksum,
            :checksum_type,
            :default_architecture,
            :url))

          # Save any updated state
          @env.ui.warn(I18n.t("cloud_command.publish.box_save"))
          box.save

          # If we have a box file asset, upload it
          if box_file
            upload_box_file(box_p, box_file, options.slice(:direct_upload))
          end

          # If configured to release the box, release it
          if options[:release] && !box_v.released?
            release_version(box_v)
          end

          # And we're done!
          @env.ui.success(I18n.t("cloud_command.publish.complete", org: org, box_name: box_name))
          format_box_results(box_p, @env)
          0
        rescue VagrantCloud::Error => err
          @env.ui.error(I18n.t("cloud_command.errors.publish.fail", org: org, box_name: box_name))
          @env.ui.error(err.message)
          1
        end

        # Upload the file for the given box provider
        #
        # @param [VagrantCloud::Box::Provider] provider Vagrant Cloud box version provider
        # @param [String] box_file Path to local asset for upload
        # @param [Hash] options
        # @option options [Boolean] :direct_upload Upload directly to backend storage
        # @return [nil]
        def upload_box_file(provider, box_file, options={})
          box_file = File.absolute_path(box_file)
          @env.ui.info(I18n.t("cloud_command.publish.upload_provider", file: box_file))
          # Include size check on file and disable direct if over 5G
          if options[:direct_upload]
            fsize = File.stat(box_file).size
            if fsize > (5 * Vagrant::Util::Numeric::GIGABYTE)
              box_size = Vagrant::Util::Numeric.bytes_to_string(fsize)
              @env.ui.warn(I18n.t("cloud_command.provider.direct_disable", size: box_size))
              options[:direct_upload] = false
            end
          end

          provider.upload(direct: options[:direct_upload]) do |upload_url|
            Vagrant::Util::Uploader.new(upload_url, box_file, ui: @env.ui, method: :put).upload!
          end
          nil
        end

        # Release the box version
        #
        # @param [VagrantCloud::Box::Version] version Vagrant Cloud box version
        # @return [nil]
        def release_version(version)
          @env.ui.info(I18n.t("cloud_command.publish.release"))
          version.release
          nil
        end

        # Set any box related attributes that were provided
        #
        # @param [VagrantCloud::Box] box Vagrant Cloud box
        # @param [Hash] options
        # @option options [Boolean] :private Box access is private
        # @option options [String] :short_description Short description of box
        # @option options [String] :description Full description of box
        # @return [VagrantCloud::Box]
        def set_box_info(box, options={})
          box.private = options[:private] if options.key?(:private)
          box.short_description = options[:short_description] if options.key?(:short_description)
          box.description = options[:description] if options.key?(:description)
          box
        end

        # Set any version related attributes that were provided
        #
        # @param [VagrantCloud::Box::Version] version Vagrant Cloud box version
        # @param [Hash] options
        # @option options [String] :version_description Description for this version
        # @return [VagrantCloud::Box::Version]
        def set_version_info(version, options={})
          version.description = options[:version_description] if options.key?(:version_description)
          version
        end

        # Set any provider related attributes that were provided
        #
        # @param [VagrantCloud::Box::Provider] provider Vagrant Cloud box version provider
        # @param [Hash] options
        # @option options [String] architecture Guest architecture of box
        # @option options [String] :url Remote URL for self hosted
        # @option options [String] :checksum_type Type of checksum value provided
        # @option options [String] :checksum Checksum of the box asset
        # @option options [Boolean] :default_architecture Default architecture for named provider
        # @return [VagrantCloud::Box::Provider]
        def set_provider_info(provider, options={})
          provider.url = options[:url] if options.key?(:url)
          provider.checksum_type = options[:checksum_type] if options.key?(:checksum_type)
          provider.checksum = options[:checksum] if options.key?(:checksum)
          provider.architecture = options[:architecture] if options.key?(:architecture)
          provider.default_architecture = options[:default_architecture] if options.key?(:default_architecture)
          provider
        end

        # Load the requested version provider
        #
        # @param [VagrantCloud::Box::Version] version The version of the Vagrant Cloud box
        # @param [String] provider_name Name of the provider
        # @return [VagrantCloud::Box::Provider]
        def load_version_provider(version, provider_name, architecture)
          provider = version.providers.detect { |pv|
            pv.name == provider_name &&
              pv.architecture == architecture
          }
          return provider if provider
          version.add_provider(provider_name, architecture)
        end

        # Load the requested box version
        #
        # @param [VagrantCloud::Box] box The Vagrant Cloud box
        # @param [String] version Version of the box
        # @return [VagrantCloud::Box::Version]
        def load_box_version(box, version)
          v = box.versions.detect { |v| v.version == version }
          return v if v
          box.add_version(version)
        end

        # Load the requested box
        #
        # @param [String] org Organization name for box
        # @param [String] box_name Name of the box
        # @param [String] access_token User access token
        # @return [VagrantCloud::Box]
        def load_box(org, box_name, access_token)
          account = VagrantCloud::Account.new(
            custom_server: api_server_url,
            access_token: access_token
          )
          box = account.organization(name: org).boxes.detect { |b| b.name == box_name }
          return box if box
          account.organization(name: org).add_box(box_name)
        end

        # Display publishing information to user before starting process
        #
        # @param [String] org Organization name
        # @param [String] box_name Name of the box to publish
        # @param [String] version Version of the box to publish
        # @param [String] provider_name Name of the provider being published
        # @param [Hash] options
        # @option options [String] :architecture Name of architecture of provider being published#
        # @option options [Boolean] :private Box is private
        # @option options [Boolean] :release Box should be released
        # @option options [String] :url Remote URL for self-hosted boxes
        # @option options [Boolean] :default_architecture Architecture is default for provider name
        # @option options [String] :description Description of the box
        # @option options [String] :short_description Short description of the box
        # @option options [String] :version_description Description of the box version
        # @return [nil]
        def display_preamble(org, box_name, version, provider_name, options={})
          @env.ui.warn(I18n.t("cloud_command.publish.confirm.warn"))
          @env.ui.info(I18n.t("cloud_command.publish.confirm.box", org: org,
            box_name: box_name, version: version, provider_name: provider_name))
          @env.ui.info(I18n.t("cloud_command.publish.confirm.private")) if options[:private]
          @env.ui.info(I18n.t("cloud_command.publish.confirm.release")) if options[:release]
          @env.ui.info(I18n.t("cloud_command.publish.confirm.architecture",
            architecture: options[:architecture]))
          @env.ui.info(I18n.t("cloud_command.publish.confirm.default_architecture")) if options[:default_architecture]
          @env.ui.info(I18n.t("cloud_command.publish.confirm.box_url",
            url: options[:url])) if options[:url]
          @env.ui.info(I18n.t("cloud_command.publish.confirm.box_description",
            description: options[:description])) if options[:description]
          @env.ui.info(I18n.t("cloud_command.publish.confirm.box_short_desc",
            short_description: options[:short_description])) if options[:short_description]
          @env.ui.info(I18n.t("cloud_command.publish.confirm.version_desc",
            version_description: options[:version_description])) if options[:version_description]
          nil
        end
      end
    end
  end
end
