package setting

import (
	"gitote/gitote/pkg/bindata"
	"gitote/gitote/pkg/process"
	"gitote/gitote/pkg/user"
	"net/mail"
	"net/url"
	"os"
	"os/exec"
	"path"
	"path/filepath"
	"runtime"
	"strconv"
	"strings"
	"time"

	"github.com/Unknwon/com"
	raven "github.com/getsentry/raven-go"
	_ "github.com/go-macaron/cache/memcache"
	_ "github.com/go-macaron/cache/redis"
	"github.com/go-macaron/session"
	_ "github.com/go-macaron/session/redis"
	"github.com/mcuadros/go-version"
	"gitlab.com/gitote/go-libravatar"
	log "gopkg.in/clog.v1"
	"gopkg.in/ini.v1"
)

type Scheme string

const (
	SCHEME_HTTP        Scheme = "http"
	SCHEME_HTTPS       Scheme = "https"
	SCHEME_FCGI        Scheme = "fcgi"
	SCHEME_UNIX_SOCKET Scheme = "unix"
)

type LandingPage string

const (
	LANDING_PAGE_HOME    LandingPage = "/"
	LANDING_PAGE_EXPLORE LandingPage = "/explore"
)

var (
	// Build information should only be set by -ldflags.
	BuildTime    string
	BuildGitHash string

	// App settings
	AppVer         string
	APIVer         string
	AppURL         string
	AppSubURL      string
	AppSubURLDepth int // Number of slashes
	AppPath        string
	AppDataPath    string

	// Server settings
	Protocol             Scheme
	Domain               string
	HTTPAddr             string
	HTTPPort             string
	LocalURL             string
	OfflineMode          bool
	DisableRouterLog     bool
	CertFile             string
	KeyFile              string
	TLSMinVersion        string
	StaticRootPath       string
	EnableGzip           bool
	LandingPageURL       LandingPage
	UnixSocketPermission uint32

	HTTP struct {
		AccessControlAllowOrigin string
	}

	SSH struct {
		Disabled                     bool           `ini:"DISABLE_SSH"`
		StartBuiltinServer           bool           `ini:"START_SSH_SERVER"`
		Domain                       string         `ini:"SSH_DOMAIN"`
		Port                         int            `ini:"SSH_PORT"`
		ListenHost                   string         `ini:"SSH_LISTEN_HOST"`
		ListenPort                   int            `ini:"SSH_LISTEN_PORT"`
		RootPath                     string         `ini:"SSH_ROOT_PATH"`
		RewriteAuthorizedKeysAtStart bool           `ini:"REWRITE_AUTHORIZED_KEYS_AT_START"`
		ServerCiphers                []string       `ini:"SSH_SERVER_CIPHERS"`
		KeyTestPath                  string         `ini:"SSH_KEY_TEST_PATH"`
		KeygenPath                   string         `ini:"SSH_KEYGEN_PATH"`
		MinimumKeySizeCheck          bool           `ini:"MINIMUM_KEY_SIZE_CHECK"`
		MinimumKeySizes              map[string]int `ini:"-"`
	}

	// Security settings
	InstallLock             bool
	SecretKey               string
	LoginRememberDays       int
	CookieUserName          string
	CookieRememberName      string
	CookieSecure            bool
	ReverseProxyAuthUser    string
	EnableLoginStatusCookie bool
	LoginStatusCookieName   string

	// Database settings
	UseSQLite3    bool
	UseMySQL      bool
	UsePostgreSQL bool
	UseMSSQL      bool

	// Repository settings
	Repository struct {
		AnsiCharset              string
		ForcePrivate             bool
		MaxCreationLimit         int
		MirrorQueueLength        int
		PullRequestQueueLength   int
		PreferredLicenses        []string
		DisableHTTPGit           bool `ini:"DISABLE_HTTP_GIT"`
		EnableLocalPathMigration bool
		CommitsFetchConcurrency  int
		EnableRawFileRenderMode  bool

		// Repository editor settings
		Editor struct {
			LineWrapExtensions   []string
			PreviewableFileModes []string
		} `ini:"-"`

		// Repository upload settings
		Upload struct {
			Enabled      bool
			TempPath     string
			AllowedTypes []string `delim:"|"`
			FileMaxSize  int64
			MaxFiles     int
		} `ini:"-"`
	}
	RepoRootPath string
	ScriptType   string

	// Webhook settings
	Webhook struct {
		Types          []string
		QueueLength    int
		DeliverTimeout int
		SkipTLSVerify  bool `ini:"SKIP_TLS_VERIFY"`
		PagingNum      int
	}

	// Release settings
	Release struct {
		Attachment struct {
			Enabled      bool
			TempPath     string
			AllowedTypes []string `delim:"|"`
			MaxSize      int64
			MaxFiles     int
		} `ini:"-"`
	}

	// Markdown sttings
	Markdown struct {
		EnableHardLineBreak bool
		CustomURLSchemes    []string `ini:"CUSTOM_URL_SCHEMES"`
		FileExtensions      []string
	}

	// Smartypants settings
	Smartypants struct {
		Enabled      bool
		Fractions    bool
		Dashes       bool
		LatexDashes  bool
		AngledQuotes bool
	}

	// Admin settings
	Admin struct {
		DisableRegularOrgCreation bool
	}

	// Picture settings
	AvatarUploadPath           string
	RepositoryAvatarUploadPath string
	GravatarSource             string
	DisableGravatar            bool
	EnableFederatedAvatar      bool
	LibravatarService          *libravatar.Libravatar

	// Log settings
	LogRootPath string
	LogModes    []string
	LogConfigs  []interface{}

	// Attachment settings
	AttachmentPath         string
	AttachmentAllowedTypes string
	AttachmentMaxSize      int64
	AttachmentMaxFiles     int
	AttachmentEnabled      bool

	// Time settings
	TimeFormat string

	// Cache settings
	CacheAdapter  string
	CacheInterval int
	CacheConn     string

	// Session settings
	SessionConfig  session.Options
	CSRFCookieName string

	// Cron tasks
	Cron struct {
		UpdateMirror struct {
			Enabled    bool
			RunAtStart bool
			Schedule   string
		} `ini:"cron.update_mirrors"`
		RepoHealthCheck struct {
			Enabled    bool
			RunAtStart bool
			Schedule   string
			Timeout    time.Duration
			Args       []string `delim:" "`
		} `ini:"cron.repo_health_check"`
		CheckRepoStats struct {
			Enabled    bool
			RunAtStart bool
			Schedule   string
		} `ini:"cron.check_repo_stats"`
		RepoArchiveCleanup struct {
			Enabled    bool
			RunAtStart bool
			Schedule   string
			OlderThan  time.Duration
		} `ini:"cron.repo_archive_cleanup"`
	}

	// Git settings
	Git struct {
		Version                  string `ini:"-"`
		DisableDiffHighlight     bool
		MaxGitDiffLines          int
		MaxGitDiffLineCharacters int
		MaxGitDiffFiles          int
		GCArgs                   []string `ini:"GC_ARGS" delim:" "`
		Timeout                  struct {
			Migrate int
			Mirror  int
			Clone   int
			Pull    int
			GC      int `ini:"GC"`
		} `ini:"git.timeout"`
	}

	// Mirror settings
	Mirror struct {
		DefaultInterval int
	}

	// API settings
	API struct {
		MaxResponseItems int
	}

	// UI settings
	UI struct {
		ExplorePagingNum   int
		IssuePagingNum     int
		FeedMaxCommitNum   int
		MaxDisplayFileSize int64

		Admin struct {
			UserPagingNum   int
			RepoPagingNum   int
			NoticePagingNum int
			OrgPagingNum    int
		} `ini:"ui.admin"`
		User struct {
			RepoPagingNum     int
			NewsFeedPagingNum int
			CommitsPagingNum  int
		} `ini:"ui.user"`
	}

	// Prometheus settings
	Prometheus struct {
		Enabled           bool
		EnableBasicAuth   bool
		BasicAuthUsername string
		BasicAuthPassword string
	}

	// I18n settings
	Langs     []string
	Names     []string
	dateLangs map[string]string

	// Highlight settings are loaded in modules/template/hightlight.go

	// Other settings
	SupportMiniWinService bool

	// Global setting objects
	Cfg          *ini.File
	CustomPath   string // Custom directory path
	CustomConf   string
	ProdMode     bool
	RunUser      string
	IsWindows    bool
	HasRobotsTxt bool
)

