// Copyright 2016 Google, 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 config

import (
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
	"strings"

	"github.com/GoogleCloudPlatform/docker-credential-gcr/v2/util"
)

const (
	configFileEnvVariable = "DOCKER_CREDENTIAL_GCR_CONFIG"
	configFileName        = "docker_credential_gcr_config.json"
)

// DefaultTokenSources designates which default source(s) should be used to
// fetch a GCR access_token, and in which order.
var DefaultTokenSources = [...]string{"store", "env"}

// UserConfig describes
type UserConfig interface {
	TokenSources() []string
	SetTokenSources([]string) error
	ResetAll() error
}

// configFile describes the structure of the persistent config store.
type configFile struct {
	TokenSrcs []string `json:"TokenSources,omitempty"`

	// package private helper, made a member variable and exposed for testing
	persist func(*configFile) error
}

// LoadUserConfig returns the UserConfig which provides user-configurable
// application settings, or a new on if it doesn't exist.
func LoadUserConfig() (UserConfig, error) {
	config, err := load()
	if err != nil {
		if !os.IsNotExist(err) {
			return nil, err
		}
		config = &configFile{}
	}
	config.persist = persist
	return config, nil
}

func load() (*configFile, error) {
	path, err := configPath()
	if err != nil {
		return nil, err
	}
	f, err := os.Open(path)
	if err != nil {
		return nil, err
	}
	defer f.Close()

	var config configFile
	if err := json.NewDecoder(f).Decode(&config); err != nil {
		return nil, fmt.Errorf("failed to load config from %s: %v", path, err)
	}

	return &config, nil
}

// TokenSources returns the configured token sources, or the DefaultTokenSources
// if none are set.
func (c *configFile) TokenSources() []string {
	if len(c.TokenSrcs) == 0 { // if nil or empty
		return DefaultTokenSources[:]
	}
	ret := make([]string, len(c.TokenSrcs))
	copy(ret, c.TokenSrcs)
	return ret
}

// SetTokenSources sets (and persists) the token sources. Valid token sources
// are defined by config.SupportedGCRTokenSources.
func (c *configFile) SetTokenSources(newSources []string) error {
	if len(newSources) == 0 {
		newSources = nil
	}
	// Don't touch the file unless we need to.
	if equal(newSources, c.TokenSrcs) {
		return nil
	}

	for _, source := range newSources {
		if _, supported := SupportedGCRTokenSources[source]; !supported {
			return fmt.Errorf("Unsupported token source: %s", source)
		}
	}

	c.TokenSrcs = newSources

	return c.persist(c)
}

func persist(c *configFile) error {
	f, err := createConfigFile()
	if err != nil {
		return err
	}
	defer f.Close()

	return json.NewEncoder(f).Encode(c)
}

func equal(a, b []string) bool {
	if a == nil && b == nil {
		return true
	}
	if a == nil || b == nil {
		return false
	}
	if len(a) != len(b) {
		return false
	}
	for i := range a {
		if a[i] != b[i] {
			return false
		}
	}
	return true
}

// ResetAll clears all user configuration.
func (c *configFile) ResetAll() error {
	err := deleteConfigFile()
	if err != nil {
		return err
	}
	c.TokenSrcs = nil
	return nil
}

func deleteConfigFile() error {
	path, err := configPath()
	if err != nil && !os.IsNotExist(err) {
		return err
	}
	return os.Remove(path)
}

// createConfigFile creates (or truncates) and returns an os.File for the
// user config.
func createConfigFile() (*os.File, error) {
	path, err := configPath()
	if err != nil {
		return nil, err
	}
	// create the gcloud config dir, if it doesnt exist
	if err = os.MkdirAll(filepath.Dir(path), 0777); err != nil {
		return nil, err
	}

	// create or truncate the config file and return it
	return os.Create(path)
}

// configPath returns the full path of our user config file.
func configPath() (string, error) {
	if path := os.Getenv(configFileEnvVariable); strings.TrimSpace(path) != "" {
		return path, nil
	}

	sdkConfigPath, err := util.SdkConfigPath()
	if err != nil {
		return "", fmt.Errorf("couldn't construct config path: %v", err)
	}
	return filepath.Join(sdkConfigPath, configFileName), nil
}
