#!/usr/bin/env ruby

require 'optparse'
require 'socket'
require 'base64'
require 'highline/import'

# monkey patch
class HighLine::Question
  def append_default()
    @question
  end
end

begin
  ScriptDir = File.dirname(__FILE__)
  Dir.glob(ScriptDir + "/lib/*.rb").each {|rb|
    require rb
  }

  Windows::Authorization.runas_admin

  home = File.expand_path(ENV["PACKAGE_HOME"])
  host = Socket.gethostbyname(Socket.gethostname).first
  options = {
    :interact => true,
    :home => home,
    :fqdn_or_ipaddr => host.include?(".") ? host : IPSocket.getaddress(host),
    :apache_root => File.join(home, "apache"),
    :redmine_root => File.join(home, "redmine"),
    :opends_root => File.join(home, "opends"),
    :opends_manager_dn => "cn=Directory Manager",
    :opends_manager_password => Account.random_password(10)
  }
  options[:apache_host] = options[:fqdn_or_ipaddr]

  config = [
    {:name => :apache_name, :default => "Apache"},
    {:name => :apache_port, :type => :int, :default => 80},
    {:name => :redmine_name, :default => "Redmine"},
    {:name => :redmine_port, :type => :int, :default => 8000},
    {:name => :jenkins_name, :default => "Jenkins"},
    {:name => :jenkins_port, :type => :int, :default => 8001},

    {:name => :ldap_setting, :type => :bool,
      :message => "Use external LDAP server? (y/n): "},
    {:name => :opends_name, :default => "OpenDS", :parent => :ldap_setting, :if => false},
    {:name => :opends_port, :type => :int, :default => 389, :parent => :ldap_setting, :if => false},
    {:name => :opends_admin_port, :type => :int, :default => 8002, :parent => :ldap_setting, :if => false},
    {:name => :opends_base_dn, :default => "dc=redminele,dc=local", :parent => :ldap_setting, :if => false},

    {:name => :ldap_host, :default => "localhost", :parent => :ldap_setting, :if => true},
    {:name => :ldap_port, :type => :int, :default => 389, :parent => :ldap_setting, :if => true},
    {:name => :ldap_base_dn, :parent => :ldap_setting, :if => true, :message => "Enter LDAP Base DN (ex. ou=users,dc=local): "},
    {:name => :ldap_anonymous, :type => :bool, :message => "LDAP server permits anonymous bind? ([y]/n): ", :default => true, :parent => :ldap_setting, :if => true},
    {:name => :ldap_bind_dn, :message => "Enter LDAP bind DN (ex. uid=xxx,ou=users,dc=local): ", :parent => :ldap_anonymous, :if => false},
    {:name => :ldap_bind_password, :message => "Enter LDAP bind password: ", :parent => :ldap_anonymous, :if => false, :type => :password},
    {:name => :ldap_user_attribute, :default => "uid", :parent => :ldap_setting, :if => true},
    {:name => :ldap_first_name_attribute, :default => "givenName", :parent => :ldap_setting, :if => true},
    {:name => :ldap_last_name_attribute, :default => "sn", :parent => :ldap_setting, :if => true},
    {:name => :ldap_mail_attribute, :default => "mail", :parent => :ldap_setting, :if => true},

    {:name => :admin_account},
    {:name => :admin_password, :type => :password},
    {:name => :admin_mail, :type => :mail},
    {:name => :admin_first_name},
    {:name => :admin_last_name},

    {:name => :smtp_setting, :type => :bool,
      :message => "Use redmine email notification? (y/n): "},
    {:name => :mail_sender_address, :parent => :smtp_setting, :if => true, :type => :mail},
    {:name => :smtp_host, :parent => :smtp_setting, :if => true},
    {:name => :smtp_port, :parent => :smtp_setting,:type => :int, :default => 25, :if => true},
    {:name => :smtp_domain, :parent => :smtp_setting, :if => true,
      :default => options[:fqdn_or_ipaddr]},
    {:name => :smtp_auth, :parent => :smtp_setting, :if => true, :type => :bool,
      :message => "Use smtp auth? (y/n): "},
    {:name => :smtp_user, :parent => :smtp_auth, :if => true},
    {:name => :smtp_password, :parent => :smtp_auth, :if => true, :type => :password}
  ]

  option_keys = []
  config_hash = {}
  config.each {|h|
    option_keys.push(key = h[:name])
    name = key.to_s

    if h[:message].nil?
      default = h[:default]
      default = default.nil? ? "" : " (default: #{default})"
      h[:message] = "Enter #{name.gsub('_', ' ')}#{default}: "
    end

    optname = name.gsub('_', '-')
    h[:option_name] = (h[:type] == :bool) ?
      "--[no-]" + optname : "--#{optname}=VAL"
    h[:class] = Integer if h[:type] == :int
    
    config_hash[key] = h
  }

  OptionParser.new {|opt|
    opt.banner = "Usage: install [options] [file|-]"
    opt.on('--[no-]interact') {|v| options[:interact] = v}

    option_keys.each {|key|
      conf = config_hash[key]
      args = conf[:option_name].to_a
      args.push conf[:class] if conf[:class]
      opt.on(*args) {|v| options[key] = v.toutf8}
    }
    opt.parse!
  }

  if config_file = ARGV.first
    hash = if config_file == "-"
      YAML.load(STDIN.read.toutf8)
    else
      YAML.load_file(config_file)
    end || {}
    hash.each {|key, val| options[key.to_sym] = val} if hash.is_a?(Hash)
    options[:interact] = false
  end

  def bind_check(options)
    LDAP.setup_connection(options)
    unless LDAP.check_connection
      %w[ldap_host ldap_port ldap_anonymous ldap_bind_dn ldap_bind_password
      ].each {|key| options.delete(key.to_sym)}
      return false
    end
    true
  end

  def admin_check(options)
    LDAP.setup_connection(options)
    unless LDAP.check_bind(options[:admin_account], options[:admin_password])
      %w[ldap_base_dn ldap_user_attribute admin_account admin_password
      ].each {|key| options.delete(key.to_sym)}
      return false
    end

    attrs = LDAP.search_attributes(options[:admin_account])
    %w[first_name last_name mail].each {|v|
      if value = attrs[options[:"ldap_#{v}_attribute"]]
        options[:"admin_#{v}"] = value
      end
    }
    true
  end

  if options[:interact]
    option_keys.each {|key|
      if options[:ldap_setting]
        if key == :ldap_user_attribute && !bind_check(options)
          puts "Failed to bind to LDAP server."
          retry
        end
        if key == :admin_mail && !admin_check(options)
          puts "Failed to bind as admin account."
          retry
        end
      end
      next unless options[key].nil?

      conf = config_hash[key]
      type = conf[:type]
      default = conf[:default]
      p_key = conf[:parent]
      if p_key && options[p_key] != conf[:if]
        options[key] = type == :bool ? nil : default
        next
      end

      msg = conf[:message]
      options[key] = case type
      when :bool
        agree(msg) {|q|
          q.default = default ? 'y' : 'n' unless default.nil?
          q.responses[:ask_on_error] = :question
        }
      when :int
        ask(msg, Integer) {|q|
          q.default = default
          q.responses[:ask_on_error] = :question
        }
      when :password
        password = ask(msg) {|q| q.echo = '*'}
        redo if password.empty?
        confirmation = ask("Re-" + msg) {|q| q.echo = '*'}
        if password != confirmation
          puts "Password doesn't match."
          redo
        end
        password
      when :mail
        ask(msg) {|q|
          q.validate = /^[\x01-\x7f]+@(\w+\.)+[a-zA-Z]+$/
          q.responses[:ask_on_error] = :question
          q.responses[:not_valid] = "Invalid email address."
        }
      else
        ans = ask(msg) {|q| q.default = default}.toutf8
        redo if ans.empty?
        ans
      end
    }
  else
    config_hash.each {|key, conf|
      options[key] = conf[:default] if options[key].nil?
    }

    if options[:ldap_setting]
      raise "Failed to bind to LDAP server" unless bind_check(options)
      raise "Failed to bind as admin account" unless admin_check(options)
    end

    config_hash.each {|key, conf|
      next unless options[key].nil?
      p_key = conf[:parent]
      next if p_key && options[p_key].nil?
      raise "#{key.to_s} is not specified" if p_key.nil?
    }
  end
  options[:apache_host] += ":#{options[:apache_port]}" unless options[:apache_port] == 80

  options.each {|key, value|
    next unless value.is_a?(String)
    next if key.to_s.index("password")
    options[key].strip!
  }

  unless options[:ldap_setting]
    options[:ldap_host] = "localhost"
    options[:ldap_port] = options[:opends_port]
    options[:ldap_base_dn] = options[:opends_base_dn]
    options[:ldap_anonymous] = false
    options[:ldap_bind_dn] = "uid=#{options[:admin_account]},ou=users,#{options[:ldap_base_dn]}"
    options[:ldap_bind_password] = options[:admin_password]
    options[:ldap_user_attribute] = "uid"
    options[:ldap_first_name_attribute] = "givenName"
    options[:ldap_last_name_attribute] = "sn"
    options[:ldap_mail_attribute] = "mail"
  end

  Template.install(options)
  OpenDS.install(options) unless options[:ldap_setting]
  Redmine.install(options)
  system(%Q["#{ScriptDir}/service.bat" install])
  Windows::Shortcut.install(options)
rescue => e
  if options[:interact]
    puts "Error: #{$!}"
    puts "\t" + e.backtrace.join("\n\t")
    ask("Press any key to exit ... ") {|q|
      q.character = true
      q.echo = false
    }
  else
    raise e
  end
end
