#!/usr/bin/env ruby
require 'tmpdir'
require 'optparse'
require 'etc'

$root = (ARGV.delete '--sudo') || (Process.uid == 0)
$apache_would_have_been_restarted = (ARGV.delete '--apache-restarted')

#########################################################################
#
# Sets up a macOS machine for whimsy development.  Pass --help as an
# option to get a description of the arguments.  See
# https://github.com/apache/whimsy/blob/master/SETUPMYMAC.md#readme
# for a more complete description.
#
#########################################################################

unless RUBY_PLATFORM.include? 'darwin'
  STDERR.puts 'This script is intended to be run on macOS'
  exit 1
end

unless (RUBY_VERSION.split('.').map(&:to_i) <=> [2, 4, 1]) >= 0
  STDERR.puts 'Ruby 2.4.1 or later is required'
  exit 1
end

WHIMSY = File.realpath File.expand_path('..', __dir__)
COMMAND = File.realpath($0)
ARGS = ARGV.dup
Dir.chdir WHIMSY

$dry_run = false
restart_apache = false
$brew_updated = false

force = {clean: false, prune: false, toucher: nil, ws: nil}
force[:svn] = true unless Dir.exist? '/srv/svn'
force[:git] = true unless Dir.exist? '/srv/git'

### Convenience methods

# output a header line, in color if available
def color(line)
  if STDOUT.isatty and ENV['TERM'].to_s.include? 'color'
    puts "\n\u001b[35;1m#{line}\u001b[0m"
  else
    puts "\n" + line
  end
end

# echo a command and run it
def run *args
  color '$ ' + "#{'sudo ' if $root}" + Array(args).join(' ')
  return if $dry_run
  Kernel.system(*args)
end

# run brew, making sure that a brew update is done before the first command
def brew *args
  unless $brew_updated
    run 'brew', 'update'
    $brew_updated = true
  end

  run 'brew', *args
end

# TODO: this does not work well for dry-run;
# the script can be called multiple times
# Switch to root
def sudo
  if $root
    yield
  elsif $dry_run
    ARGS.push '--apache-restarted' if $apache_would_have_been_restarted
    system RbConfig.ruby, COMMAND, *ARGS, '--sudo'
    $apache_would_have_been_restarted = true
  else
    system 'sudo', RbConfig.ruby, COMMAND, *ARGS
    exit $?.exitstatus unless $?.success?
  end
end

def findDocker
  if Dir.exist? "#{Dir.home}/Applications/Docker.app"
    return "#{Dir.home}/Applications/Docker.app"
  elsif Dir.exist? '/Applications/Docker.app'
    return '/Applications/Docker.app'
  end
  return nil
end

### Parse options to determine how whimsy code is to be run

option = :www

OptionParser.new do |opts|
  opts.banner = "Usage: #{$0} [options]"

  opts.on('-u', '--user', 'Run whimsy under your user') do |_opt|
    option = :user
  end

  opts.on('-w', '--web', 'Run whimsy under the Apache web user') do |_opt|
    option = :web
  end

  opts.on('-d', '--docker', 'Run whimsy on docker') do |_opt|
    option = :docker
  end

  opts.on('--[no-]gem', '--gems', 'Upgrade gem dependencies') do |opt|
    force[:gems] = opt
  end

  opts.on('--[no-]bundle', '--bundler', 'Upgrade bundler') do |opt|
    force[:bundler] = opt
  end

  opts.on('--[no-]node', 'Upgrade to the latest node.js') do |opt|
    force[:node] = opt
  end

  opts.on('--[no-]passenger', 'Upgrade to the latest Phusion Passenger') do |opt|
    force[:passenger] = opt
  end

  opts.on('--[no-]ldap', 'Reconfigure LDAP') do |_opt|
    force[:ldap] = opts
  end

  opts.on('--[no-]svn', 'Checkout/update svn repositories') do |_opt|
    force[:svn] = opts
  end

  opts.on('--[no-]git', 'Clone/pull git repositories') do |_opt|
    force[:git] = opts
  end

  opts.on('--[no-]source', 'Pull the latest changes to the source code') do |opt|
    force[:source] = opt
  end

  opts.on('--[no-]minutes', 'Collate board minutes') do |opt|
    force[:minutes] = opt
  end

  opts.on('--update-all', 'Update everything') do |_opt|
    force.default = true
  end

  opts.on('--[no-]toucher', 'Restart rack applications on source change') do |opt|
    force[:toucher] = opt
  end

  opts.on('--[no-]ws', 'Start board agenda websocket') do |opt|
    force[:ws] = opt
  end

  opts.on('--all', 'Update and launch everything') do |_opt|
    force[:ws] = true if force[:ws].nil?
    force[:toucher] = true if force[:toucher].nil?
    force.default = true
  end

  opts.on('--[no-]clean', 'Clean up source directory') do |opt|
    force[:clean] = opt
  end

  opts.on('--[no-]prune', 'Prune docker containers and images') do |opt|
    force[:prune] = opt
  end

  opts.on('--dry-run', 'Only indicate what commands would be run') do |_opt|
    $dry_run = true
  end
