-- @@SHEBANG
-- -*- Lua -*-
-- gitano-setup
--
-- Git (with) Augmented network operations -- Instance setup tool
--
-- Copyright 2012-2017 Daniel Silverstone <dsilvers@digital-scurf.org>
-- All rights reserved.
--
-- Redistribution and use in source and binary forms, with or without
-- modification, are permitted provided that the following conditions
-- are met:
-- 1. Redistributions of source code must retain the above copyright
--    notice, this list of conditions and the following disclaimer.
-- 2. Redistributions in binary form must reproduce the above copyright
--    notice, this list of conditions and the following disclaimer in the
--    documentation and/or other materials provided with the distribution.
-- 3. Neither the name of the author nor the names of their contributors
--    may be used to endorse or promote products derived from this software
--    without specific prior written permission.
--
-- THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
-- ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-- IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
-- ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
-- FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-- DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
-- OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
-- HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
-- LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
-- OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
-- SUCH DAMAGE.
--
--

-- @@GITANO_LUA_PATH

local gitano = require "gitano"
local gall = require "gall"
local luxio = require "luxio"
local sio = require "luxio.simple"
local clod = require "clod"

-- @@GITANO_BIN_PATH
-- @@GITANO_SHARE_PATH
-- @@GITANO_I18N_PATH
-- @@GITANO_PLUGIN_PATH

local possible_answers = {...}

if (possible_answers[1] == "--help"
       or possible_answers[1] == "-h"
    or possible_answers[1] == "--usage") then
   sio.stderr:write(gitano.i18n.expand("SETUP_USAGE"))
   return 1
end

local conf = clod.parse("")

gitano.log.set_prefix("gitano-setup")
gitano.log.bump_level(gitano.log.level.CHAT)

for i = #possible_answers, 1, -1 do
   gitano.log.debug(gitano.i18n.expand("SETUP_DEBUG_PARSING_ANSWERS", { file=possible_answers[1] }))
   local one_conf = assert(clod.parse(assert(io.open(possible_answers[1], "r")):read "*a",
                                      "@" .. possible_answers[1]))
   gitano.log.debug(gitano.i18n.expand("SETUP_DEBUG_COMBINE_ANSWERS"))
   for k,v in one_conf:each() do
      gitano.log.ddebug(tostring(k) .. " = " .. tostring(v))
      conf.settings[k] = v
   end
end

gitano.log.chat(gitano.i18n.expand("SETUP_WELCOME"))

gitano.log.chat(gitano.i18n.expand("SETUP_DO_CHECKS"))

-- Check that Supple functions properly
local ok, msg = pcall(function()
      gitano.log.chat(gitano.i18n.expand("SETUP_CHECK_SUPPLE"))
      local s = require 'supple'
      local ok, a, b, c = s.host.run("return ...", "test", 12, 'hello', true)
      if not ok then
         gitano.log.chat(gitano.i18n.expand("SETUP_CHECK_SUPPLE_UNABLE"))
         os.exit(1)
      end
      if a ~= 12 or b ~= "hello" or c ~= true then
         gitano.log.chat(gitano.i18n.expand("SETUP_CHECK_SUPPLE_VALUES"))
         os.exit(1)
      end
      local ok, v = s.host.run("local a = ...\nreturn a.banana", "test", {banana=14})
      if not ok then
         gitano.log.chat(gitano.i18n.expand("SETUP_CHECK_SUPPLE_UNABLE"))
         os.exit(1)
      end
      if v ~= 14 then
         gitano.log.chat(gitano.i18n.expand("SETUP_CHECK_SUPPLE_BIDIRECTIONAL"))
         os.exit(1)
      end
end)

if not ok then
   gitano.log.chat(gitano.i18n.expand("SETUP_CHECK_FAILED"))
   gitano.log.chat("... " .. tostring(msg))
   os.exit(1)
end

gitano.log.chat(gitano.i18n.expand("SETUP_CHECK_OK"))

function get(key)
   return conf.settings[key]
end

function ask_for(key, prompt, default)
   local cur_value = conf.settings[key] or default
   gitano.log.ddebug("ask_for(", tostring(key), ", ", tostring(prompt), ", ",
                     tostring(default), ") [", tostring(cur_value), " ]")
   if not conf.settings["setup.batch"] then
      local default_str = (cur_value == nil) and "" or  " [" .. tostring(cur_value) .. "]"
      sio.stdout:write((prompt or key) .. default_str .. ": ")
      local new_value = sio.stdin:read("*l")
      if new_value and new_value ~= "" then
         cur_value = new_value
      end
   end
   gitano.log.info(gitano.i18n.expand("SETUP_SETTING_IS",
                                      { key = key, value = tostring(cur_value)}))
   conf.settings[key] = cur_value
   return cur_value