// DateLang transforms standard language locale name to corresponding value in datetime plugin.
func DateLang(lang string) string {
	name, ok := dateLangs[lang]
	if ok {
		return name
	}
	return "en"
}

// execPath returns the executable path.
func execPath() (string, error) {
	file, err := exec.LookPath(os.Args[0])
	if err != nil {
		return "", err
	}
	return filepath.Abs(file)
}

func init() {
	IsWindows = runtime.GOOS == "windows"
	log.New(log.CONSOLE, log.ConsoleConfig{})

	var err error
	if AppPath, err = execPath(); err != nil {
		raven.CaptureErrorAndWait(err, nil)
		log.Fatal(2, "Fail to get app path: %v\n", err)
	}

	// Note: we don't use path.Dir here because it does not handle case
	//	which path starts with two "/" in Windows: "//psf/Home/..."
	AppPath = strings.Replace(AppPath, "\\", "/", -1)
}

// WorkDir returns absolute path of work directory.
func WorkDir() (string, error) {
	wd := os.Getenv("GITOTE_WORK_DIR")
	if len(wd) > 0 {
		return wd, nil
	}

	i := strings.LastIndex(AppPath, "/")
	if i == -1 {
		return AppPath, nil
	}
	return AppPath[:i], nil
}

func forcePathSeparator(path string) {
	if strings.Contains(path, "\\") {
		log.Fatal(2, "Do not use '\\' or '\\\\' in paths, instead, please use '/' in all places")
	}
}

