require "json"
require "fileutils"
require "tempfile"

module Vagrant
  module Plugin
    # This is a helper to deal with the plugin state file that Vagrant
    # uses to track what plugins are installed and activated and such.
    class StateFile

      # @return [Pathname] path to file
      attr_reader :path

      def initialize(path, system = false)
        @path = path
        @system = system

        @data = {}
        if @path.exist?
          begin
            @data = JSON.parse(@path.read)
          rescue JSON::ParserError => e
            raise Vagrant::Errors::PluginStateFileParseError,
              path: path, message: e.message
          end

          upgrade_v0! if !@data["version"]
        end

        @data["version"] ||= "1"
        @data["installed"] ||= {}
        load_extra_plugins
      end

      def load_extra_plugins
        extra_plugins = Dir.glob(@path.dirname.join('plugins.d', '*.json'))
        extra_plugins.each do |filename|
          json = File.read(filename)
          begin
            plugin_data = JSON.parse(json)
            @data["installed"].merge!(plugin_data)
          rescue JSON::ParserError => e
            raise Vagrant::Errors::PluginStateFileParseError,
              path: filename, message: e.message
          end
        end
      end

      # Add a plugin that is installed to the state file.
      #
      # @param [String] name The name of the plugin
      def add_plugin(name, **opts)
        @data["installed"][name] = {
          "ruby_version"          => RUBY_VERSION,
          "vagrant_version"       => Vagrant::VERSION,
          "gem_version"           => opts[:version] || "",
          "require"               => opts[:require] || "",
          "sources"               => opts[:sources] || [],
          "installed_gem_version" => opts[:installed_gem_version],
          "env_local"             => !!opts[:env_local]
        }

        save!
      end

      # Adds a RubyGems index source to look up gems.
      #
      # @param [String] url URL of the source.
      def add_source(url)
        @data["sources"] ||= []
        @data["sources"] << url if !@data["sources"].include?(url)
        save!
      end

      # This returns a hash of installed plugins according to the state
      # file. Note that this may _not_ directly match over to actually
      # installed gems.
      #
      # @return [Hash]
      def installed_plugins
        @data["installed"]
      end

      # Returns true/false if the plugin is present in this state file.
      #
      # @return [Boolean]
      def has_plugin?(name)
        @data["installed"].key?(name)
      end

      # Remove a plugin that is installed from the state file.
      #
      # @param [String] name The name of the plugin.
      def remove_plugin(name)
        @data["installed"].delete(name)
        save!
      end

      # Remove a source for RubyGems.
      #
      # @param [String] url URL of the source
      def remove_source(url)
        @data["sources"] ||= []
        @data["sources"].delete(url)
        save!
      end

      # Returns the list of RubyGems sources that will be searched for
      # plugins.
      #
      # @return [Array<String>]
      def sources
        @data["sources"] || []
      end

      # This saves the state back into the state file.
      def save!
        Tempfile.open(@path.basename.to_s, @path.dirname.to_s) do |f|
          f.binmode
          f.write(JSON.dump(@data))
          f.fsync
          f.chmod(0644)
          f.close
          FileUtils.mv(f.path, @path)
        end
      rescue Errno::EACCES
        # Ignore permission denied against system-installed plugins; regular
        # users are not supposed to write there.
        raise unless @system
      end

      protected

      # This upgrades the internal data representation from V0 (the initial
      # version) to V1.
      def upgrade_v0!
        @data["version"] = "1"

        new_installed = {}
        (@data["installed"] || []).each do |plugin|
          new_installed[plugin] = {
            "ruby_version"    => "0",
            "vagrant_version" => "0",
          }
        end

        @data["installed"] = new_installed

        save!
      end
    end
  end
end
