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

# Patch things to produce proto messages
require "pathname"
require "securerandom"
require "google/protobuf/wrappers_pb"
require "google/protobuf/well_known_types"

PROTO_LOGGER = Log4r::Logger.new("vagrant::protologger")

# Default proto mapping
class Object
  def self.to_proto
    Hashicorp::Vagrant::Sdk::Args::Class.new(name: name)
  end

  # simple stub so non-proto objects won't
  # error if conversion is attempted
  def to_ruby
    self
  end

  def to_any
    source_proto = if !self.class.ancestors.include?(Google::Protobuf::MessageExts)
                     to_proto
                   else
                     self
                   end
    begin
      Google::Protobuf::Any.pack(source_proto)
    rescue
      PROTO_LOGGER.warn("failed to any this type: #{self.class} value: #{self}")
      raise
    end
  end

  def to_proto
    begin
      klass = self.class.to_proto
      data = Hash.new.tap do |h|
        instance_variables.each do |v|
          h[v.to_s.sub('@', '')] = instance_variable_get(v)
        end
      end

      entries = data.map do |k, v|
        Hashicorp::Vagrant::Sdk::Args::HashEntry.new(
          key: k.to_any,
          value: v.to_any,
        )
      end.compact
      Hashicorp::Vagrant::Sdk::Config::RawRubyValue.new(
        source: klass,
        data: Hashicorp::Vagrant::Sdk::Args::Hash.new(entries: entries)
      )
    end
  rescue => err
    PROTO_LOGGER.warn("failed to proto #{self.class} | reason: #{err}")
    raise
  end
end

# Base types
class Array
  def to_proto
    Hashicorp::Vagrant::Sdk::Args::Array.new(
      list: map(&:to_any)
    )
  end
end

class FalseClass
  def to_proto
    Google::Protobuf::BoolValue.new(value: false)
  end
end

class Float
  def to_proto
    Google::Protobuf::FloatValue.new(value: self)
  end
end

class Hash
  def to_proto
    entries = map do |k, v|
      Hashicorp::Vagrant::Sdk::Args::HashEntry.new(
        key: k.to_any,
        value: v.to_any,
      )
    end
    Hashicorp::Vagrant::Sdk::Args::Hash.new(entries: entries)
  end
end

class Integer
  def to_proto
    Google::Protobuf::Int64Value.new(value: self)
  end
end

class NilClass
  def to_proto
    Hashicorp::Vagrant::Sdk::Args::Null.new
  end
end

class Pathname
  def to_proto
    Hashicorp::Vagrant::Sdk::Args::Path.new(
      path: to_s
    )
  end
end

class Proc
  def to_proto
    Hashicorp::Vagrant::Sdk::Args::ProcRef.new(
      id: VagrantPlugins::CommandServe::Mappers::ProcRegistry.instance.register(self)
    )
  end
end

class Range
  def to_proto
    Hashicorp::Vagrant::Sdk::Args::Range.new(start: first, end: last)
  end
end

class Set
  def to_proto
    Hashicorp::Vagrant::Sdk::Args::Set.new(list: to_a.to_proto)
  end
end

class String
  def to_proto
    Google::Protobuf::StringValue.new(value: to_s)
  end
end

class Symbol
  def to_proto
    Hashicorp::Vagrant::Sdk::Args::Symbol.new(str: to_s)
  end
end

class TrueClass
  def to_proto
    Google::Protobuf::BoolValue.new(value: true)
  end
end

# Complex types
class Vagrant::Plugin::V2::Config
  def to_proto
    data = Hash.new.tap do |h|
      instance_variables.each do |v|
        h[v.to_s.sub('@', '')] = instance_variable_get(v)
      end
    end

    # Include a unique identifier for this configuration instance. This
    # will allow us to identifier it later when it is decoded.
    if !data.key?("_vagrant_config_identifier")
      data["_vagrant_config_identifier"] = SecureRandom.uuid
    end

    entries = data.map do |k, v|
      Hashicorp::Vagrant::Sdk::Args::HashEntry.new(
        key: k.to_any,
        value: v.to_any,
      )
    end
    Hashicorp::Vagrant::Sdk::Args::ConfigData.new(
      data: Hashicorp::Vagrant::Sdk::Args::Hash.new(entries: entries),
      source: self.class.to_proto,
    )
  end
end

class Vagrant::Config::V2::Root
  def to_proto
    __internal_state["keys"].to_proto
  end
end

class VagrantPlugins::CommandServe::Type::CommunicatorCommandArguments
  def to_proto
    Hashicorp::Vagrant::Sdk::Communicator::Command.new(command: value)
  end
end

