/*
 * MinIO Cloud Storage, (C) 2017, 2018 MinIO, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package cmd

import (
	"crypto/tls"
	"errors"
	"net"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"time"

	etcd "github.com/coreos/etcd/clientv3"
	dns2 "github.com/miekg/dns"
	"github.com/minio/cli"
	"github.com/minio/minio-go/v6/pkg/set"
	"github.com/minio/minio/cmd/logger"
	"github.com/minio/minio/cmd/logger/target/console"
	"github.com/minio/minio/cmd/logger/target/http"
	"github.com/minio/minio/pkg/auth"
	"github.com/minio/minio/pkg/dns"
	xnet "github.com/minio/minio/pkg/net"
)

// Check for updates and print a notification message
func checkUpdate(mode string) {
	// Its OK to ignore any errors during doUpdate() here.
	if updateMsg, _, currentReleaseTime, latestReleaseTime, err := getUpdateInfo(2*time.Second, mode); err == nil {
		if globalInplaceUpdateDisabled {
			logger.StartupMessage(updateMsg)
		} else {
			logger.StartupMessage(prepareUpdateMessage("Run `minio update`", latestReleaseTime.Sub(currentReleaseTime)))
		}
	}
}

// Load logger targets based on user's configuration
func loadLoggers() {
	auditEndpoint, ok := os.LookupEnv("MINIO_AUDIT_LOGGER_HTTP_ENDPOINT")
	if ok {
		// Enable audit HTTP logging through ENV.
		logger.AddAuditTarget(http.New(auditEndpoint, NewCustomHTTPTransport()))
	}

	loggerEndpoint, ok := os.LookupEnv("MINIO_LOGGER_HTTP_ENDPOINT")
	if ok {
		// Enable HTTP logging through ENV.
		logger.AddTarget(http.New(loggerEndpoint, NewCustomHTTPTransport()))
	} else {
		for _, l := range globalServerConfig.Logger.HTTP {
			if l.Enabled {
				// Enable http logging
				logger.AddTarget(http.New(l.Endpoint, NewCustomHTTPTransport()))
			}
		}
	}

	if globalServerConfig.Logger.Console.Enabled {
		// Enable console logging
		logger.AddTarget(console.New())
	}

}

func newConfigDirFromCtx(ctx *cli.Context, option string, getDefaultDir func() string) (*ConfigDir, bool) {
	var dir string
	var dirSet bool

	switch {
	case ctx.IsSet(option):
		dir = ctx.String(option)
		dirSet = true
	case ctx.GlobalIsSet(option):
		dir = ctx.GlobalString(option)
		dirSet = true
		// cli package does not expose parent's option option.  Below code is workaround.
		if dir == "" || dir == getDefaultDir() {
			dirSet = false // Unset to false since GlobalIsSet() true is a false positive.
			if ctx.Parent().GlobalIsSet(option) {
				dir = ctx.Parent().GlobalString(option)
				dirSet = true
			}
		}
	default:
		// Neither local nor global option is provided.  In this case, try to use
		// default directory.
		dir = getDefaultDir()
		if dir == "" {
			logger.FatalIf(errInvalidArgument, "%s option must be provided", option)
		}
	}

	if dir == "" {
		logger.FatalIf(errors.New("empty directory"), "%s directory cannot be empty", option)
	}

	// Disallow relative paths, figure out absolute paths.
	dirAbs, err := filepath.Abs(dir)
	logger.FatalIf(err, "Unable to fetch absolute path for %s=%s", option, dir)

	logger.FatalIf(mkdirAllIgnorePerm(dirAbs), "Unable to create directory specified %s=%s", option, dir)

	return &ConfigDir{path: dirAbs}, dirSet
}

func handleCommonCmdArgs(ctx *cli.Context) {

	// Get "json" flag from command line argument and
	// enable json and quite modes if json flag is turned on.
	globalCLIContext.JSON = ctx.IsSet("json") || ctx.GlobalIsSet("json")
	if globalCLIContext.JSON {
		logger.EnableJSON()
	}

	// Get quiet flag from command line argument.
	globalCLIContext.Quiet = ctx.IsSet("quiet") || ctx.GlobalIsSet("quiet")
	if globalCLIContext.Quiet {
		logger.EnableQuiet()
	}

	// Get anonymous flag from command line argument.
	globalCLIContext.Anonymous = ctx.IsSet("anonymous") || ctx.GlobalIsSet("anonymous")
	if globalCLIContext.Anonymous {
		logger.EnableAnonymous()
	}

	// Fetch address option
	globalCLIContext.Addr = ctx.GlobalString("address")
	if globalCLIContext.Addr == "" || globalCLIContext.Addr == ":"+globalMinioDefaultPort {
		globalCLIContext.Addr = ctx.String("address")
	}

	// Set all config, certs and CAs directories.
	var configSet, certsSet bool
	globalConfigDir, configSet = newConfigDirFromCtx(ctx, "config-dir", defaultConfigDir.Get)
	globalCertsDir, certsSet = newConfigDirFromCtx(ctx, "certs-dir", defaultCertsDir.Get)

	// Remove this code when we deprecate and remove config-dir.
	// This code is to make sure we inherit from the config-dir
	// option if certs-dir is not provided.
	if !certsSet && configSet {
		globalCertsDir = &ConfigDir{path: filepath.Join(globalConfigDir.Get(), certsDir)}
	}

	globalCertsCADir = &ConfigDir{path: filepath.Join(globalCertsDir.Get(), certsCADir)}

	logger.FatalIf(mkdirAllIgnorePerm(globalCertsCADir.Get()), "Unable to create certs CA directory at %s", globalCertsCADir.Get())

	// Check "compat" flag from command line argument.
	globalCLIContext.StrictS3Compat = ctx.IsSet("compat") || ctx.GlobalIsSet("compat")
}

// Parses the given compression exclude list `extensions` or `content-types`.
func parseCompressIncludes(includes []string) ([]string, error) {
	for _, e := range includes {
		if len(e) == 0 {
			return nil, uiErrInvalidCompressionIncludesValue(nil).Msg("extension/mime-type (%s) cannot be empty", e)
		}
	}
	return includes, nil
}

func handleCommonEnvVars() {
	compressEnvDelimiter := ","
	// Start profiler if env is set.
	if profiler := os.Getenv("_MINIO_PROFILER"); profiler != "" {
		var err error
		globalProfiler, err = startProfiler(profiler, "")
		logger.FatalIf(err, "Unable to setup a profiler")
	}

	accessKey := os.Getenv("MINIO_ACCESS_KEY")
	secretKey := os.Getenv("MINIO_SECRET_KEY")
	if accessKey != "" && secretKey != "" {
		cred, err := auth.CreateCredentials(accessKey, secretKey)
		if err != nil {
			logger.Fatal(uiErrInvalidCredentials(err), "Unable to validate credentials inherited from the shell environment")
		}
		cred.Expiration = timeSentinel

		// credential Envs are set globally.
		globalIsEnvCreds = true
		globalActiveCred = cred
	}

	if browser := os.Getenv("MINIO_BROWSER"); browser != "" {
		browserFlag, err := ParseBoolFlag(browser)
		if err != nil {
			logger.Fatal(uiErrInvalidBrowserValue(nil).Msg("Unknown value `%s`", browser), "Invalid MINIO_BROWSER value in environment variable")
		}

		// browser Envs are set globally, this does not represent
		// if browser is turned off or on.
		globalIsEnvBrowser = true
		globalIsBrowserEnabled = bool(browserFlag)
	}

	etcdEndpointsEnv, ok := os.LookupEnv("MINIO_ETCD_ENDPOINTS")
	if ok {
		etcdEndpoints := strings.Split(etcdEndpointsEnv, ",")

		var etcdSecure bool
		for _, endpoint := range etcdEndpoints {
			u, err := xnet.ParseURL(endpoint)
			if err != nil {
				logger.FatalIf(err, "Unable to initialize etcd with %s", etcdEndpoints)
			}
			// If one of the endpoint is https, we will use https directly.
			etcdSecure = etcdSecure || u.Scheme == "https"
		}

		var err error
		if etcdSecure {
			// This is only to support client side certificate authentication
			// https://coreos.com/etcd/docs/latest/op-guide/security.html
			etcdClientCertFile, ok1 := os.LookupEnv("MINIO_ETCD_CLIENT_CERT")
			etcdClientCertKey, ok2 := os.LookupEnv("MINIO_ETCD_CLIENT_CERT_KEY")
			var getClientCertificate func(*tls.CertificateRequestInfo) (*tls.Certificate, error)
			if ok1 && ok2 {
				getClientCertificate = func(unused *tls.CertificateRequestInfo) (*tls.Certificate, error) {
					cert, terr := tls.LoadX509KeyPair(etcdClientCertFile, etcdClientCertKey)
					return &cert, terr
				}
			}

			globalEtcdClient, err = etcd.New(etcd.Config{
				Endpoints:         etcdEndpoints,
				DialTimeout:       defaultDialTimeout,
				DialKeepAliveTime: defaultDialKeepAlive,
				TLS: &tls.Config{
					RootCAs:              globalRootCAs,
					GetClientCertificate: getClientCertificate,
				},
			})
		} else {
			globalEtcdClient, err = etcd.New(etcd.Config{
				Endpoints:         etcdEndpoints,
				DialTimeout:       defaultDialTimeout,
				DialKeepAliveTime: defaultDialKeepAlive,
			})
		}
		logger.FatalIf(err, "Unable to initialize etcd with %s", etcdEndpoints)
	}

	v, ok := os.LookupEnv("MINIO_DOMAIN")
	if ok {
		for _, domainName := range strings.Split(v, ",") {
			if _, ok = dns2.IsDomainName(domainName); !ok {
				logger.Fatal(uiErrInvalidDomainValue(nil).Msg("Unknown value `%s`", domainName),
					"Invalid MINIO_DOMAIN value in environment variable")
			}
			globalDomainNames = append(globalDomainNames, domainName)
		}
	}

	minioEndpointsEnv, ok := os.LookupEnv("MINIO_PUBLIC_IPS")
	if ok {
		minioEndpoints := strings.Split(minioEndpointsEnv, ",")
		var domainIPs = set.NewStringSet()
		for _, endpoint := range minioEndpoints {
			if net.ParseIP(endpoint) == nil {
				// Checking if the IP is a DNS entry.
				addrs, err := net.LookupHost(endpoint)
				if err != nil {
					logger.FatalIf(err, "Unable to initialize MinIO server with [%s] invalid entry found in MINIO_PUBLIC_IPS", endpoint)
				}
				for _, addr := range addrs {
					domainIPs.Add(addr)
				}
				continue
			}
			domainIPs.Add(endpoint)
		}
		updateDomainIPs(domainIPs)
	} else {
		// Add found interfaces IP address to global domain IPS,
		// loopback addresses will be naturally dropped.
		updateDomainIPs(localIP4)
	}

	if len(globalDomainNames) != 0 && !globalDomainIPs.IsEmpty() && globalEtcdClient != nil {
		var err error
		globalDNSConfig, err = dns.NewCoreDNS(globalDomainNames, globalDomainIPs, globalMinioPort, globalEtcdClient)
		logger.FatalIf(err, "Unable to initialize DNS config for %s.", globalDomainNames)
	}

	if drives := os.Getenv("MINIO_CACHE_DRIVES"); drives != "" {
		driveList, err := parseCacheDrives(strings.Split(drives, cacheEnvDelimiter))
		if err != nil {
			logger.Fatal(err, "Unable to parse MINIO_CACHE_DRIVES value (%s)", drives)
		}
		globalCacheDrives = driveList
		globalIsDiskCacheEnabled = true
	}

	if excludes := os.Getenv("MINIO_CACHE_EXCLUDE"); excludes != "" {
		excludeList, err := parseCacheExcludes(strings.Split(excludes, cacheEnvDelimiter))
		if err != nil {
			logger.Fatal(err, "Unable to parse MINIO_CACHE_EXCLUDE value (`%s`)", excludes)
		}
		globalCacheExcludes = excludeList
	}

	if expiryStr := os.Getenv("MINIO_CACHE_EXPIRY"); expiryStr != "" {
		expiry, err := strconv.Atoi(expiryStr)
		if err != nil {
			logger.Fatal(uiErrInvalidCacheExpiryValue(err), "Unable to parse MINIO_CACHE_EXPIRY value (`%s`)", expiryStr)
		}
		globalCacheExpiry = expiry
	}

	if maxUseStr := os.Getenv("MINIO_CACHE_MAXUSE"); maxUseStr != "" {
		maxUse, err := strconv.Atoi(maxUseStr)
		if err != nil {
			logger.Fatal(uiErrInvalidCacheMaxUse(err), "Unable to parse MINIO_CACHE_MAXUSE value (`%s`)", maxUseStr)
		}
		// maxUse should be a valid percentage.
		if maxUse > 0 && maxUse <= 100 {
			globalCacheMaxUse = maxUse
		}
	}
	// In place update is true by default if the MINIO_UPDATE is not set
	// or is not set to 'off', if MINIO_UPDATE is set to 'off' then
	// in-place update is off.
	globalInplaceUpdateDisabled = strings.EqualFold(os.Getenv("MINIO_UPDATE"), "off")

	// Validate and store the storage class env variables only for XL/Dist XL setups
	if globalIsXL {
		var err error

		// Check for environment variables and parse into storageClass struct
		if ssc := os.Getenv(standardStorageClassEnv); ssc != "" {
			globalStandardStorageClass, err = parseStorageClass(ssc)
			logger.FatalIf(err, "Invalid value set in environment variable %s", standardStorageClassEnv)
		}

		if rrsc := os.Getenv(reducedRedundancyStorageClassEnv); rrsc != "" {
			globalRRStorageClass, err = parseStorageClass(rrsc)
			logger.FatalIf(err, "Invalid value set in environment variable %s", reducedRedundancyStorageClassEnv)
		}

		// Validation is done after parsing both the storage classes. This is needed because we need one
		// storage class value to deduce the correct value of the other storage class.
		if globalRRStorageClass.Scheme != "" {
			err = validateParity(globalStandardStorageClass.Parity, globalRRStorageClass.Parity)
			logger.FatalIf(err, "Invalid value set in environment variable %s", reducedRedundancyStorageClassEnv)
			globalIsStorageClass = true
		}

		if globalStandardStorageClass.Scheme != "" {
			err = validateParity(globalStandardStorageClass.Parity, globalRRStorageClass.Parity)
			logger.FatalIf(err, "Invalid value set in environment variable %s", standardStorageClassEnv)
			globalIsStorageClass = true
		}
	}

	// Get WORM environment variable.
	if worm := os.Getenv("MINIO_WORM"); worm != "" {
		wormFlag, err := ParseBoolFlag(worm)
		if err != nil {
			logger.Fatal(uiErrInvalidWormValue(nil).Msg("Unknown value `%s`", worm), "Invalid MINIO_WORM value in environment variable")
		}

		// worm Envs are set globally, this does not represent
		// if worm is turned off or on.
		globalIsEnvWORM = true
		globalWORMEnabled = bool(wormFlag)
	}

	if compress := os.Getenv("MINIO_COMPRESS"); compress != "" {
		globalIsCompressionEnabled = strings.EqualFold(compress, "true")
	}

	compressExtensions := os.Getenv("MINIO_COMPRESS_EXTENSIONS")
	compressMimeTypes := os.Getenv("MINIO_COMPRESS_MIMETYPES")
	if compressExtensions != "" || compressMimeTypes != "" {
		globalIsEnvCompression = true
		if compressExtensions != "" {
			extensions, err := parseCompressIncludes(strings.Split(compressExtensions, compressEnvDelimiter))
			if err != nil {
				logger.Fatal(err, "Invalid MINIO_COMPRESS_EXTENSIONS value (`%s`)", extensions)
			}
			globalCompressExtensions = extensions
		}
		if compressMimeTypes != "" {
			contenttypes, err := parseCompressIncludes(strings.Split(compressMimeTypes, compressEnvDelimiter))
			if err != nil {
				logger.Fatal(err, "Invalid MINIO_COMPRESS_MIMETYPES value (`%s`)", contenttypes)
			}
			globalCompressMimeTypes = contenttypes
		}
	}
}