require 'r10k/module'
require 'r10k/errors'
require 'r10k/module/metadata'
require 'r10k/util/subprocess'
require 'r10k/module_repository/forge'

require 'pathname'
require 'fileutils'
require 'r10k/semver'

class R10K::Module::Forge < R10K::Module::Base

  R10K::Module.register(self)

  def self.implement?(name, args)
    !!(name.match %r[\w+/\w+])
  end

  include R10K::Logging

  # @!attribute [r] author
  #   @return [String] The Forge module author
  attr_reader :author

  # @deprecated
  def owner
    logger.warn "#{self.inspect}#owner is deprecated; use #author instead"
    @author
  end

  # @!attribute [r] full_name
  #   @return [String] The fully qualified module name
  attr_reader :full_name

  def initialize(full_name, basedir, args)
    @full_name = full_name
    @basedir   = basedir

    @author, @name = full_name.split('/')

    @full_path = Pathname.new(File.join(@basedir, @name))

    @metadata = R10K::Module::Metadata.new(@full_path + 'metadata.json')

    if args.is_a? String
      @expected_version = R10K::SemVer.new(args)
    elsif args.is_a? Symbol and args == :latest
      @expected_version = args
    end
  end

  def sync(options = {})
    case status
    when :absent
      install
    when :outdated
      upgrade
    when :mismatched
      reinstall
    end
  end

  # @return [R10K::SemVer] The expected version that the module
  def expected_version
    if @expected_version == :latest
      set_version_from_forge
    end
    @expected_version
  end


  # @return [R10K::SemVer] The version of the currently installed module
  def current_version
    @metadata.version
  end

  alias version current_version

  def exist?
    @full_path.exist?
  end

  def insync?
    status == :insync
  end

  # Determine the status of the forge module.
  #
  # @return [Symbol] :absent If the directory doesn't exist
  # @return [Symbol] :mismatched If the module is not a forge module, or
  #   isn't the right forge module
  # @return [Symbol] :outdated If the installed module is older than expected
  # @return [Symbol] :insync If the module is in the desired state
  def status
    if not self.exist?
      # The module is not installed
      return :absent
    elsif not @metadata.exist?
      # The directory exists but doesn't have a metadata file; it probably
      # isn't a forge module.
      return :mismatched
    end

    # The module is present and has a metadata file, read the metadata to
    # determine the state of the module.
    @metadata.read

    if not @author == @metadata.author
      # This is a forge module but the installed module is a different author
      # than the expected author.
      return :mismatched
    end

    if expected_version && (expected_version != @metadata.version)
      return :outdated
    end

    return :insync
  end

  private

  def install
    FileUtils.mkdir @basedir unless File.directory? @basedir
    cmd = []
    cmd << 'install'
    cmd << "--version=#{expected_version}" if expected_version
    cmd << "--ignore-dependencies"
    cmd << @full_name
    pmt cmd
  end

  def upgrade
    cmd = []
    cmd << 'upgrade'
    cmd << "--version=#{expected_version}" if expected_version
    cmd << "--ignore-dependencies"
    cmd << @full_name
    pmt cmd
  end

  def uninstall
    FileUtils.rm_rf full_path
  end

  def reinstall
    uninstall
    install
  end

  # Wrap puppet module commands
  #
  # @param argv [Array<String>]
  #
  # @return [String] The stdout from the executed command
  def pmt(argv)
    argv = ['puppet', 'module', '--modulepath', @basedir] + argv

    subproc = R10K::Util::Subprocess.new(argv)
    subproc.raise_on_fail = true
    subproc.logger = self.logger

    result = subproc.execute

    result.stdout
  end

  def set_version_from_forge
    repo = R10K::ModuleRepository::Forge.new
    expected = repo.latest_version(@full_name)
    @expected_version = R10K::SemVer.new(expected)
  end
end