// IsRunUserMatchCurrentUser returns false if configured run user does not match
// actual user that runs the app. The first return value is the actual user name.
// This check is ignored under Windows since SSH remote login is not the main
// method to login on Windows.
func IsRunUserMatchCurrentUser(runUser string) (string, bool) {
	if IsWindows {
		return "", true
	}

	currentUser := user.CurrentUsername()
	return currentUser, runUser == currentUser
}

// getOpenSSHVersion parses and returns string representation of OpenSSH version
// returned by command "ssh -V".
func getOpenSSHVersion() string {
	// Note: somehow version is printed to stderr
	_, stderr, err := process.Exec("getOpenSSHVersion", "ssh", "-V")
	if err != nil {
		raven.CaptureErrorAndWait(err, nil)
		log.Fatal(2, "Fail to get OpenSSH version: %v - %s", err, stderr)
	}

	// Trim unused information
	version := strings.TrimRight(strings.Fields(stderr)[0], ",1234567890")
	version = strings.TrimSuffix(strings.TrimPrefix(version, "OpenSSH_"), "p")
	return version
}

// NewContext initializes configuration context.
// NOTE: do not print any log except error.
func NewContext() {
	workDir, err := WorkDir()
	if err != nil {
		raven.CaptureErrorAndWait(err, nil)
		log.Fatal(2, "Fail to get work directory: %v", err)
	}

	Cfg, err = ini.LoadSources(ini.LoadOptions{
		IgnoreInlineComment: true,
	}, bindata.MustAsset("conf/app.ini"))
	if err != nil {
		raven.CaptureErrorAndWait(err, nil)
		log.Fatal(2, "Fail to parse 'conf/app.ini': %v", err)
	}

	CustomPath = os.Getenv("GITOTE_CUSTOM")
	if len(CustomPath) == 0 {
		CustomPath = workDir + "/custom"
	}

	if len(CustomConf) == 0 {
		CustomConf = CustomPath + "/conf/app.ini"
	}

	if com.IsFile(CustomConf) {
		if err = Cfg.Append(CustomConf); err != nil {
			raven.CaptureErrorAndWait(err, nil)
			log.Fatal(2, "Fail to load custom conf '%s': %v", CustomConf, err)
		}
	} else {
		log.Warn("Custom config '%s' not found, ignore this if you're running first time", CustomConf)
	}
	Cfg.NameMapper = ini.AllCapsUnderscore

	homeDir, err := com.HomeDir()
	if err != nil {
		raven.CaptureErrorAndWait(err, nil)
		log.Fatal(2, "Fail to get home directory: %v", err)
	}
	homeDir = strings.Replace(homeDir, "\\", "/", -1)

	LogRootPath = Cfg.Section("log").Key("ROOT_PATH").MustString(path.Join(workDir, "log"))
	forcePathSeparator(LogRootPath)

	sec := Cfg.Section("server")
	AppURL = sec.Key("ROOT_URL").MustString("http://localhost:3000/")
	if AppURL[len(AppURL)-1] != '/' {
		AppURL += "/"
	}

	// Check if has app suburl.
	url, err := url.Parse(AppURL)
	if err != nil {
		raven.CaptureErrorAndWait(err, nil)
		log.Fatal(2, "Invalid ROOT_URL '%s': %s", AppURL, err)
	}
	// Suburl should start with '/' and end without '/', such as '/{subpath}'.
	// This value is empty if site does not have sub-url.
	AppSubURL = strings.TrimSuffix(url.Path, "/")
	AppSubURLDepth = strings.Count(AppSubURL, "/")

	Protocol = SCHEME_HTTP
	if sec.Key("PROTOCOL").String() == "https" {
		Protocol = SCHEME_HTTPS
		CertFile = sec.Key("CERT_FILE").String()
		KeyFile = sec.Key("KEY_FILE").String()
		TLSMinVersion = sec.Key("TLS_MIN_VERSION").String()
	} else if sec.Key("PROTOCOL").String() == "fcgi" {
		Protocol = SCHEME_FCGI
	} else if sec.Key("PROTOCOL").String() == "unix" {
		Protocol = SCHEME_UNIX_SOCKET
		UnixSocketPermissionRaw := sec.Key("UNIX_SOCKET_PERMISSION").MustString("666")
		UnixSocketPermissionParsed, err := strconv.ParseUint(UnixSocketPermissionRaw, 8, 32)
		if err != nil || UnixSocketPermissionParsed > 0777 {
			raven.CaptureErrorAndWait(err, nil)
			log.Fatal(2, "Fail to parse unixSocketPermission: %s", UnixSocketPermissionRaw)
		}
		UnixSocketPermission = uint32(UnixSocketPermissionParsed)
	}
	Domain = sec.Key("DOMAIN").MustString("localhost")
	HTTPAddr = sec.Key("HTTP_ADDR").MustString("0.0.0.0")
	HTTPPort = sec.Key("HTTP_PORT").MustString("3000")
	LocalURL = sec.Key("LOCAL_ROOT_URL").MustString(string(Protocol) + "://localhost:" + HTTPPort + "/")
	OfflineMode = sec.Key("OFFLINE_MODE").MustBool()
	DisableRouterLog = sec.Key("DISABLE_ROUTER_LOG").MustBool()
	StaticRootPath = sec.Key("STATIC_ROOT_PATH").MustString(workDir)
	AppDataPath = sec.Key("APP_DATA_PATH").MustString("data")
	EnableGzip = sec.Key("ENABLE_GZIP").MustBool()

	switch sec.Key("LANDING_PAGE").MustString("home") {
	case "explore":
		LandingPageURL = LANDING_PAGE_EXPLORE
	default:
		LandingPageURL = LANDING_PAGE_HOME
	}

	SSH.RootPath = path.Join(homeDir, ".ssh")
	SSH.RewriteAuthorizedKeysAtStart = sec.Key("REWRITE_AUTHORIZED_KEYS_AT_START").MustBool()
	SSH.ServerCiphers = sec.Key("SSH_SERVER_CIPHERS").Strings(",")
	SSH.KeyTestPath = os.TempDir()
	if err = Cfg.Section("server").MapTo(&SSH); err != nil {
		raven.CaptureErrorAndWait(err, nil)
		log.Fatal(2, "Fail to map SSH settings: %v", err)
	}
	if SSH.Disabled {
		SSH.StartBuiltinServer = false
		SSH.MinimumKeySizeCheck = false
	}

	if !SSH.Disabled && !SSH.StartBuiltinServer {
		if err := os.MkdirAll(SSH.RootPath, 0700); err != nil {
			raven.CaptureErrorAndWait(err, nil)
			log.Fatal(2, "Fail to create '%s': %v", SSH.RootPath, err)
		} else if err = os.MkdirAll(SSH.KeyTestPath, 0644); err != nil {
			raven.CaptureErrorAndWait(err, nil)
			log.Fatal(2, "Fail to create '%s': %v", SSH.KeyTestPath, err)
		}
	}

	if SSH.StartBuiltinServer {
		SSH.RewriteAuthorizedKeysAtStart = false
	}

	// Check if server is eligible for minimum key size check when user choose to enable.
	// Windows server and OpenSSH version lower than 5.1
	// are forced to be disabled because the "ssh-keygen" in Windows does not print key type.
	if SSH.MinimumKeySizeCheck &&
		(IsWindows || version.Compare(getOpenSSHVersion(), "5.1", "<")) {
		SSH.MinimumKeySizeCheck = false
		log.Warn(`SSH minimum key size check is forced to be disabled because server is not eligible:
1. Windows server
2. OpenSSH version is lower than 5.1`)
	}

	if SSH.MinimumKeySizeCheck {
		SSH.MinimumKeySizes = map[string]int{}
		for _, key := range Cfg.Section("ssh.minimum_key_sizes").Keys() {
			if key.MustInt() != -1 {
				SSH.MinimumKeySizes[strings.ToLower(key.Name())] = key.MustInt()
			}
		}
	}

	sec = Cfg.Section("security")
	InstallLock = sec.Key("INSTALL_LOCK").MustBool()
	SecretKey = sec.Key("SECRET_KEY").String()
	LoginRememberDays = sec.Key("LOGIN_REMEMBER_DAYS").MustInt()
	CookieUserName = sec.Key("COOKIE_USERNAME").String()
	CookieRememberName = sec.Key("COOKIE_REMEMBER_NAME").String()
	CookieSecure = sec.Key("COOKIE_SECURE").MustBool(false)
	ReverseProxyAuthUser = sec.Key("REVERSE_PROXY_AUTHENTICATION_USER").MustString("X-WEBAUTH-USER")
	EnableLoginStatusCookie = sec.Key("ENABLE_LOGIN_STATUS_COOKIE").MustBool(false)
	LoginStatusCookieName = sec.Key("LOGIN_STATUS_COOKIE_NAME").MustString("login_status")

	sec = Cfg.Section("attachment")
	AttachmentPath = sec.Key("PATH").MustString(path.Join(AppDataPath, "attachments"))
	if !filepath.IsAbs(AttachmentPath) {
		AttachmentPath = path.Join(workDir, AttachmentPath)
	}
	AttachmentAllowedTypes = strings.Replace(sec.Key("ALLOWED_TYPES").MustString("image/jpeg,image/png"), "|", ",", -1)
	AttachmentMaxSize = sec.Key("MAX_SIZE").MustInt64(4)
	AttachmentMaxFiles = sec.Key("MAX_FILES").MustInt(5)
	AttachmentEnabled = sec.Key("ENABLED").MustBool(true)

	TimeFormat = map[string]string{
		"ANSIC":       time.ANSIC,
		"UnixDate":    time.UnixDate,
		"RubyDate":    time.RubyDate,
		"RFC822":      time.RFC822,
		"RFC822Z":     time.RFC822Z,
		"RFC850":      time.RFC850,
		"RFC1123":     time.RFC1123,
		"RFC1123Z":    time.RFC1123Z,
		"RFC3339":     time.RFC3339,
		"RFC3339Nano": time.RFC3339Nano,
		"Kitchen":     time.Kitchen,
		"Stamp":       time.Stamp,
		"StampMilli":  time.StampMilli,
		"StampMicro":  time.StampMicro,
		"StampNano":   time.StampNano,
	}[Cfg.Section("time").Key("FORMAT").MustString("RFC1123")]

	RunUser = Cfg.Section("").Key("RUN_USER").String()
	// Does not check run user when the install lock is off.
	if InstallLock {
		currentUser, match := IsRunUserMatchCurrentUser(RunUser)
		if !match {
			raven.CaptureErrorAndWait(err, nil)
			log.Fatal(2, "Expect user '%s' but current user is: %s", RunUser, currentUser)
		}
	}

	ProdMode = Cfg.Section("").Key("RUN_MODE").String() == "prod"

	// Determine and create root git repository path.
	sec = Cfg.Section("repository")
	RepoRootPath = sec.Key("ROOT").MustString(path.Join(homeDir, "gitote-repositories"))
	forcePathSeparator(RepoRootPath)
	if !filepath.IsAbs(RepoRootPath) {
		RepoRootPath = path.Join(workDir, RepoRootPath)
	} else {
		RepoRootPath = path.Clean(RepoRootPath)
	}
	ScriptType = sec.Key("SCRIPT_TYPE").MustString("bash")
	if err = Cfg.Section("repository").MapTo(&Repository); err != nil {
		raven.CaptureErrorAndWait(err, nil)
		log.Fatal(2, "Fail to map Repository settings: %v", err)
	} else if err = Cfg.Section("repository.editor").MapTo(&Repository.Editor); err != nil {
		raven.CaptureErrorAndWait(err, nil)
		log.Fatal(2, "Fail to map Repository.Editor settings: %v", err)
	} else if err = Cfg.Section("repository.upload").MapTo(&Repository.Upload); err != nil {
		raven.CaptureErrorAndWait(err, nil)
		log.Fatal(2, "Fail to map Repository.Upload settings: %v", err)
	}

	if !filepath.IsAbs(Repository.Upload.TempPath) {
		Repository.Upload.TempPath = path.Join(workDir, Repository.Upload.TempPath)
	}

	sec = Cfg.Section("picture")
	AvatarUploadPath = sec.Key("AVATAR_UPLOAD_PATH").MustString(path.Join(AppDataPath, "avatars"))
	forcePathSeparator(AvatarUploadPath)
	if !filepath.IsAbs(AvatarUploadPath) {
		AvatarUploadPath = path.Join(workDir, AvatarUploadPath)
	}
	RepositoryAvatarUploadPath = sec.Key("REPOSITORY_AVATAR_UPLOAD_PATH").MustString(path.Join(AppDataPath, "repo-avatars"))
	forcePathSeparator(RepositoryAvatarUploadPath)
	if !filepath.IsAbs(RepositoryAvatarUploadPath) {
		RepositoryAvatarUploadPath = path.Join(workDir, RepositoryAvatarUploadPath)
	}
	switch source := sec.Key("GRAVATAR_SOURCE").MustString("gravatar"); source {
	case "duoshuo":
		GravatarSource = "http://gravatar.duoshuo.com/avatar/"
	case "gravatar":
		GravatarSource = "https://secure.gravatar.com/avatar/"
	case "libravatar":
		GravatarSource = "https://seccdn.libravatar.org/avatar/"
	default:
		GravatarSource = source
	}
	DisableGravatar = sec.Key("DISABLE_GRAVATAR").MustBool()
	EnableFederatedAvatar = sec.Key("ENABLE_FEDERATED_AVATAR").MustBool(true)
	if OfflineMode {
		DisableGravatar = true
		EnableFederatedAvatar = false
	}
	if DisableGravatar {
		EnableFederatedAvatar = false
	}

	if EnableFederatedAvatar {
		LibravatarService = libravatar.New()
		parts := strings.Split(GravatarSource, "/")
		if len(parts) >= 3 {
			if parts[0] == "https:" {
				LibravatarService.SetUseHTTPS(true)
				LibravatarService.SetSecureFallbackHost(parts[2])
			} else {
				LibravatarService.SetUseHTTPS(false)
				LibravatarService.SetFallbackHost(parts[2])
			}
		}
	}

	if err = Cfg.Section("http").MapTo(&HTTP); err != nil {
		raven.CaptureErrorAndWait(err, nil)
		log.Fatal(2, "Failed to map HTTP settings: %v", err)
	} else if err = Cfg.Section("webhook").MapTo(&Webhook); err != nil {
		raven.CaptureErrorAndWait(err, nil)
		log.Fatal(2, "Failed to map Webhook settings: %v", err)
	} else if err = Cfg.Section("release.attachment").MapTo(&Release.Attachment); err != nil {
		raven.CaptureErrorAndWait(err, nil)
		log.Fatal(2, "Failed to map Release.Attachment settings: %v", err)
	} else if err = Cfg.Section("markdown").MapTo(&Markdown); err != nil {
		raven.CaptureErrorAndWait(err, nil)
		log.Fatal(2, "Failed to map Markdown settings: %v", err)
	} else if err = Cfg.Section("smartypants").MapTo(&Smartypants); err != nil {
		raven.CaptureErrorAndWait(err, nil)
		log.Fatal(2, "Failed to map Smartypants settings: %v", err)
	} else if err = Cfg.Section("admin").MapTo(&Admin); err != nil {
		raven.CaptureErrorAndWait(err, nil)
		log.Fatal(2, "Failed to map Admin settings: %v", err)
	} else if err = Cfg.Section("cron").MapTo(&Cron); err != nil {
		raven.CaptureErrorAndWait(err, nil)
		log.Fatal(2, "Failed to map Cron settings: %v", err)
	} else if err = Cfg.Section("git").MapTo(&Git); err != nil {
		raven.CaptureErrorAndWait(err, nil)
		log.Fatal(2, "Failed to map Git settings: %v", err)
	} else if err = Cfg.Section("mirror").MapTo(&Mirror); err != nil {
		raven.CaptureErrorAndWait(err, nil)
		log.Fatal(2, "Failed to map Mirror settings: %v", err)
	} else if err = Cfg.Section("api").MapTo(&API); err != nil {
		raven.CaptureErrorAndWait(err, nil)
		log.Fatal(2, "Failed to map API settings: %v", err)
	} else if err = Cfg.Section("ui").MapTo(&UI); err != nil {
		raven.CaptureErrorAndWait(err, nil)
		log.Fatal(2, "Failed to map UI settings: %v", err)
	} else if err = Cfg.Section("prometheus").MapTo(&Prometheus); err != nil {
		raven.CaptureErrorAndWait(err, nil)
		log.Fatal(2, "Failed to map Prometheus settings: %v", err)
	}

	if Mirror.DefaultInterval <= 0 {
		Mirror.DefaultInterval = 24
	}

	Langs = Cfg.Section("i18n").Key("LANGS").Strings(",")
	Names = Cfg.Section("i18n").Key("NAMES").Strings(",")
	dateLangs = Cfg.Section("i18n.datelang").KeysHash()

	HasRobotsTxt = com.IsFile(path.Join(CustomPath, "robots.txt"))
}

