#!/usr/bin/env ruby

require 'pathname'
require 'pp'
require 'yaml'
require 'json'
require 'fileutils'

require 'cmds'

require 'qb'

# constants
# =========

ROOT = QB::ROOT
ROLES_DIR = QB::ROLES_DIR
ROLES = Pathname.glob(ROLES_DIR + 'qb.*').map {|path| path.basename.to_s}
DEBUG_ARGS = ['-D', '--DEBUG']
TMP_DIR = ROOT + 'tmp'

# globals
# =======

# @api util
# *pure*
#
# format a debug message with optional key / values to print
#
# @param msg [String] message to print.
# @param dump [Hash] optional hash of keys and values to dump.
def format msg, dump = {}
  unless dump.empty?
    msg += "\n" + dump.map {|k, v| "  #{ k }: #{ v.inspect }" }.join("\n")
  end
  msg
end

def debug *args
  QB.debug *args
end

def set_debug! args
  if DEBUG_ARGS.any? {|arg| args.include? arg}
    QB.debug = true
    debug "ON"
    DEBUG_ARGS.each {|arg| args.delete arg}
  end
end

# needed for to clean the env if using bundler (like in dev)
def with_clean_env &block
  if defined? Bundler
    Bundler.with_clean_env &block
  else
    block.call
  end
end

def metadata
  if QB.gemspec.metadata && !QB.gemspec.metadata.empty?
    "metadata:\n" + QB.gemspec.metadata.map {|key, value|
      "  #{ key }: #{ value }"
    }.join("\n") + "\n"
  end
end

def help
  puts <<-END
version: #{ QB::VERSION }
#{ metadata }
syntax:

    qb ROLE [OPTIONS] DIRECTORY

use `qb ROLE -h` for role options.
  
available roles:

END
  puts QB::Role.available
  puts
  exit 1
end

