#!/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 vaues 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.parse! role, args
  
  debug "options set on cli", options.select {|k, o| !o.value.nil?}
  
  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
  
  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
  
  playbook = [
    {
      'hosts' => 'localhost',
      '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
      ],
    }
  ]
  
  debug "playbook", playbook
  
  playbook_path = ROOT + '.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 = (
    [tmp_roles_path] +
    QB::Role.search_path
  ).join(':')
  
  Dir.chdir ROOT do
    # install requirements
    # unless (TMP_DIR + 'roles').directory?
    #   with_clean_env do
    #     Cmds.stream! "ANSIBLE_ROLES_PATH=<%= roles_path %> ansible-galaxy install --ignore-errors -r <%= req_path%>",
    #       req_path: (ROOT + 'requirements.yml'),
    #       roles_path: tmp_roles_path.to_s
    #   end
    # end
    
    # the `gem` ansible module doesn't work right when the bunlder env vars 
    # are set... so we need to clear them.
    with_clean_env do
      Cmds.stream! "ANSIBLE_ROLES_PATH=<%= roles_path %> ansible-playbook <%= playbook_path %>", 
        roles_path: ansible_roles_path,
        playbook_path: playbook_path.to_s
    end
  end
end

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