end.parse!

user = option == :www ? '_www' : (ENV['SUDO_USER'] || Etc.getlogin)
uid = Etc.getpwnam(user).uid
gid = Etc.getpwnam(user).gid
group = Etc.getgrgid(gid).name

sudo_user = ENV['SUDO_USER'] ?
  Etc.getpwnam(ENV['SUDO_USER']) : Etc.getpwuid(Process.uid)

### Install Homebrew

unless $root and (option != :docker or not findDocker)
  if `which brew`.empty?
    script = 'https://raw.githubusercontent.com/Homebrew/install/master/install'
    color %($ ruby -e "$(curl -fsSL #{script})")
    eval `curl -fsSL #{script}` unless $dry_run
  end
end

## Install Node.js

unless $root and option != :docker
  if `which n`.empty?
    brew 'install', 'n'
  elsif force[:node]
    brew 'upgrade', 'n'
  end
end

unless Dir.exist? '/usr/local/n'
  sudo { run 'mkdir', '/usr/local/n' }
end

unless File.stat('/usr/local/n').uid == sudo_user.uid
  sudo {run 'chown', '-R', "#{sudo_user.uid}:#{sudo_user.gid}", '/usr/local/n'}
end

unless $root and option != :docker
  if `which node`.empty?
    run 'n lts'
  elsif force[:node] and "v#{`n ls-remote stable`}" != `node --version`
    run 'n lts'
  end

  # Prompt for xcode installation
  `git --version` unless $dry_run

  # unless (svn installed and svnmucc is on path)
  unless system 'svn --help > /dev/null 2>&1' and not `which svnmucc`.empty?
    brew 'install', 'subversion'
  end
end

## Install Passenger

unless $root and option != :docker
  if `which passenger`.empty?
    brew 'install', 'passenger'
  elsif force[:passenger]
    brew 'upgrade', 'passenger'
  end
end

### Create /srv

mac_version = `sw_vers`[/ProductVersion:\s+(.*)/, 1]
unless Dir.exist? '/srv'
  sudo do
    if (mac_version.split('.').map(&:to_i) <=> [10, 15, 0]) >= 0
      # Catalina or later
      run 'mkdir', '/var/whimsy' unless Dir.exist? '/var/whimsy'
      run 'chown', "#{sudo_user.uid}:#{sudo_user.gid}", '/var/whimsy'
      run 'touch', '/etc/synthetic.conf'
      SYNTHETIC = '/etc/synthetic.conf'
      unless File.read(SYNTHETIC).include? '/var/whimsy'
        color "$ sudo edit #{SYNTHETIC}"
        unless $dry_run
          File.write SYNTHETIC, File.read(SYNTHETIC) + "srv\t/var/whimsy\n"
        end
        STDERR.puts "#{SYNTHETIC} updated; reboot machine and rerun this script"
        puts %(\nPress "y" to reboot now, anything else to exit)
        run 'shutdown -r now' if gets.strip.downcase == 'y'
        exit 1
      end
    else
      # prior to Catalina
      run 'mkdir', '/srv'
      run 'chown', "#{sudo_user}:#{sudo_group}", '/srv'
    end
  end
end

# relocate whimsy clone
unless Dir.exist? '/srv/whimsy'
  sudo do
    run 'mv', WHIMSY, '/srv/whimsy'
    run 'ln', '-s', '/srv/whimsy', WHIMSY
  end
end

# clean source
if force[:clean] and not $root
  Dir.chdir '/srv/whimsy' do
    run 'git', 'reset', '--hard'
    run 'git', 'clean', '-fxd'
  end
end

# update source
if force[:source] and not $root
  Dir.chdir '/srv/whimsy' do
    run 'git', 'pull'
  end
