#! /usr/bin/env ruby

##
# Load RubyGems for Ruby <= 1.8.7
require 'rubygems'
require 'tempfile'
require 'fileutils'

##
# Load Thor for the Command Line Interface
begin
  require 'thor'
rescue LoadError
  puts 'Backup uses Thor as CLI (Command Line Interface).'
  puts 'Please install Thor first: `gem install thor`'
end

##
# Load the Backup source
require File.expand_path("../../lib/backup", __FILE__)

##
# Build the Backup Command Line Interface using Thor
class BackupCLI < Thor
  include Thor::Actions

  TEMPLATE_DIR = File.expand_path("../../lib/templates", __FILE__)

  ##
  # [Perform]
  # Performs the backup process. The only required option is the --trigger [-t].
  # If the other options (--config_file, --data_path, --tmp_path) aren't specified
  # it'll fallback to the (good) defaults
  method_option :trigger,     :type => :string, :aliases => ['-t', '--triggers'], :required => true
  method_option :config_file, :type => :string, :aliases => '-c'
  method_option :data_path,   :type => :string, :aliases => '-d'
  method_option :log_path,    :type => :string, :aliases => '-l'
  method_option :tmp_path,    :type => :string
  desc 'perform', "Performs the backup for the specified trigger.\n" + 
                  "You may perform multiple backups by providing multiple triggers, separated by commas.\n\n" +
                  "Example:\n\s\s$ backup perform --triggers backup1,backup2,backup3,backup4\n\n" +
                  "This will invoke 4 backups, and they will run in the order specified (not asynchronous)."
  def perform

    ##
    # Overwrites the CONFIG_FILE location, if --config-file was specified
    if options[:config_file]
      Backup.send(:remove_const, :CONFIG_FILE)
      Backup.send(:const_set, :CONFIG_FILE, options[:config_file])
    end

    ##
    # Overwrites the DATA_PATH location, if --data-path was specified
    if options[:data_path]
      Backup.send(:remove_const, :DATA_PATH)
      Backup.send(:const_set, :DATA_PATH, options[:data_path])
    end

    ##
    # Overwrites the LOG_PATH location, if --log-path was specified
    if options[:log_path]
      Backup.send(:remove_const, :LOG_PATH)
      Backup.send(:const_set, :LOG_PATH, options[:log_path])
    end

    ##
    # Overwrites the TMP_PATH location, if --tmp-path was specified
    if options[:tmp_path]
      Backup.send(:remove_const, :TMP_PATH)
      Backup.send(:const_set, :TMP_PATH, options[:tmp_path])
    end

    ##
    # Ensure the TMP_PATH and LOG_PATH are created if they do not yet exist
    Array.new([Backup::TMP_PATH, Backup::LOG_PATH]).each do |path|
      FileUtils.mkdir_p(path)
    end

    ##
    # Process each trigger
    options[:trigger].split(",").map(&:strip).each do |trigger|

      ##
      # Defines the TRIGGER constant
      Backup.send(:const_set, :TRIGGER, trigger)

      ##
      # Define the TIME constants
      Backup.send(:const_set, :TIME, Time.now.strftime("%Y.%m.%d.%H.%M.%S"))

      ##
      # Ensure DATA_PATH and DATA_PATH/TRIGGER are created if they do not yet exist
      FileUtils.mkdir_p(File.join(Backup::DATA_PATH, Backup::TRIGGER))

      ##
      # Parses the backup configuration file and returns the model instance by trigger
      model = Backup::Finder.new(trigger, Backup::CONFIG_FILE).find

      ##
      # Runs the returned model
      Backup::Logger.message "Performing backup for #{model.label}!"
      model.perform!

      ##
      # Removes the TRIGGER constant
      Backup.send(:remove_const, :TRIGGER) if defined? Backup::TRIGGER

      ##
      # Removes the TIME constant
      Backup.send(:remove_const, :TIME) if defined? Backup::TIME

      ##
      # Reset the Backup::Model.current to nil for the next potential run
      Backup::Model.current = nil

      ##
      # Reset the Backup::Model.all to an empty array since this will be
      # re-filled during the next Backup::Finder.new(arg1, arg2).find
      Backup::Model.all = Array.new

      ##
      # Reset the Backup::Model.extension to 'tar' so it's at it's
      # initial state when the next Backup::Model initializes
      Backup::Model.extension = 'tar'
    end
  end

  ##
  # [Generate]
  # Generates a configuration file based on the arguments passed in.
  # For example, running $ backup generate --databases='mongodb' will generate a pre-populated
  # configuration file with a base MongoDB setup
  desc 'generate', 'Generates configuration blocks based on the arguments you pass in'
  method_option :path,        :type => :string
  method_option :databases,   :type => :string
  method_option :storages,    :type => :string
  method_option :syncers,     :type => :string
  method_option :encryptors,  :type => :string
  method_option :compressors, :type => :string
  method_option :notifiers,   :type => :string
  method_option :archives,    :type => :boolean
  def generate
    temp_file = Tempfile.new('backup.rb')
    temp_file << File.read( File.join(TEMPLATE_DIR, 'readme') )
    temp_file << "Backup::Model.new(:my_backup, 'My Backup') do\n\n"

    if options[:archives]
      temp_file << File.read( File.join(TEMPLATE_DIR, 'archive') ) + "\n\n"
    end

    [:databases, :storages, :syncers, :encryptors, :compressors, :notifiers].each do |item|
      if options[item]
        options[item].split(',').map(&:strip).uniq.each do |entry|
          if File.exist?( File.join(TEMPLATE_DIR, item.to_s[0..-2], entry) )
            temp_file << File.read( File.join(TEMPLATE_DIR, item.to_s[0..-2], entry) ) + "\n\n"
          end
        end
      end
    end

    temp_file << "end\n\n"
    temp_file.close

    path   = options[:path] || Backup::PATH
    config = File.join(path, 'config.rb')

    if overwrite?(config)
      FileUtils.mkdir_p(path)
      File.open(config, 'w') do |file|
        file.write( File.read(temp_file.path) )
        puts "Generated configuration file in '#{ config }'"
      end
    end    
    temp_file.unlink
  end

  ##
  # [Decrypt]
  # Shorthand for decrypting encrypted files
  desc 'decrypt', 'Decrypts encrypted files'
  method_option :encryptor, :type => :string,  :required => true
  method_option :in,        :type => :string,  :required => true
  method_option :out,       :type => :string,  :required => true
  method_option :base64,    :type => :boolean, :default  => false
  def decrypt
    case options[:encryptor].downcase
    when 'openssl'
      base64 = options[:base64] ? '-base64' : ''
      %x[openssl aes-256-cbc -d #{base64} -in '#{options[:in]}' -out '#{options[:out]}']
    when 'gpg'
      %x[gpg -o '#{options[:out]}' -d '#{options[:in]}']
    else
      puts "Unknown encryptor: #{options[:encryptor]}"
      puts "Use either 'openssl' or 'gpg'"
    end
  end

  ##
  # [Dependencies]
  # Returns a list of Backup's dependencies
  desc 'dependencies', 'Display the list of dependencies for Backup, or install them through Backup.'
  method_option :install, :type => :string
  method_option :list,    :type => :boolean
  def dependencies
    unless options.any?
      puts
      puts "To display a list of available dependencies, run:\n\n"
      puts "  backup dependencies --list"
      puts
      puts "To install one of these dependencies (with the correct version), run:\n\n"
      puts "  backup dependencies --install <name>"
      exit
    end

    if options[:list]
      Backup::Dependency.all.each do |name, gemspec|
        puts
        puts name
        puts "--------------------------------------------------"
        puts "version:       #{gemspec[:version]}"
        puts "lib required:  #{gemspec[:require]}"
        puts "used for:      #{gemspec[:for]}"
      end
    end

    if options[:install]
      puts
      puts "Installing \"#{options[:install]}\" version \"#{Backup::Dependency.all[options[:install]][:version]}\".."
      puts "If this doesn't work, please issue the following command yourself:\n\n"
      puts "  gem install #{options[:install]} -v '#{Backup::Dependency.all[options[:install]][:version]}'\n\n"
      puts "Please wait..\n\n"
      puts %x[gem install #{options[:install]} -v '#{Backup::Dependency.all[options[:install]][:version]}']
    end
  end

  ##
  # [Version]
  # Returns the current version of the Backup gem
  map '-v' => :version
  desc 'version', 'Display installed Backup version'
  def version
    puts "Backup #{Backup::Version.current}"
  end

private

  ##
  # Helper method for asking the user if he/she wants to overwrite the file
  def overwrite?(path)
    if File.exist?(path)
      return yes? "A configuration file already exists in #{ path }. Do you want to overwrite? [y/n]"
    end
    true
  end

end

##
# Enable the CLI for the Backup binary
BackupCLI.start