// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge 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 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge 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 ProtonMail Bridge.  If not, see <https://www.gnu.org/licenses/>.

package config

import (
	"io/ioutil"
	"net"
	"net/http"
	"os"
	"path/filepath"
	"strings"
	"time"

	"github.com/ProtonMail/go-appdir"
	"github.com/ProtonMail/proton-bridge/pkg/pmapi"
	"github.com/hashicorp/go-multierror"
)

var (
	log = GetLogEntry("config") //nolint[gochecknoglobals]
)

type appDirProvider interface {
	UserConfig() string
	UserCache() string
	UserLogs() string
}

type Config struct {
	appName        string
	version        string
	revision       string
	cacheVersion   string
	appDirs        appDirProvider
	appDirsVersion appDirProvider
	apiConfig      *pmapi.ClientConfig
}

// New returns fully initialized config struct.
// `appName` should be in camelCase format for folder or file names. It's also used in API
// as `AppVersion` which is converted to CamelCase.
// `version` is the version of the app (e.g. v1.2.3).
// `cacheVersion` is the version of the cache files (setting a different number will remove the old ones).
func New(appName, version, revision, cacheVersion string) *Config {
	appDirs := appdir.New(filepath.Join("protonmail", appName))
	appDirsVersion := appdir.New(filepath.Join("protonmail", appName, cacheVersion))
	return newConfig(appName, version, revision, cacheVersion, appDirs, appDirsVersion)
}

func newConfig(appName, version, revision, cacheVersion string, appDirs, appDirsVersion appDirProvider) *Config {
	return &Config{
		appName:        appName,
		version:        version,
		revision:       revision,
		cacheVersion:   cacheVersion,
		appDirs:        appDirs,
		appDirsVersion: appDirsVersion,
		apiConfig: &pmapi.ClientConfig{
			AppVersion: strings.Title(appName) + "_" + version,
			ClientID:   appName,
			Transport: &http.Transport{
				DialContext:           (&net.Dialer{Timeout: 3 * time.Second}).DialContext,
				TLSHandshakeTimeout:   10 * time.Second,
				ResponseHeaderTimeout: 10 * time.Second,
			},
			// TokenManager should not be required, but PMAPI still doesn't handle not-set cases everywhere.
			TokenManager: pmapi.NewTokenManager(),
		},
	}
}

// CreateDirs creates all folders that are necessary for bridge to properly function.
func (c *Config) CreateDirs() error {
	// Log files.
	if err := os.MkdirAll(c.appDirs.UserLogs(), 0700); err != nil {
		return err
	}
	// TLS files.
	if err := os.MkdirAll(c.appDirs.UserConfig(), 0750); err != nil {
		return err
	}
	// Lock, events, preferences, user_info, db files.
	if err := os.MkdirAll(c.appDirsVersion.UserCache(), 0750); err != nil {
		return err
	}
	return nil
}

// ClearData removes all files except the lock file.
// The lock file will be removed when the Bridge stops.
func (c *Config) ClearData() error {
	dirs := []string{
		c.appDirs.UserLogs(),
		c.appDirs.UserConfig(),
		c.appDirs.UserCache(),
	}
	shouldRemove := func(filePath string) bool {
		return filePath != c.GetLockPath()
	}
	return c.removeAllExcept(dirs, shouldRemove)
}

// ClearOldData removes all old files, such as old log files or old versions of cache and so on.
func (c *Config) ClearOldData() error {
	// `appDirs` is parent for `appDirsVersion`.
	// `dir` then contains all subfolders and only `cacheVersion` should stay.
	// But on Windows all files (dirs) are in the same one - we cannot remove log, lock or tls files.
	dir := c.appDirs.UserCache()

	return c.removeExcept(dir, func(filePath string) bool {
		fileName := filepath.Base(filePath)
		return (fileName != c.cacheVersion &&
			!logFileRgx.MatchString(fileName) &&
			filePath != c.GetTLSCertPath() &&
			filePath != c.GetTLSKeyPath() &&
			filePath != c.GetEventsPath() &&
			filePath != c.GetIMAPCachePath() &&
			filePath != c.GetLockPath() &&
			filePath != c.GetPreferencesPath())
	})
}

