package service

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"os"
	"path/filepath"
	"strconv"
	"strings"

	"github.com/keys-pub/keys/env"
	"github.com/pkg/errors"
)

// Env for app runtime.
// Do not store anything sensitive in here, values are saved clear and can be
// modified at will.
// Env is not authenticated.
type Env struct {
	appName string
	values  map[string]string
	linkDir string
}

// NewEnv loads the Env.
func NewEnv(appName string) (*Env, error) {
	if appName == "" {
		return nil, errors.Errorf("no app name")
	}
	env := &Env{
		appName: appName,
		linkDir: filepath.Join("usr", "local", "bin"),
	}
	if err := env.Load(); err != nil {
		return nil, err
	}
	return env, nil
}

func (c Env) linkPath() string {
	return filepath.Join(c.linkDir, "keys")
}

// Env key names
const serverCfgKey = "server"
const portCfgKey = "port"

var configKeys = []string{serverCfgKey, portCfgKey}

// IsKey returns true if config key is recognized.
func (c Env) IsKey(s string) bool {
	for _, k := range configKeys {
		if s == k {
			return true
		}
	}
	return false
}

const defaultPort = 22405

// Port to connect.
func (c Env) Port() int {
	return c.GetInt(portCfgKey, defaultPort)
}

// Server to connect to.
func (c Env) Server() string {
	return c.Get(serverCfgKey, "https://keys.pub")
}

// Build describes build flags.
type Build struct {
	Version string
	Commit  string
	Date    string
}

func (b Build) String() string {
	return fmt.Sprintf("%s %s %s", b.Version, b.Commit, b.Date)
}

// AppName returns current app name.
func (c Env) AppName() string {
	return c.appName
}

// AppDir is where app related files are persisted.
func (c Env) AppDir() string {
	p, err := c.AppPath("", false)
	if err != nil {
		panic(err)
	}
	return p
}

// LogsDir is where logs are written.
func (c Env) LogsDir() string {
	p, err := c.LogsPath("", false)
	if err != nil {
		panic(err)
	}
	return p
}

// AppPath ...
func (c Env) AppPath(file string, makeDir bool) (string, error) {
	opts := []env.PathOption{env.Dir(c.AppName()), env.File(file)}
	if makeDir {
		opts = append(opts, env.Mkdir())
	}
	return env.AppPath(opts...)
}

// LogsPath ...
func (c Env) LogsPath(file string, makeDir bool) (string, error) {
	opts := []env.PathOption{env.Dir(c.AppName()), env.File(file)}
	if makeDir {
		opts = append(opts, env.Mkdir())
	}
	return env.LogsPath(opts...)
}

func (c Env) certPath(makeDir bool) (string, error) {
	return c.AppPath("ca.pem", makeDir)
}

// Path to config file.
func (c *Env) Path(makeDir bool) (string, error) {
	return c.AppPath("config.json", makeDir)
}

// Load ...
func (c *Env) Load() error {
	path, err := c.Path(false)
	if err != nil {
		return err
	}

	var values map[string]string

	exists, err := pathExists(path)
	if err != nil {
		return err
	}
	if exists {
		b, err := ioutil.ReadFile(path) // #nosec
		if err != nil {
			return err
		}
		if err := json.Unmarshal(b, &values); err != nil {
			return err
		}
	}
	if values == nil {
		values = map[string]string{}
	}
	c.values = values
	return nil
}

// Save ...
func (c *Env) Save() error {
	path, err := c.Path(true)
	if err != nil {
		return err
	}
	b, err := json.Marshal(c.values)
	if err != nil {
		return err
	}
	if err := ioutil.WriteFile(path, b, filePerms); err != nil {
		return err
	}
	return nil
}

// Reset removes saved values.
func (c *Env) Reset() error {
	path, err := c.Path(false)
	if err != nil {
		return err
	}

	exists, err := pathExists(path)
	if err != nil {
		return err
	}
	if !exists {
		return nil
	}
	return os.Remove(path)
}

// Export ...
func (c Env) Export() ([]byte, error) {
	return json.MarshalIndent(c.values, "", "  ")
}

// Get config value.
func (c *Env) Get(key string, dflt string) string {
	v, ok := c.values[key]
	if !ok {
		return dflt
	}
	return v
}

// GetInt gets config value as int.
func (c *Env) GetInt(key string, dflt int) int {
	v, ok := c.values[key]
	if !ok {
		return dflt
	}
	n, err := strconv.Atoi(v)
	if err != nil {
		logger.Warningf("config value %s not an int", key)
		return 0
	}
	return n

}

// GetBool gets config value as bool.
func (c *Env) GetBool(key string) bool {
	v, ok := c.values[key]
	if !ok {
		return false
	}
	b, _ := truthy(v)
	return b
}

// SetBool sets bool value for key.
func (c *Env) SetBool(key string, b bool) {
	c.Set(key, truthyString(b))
}

// SetInt sets int value for key.
func (c *Env) SetInt(key string, n int) {
	c.Set(key, strconv.Itoa(n))
}

// Set value.
func (c *Env) Set(key string, value string) {
	c.values[key] = value
}

func (c *Env) savePortFlag(port int) error {
	if port == 0 {
		return nil
	}
	c.SetInt(portCfgKey, port)
	return c.Save()
}

func truthy(s string) (bool, error) {
	s = strings.TrimSpace(s)
	switch s {
	case "1", "t", "true", "y", "yes":
		return true, nil
	case "0", "f", "false", "n", "no":
		return false, nil
	default:
		return false, errors.Errorf("invalid value: %s", s)
	}
}

func truthyString(b bool) string {
	if b {
		return "1"
	}
	return "0"
}
