require "vagrant/util/retryable"
require "vagrant/util/subprocess"

require "vagrant-lxc/errors"

module Vagrant
  module LXC
    class Driver
      class CLI
        attr_accessor :name

        class TransitionBlockNotProvided < RuntimeError; end
        class TargetStateNotReached < RuntimeError
          def initialize(target_state, state)
            msg = "Target state '#{target_state}' not reached, currently on '#{state}'"
            super(msg)
          end
        end

        # Include this so we can use `Subprocess` more easily.
        include Vagrant::Util::Retryable

        def initialize(name = nil)
          @name   = name
          @logger = Log4r::Logger.new("vagrant::provider::lxc::container::cli")
        end

        def list
          run(:ls).split(/\s+/).uniq
        end

        def version
          if run(:version) =~ /lxc version:\s+(.+)\s*$/
            $1.downcase
          else
            # TODO: Raise an user friendly error
            raise 'Unable to parse lxc version!'
          end
        end

        def state
          if @name && run(:info, '--name', @name) =~ /^state:[^A-Z]+([A-Z]+)$/
            $1.downcase.to_sym
          elsif @name
            :unknown
          end
        end

        def create(template, template_opts = {})
          extra = template_opts.to_a.flatten
          extra.unshift '--' unless extra.empty?

          run :create,
              '--template', template,
              '--name',     @name,
              *extra
        end

        def destroy
          run :destroy, '--name', @name
        end

        def start(overrides = [], extra_opts = [])
          options = overrides.map { |key, value| ["-s", "lxc.#{key}=#{value}"] }.flatten
          options += extra_opts if extra_opts
          run :start, '-d', '--name', @name, *options
        end

        def stop
          run :stop, '--name', @name
        end

        def shutdown
          run :shutdown, '--name', @name
        end

        def attach(*cmd)
          cmd = ['--'] + cmd

          if cmd.last.is_a?(Hash)
            opts       = cmd.pop
            namespaces = Array(opts[:namespaces]).map(&:upcase).join('|')
            extra      = ['--namespaces', namespaces] if namespaces
          end

          run :attach, '--name', @name, *((extra || []) + cmd)
        end

        def transition_to(target_state, tries = 30, timeout = 1, &block)
          raise TransitionBlockNotProvided unless block_given?

          yield self

          while (last_state = self.state) != target_state && tries > 0
            @logger.debug "Target state '#{target_state}' not reached, currently on '#{last_state}'"
            sleep timeout
            tries -= 1
          end

          unless last_state == target_state
            raise TargetStateNotReached.new target_state, last_state
          end
        end

        private

        def run(command, *args)
          execute('sudo', "lxc-#{command}", *args)
        end

        # TODO: Review code below this line, it was pretty much a copy and
        #       paste from VirtualBox base driver and has no tests
        def execute(*command, &block)
          # Get the options hash if it exists
          opts = {}
          opts = command.pop if command.last.is_a?(Hash)

          tries = 0
          tries = 3 if opts[:retryable]

          sleep = opts.fetch(:sleep, 1)

          # Variable to store our execution result
          r = nil

          retryable(:on => LXC::Errors::ExecuteError, :tries => tries, :sleep => sleep) do
            # Execute the command
            r = raw(*command, &block)

            # If the command was a failure, then raise an exception that is
            # nicely handled by Vagrant.
            if r.exit_code != 0
              if @interrupted
                @logger.info("Exit code != 0, but interrupted. Ignoring.")
              else
                raise LXC::Errors::ExecuteError, :command => command.inspect
              end
            end
          end

          # Return the output, making sure to replace any Windows-style
          # newlines with Unix-style.
          r.stdout.gsub("\r\n", "\n")
        end

        def raw(*command, &block)
          int_callback = lambda do
            @interrupted = true
            @logger.info("Interrupted.")
          end

          # Append in the options for subprocess
          command << { :notify => [:stdout, :stderr] }

          Vagrant::Util::Busy.busy(int_callback) do
            Vagrant::Util::Subprocess.execute(*command, &block)
          end
        end
      end
    end
  end
end