def main args
  set_debug! args
  debug args: args
  
  role_arg = args.shift
  debug "role arg" => role_arg
  
  help if role_arg.nil? || ['-h', '--help', 'help'].include?(role_arg)
  
  begin
    role = QB::Role.require role_arg
  rescue QB::Role::NoMatchesError => e
    puts "ERROR - #{ e.message }\n\n"
    # exits with status code 1
    help
  rescue QB::Role::MultipleMatchesError => e
    puts "ERROR - #{ e.message }\n\n"
    exit 1
  end
  
  options, qb_options = QB::Options.parse! role, args
  
  debug "options set on cli", options.select {|k, o| !o.value.nil?}
  debug "qb options", qb_options
  
  cwd = Dir.getwd
  
  # get the target dir
  dir = case args.length
  when 0
    # in this case, a dir has not been provided
    # 
    # in some cases (like projects) the dir can be figured out in other ways:
    # 
    QB.get_default_dir role, cwd, options
    
  when 1
    # there is a single positional arg, which is used as dir
    args[0]
    
  else
    # there are multiple positional args, which is not allowed
    raise "can't supply more than one argument: #{ args.inspect }"
    
  end
  
  debug "input_dir", dir
  
  # normalize to expanded path (has no trailing slash)
  dir = File.expand_path dir
  
  debug "normalized_dir", dir
  
  # create the dir if it doesn't exist (so don't have to cover this in
  # every role)
  if role.mkdir
    FileUtils.mkdir_p dir unless File.exists? dir
  end
  
  saved_options_path = Pathname.new(dir) + '.qb-options.yml'
  
  saved_options = if saved_options_path.exist?
    # convert old _ separated names to - separated
    YAML.load(saved_options_path.read).map {|role_options_key, role_options|
      [
        role_options_key,
        role_options.map {|name, value|
          [QB::Options.cli_ize_name(name), value]
        }.to_h
      ]
    }.to_h.tap {|saved_options|
      debug "found saved options", saved_options
    }
  else
    debug "no saved options"
    {}
  end
  
  if saved_options.key? role.options_key
    role_saved_options = saved_options[role.options_key]
    
    debug "found saved options for role", role_saved_options
    
    role_saved_options.each do |option_cli_name, value|
      option = options[option_cli_name]
      
      if option.value.nil?
        debug "setting from saved options", option: option, value: value
        
        option.value = value
      end
    end
  end
  
  # check that required options are present
  missing = options.values.select {|option|
    option.required? && option.value.nil?
  }
  
  unless missing.empty?
    puts "ERROR: options #{ missing.map {|o| o.cli_name } } are required."
    exit 1
  end
  
  set_options = options.select {|k, o| !o.value.nil?}
  
  debug "set options", set_options
  
  playbook_role = {'role' => role.name}
  
  playbook_vars = {
    'qb_dir' => dir,
    # depreciated due to mass potential for conflict
    'dir' => dir,
    'qb_cwd' => cwd,
  }
  
  set_options.values.each do |option|
    playbook_role[option.var_name] = option.value
  end
  
  play =
  {
    'hosts' => qb_options['hosts'],
    'vars' => playbook_vars,
    'pre_tasks' => [
      # need ansible 2.1.2.0 at least to run
      {
        'assert' => {
          'that' => "'ansible 2.1.2' in lookup('pipe', 'ansible --version')",
        },
      },
      {
        'qb_facts' => {
          'qb_dir' => dir,
        }
      },
    ],
    'roles' => [
      'nrser.blockinfile',
      playbook_role
    ],
  }
  
  if qb_options['user']
    play['become'] = true
    play['become_user'] = qb_options['user']
  end
  
  playbook = [play]
  
  debug "playbook", playbook
  
  playbook_path = Pathname.new(Dir.getwd) + '.qb-playbook.yml'
  debug playbook_path: playbook_path.to_s
  
  playbook_path.open('w') do |f|
    f.write YAML.dump(playbook)
  end
  
  # save the options back
  if (
    # we set some options that we can save
    set_options.values.select {|o| o.save? }.length > 0 &&
    # the role says to save options
    role.save_options
  )
    saved_options[role.options_key] = set_options.select{|key, option|
      option.save?
    }.map {|key, option|
      [key, option.value]
    }.to_h
    
    unless saved_options_path.dirname.exist?
      FileUtils.mkdir_p saved_options_path.dirname
    end
    
    saved_options_path.open('w') do |f|
      f.write YAML.dump(saved_options)
    end
  end
  
  tmp_roles_path = QB::ROOT + 'tmp' + 'roles'
  
  ansible_roles_path = (
    [
      role.path.expand_path.dirname,
      tmp_roles_path
    ] + QB::Role.search_path
  ).join(':')
  
  ansible_library_path = [
    ROOT + 'library',
  ].join(':')
  
  Dir.chdir QB::ROOT do
    # the `gem` ansible module doesn't work right when the bundler env vars 
    # are set... so we need to clear them.
    with_clean_env do
      template = []
      template << "ANSIBLE_ROLES_PATH=<%= roles_path %>"
      template << "ANSIBLE_LIBRARY=<%= library_path %>"
      template << "ansible-playbook"
      
      if play['hosts'] != ['localhost']
        template << "-i <%= hosts %>"
      end
      
      if qb_options['tags']
        template << "--tags=<%= tags %>"
      end
      
      if qb_options['verbose']
        template << "-#{ 'v' * qb_options['verbose'] }"
      end
      
      template << "<%= playbook_path %>"
      
      cmd = Cmds.sub template.join(" "), [], {
        roles_path: ansible_roles_path,
        library_path: ansible_library_path,
        playbook_path: playbook_path.to_s,
        hosts: "#{ play['hosts'].join(',') },",
        tags: (qb_options['tags'] ? qb_options['tags'].join(',') : nil),
      }
      
      puts "COMMAND: #{ cmd }"
      
      status = Cmds.stream cmd
      
      if status != 0
        puts "ERROR ansible-playbook failed."
      end
      
      exit status
    end
  end
end

main(ARGV) # if __FILE__ == $0 # doesn't work with gem stub or something?