#!/usr/bin/env ruby
# coding: utf-8

# Copyright 2014, 2016 Luke Shumaker <lukeshu@sbcglobal.net>.
# Copyright 2015 Márcio Alexandre Silva Delgado <coadde@parabola.nu>.
#
# This is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation; either version 2 of
# the License, or (at your option) any later version.
#
# This software is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public
# License along with this manual; if not, see
# <http://www.gnu.org/licenses/>.

# First we define a bunch of code-generators, then at the end is a
# very neat and readable definition of the format of the YAML files.

require 'yaml'

def error(msg)
	$stderr.puts "ERROR: #{msg}"
	@err = 1
end

def warning(msg)
	$stderr.puts "WARNING: #{msg}"
end


# Generic validators/formatters

def semiordered_list(cnt, validator)
	lambda {|name,ary|
		if !ary.is_a?(Array)
			error "`#{name}' must be a list"
		else
			ary.each_index{|i| ary[i] = validator.call("#{name}[#{i}]", ary[i])}
			ary = ary.first(cnt).concat(ary.last(ary.count-cnt).sort)
		end
		ary
	}
end

def unordered_list(validator)
	semiordered_list(0, validator)
end

def _unknown(map_name, key)
	error "Unknown item: #{map_name}[#{key.inspect}]"
	0
end
def unordered_map1(validator)
	lambda {|name,hash|
		if !hash.is_a?(Hash)
			error "`#{name}' must be a map"
		else
			order = Hash[[*validator.keys.map.with_index]]
			hash = Hash[hash.sort_by{|k,v| order[k] || _unknown(name,k) }]
			hash.keys.each{|k|
				if validator[k]
					hash[k] = validator[k].call("#{name}[#{k.inspect}]", hash[k])
				end
			}
		end
		hash
	}
end

def unordered_map2(key_validator, val_validator)
	lambda {|name,hash|
		if !hash.is_a?(Hash)
			error "`#{name}' must be a map"
		else
			hash = Hash[hash.sort_by{|k,v| k}]
			hash.keys.each{|k|
				key_validator.call("#{name} key #{k.inspect}", k)
				hash[k] = val_validator.call("#{name}[#{k.inspect}]", hash[k])
			}
		end
		hash
	}
end

string = lambda {|name,str|
	if !str.is_a?(String)
		error "`#{name}' must be a string"
	else
		str
	end
}

# Regular Expression String
def restring(re)
	lambda {|name,str|
		if !str.is_a?(String)
			error "`#{name}' must be a string"
		else
			unless re =~ str
				error "`#{name}' does not match #{re.inspect}: #{str}"
			end
			str
		end
	}
end


# Specific validators/formatters

year = lambda {|name, num|
	if !num.is_a?(Integer)
		error "`#{name}' must be an integer year"
	else
		if (num < 1900 || num > 3000)
			error "`#{name}' is a number, but doesn't look like a year"
		end
		num
	end
}

# This regex is taken from http://www.w3.org/TR/html5/forms.html#valid-e-mail-address
_email_regex = /^[a-zA-Z0-9.!\#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/
email_list = lambda {|name, ary|
	if !ary.is_a?(Array)
		error "`#{name}' must be a list"
	elsif not ary.empty?
		preserve = 1
		if ary.first.end_with?("@parabola.nu") and ary.count >= 2
			preserve = 2
		end
		ary = semiordered_list(preserve, restring(_email_regex)).call(name, ary)
	end
	ary
}

shell = lambda {|name, sh|
	if !sh.is_a?(String)
		error "`#{name}' must be a string"
	else
		@valid_shells ||= open("/etc/shells").read.split("\n")
						  .find_all{|line| /^[^\#]/ =~ line}
						  .push("/usr/bin/nologin")
		unless @valid_shells.include?(sh)
			warning "shell not listed in /etc/shells: #{sh}"
		end
	end
	sh
}


# The format of the YAML files

format = unordered_map1(
	{
		"username" => restring(/^[a-z][a-z0-9-]*$/),
		"fullname" => string,
		"email" => email_list,
		"groups" => semiordered_list(1, string),
		"pgp_keyid" => restring(/^[0-9A-F]{40}$/),
		"pgp_revoked_keyids" => unordered_list(restring(/^[0-9A-F]{40}$/)),
		"ssh_keys" => unordered_map2(string, string),
		"shell" => shell,
		"extra" => unordered_map1(
			{
				"alias" => string,
				"other_contact" => string,
				"roles" => string,
				"website" => string,
				"occupation" => string,
				"yob" => year,
				"location" => string,
				"languages" => string,
				"interests" => string,
				"favorite_distros" => string,
			})
	})



@err = 0
user = format.call("user", YAML::load(STDIN))
if @err != 0
	exit @err
end
print user.to_yaml