#!/usr/bin/ruby

require "puavo"
require "optparse"
require "httparty"
require "socket"

require "puavo/api-server"

ENV['SSL_CERT_FILE'] = '/etc/puavo-conf/rootca.pem'

# !!! Note that this script should not modify anything under /etc/cups in any
# !!! way, because this might be run from incron (monitoring /etc/cups)
# !!! that triggers this script when something changes there.

def assert_response(res)
  if ![200, 201, 202].include?(res.code)
    raise "Bad response status #{ res.code } for #{ res.request.path.to_s }: #{ res }"
  end
end

# From https://github.com/opinsys/liitu-puppet/blob/25c92e5c85e8637cdbd04c390f98eca69d3cc68b/modules/service/templates/usr/local/sbin/send_printers_to_puavo#L18-L40
def parse_cups_printers_conf(conf_path)
  begin
    printers_conf = File.readlines(conf_path)
  rescue Errno::ENOENT
    return {}
  end

  printers = {}
  description  = nil

  printers_conf.each do |line|
    match = line.match(/^<(?:DefaultPrinter|Printer) (.*)>$/)
    if match
      description = match[1]
      printers[description] = {}
    elsif line.match(%r{^</Printer>$})
      description = nil
    else
      if description then
        _, key, value = * line.match(/^([^ ]+) (.*)$/)
        printers[description][key] = value
      end
    end
  end

  cups_dir = File.dirname(conf_path)
  ppd_dir = "#{ cups_dir }/ppd"

  # check ppd-files for MakeModel,
  # because printers.conf apparently frequently lacks it
  printers.keys.each do |description|
    make_and_model = 'Unknown'
    begin
      ppd_file = "#{ ppd_dir }/#{ description }.ppd"
      ppd_file_data = IO.read(ppd_file)
      match = ppd_file_data.match(/^\*ShortNickName: "(.*)"$/)
      if match
        make_and_model = match[1]
      else
        warn("Did not find MakeModel from #{ ppd_file }")
      end
    rescue Errno::ENOENT
      # frequent enough that no point in telling about this
    rescue StandardError => e
      warn("Could not read MakeModel from #{ ppd_file }: #{ e.message }")
    end

    printers[description]['MakeModel'] = make_and_model
  end

  printers
end

exitcode = 0

options = {
  :printers_conf => "/etc/cups/printers.conf"
}

parser = OptionParser.new do |opts|
  opts.banner = "
  Usage: #{ File.basename(__FILE__) } [options] [cups printers config]

  Synchronize CUPS printers to Puavo. Printers are identified by their
  description in the CUPS config.

  Be default printers are read from #{ options[:printers_conf] }
  "

  opts.on("--post-printers-url [URL]", "Url where to post printers") do |url|
    options[:post_printers] = url
  end

  opts.on("--api-server [URL]", "API server root") do |url|
    options[:api_server] = url
  end

  opts.on("-h", "--hostname [HOSTNAME]", "Boot server hostname") do |hostname|
    options[:hostname] = hostname
  end

  opts.on("-u", "--user [USER]", "Username or dn") do |username|
    options[:user] = username
  end

  opts.on("-p", "--password [PASSWORD]", "Password") do |pw|
    options[:password] = pw
  end

  opts.on("--log [FILE]", "Log output to file. Defaults to stdout/stderr") do |log_file|
    options[:log_file] = log_file
  end

  opts.on_tail("-h", "--help", "Show this message") do
    STDERR.puts opts
    exit
  end

end

parser.parse!

if options[:log_file]
  $stdout.reopen(options[:log_file], "a")
  $stderr.reopen(options[:log_file], "a")
end

options[:api_server] ||= Puavo.resolve_api_server!
options[:post_printers] ||= "https://#{ PUAVO_ETC.domain }/devices/printers.json"
options[:user] ||= PUAVO_ETC.ldap_dn
options[:password] ||= PUAVO_ETC.ldap_password
options[:hostname] ||= Socket.gethostname

options[:get_printers] = "#{ options[:api_server] }/v3/printer_queues"
options[:printers_conf] = ARGV[0] if ARGV[0]


boot_server_resource =  "#{ options[:api_server] }/v3/boot_servers/#{ options[:hostname] }"
puts "Fetching boot server info from: #{ boot_server_resource }"
res = HTTParty.get boot_server_resource,
  :headers => {
    "Authorization" => "Bootserver"
  }
assert_response res
options[:server_dn] = res["dn"]



puts "Fetching existing printers from #{ options[:get_printers] }"

current_printers = HTTParty.get(options[:get_printers],
  :query => {
    "server_dn" => options[:server_dn]
  },
  :basic_auth => {
    :username => options[:user],
    :password => options[:password]
  },
  :headers => {
    "Content-Type" => "application/json",
    "Accept" => "application/json"
  }
 )
assert_response res

server_printers = parse_cups_printers_conf(options[:printers_conf])
puts "Ensuring #{ server_printers.size } printers to puavo"
server_printers.each do |description, info|
  begin
    cp = current_printers.select {|thispr, val| thispr["description"] == description}
    existing = -1
    for i in (0..cp.size-1)
      next if cp[i]["server_dn"] != options[:server_dn] #different cupses can have printers with same descriptions
      existing = i
    end

    if existing > -1
      next if cp[existing]["model"] == info["MakeModel"] && cp[existing]["type"] == info["Type"] \
        && (info["Info"]==nil || info["Info"] == "" || cp[existing]["info"] == info["Info"]) \
        && (info["Location"]==nil || info["Location"]=="" || cp[existing]["location"] == info["Location"]) \
        && cp[existing]["local_uri"] == info["DeviceURI"] # ^ "" is somewhy saved as "-" in LDAP
      puts "Printer details changed, updating existing #{description}"
    end

    printer = {
      "printerDescription" => description,
      "printerInfo" => info["Info"] || "",
      "printerLocation" => info["Location"] || "",
      "printerMakeAndModel" => info["MakeModel"] || "",
      "printerType" => info["Type"] || "" ,
      "printerURI" => info["DeviceURI"] || "" ,
      "puavoServer" => options[:server_dn]
    }

    json = printer.to_json

    puts "Sending to #{ options[:post_printers] } : '#{ json }'"
    res = HTTParty.post(options[:post_printers],
      :body => json,
      :basic_auth => {
        :username => options[:user],
        :password => options[:password]
      },
      :headers => {
        "Content-Type" => "application/json",
        "Accept" => "application/json"
      }
    )
    assert_response res
    puts 'OK'
    puts
  rescue StandardError => e
    warn("Problem sending printer information for '#{ description }'" \
           + 'to Puavo: ' + e.message)
    exitcode = 1
  end
end

if exitcode != 0 then
  warn('Problems when sending printers to Puavo')
end

exit(exitcode)