var Service struct {
	ActiveCodeLives                int
	ResetPwdCodeLives              int
	RegisterEmailConfirm           bool
	DisableRegistration            bool
	ShowRegistrationButton         bool
	RequireSignInView              bool
	EnableNotifyMail               bool
	EnableReverseProxyAuth         bool
	EnableReverseProxyAutoRegister bool
	EnableCaptcha                  bool
}

func newService() {
	sec := Cfg.Section("service")
	Service.ActiveCodeLives = sec.Key("ACTIVE_CODE_LIVE_MINUTES").MustInt(180)
	Service.ResetPwdCodeLives = sec.Key("RESET_PASSWD_CODE_LIVE_MINUTES").MustInt(180)
	Service.DisableRegistration = sec.Key("DISABLE_REGISTRATION").MustBool()
	Service.ShowRegistrationButton = sec.Key("SHOW_REGISTRATION_BUTTON").MustBool(!Service.DisableRegistration)
	Service.RequireSignInView = sec.Key("REQUIRE_SIGNIN_VIEW").MustBool()
	Service.EnableReverseProxyAuth = sec.Key("ENABLE_REVERSE_PROXY_AUTHENTICATION").MustBool()
	Service.EnableReverseProxyAutoRegister = sec.Key("ENABLE_REVERSE_PROXY_AUTO_REGISTRATION").MustBool()
	Service.EnableCaptcha = sec.Key("ENABLE_CAPTCHA").MustBool()
}