class VagrantPlugins::CommandServe::Type::CommandInfo
  def to_proto
    flags = info.flags.map do |f|
      Vagrant::Hashicorp::Sdk::Command::Flag.new(
        long_name: f.long_name,
        short_name: f.short_name,
        description: f.description,
        default_value: f.default_value,
        type: f.type == :BOOL ? Vagrant::Hashicorp::Sdk::Command::Flag::Type::BOOL :
          Hashicorp::Vagrant::Sdk::Command::Flag::Type::STRING
      )
    end
    subcommands = info.subcommands.map do |s_info|
      converter(s_info)
    end
    Hashicorp::Vagrant::Sdk::Command::CommandInfo.new(
      name: info.name,
      help: info.help,
      synopsis: info.synopsis,
      flags: flags,
      subcommands: subcommands,
      primary: info.primary,
    )
  end
end

class VagrantPlugins::CommandServe::Type::Direct
  def to_proto
    Hashicorp::Vagrant::Sdk::Args::Direct.new(
      arguments: value.to_proto
    )
  end
end

class VagrantPlugins::CommandServe::Type::Duration
  def to_proto
    Hashicorp::Vagrant::Sdk::Args::Duration.new(
      duration: value
    )
  end
end

class VagrantPlugins::CommandServe::Type::Folders
  def to_proto
    Hashicorp::Vagrant::Sdk::Args::Folders.new(
      folders: value.to_proto
    )
  end
end

class VagrantPlugins::CommandServe::Type::Options
  def to_proto
    Hashicorp::Vagrant::Sdk::Args::Options.new(
      options: value.to_proto
    )
  end
end

class Log4r::Logger
  def to_proto
    Hashicorp::Vagrant::Sdk::Args::RubyLogger.new(name: fullname)
  end
end

# Proto conversions
class Google::Protobuf::Any
  def to_ruby
    _vagrant_unany(self).to_ruby
  end
end

module Google::Protobuf::MessageExts
  def to_ruby
    return value if self.respond_to?(:value)

    raise NotImplementedError,
          "#{self.class}#to_ruby has not been implemented"
  end

  # Convert Any proto message to actual message type
  #
  # @param any [Google::Protobuf::Any]
  # @return [Google::Protobuf::MessageExts]
  def _vagrant_unany(any)
    type = _vagrant_find_type(any.type_name.split("/").last.to_s)
    any.unpack(type)
  end

  # Get const from name
  #
  # @param name [String]
  # @return [Class]
  def _vagrant_find_type(name)
    parent_module_options = []
    name.to_s.split(".").inject(Object) { |memo, n|
      c = memo.constants.detect { |mc| mc.to_s.downcase == n.to_s.downcase }
      if c.nil?
        parent_module_options.delete(memo)
        parent_module_options.each do |pm|
          c = pm.constants.detect { |mc| mc.to_s.downcase == n.to_s.downcase }
          if !c.nil?
            memo = pm
            break
          end
        end
      end

      raise NameError,
            "Failed to find constant for `#{name}'" if c.nil?

      parent_module_options = memo.constants.select {
        |mc| mc.to_s.downcase == n.to_s.downcase
      }.map {
        |mc| memo.const_get(mc)
      }
      memo.const_get(c)
    }
  end

  def _vagrant_load_client(klass)
    raise TypeError,
          "Proto is not a valid client message type (#{self.class})" if
      !respond_to?(:addr)

    cid = "client+" + addr.to_s
    return VagrantPlugins::CommandServe.cache.get(cid) if
      VagrantPlugins::CommandServe.cache.registered?(cid)

    v = klass.load(self, broker: VagrantPlugins::CommandServe.broker)
    VagrantPlugins::CommandServe.cache.register(cid, v)
    v
  end
end

class Hashicorp::Vagrant::Sdk::Args::Array
  def to_ruby
    list.map { |a|
      val = _vagrant_unany(a).to_ruby
      val.is_a?(VagrantPlugins::CommandServe::Type) ? val.value : val
    }
  end
end

class Hashicorp::Vagrant::Sdk::Args::BoxMetadata
  # TODO(spox): should this be returning a box metadata instance instead of client?
  def to_ruby
    _vagrant_load_client(VagrantPlugins::CommandServe::Client::BoxMetadata)
  end
end

class Hashicorp::Vagrant::Sdk::Args::Class
  def to_ruby
    if name.to_s.empty?
      raise NameError,
            "No name defined for for class"
    end
    name.to_s.split("::").inject(Object) { |memo, n|
      c = memo.constants.detect { |mc| mc.to_s.downcase == n.to_s.downcase }
      raise NameError,
            "Failed to find constant for `#{name}'" if c.nil?
      memo.const_get(c)
    }
  end
end