end

### Define directories

directories = [
  '/srv/agenda',
  '/srv/cache',
  '/srv/mail',
  '/srv/mail/secretary',
  '/srv/secretary',
  '/srv/secretary/tlpreq',
  '/srv/whimsy/www/logs',
  '/srv/whimsy/www/public',
]

files = [
  '/srv/whimsy/www/status/status.json'
]

directories.each do |dir|
  sudo {run 'mkdir', '-p', dir} unless Dir.exist? dir

  unless File.stat(dir).uid == uid
    sudo {run 'chown', '-R', "#{uid}:#{gid}", dir}
  end
end

files.each do |file|
  sudo {run 'touch', file} unless File.exist? file

  unless File.stat(file).uid == uid
    sudo {run 'chown', "#{uid}:#{gid}", file}
  end
end

unless File.exist? '/srv/whimsy/www/members/log'
  run 'ln -s /var/log/apache2 /srv/whimsy/www/members/log'
end

### Docker installation

if force[:prune]
  Dir.chdir 'docker' do
    `docker compose ps -q`.lines.each do |line|
      run 'docker', 'stop', line.chomp
    end
  end

  run 'docker', 'container', 'prune', '--force'
  run 'docker', 'image', 'prune', '--force'
  exit
end

if option == :docker
  dockerapp = findDocker
  unless dockerapp
    dockerapp = '/Applications/Docker.app'
    brew 'cask', 'install', 'docker'
  end

  if `which docker-compose`.empty?
    run dockerapp
  end

  unless system 'docker info > /dev/null 2>&1'
    run dockerapp
  end

  unless $root
    # TODO: this is wrong if Docker is to be used alongside a local install
    Dir.chdir '/srv/whimsy' do
      run 'rake docker:update'
    end
  end

  exit
end

### Configure passenger

passenger_conf = '/etc/apache2/other/passenger.conf'

if Process.uid == 0
  instructions = `su $SUDO_USER -c "brew info passenger"`
else
  instructions = `brew info passenger`
end

section = instructions[/To activate Phusion Passenger for Apache.*(\n\n|\z)/m]
snippet = section.scan(/^ .*/).join("\n") + "\n"
snippet[/Passenger\w*Ruby\s+(.*)/, 1] = RbConfig.ruby

if option != :user
  snippet += "PassengerUser #{user}\nPassengerGroup #{group}\n"
end

if !File.exist?(passenger_conf) or File.read(passenger_conf) != snippet
  sudo do
    color "$ sudo edit #{passenger_conf}"
    File.write passenger_conf, snippet unless $dry_run

    restart_apache = true
  end
end

### Install bundler

if `which bundle`.empty?
  if File.writable? Gem.dir
    run 'gem install bundler'
  else
    sudo {run 'gem install bundler'}
  end
elsif force[:bundler]
  if File.writable? Gem.dir
    run 'gem update bundler'
  else
    sudo {run 'gem update bundler'}
  end

  ARGS.push '--no-bundle'
end

### Install gems

unless $root
  if force[:gems] or not File.exist?("#{WHIMSY}/Gemfile.lock")
    Dir.chdir WHIMSY do
      run 'rake', 'update'
    end
  end
end

### Checkout/clone repositories

if force[:svn] and not $root
  run 'rake', 'svn:update'
end

if force[:git] and not $root
  run 'rake', 'git:pull'
end

### Collate minutes

unless $root
  if force[:minutes] or not Dir.exist? '/srv/whimsy/www/board/minutes'
    run 'tools/collate_minutes.rb'
  end
end

### Configure LDAP

if File.exist? "#{WHIMSY}/Gemfile.lock"
  $LOAD_PATH.unshift '/srv/whimsy/lib'
  require 'whimsy/asf'
  if force[:ldap] or not ASF::LDAP.configured?
    sudo do
      color '$ ruby -I lib -r whimsy/asf -e "ASF::LDAP.configure"'
      ASF::LDAP.configure unless $dry_run
    end
    ARGS.push '--no-ldap'
  end
end

### Make whimsy.local an alias for your machine

hosts = File.read('/etc/hosts')
unless hosts.include? 'whimsy.local'
  sudo do
    color '$ sudo edit /etc/hosts'
    hosts[/^[:\d].*\slocalhost\b.*()/, 1] = ' whimsy.local'
    File.write '/etc/hosts', hosts unless $dry_run
  end