end

function look_for_path(path)
   local ret, stat = luxio.stat(path)
   if ret ~= 0 then
      return false, path .. ": " .. luxio.strerror(stat)
   end
   if luxio.S_ISDIR(stat.mode) == 0 then
      return false, gitano.i18n.expand("SETUP_NOT_A_DIR", { path=path })
   end
   return true
end

function validate_path(path)
   local ok, msg = look_for_path(path)
   if not ok then
      error(msg, 2)
   end
end

function file_exists(path)
   local fh = io.open(tostring(path), "r")
   if not fh then
      return false
   end
   fh:close()
   return true
end

function validate_name(n)
   if not n:match("^[a-z_][a-z0-9_%-]*$") then
      error(gitano.i18n.expand("SETUP_ERROR_INVALID_NAME", { name=n }), 2)
   end
end

if conf.settings["setup.batch"] then
   gitano.log.info(gitano.i18n.expand("SETUP_BATCH_MODE"))
else
   gitano.log.info(gitano.i18n.expand("SETUP_INTERACTIVE_MODE"))
end

gitano.log.chat(gitano.i18n.expand("SETUP_STEP_1"))

validate_path(ask_for("paths.home", gitano.i18n.expand("SETUP_PATHS_HOME_INFO"),
                      os.getenv "HOME"))

ask_for("paths.ssh", gitano.i18n.expand("SETUP_PATHS_SSH_INFO"),
        get("paths.home") .. "/.ssh")

local pubkey_path
if look_for_path(get("paths.ssh")) then
   -- Try and find a pubkey to use
   for _, ktype in ipairs { "rsa", "ecdsa" } do
      local pk = get("paths.ssh") .. "/id_" .. ktype .. ".pub"
      if file_exists(pk) then
         pubkey_path = pk
         break
      end
   end
end

assert(file_exists(ask_for("paths.bypasskey", gitano.i18n.expand("SETUP_PATHS_BYPASSKEY_INFO"),
                           pubkey_path)),
       gitano.i18n.expand("SETUP_PATHS_BYPASSKEY_NOT_FOUND"))

assert(file_exists(ask_for("paths.pubkey", gitano.i18n.expand("SETUP_PATHS_PUBKEY_INFO"),
                           get("paths.home") .. "/admin.pub")),
       gitano.i18n.expand("SETUP_PATHS_PUBKEY_NOT_FOUND"))

ask_for("paths.repos", gitano.i18n.expand("SETUP_PATHS_REPOS_INFO"),
        get("paths.home") .. "/repos")

validate_name(ask_for("admin.username", gitano.i18n.expand("SETUP_ADMIN_USERNAME_INFO"),
                      "admin"))
ask_for("admin.realname", gitano.i18n.expand("SETUP_ADMIN_REALNAME_INFO"),
        "Administrator")

ask_for("admin.email", gitano.i18n.expand("SETUP_ADMIN_EMAIL_INFO"),
        "admin@administrator.local")

validate_name(ask_for("admin.keyname", gitano.i18n.expand("SETUP_ADMIN_KEYNAME_INFO"),
                      "default"))

ask_for("site.name", gitano.i18n.expand("SETUP_SITE_NAME_INFO"), "a random Gitano instance")
ask_for("log.prefix", gitano.i18n.expand("SETUP_LOG_PREFIX_INFO"), "gitano")
ask_for("use.htpasswd", gitano.i18n.expand("SETUP_USE_HTPASSWD_INFO"), "no")

ask_for("paths.skel", gitano.i18n.expand("SETUP_PATHS_SKEL_INFO"),
        gitano.config.share_path() .. "/skel/gitano-admin")

gitano.log.chat(gitano.i18n.expand("SETUP_STEP_2"))

gitano.log.info(gitano.i18n.expand("SETUP_PREP_SITE_CONFIG"))
local completely_flat = {}
local site_conf = clod.parse("")
site_conf.settings["site_name"] = get "site.name"
site_conf.settings["log.prefix"] = get "log.prefix"
site_conf.settings["use_htpasswd"] = get "use.htpasswd"
completely_flat["site.conf"] = site_conf:serialise()

-- Acquire the contents of the skeleton gitano-admin repository
gitano.log.info(gitano.i18n.expand("SETUP_ACQUIRE_SKEL"))
local skel_path = get "paths.skel"
local skel = assert(sio.opendir(skel_path))
local function acquire(dir, base, path)
   gitano.log.ddebug("Acquire skeleton in:", path)
   for ent in dir:iterate() do
      if not (ent == "." or ent == "..") then
         local entpath = path .. "/" .. ent
         local treeent = base .. ent
         if look_for_path(entpath) then
            local subdir = assert(sio.opendir(entpath))
            acquire(subdir, treeent .. "/", entpath)
            subdir:close()
         else
            local fh = io.open(entpath, "r")
            completely_flat[treeent] = fh:read "*a"
            fh:close()
         end
      end
   end