class Hashicorp::Vagrant::Sdk::Args::CorePluginManager
  def to_ruby
    _vagrant_load_client(VagrantPlugins::CommandServe::Client::CorePluginManager)
  end
end

class Hashicorp::Vagrant::Sdk::Args::ConfigData
  def to_ruby
    base_klass = source.to_ruby
    if [0, -1].include?(base_klass.instance_method(:initialize).arity)
      klass = base_klass
    else
      klass = Class.new(base_klass)
      klass.class_eval("
              def self.to_proto
                #{base_klass.name}.to_proto
              end
              def self.class
                #{base_klass.name}
              end
              def initialize
              end
            ")
    end
    instance = klass.new
    d = data.to_ruby

    # Since we are restoring the config, if the config this
    # represents was already finalized we finalize it first
    # before we inject the instance variables to get as close
    # to the correct original state as possible
    if d.key?("__finalized")
      instance.finalize!
      instance._finalize!
    end

    # Now set our data into the instance
    d.each_pair do |k, v|
      instance.instance_variable_set("@#{k}", v)
    end

    instance
  end
end

class Hashicorp::Vagrant::Sdk::Args::Direct
  def to_ruby
    VagrantPlugins::CommandServe::Type::Direct.new(
      arguments: arguments.map { |arg|
        val = arg.to_ruby
        val.is_a?(VagrantPlugins::CommandServe::Type) ? val.value : val
      }
    )
  end
end

class Hashicorp::Vagrant::Sdk::Args::Folders
  def to_ruby
    VagrantPlugins::CommandServe::Type::Folders.new(value: folders.to_ruby)
  end
end

class Hashicorp::Vagrant::Sdk::Args::Guest
  def to_ruby
    _vagrant_load_client(VagrantPlugins::CommandServe::Client::Guest)
  end
end

class Hashicorp::Vagrant::Sdk::Args::Hash
  def to_ruby
    data = Hash.new
    entries.each do |e|
      key = _vagrant_unany(e.key).to_ruby
      value = _vagrant_unany(e.value).to_ruby
      data[key] = value.is_a?(VagrantPlugins::CommandServe::Type) ? value.value : value
    end

    data
  end
end

class Hashicorp::Vagrant::Sdk::Args::Host
  def to_ruby
    client = _vagrant_load_client(VagrantPlugins::CommandServe::Client::Host)
    Vagrant::Host.new(client, nil, Vagrant.plugin("2").local_manager.host_capabilities)
  end
end

class Hashicorp::Vagrant::Sdk::Args::MetadataSet
  def to_ruby
    {}
  end
end

class Hashicorp::Vagrant::Sdk::Args::NamedCapability
  def to_ruby
    capability.to_s.to_sym
  end
end

class Hashicorp::Vagrant::Sdk::Args::Null
  def to_ruby
    nil
  end
end

class Hashicorp::Vagrant::Sdk::Args::Options
  def to_ruby
    VagrantPlugins::CommandServe::Type::Options.new(value: options.to_ruby)
  end
end

class Hashicorp::Vagrant::Sdk::Args::Path
  def to_ruby
    Pathname.new(path.to_s)
  end
end

class Hashicorp::Vagrant::Sdk::Args::ProcRef
  def to_ruby
    VagrantPlugins::CommandServe::Mappers::ProcRegistry.instance.fetch(id)
  end
end

class Hashicorp::Vagrant::Sdk::Args::Project
  def to_ruby
    cid = "environment"+addr.to_s
    return VagrantPlugins::CommandServe.cache.get(cid) if
      VagrantPlugins::CommandServe.cache.registered?(cid)

    client = _vagrant_load_client(VagrantPlugins::CommandServe::Client::Project)
    ui = client.ui.to_ui

    env = Vagrant::Environment.new(client: client, ui: ui)
    VagrantPlugins::CommandServe.cache.register(cid, env)
    env
  end
end

class Hashicorp::Vagrant::Sdk::Args::Provisioner
  def to_ruby
    _vagrant_load_client(VagrantPlugins::CommandServe::Client::Provisioner)
  end
end

class Hashicorp::Vagrant::Sdk::Args::Provider
  def to_ruby
    _vagrant_load_client(VagrantPlugins::CommandServe::Client::Provider)
  end
end

class Hashicorp::Vagrant::Sdk::Args::Range
  def to_ruby
    Range.new(self.start, self.end)
  end
end

class Hashicorp::Vagrant::Sdk::Config::RawRubyValue
  def to_ruby
    base_klass = source.to_ruby
    if [0, -1].include?(base_klass.instance_method(:initialize).arity)
      klass = base_klass
    else
      klass = Class.new(base_klass)
      klass.class_eval("
              def self.class
                #{base_klass.name}
              end
              def initialize
              end
            ")
    end

    instance = klass.new
    d = data.to_ruby

    d.each_pair do |k, v|
      instance.instance_variable_set("@#{k}", v)
    end

    instance
  end
end

class Hashicorp::Vagrant::Sdk::Args::RubyLogger
  def to_ruby
    Log4r::Logger.new(name)
  end
end

class Hashicorp::Vagrant::Sdk::Args::Set
  def to_ruby
    ::Set.new(list.to_ruby)
  end
end

class Hashicorp::Vagrant::Sdk::Args::StateBag
  def to_ruby
    _vagrant_load_client(VagrantPlugins::CommandServe::Client::StateBag)
  end
end

class Hashicorp::Vagrant::Sdk::Args::SyncedFolder
  def to_ruby
    cid = "syncedfolder+" + addr.to_s
    return VagrantPlugins::CommandServe.cache.get(cid) if
      VagrantPlugins::CommandServe.cache.registered?(cid)

    client = _vagrant_load_client(VagrantPlugins::CommandServe::Client::SyncedFolder)
    fld = Vagrant::Plugin::Remote::SyncedFolder.new(client: client)
    VagrantPlugins::CommandServe.cache.register(cid, fld)
    fld
  end
end

class Hashicorp::Vagrant::Sdk::Args::Symbol
  def to_ruby
    str.to_sym
  end
end

class Hashicorp::Vagrant::Sdk::Args::Target
  def to_ruby
    cid = "machine+" + addr.to_s
    return VagrantPlugins::CommandServe.cache.get(cid) if
      VagrantPlugins::CommandServe.cache.registered?(cid)

    client = _vagrant_load_client(VagrantPlugins::CommandServe::Client::Target)
    env = client.environment.to_ruby
    machine = env.machine(client.name.to_sym, client.provider_name.to_sym)
    VagrantPlugins::CommandServe.cache.register(cid, machine)
    machine
  end
end

class Hashicorp::Vagrant::Sdk::Args::Target::Machine
  def to_ruby
    cid = "machine+" + addr.to_s
    return VagrantPlugins::CommandServe.cache.get(cid) if
      VagrantPlugins::CommandServe.cache.registered?(cid)

    client = _vagrant_load_client(VagrantPlugins::CommandServe::Client::Target::Machine)
    m = Vagrant::Machine.new(client: client)
    VagrantPlugins::CommandServe.cache.register(cid, m)
    m
  end
end

class Hashicorp::Vagrant::Sdk::Args::Target::Machine::State
  def to_ruby
    Vagrant::MachineState.new(
      id.to_sym, short_description, long_description
    )
  end
end

class Hashicorp::Vagrant::Sdk::Args::TargetIndex
  def to_ruby
    _vagrant_load_client(VagrantPlugins::CommandServe::Client::TargetIndex)
  end
end

class Hashicorp::Vagrant::Sdk::Args::TimeDuration
  def to_ruby
    VagrantPlugins::CommandServe::Type::Duration.new(value: duration)
  end
end

class Hashicorp::Vagrant::Sdk::Args::TerminalUI
  def to_ruby
    client = _vagrant_load_client(VagrantPlugins::CommandServe::Client::Terminal)
    Vagrant::UI::Remote.new(client)
  end
end

class Hashicorp::Vagrant::Sdk::Args::Vagrantfile
  def to_ruby
    client = _vagrant_load_client(VagrantPlugins::CommandServe::Client::Vagrantfile)
    Vagrant::Vagrantfile.new(client: client)
  end
end

class Hashicorp::Vagrant::Sdk::Command::Arguments
  def to_ruby
    _args = args.to_a
    _flags = Hash.new.tap do |flgs|
      flags.each do |f|
        if f.type == :BOOL
          flgs[f.name] = f.bool
        else
          flgs[f.name] = f.string
        end
      end
    end
    VagrantPlugins::CommandServe::Type::CommandArguments.new(args: _args, flags: _flags)
  end
end

class Hashicorp::Vagrant::Sdk::Command::CommandInfo
  def to_ruby
    VagrantPlugins::CommandServe::Type::CommandInfo.new(
      name: name,
      help: help,
      synopsis: synopsis,
      primary: primary,
    ).tap do |c|
      flags.each do |f|
        c.add_flag(
          long_name: f.long_name,
          short_name: f.short_name,
          description: f.description,
          default_value: f.default_value,
          type: f.type,
        )
      end
      subcommands.each do |s_proto|
        c.add_subcommand(s_proto.to_ruby)
      end
    end
  end
end

class Hashicorp::Vagrant::Sdk::Communicator::Command
  def to_ruby
    VagrantPlugins::CommandServe::Type::CommunicatorCommandArguments.new(value: command)
  end
end
