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

# TODO: add 'owner' to get/set_meta

load 'git-mirror-backend.rb'
load 'libremessages.rb'
require 'net/http'
require 'uri'
require 'cgi'
require 'json'

class GitLabCE < GitMirrorBackend
	class Error < RuntimeError
		def initialize(obj)
			@obj = obj
		end
		def obj
			return @obj
		end
		def to_s
			return @obj.to_s
		end
	end

	def initialize()
		@connections = {}
		@projects = {}
		@config = {}
	end

	def connection(uri)
		key=URI(uri.scheme+":")
		key.host = uri.host
		key.port = uri.port

		@connections[key] ||= Net::HTTP::start(uri.host, uri.port, :use_ssl => uri.scheme == 'https')
		return @connections[key]
	end

	def finish
		@connections.each do |k,v|
			v.finish()
		end
		@connections = {}
		super
	end

	def config
			return @config
	end

	# Project
	def project(project_id)
		unless @projects.has_key?(project_id)
			@projects[project_id] = Project.new(self, project_id)
		end
		return @projects[project_id]
	end
	class Project
		def initialize(gl, project_id)
			@gl = gl
			@project_id = project_id
			@cache = {}
		end

		def info
			unless @cache.has_key?(:info)
				req = Net::HTTP::Get.new(@gl.config['apiurl'] + "projects/" + CGI::escape(@project_id))
				req.add_field("PRIVATE-TOKEN", @gl.config['apikey'])
				res = @gl.connection(req.uri).request(req)
				case res.code
				when "200"
					@cache[:info] = JSON::parse(res.body)
				when "404"
					@cache[:info] = nil
				else
					raise Error.new(res)
				end
			end
			return @cache[:info]
		end

		def info=(i)
			@cache[:info] = i
		end

		def get_meta
			return self.info.select{|k,v| @gl.vars.include?(k.to_sym)}
		end

		def set_meta(map)
			mirror = map["mirror"]
			map.delete("mirror")

			illegal = map.select{|k,v| not @gl.vars.include?(k.to_sym)}
			if illegal.count > 0
				raise Error.new(illegal)
			end

			if self.info().nil?
				# create
				libremessages('msg2', 'Creating repo %s', @project_id)
				namespace, path = @project_id.split('/', 2)
				namespace_id = namespace_path2id(namespace)

				req = Net::HTTP::Post.new(@gl.config['apiurl'] + "projects")
				req.add_field("PRIVATE-TOKEN", @gl.config['apikey'])
				req.add_field("Content-Type", "application/json")
				map["name"] ||= path
				map["namespace_id"] = namespace_id
				map["path"] = path
				if not mirror.nil?
					map["import_url"] = mirror
				end
				req.body = JSON::dump(map)
				res = @gl.connection(req.uri).request(req)
				if res.code != "201"
					raise Error.new(res)
				end
				self.info = JSON::parse(res.body)
			else
				# update
				libremessages('msg2', 'Updating repo %s metadata', @project_id)
				req = Net::HTTP::Put.new(@gl.config['apiurl'] + "projects/" + CGI::escape(self.info["id"].to_s))
				req.add_field("PRIVATE-TOKEN", @gl.config['apikey'])
				req.add_field("Content-Type", "application/json")
				req.body = JSON::dump(map)
				res = @gl.connection(req.uri).request(req)
				if res.code != "200"
					raise Error.new(res)
				end
				self.info = JSON::parse(res.body)
			end
			return nil
		end

		def namespace_path2id(path, pageno=1)
			req = Net::HTTP::Get.new(@gl.config['apiurl'] + "namespaces?page=#{pageno}&search=#{CGI::escape(path)}")
			req.add_field("PRIVATE-TOKEN", @gl.config['apikey'])
			res = @gl.connection(req.uri).request(req)
			if res.code != "200"
				raise Error.new(res)
			end
			page = JSON::parse(res.body)
			page.each do |namespace|
				if namespace["path"] == path
					return namespace["id"].to_i
				end
			end
			if pageno < res['X-Total-Pages'].to_i
				return namespace_path2id(path, pageno+1)
			end
			return nil
		end

		def repo_mode
			return "passive"
		end
	end

	# commands
	def cmd_config(*args)
		args.each do |arg|
			key, val = arg.split('=', 2)
			key = key.gsub('-', '_')
			case key
			when "apiurl"
				val = URI(val)
				unless val.path.end_with?("/")
					val.path += "/"
				end
			end
			@config[key] = val
		end
		return nil
	end

	def cmd_get_meta(project_id)
		map = self.project(project_id).get_meta()
		ret = {}
		map.each do |key,val|
			key = key.gsub('_', '-')
			map[key] = val
		end
		return ret
	end

	def cmd_set_meta(project_id, *pairs)
		map = {}
		pairs.each do |pair|
			key, val = pair.split('=', 2)
			key = key.gsub('-', '_')
			if val.nil?
				map.delete(key)
			else
				map[key] = val
			end
		end
		self.project(project_id).set_meta(map)
		return nil
	end

	def cmd_push_url(project_id)
		return self.project(project_id).info["ssh_url_to_repo"]
	end

	def cmd_pull_url(project_id)
		return self.project(project_id).info["http_url_to_repo"]
	end

	def cmd_repo_mode(project_id)
		return self.project(project_id).repo_mode()
	end

	def vars
		# API docs suck, look at `lib/api/projects.rb` instead.
		return [
			:builds_enabled,                                  # create | create-user | edit
			:container_registry_enabled, # XXX                # create |             | edit
			:default_branch,                                  #        | create-user | edit
			:description,                                     # create | create-user | edit
			#:import_url, # XXX                               # create | create-user |
			:issues_enabled,                                  # create | create-user | edit
			:lfs_enabled,                                     # create | create-user | edit
			:merge_requests_enabled,                          # create | create-user | edit
			:name,                                            # create | create-user | edit
			#:namespace_id,                                   # create |             |
			:only_allow_merge_if_build_succeeds,              # create | create-user | edit
			#:path,                                           # create |             | edit
			:public,                                          # create | create-user | edit
			:public_builds,                                   # create | create-user | edit
			:request_access_enabled,                          # create | create-user | edit
			:shared_runners_enabled,                          # create | create-user | edit
			:snippets_enabled,                                # create | create-user | edit
			:visibility_level,                                # create | create-user | edit
			:wiki_enabled,                                    # create | create-user | edit
			:only_allow_merge_if_all_discussions_are_resolved # create | create-user | edit
		]
	end
end

if __FILE__ == $0
    if ARGV.length != 1
		raise "Usage: $0 ACCOUNT_NAME"
	end
	GitLabCE.new().repl(ARGV[1])
end