#!/usr/bin/env ruby
require 'open3'
require 'socket'
require 'openssl'
require 'timeout'
require 'base64'

# configurable environment variables
nick      = ENV['OUR_NICK']              || 'our'
channels  = ENV['OUR_CHANNELS']          || '#tildetown,#bots'
prefix    = ENV['OUR_PREFIX']            || "#{nick}/"
cmds_dir  = ENV['OUR_CMDS_DIR']          || '/town/our'
server    = ENV['OUR_SERVER']            || 'localhost'
use_ssl   = ENV['OUR_USE_SSL'] == "true" || false
port      = ENV['OUR_IRC_PORT']          || 6667
sasl_user = ENV['OUR_SASL_USER']         || nil
sasl_pass = ENV['OUR_SASL_PASS']         || nil

module IRC

class User
  attr_accessor :s

  def initialize(addr:, port:, nick:, use_ssl: false, sasl_user: nil, sasl_pass: nil)
    @hooks = []

    sock = TCPSocket.open addr, port.to_s
    if use_ssl
      puts "connecting with SSL"
      ctx = OpenSSL::SSL::SSLContext.new
      ctx.set_params(verify_mode: OpenSSL::SSL::VERIFY_PEER)
      @s = OpenSSL::SSL::SSLSocket.new(sock, ctx).tap do |socket|
        socket.sync_close = true
        socket.connect
      end
    else
      puts "connecting without SSL"
      @s = sock
    end

    if sasl_user && sasl_pass
      puts "connecting with SASL"
      s.puts "CAP REQ :sasl"
      s.puts "USER #{nick} m455.casa 1 :beep boop"
      s.puts "NICK #{nick}"
      s.puts "AUTHENTICATE PLAIN"
      plain_auth = Base64.encode64("#{sasl_user}\0#{sasl_user}\0#{sasl_pass}")
      s.puts "AUTHENTICATE #{plain_auth}"
      s.puts "CAP END"
    else
      puts "connecting without SASL"
      s.puts "USER #{nick} m455.casa 1 :beep boop"
      s.puts "NICK #{nick}"
    end

    hook do |m|
      next unless m.cmd == 'PING'
      raw "PING #{nick}"
    end
  end

  def raw msg
    @s.puts msg
  end

  def join chan
    raw "JOIN #{chan}"
  end

  def privmsg target, msg
    raw "PRIVMSG #{target} :#{msg}"
  end

  def hook &h
    @hooks << h
  end

  def loop
    while line = s.gets
      msg = Message.new line
      @hooks.each{|h| h.call(msg)}
    end
  end
end

class Message
  attr_accessor :prefix, :cmd, :args, :raw
  # TODO custom constructor
  def initialize msg
    msg = msg.delete_suffix "\r\n"
    @raw = msg
    @prefix = nil
    @prefix, msg = msg[1..].split(' ', 2) if msg[0] == ':'
    @cmd, msg = msg.split(' ', 2)

    @args = []
    while msg and not msg.empty?
      if msg[0] == ':'
        @args << msg[1..]
        break
      end
      s, msg = msg.split(' ', 2)
      @args << s
    end
  end
end

end


puts "starting"
i = IRC::User.new(addr: server, port: port, nick: nick, use_ssl: use_ssl, sasl_user: sasl_user, sasl_pass: sasl_pass)
channels.split(',').each { |channel| i.join channel }
i.hook do |msg|
  next unless msg.cmd == 'PRIVMSG'
  target, content = msg.args
  next unless content.delete_prefix! prefix
  cmd, args = content.split(' ', 2)  
  cmd = "#{cmds_dir}/#{cmd}"
  args ||= ''
  next unless File.exist? cmd
  if not File.executable? cmd
    i.privmsg target, "#{cmd} isn't executable. try chmod +x"
    next
  end
  begin
    Open3.popen2e("#{__dir__}/wrap_it.sh", cmd, args, msg.prefix, target) do |_, stdout, wait_thread|
      out = nil
      Timeout::timeout(3) do
        out = stdout.gets             # only interested in the first line of output
        stdout.gets until stdout.eof? # make sure process finishes in time allotted
      end
      i.privmsg target, out if out
    rescue Timeout::Error
      Process.kill("KILL", wait_thread.pid)
      i.privmsg  target, "[our.rb] command timed out"
    end
  rescue Exception => e
    i.privmsg  target, "[our.rb] #{e.to_s}"
  end
  next true
end
i.loop