end
acquire(skel, "", skel_path)
skel:close()

-- Now build the user files
do
   gitano.log.info(gitano.i18n.expand("SETUP_PREP_BYPASS_USER", {user="gitano-bypass"}))
   local userpath = "users/gitano-bypass/user.conf"
   local keypath = "users/gitano-bypass/initial.key"
   local userconf = clod.parse("")
   userconf.settings.real_name = gitano.i18n.expand("SETUP_BYPASS_USER_REAL_NAME")
   userconf.settings.email_address = get("admin.email")
   completely_flat[userpath] = userconf:serialise()
   completely_flat[keypath] = assert(sio.open(get("paths.bypasskey"), "r")):read "*a"
end
do
   gitano.log.info(gitano.i18n.expand("SETUP_PREP_ADMIN_USER", {user=get("admin.username")}))
   local userpath = "users/" .. get("admin.username") .. "/user.conf"
   local keypath = "users/" .. get("admin.username") .. "/" .. get("admin.keyname") .. ".key"
   local userconf = clod.parse("")
   userconf.settings.real_name = get("admin.realname")
   userconf.settings.email_address = get("admin.email")
   completely_flat[userpath] = userconf:serialise()
   completely_flat[keypath] = assert(sio.open(get("paths.pubkey"), "r")):read "*a"
end

-- And now the gitano-admin group
gitano.log.info(gitano.i18n.expand("SETUP_PREP_GITANO_ADMIN"))
local groupconf = clod.parse("")
groupconf.settings.description = "Gitano Instance Administrators"
groupconf.settings["members.*"] = get("admin.username")
completely_flat["groups/gitano-admin.conf"] = groupconf:serialise()

gitano.log.chat(gitano.i18n.expand("SETUP_STEP_3"))

gitano.log.info(gitano.i18n.expand("SETUP_MAKE_PATHS"))
gitano.util.mkdir_p(get("paths.repos") .. "/.graveyard")
gitano.util.mkdir_p(get "paths.ssh")
assert(sio.chmod(get "paths.ssh", "0700"))
gitano.config.repo_path(get "paths.repos")

gitano.log.info(gitano.i18n.expand("SETUP_PREPARE_REPO"))

local raw_repo = assert(gall.repository.create(get("paths.repos") ..
                                                  "/gitano-admin.git"))

gitano.log.info(gitano.i18n.expand("SETUP_PREPARE_FLAT_TREE"))
for k, v in pairs(completely_flat) do
   gitano.log.debug("  => Make object", k)
   completely_flat[k] = gall.object.create(raw_repo, "blob", v)
end

gitano.log.info(gitano.i18n.expand("SETUP_PREPARE_COMMIT"))
local real_tree = assert(gall.tree.create(raw_repo, completely_flat))

local person = {
   realname = get "admin.realname",
   email = get "admin.email",
}
local commit_data = {
   author = person,
   committer = person,
   tree = real_tree,
   message = "Initial setup",
}

local commit_obj = assert(gall.commit.create(raw_repo, commit_data))

gitano.log.info(gitano.i18n.expand("SETUP_PREPARE_MASTER"))

assert(raw_repo:update_ref("refs/heads/master", commit_obj.sha,
                           "Create initial master ref"))

gitano.log.info(gitano.i18n.expand("SETUP_CHECK_ADMIN_REPO"))

local admin_head = raw_repo:get(raw_repo.HEAD)

if not admin_head then
   gitano.log.fatal(gitano.i18n.expand("ERROR_BAD_ADMIN_REPO"));
end

local config = assert(gitano.config.parse(admin_head))
-- Restore the prefix for our logging
gitano.log.set_prefix("gitano-setup")
-- Verify that our user exists

assert(config.users[get "admin.username"], gitano.i18n.expand("SETUP_ERROR_NO_USER"))
assert(config.groups["gitano-admin"].filtered_members[get "admin.username"],
       gitano.i18n.expand("SETUP_ERROR_NOT_ADMIN"))

gitano.log.info(gitano.i18n.expand("SETUP_ADMIN_CONFIG"))
config.repo:conf_set_and_save("project.description", "Instance administration repository")
config.repo:conf_set_and_save("project.owner", get "admin.username")

gitano.log.info(gitano.i18n.expand("SETUP_WRITE_SSHKEYS"))

gitano.config.writessh(config, get("paths.ssh") .. "/authorized_keys")
assert(sio.chmod(get("paths.ssh") .. "/authorized_keys", "0600"))

gitano.log.info(gitano.i18n.expand("SETUP_COMPLETED"))