end

### Configure httpd

HTTPD_CONF = '/etc/apache2/httpd.conf'

config = File.read(HTTPD_CONF)

# uncomment necessary modules

instructions = File.read(File.expand_path('../MACOS.md', __dir__))

section = instructions[/^Configure whimsy.local vhost\n--+\n.*?\n--/m]

uncomment = section[/Uncomment.*?```(.*?)```/m, 1]
add = section[/Add.*?```(.*?)```/m, 1].strip

uncomment.scan(/^\S.*/).each do |line|
  config.sub!(/^\s*#\s*#{line}\s*$/) { $&.sub('#', '') }

  if config !~ /^\s*#{line}\s*$/
    STDERR.puts "Not found: #{line}"
    exit 1
  end
end

config += "\n" unless config.end_with? "\n"

# add additional lines from the instructions

add.scan(/^\S.*/).each do |line|
  if config !~ /^\s*#{line}\s*$/
    config += "#{line}\n"
  end
end

# run under the specified user

config[/^User\s+(.*)/, 1] = user
config[/^Group\s+(.*)/, 1] = group

# add index.cgi to DirectoryIndex
unless config =~ /^\s*DirectoryIndex\s+.*index\.cgi\b/i
  config[/^\s*DirectoryIndex\s.*()/, 1] = ' index.cgi'
end

# replace configuration file if changed

if config != File.read(HTTPD_CONF)
  sudo do
    color "$ sudo edit #{HTTPD_CONF}"
    return if $dry_run
    File.rename HTTPD_CONF, HTTPD_CONF + '.original'
    File.write(HTTPD_CONF, config)
  end

  restart_apache = true
end

wconf_source = "#{WHIMSY}/docker-config/whimsy.conf"
wconf_content = File.read(wconf_source)
if wconf_content =~ /^\s*SetEnv PATH /i
  wconf_content[/^\s*SetEnv PATH .*/] =
    "SetEnv PATH #{File.dirname RbConfig.ruby}:" +
    "#{File.dirname(`which svnmucc`)}:${PATH}"
end
wconf_content.sub!(/^\s*PassengerUser .*/i, "PassengerUser #{user}")
wconf_content.sub!(/^\s*PassengerGroup .*/i, "PassengerGroup #{group}")
wconf_target = '/private/etc/apache2/other/whimsy.conf'
if
  not File.exist?(wconf_target) or
  File.read(wconf_target) != wconf_content
then
  sudo do
    color "$ cp #{wconf_source} #{wconf_target}"
    File.write wconf_target, wconf_content
  end

  restart_apache = true
end

confd_source = "#{WHIMSY}/docker-config/25-authz_ldap_group_membership.conf"
confd_target = '/private/etc/apache2/other/25-authz_ldap_group_membership.conf'
if
  not File.exist?(confd_target) or
  File.read(confd_target) != File.read(confd_source)
then
  sudo do
    run 'cp', confd_source, confd_target
  end

  restart_apache = true
end

### Make applications restart on change

unless $root and force[:toucher] != nil
  plist = "#{Dir.home}/Library/LaunchAgents/toucher.plist"

  if force[:toucher]
    contents = File.read("#{__dir__}/toucher.plist")
    contents[/>(.*ruby.*)</, 1] = RbConfig.ruby

    unless Dir.exist? File.dirname(plist)
      run "mkdir -p #{File.dirname(plist)}"
    end

    unless File.exist?(plist) or File.read(plist) != contents
      color "$ edit #{plist}"
      File.write plist, contents unless $dry_run

      if `launchctl list`.include? 'org.apache.whimsy/toucher'
        run "launchctl unload #{plist}"
      end
    end

    unless `launchctl list`.include? 'org.apache.whimsy/toucher'
      run "launchctl load #{plist}"
    end
  else
    if `launchctl list`.include? 'org.apache.whimsy/toucher'
      run "launchctl unload #{plist}"
    end

    if File.exist?(plist)
      run "rm #{plist}"
    end
  end
end

### Start Apache httpd

if $root and not $apache_would_have_been_restarted
  if not `launchctl list`.include? 'org.apache.httpd'
    run 'launchctl load -w /System/Library/LaunchDaemons/org.apache.httpd.plist'
  elsif restart_apache
    run 'apachectl restart'
    sleep 0.5
  end
elsif not $dry_run
  system 'open http://whimsy.local/'
end