func newLogService() {
	if len(BuildTime) > 0 {
		log.Trace("Build Time: %s", BuildTime)
		log.Trace("Build Git Hash: %s", BuildGitHash)
	}

	// Because we always create a console logger as primary logger before all settings are loaded,
	// thus if user doesn't set console logger, we should remove it after other loggers are created.
	hasConsole := false

	// Get and check log modes.
	LogModes = strings.Split(Cfg.Section("log").Key("MODE").MustString("console"), ",")
	LogConfigs = make([]interface{}, len(LogModes))
	levelNames := map[string]log.LEVEL{
		"trace": log.TRACE,
		"info":  log.INFO,
		"warn":  log.WARN,
		"error": log.ERROR,
		"fatal": log.FATAL,
	}
	for i, mode := range LogModes {
		mode = strings.ToLower(strings.TrimSpace(mode))
		sec, err := Cfg.GetSection("log." + mode)
		if err != nil {
			raven.CaptureErrorAndWait(err, nil)
			log.Fatal(2, "Unknown logger mode: %s", mode)
		}

		validLevels := []string{"trace", "info", "warn", "error", "fatal"}
		name := Cfg.Section("log." + mode).Key("LEVEL").Validate(func(v string) string {
			v = strings.ToLower(v)
			if com.IsSliceContainsStr(validLevels, v) {
				return v
			}
			return "trace"
		})
		level := levelNames[name]

		// Generate log configuration.
		switch log.MODE(mode) {
		case log.CONSOLE:
			hasConsole = true
			LogConfigs[i] = log.ConsoleConfig{
				Level:      level,
				BufferSize: Cfg.Section("log").Key("BUFFER_LEN").MustInt64(100),
			}

		case log.FILE:
			logPath := path.Join(LogRootPath, "gitote.log")
			if err = os.MkdirAll(path.Dir(logPath), os.ModePerm); err != nil {
				raven.CaptureErrorAndWait(err, nil)
				log.Fatal(2, "Fail to create log directory '%s': %v", path.Dir(logPath), err)
			}

			LogConfigs[i] = log.FileConfig{
				Level:      level,
				BufferSize: Cfg.Section("log").Key("BUFFER_LEN").MustInt64(100),
				Filename:   logPath,
				FileRotationConfig: log.FileRotationConfig{
					Rotate:   sec.Key("LOG_ROTATE").MustBool(true),
					Daily:    sec.Key("DAILY_ROTATE").MustBool(true),
					MaxSize:  1 << uint(sec.Key("MAX_SIZE_SHIFT").MustInt(28)),
					MaxLines: sec.Key("MAX_LINES").MustInt64(1000000),
					MaxDays:  sec.Key("MAX_DAYS").MustInt64(7),
				},
			}

		case log.SLACK:
			LogConfigs[i] = log.SlackConfig{
				Level:      level,
				BufferSize: Cfg.Section("log").Key("BUFFER_LEN").MustInt64(100),
				URL:        sec.Key("URL").String(),
			}

		case log.DISCORD:
			LogConfigs[i] = log.DiscordConfig{
				Level:      level,
				BufferSize: Cfg.Section("log").Key("BUFFER_LEN").MustInt64(100),
				URL:        sec.Key("URL").String(),
				Username:   sec.Key("USERNAME").String(),
			}
		}

		log.New(log.MODE(mode), LogConfigs[i])
		log.Trace("Log Mode: %s (%s)", strings.Title(mode), strings.Title(name))
	}

	// Make sure everyone gets version info printed.
	log.Info("%s %s", "Gitote", AppVer)
	if !hasConsole {
		log.Delete(log.CONSOLE)
	}
}