func (c *Config) removeAllExcept(dirs []string, shouldRemove func(string) bool) error {
	var result *multierror.Error
	for _, dir := range dirs {
		if err := c.removeExcept(dir, shouldRemove); err != nil {
			result = multierror.Append(result, err)
		}
	}
	return result.ErrorOrNil()
}

func (c *Config) removeExcept(dir string, shouldRemove func(string) bool) error {
	files, err := ioutil.ReadDir(dir)
	if err != nil {
		return err
	}

	var result *multierror.Error
	for _, file := range files {
		filePath := filepath.Join(dir, file.Name())
		if !shouldRemove(filePath) {
			continue
		}

		if !file.IsDir() {
			if err := os.RemoveAll(filePath); err != nil {
				result = multierror.Append(result, err)
			}
			continue
		}

		subDir := filepath.Join(dir, file.Name())
		if err := c.removeExcept(subDir, shouldRemove); err != nil {
			result = multierror.Append(result, err)
		} else {
			// Remove dir itself only if it's empty.
			subFiles, err := ioutil.ReadDir(subDir)
			if err != nil {
				result = multierror.Append(result, err)
			} else if len(subFiles) == 0 {
				if err := os.RemoveAll(subDir); err != nil {
					result = multierror.Append(result, err)
				}
			}
		}
	}
	return result.ErrorOrNil()
}

// IsDevMode should be used for development conditions such us whether to send sentry reports.
func (c *Config) IsDevMode() bool {
	return os.Getenv("PROTONMAIL_ENV") == "dev"
}

// GetLogDir returns folder for log files.
func (c *Config) GetLogDir() string {
	return c.appDirs.UserLogs()
}

// GetLogPrefix returns prefix for log files. Bridge uses format vVERSION.
func (c *Config) GetLogPrefix() string {
	return "v" + c.version + "_" + c.revision
}

// GetTLSCertPath returns path to certificate; used for TLS servers (IMAP, SMTP and API).
func (c *Config) GetTLSCertPath() string {
	return filepath.Join(c.appDirs.UserConfig(), "cert.pem")
}

// GetTLSKeyPath returns path to private key; used for TLS servers (IMAP, SMTP and API).
func (c *Config) GetTLSKeyPath() string {
	return filepath.Join(c.appDirs.UserConfig(), "key.pem")
}

// GetDBDir returns folder for db files.
func (c *Config) GetDBDir() string {
	return filepath.Join(c.appDirsVersion.UserCache())
}

// GetEventsPath returns path to events file containing the last processed event IDs.
func (c *Config) GetEventsPath() string {
	return filepath.Join(c.appDirsVersion.UserCache(), "events.json")
}

// GetIMAPCachePath returns path to file with IMAP status.
func (c *Config) GetIMAPCachePath() string {
	return filepath.Join(c.appDirsVersion.UserCache(), "user_info.json")
}

// GetLockPath returns path to lock file to check if bridge is already running.
func (c *Config) GetLockPath() string {
	return filepath.Join(c.appDirsVersion.UserCache(), c.appName+".lock")
}

// GetUpdateDir returns folder for update files; such as new binary.
func (c *Config) GetUpdateDir() string {
	return filepath.Join(c.appDirsVersion.UserCache(), "updates")
}

// GetPreferencesPath returns path to preference file.
func (c *Config) GetPreferencesPath() string {
	return filepath.Join(c.appDirsVersion.UserCache(), "prefs.json")
}

// GetAPIConfig returns config for ProtonMail API.
func (c *Config) GetAPIConfig() *pmapi.ClientConfig {
	return c.apiConfig
}

// GetDefaultAPIPort returns default Bridge local API port.
func (c *Config) GetDefaultAPIPort() int {
	return 1042
}

// GetDefaultIMAPPort returns default Bridge IMAP port.
func (c *Config) GetDefaultIMAPPort() int {
	return 1143
}

// GetDefaultSMTPPort returns default Bridge SMTP port.
func (c *Config) GetDefaultSMTPPort() int {
	return 1025
}