func newCacheService() {
	CacheAdapter = Cfg.Section("cache").Key("ADAPTER").In("memory", []string{"memory", "redis", "memcache"})
	switch CacheAdapter {
	case "memory":
		CacheInterval = Cfg.Section("cache").Key("INTERVAL").MustInt(60)
	case "redis", "memcache":
		CacheConn = strings.Trim(Cfg.Section("cache").Key("HOST").String(), "\" ")
	default:
		log.Fatal(2, "Unknown cache adapter: %s", CacheAdapter)
	}

	log.Info("Cache Service Enabled")
}

func newSessionService() {
	SessionConfig.Provider = Cfg.Section("session").Key("PROVIDER").In("memory",
		[]string{"memory", "file", "redis", "mysql"})
	SessionConfig.ProviderConfig = strings.Trim(Cfg.Section("session").Key("PROVIDER_CONFIG").String(), "\" ")
	SessionConfig.CookieName = Cfg.Section("session").Key("COOKIE_NAME").MustString("gitote_sess")
	SessionConfig.CookiePath = AppSubURL
	SessionConfig.Secure = Cfg.Section("session").Key("COOKIE_SECURE").MustBool()
	SessionConfig.Gclifetime = Cfg.Section("session").Key("GC_INTERVAL_TIME").MustInt64(3600)
	SessionConfig.Maxlifetime = Cfg.Section("session").Key("SESSION_LIFE_TIME").MustInt64(86400)
	CSRFCookieName = Cfg.Section("session").Key("CSRF_COOKIE_NAME").MustString("_csrf")

	log.Info("Session Service Enabled")
}

// Mailer represents mail service.
type Mailer struct {
	QueueLength       int
	SubjectPrefix     string
	Host              string
	From              string
	FromEmail         string
	User, Passwd      string
	DisableHelo       bool
	HeloHostname      string
	SkipVerify        bool
	UseCertificate    bool
	CertFile, KeyFile string
	UsePlainText      bool
}

var (
	MailService *Mailer
)

// newMailService initializes mail service options from configuration.
// No non-error log will be printed in hook mode.
func newMailService() {
	sec := Cfg.Section("mailer")
	if !sec.Key("ENABLED").MustBool() {
		return
	}

	MailService = &Mailer{
		QueueLength:    sec.Key("SEND_BUFFER_LEN").MustInt(100),
		SubjectPrefix:  sec.Key("SUBJECT_PREFIX").MustString("[" + "Gitote" + "] "),
		Host:           sec.Key("HOST").String(),
		User:           sec.Key("USER").String(),
		Passwd:         sec.Key("PASSWD").String(),
		DisableHelo:    sec.Key("DISABLE_HELO").MustBool(),
		HeloHostname:   sec.Key("HELO_HOSTNAME").String(),
		SkipVerify:     sec.Key("SKIP_VERIFY").MustBool(),
		UseCertificate: sec.Key("USE_CERTIFICATE").MustBool(),
		CertFile:       sec.Key("CERT_FILE").String(),
		KeyFile:        sec.Key("KEY_FILE").String(),
		UsePlainText:   sec.Key("USE_PLAIN_TEXT").MustBool(),
	}
	MailService.From = sec.Key("FROM").MustString(MailService.User)

	if len(MailService.From) > 0 {
		parsed, err := mail.ParseAddress(MailService.From)
		if err != nil {
			raven.CaptureErrorAndWait(err, nil)
			log.Fatal(2, "Invalid mailer.FROM (%s): %v", MailService.From, err)
		}
		MailService.FromEmail = parsed.Address
	}

	if HookMode {
		return
	}
	log.Info("Mail Service Enabled")
}

func newRegisterMailService() {
	if !Cfg.Section("service").Key("REGISTER_EMAIL_CONFIRM").MustBool() {
		return
	} else if MailService == nil {
		log.Warn("Register Mail Service: Mail Service is not enabled")
		return
	}
	Service.RegisterEmailConfirm = true
	log.Info("Register Mail Service Enabled")
}

// newNotifyMailService initializes notification email service options from configuration.
// No non-error log will be printed in hook mode.
func newNotifyMailService() {
	if !Cfg.Section("service").Key("ENABLE_NOTIFY_MAIL").MustBool() {
		return
	} else if MailService == nil {
		log.Warn("Notify Mail Service: Mail Service is not enabled")
		return
	}
	Service.EnableNotifyMail = true

	if HookMode {
		return
	}
	log.Info("Notify Mail Service Enabled")
}

func NewService() {
	newService()
}

func NewServices() {
	newService()
	newLogService()
	newCacheService()
	newSessionService()
	newMailService()
	newRegisterMailService()
	newNotifyMailService()
}

// HookMode indicates whether program starts as Git server-side hook callback.
var HookMode bool

// NewPostReceiveHookServices initializes all services that are needed by
// Git server-side post-receive hook callback.
func NewPostReceiveHookServices() {
	HookMode = true
	newService()
	newMailService()
	newNotifyMailService()
}
