// Copyright 2015 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package google

import (
	"bufio"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"os"
	"os/user"
	"path/filepath"
	"runtime"
	"strings"
	"time"

	"golang.org/x/oauth2"
)

type sdkCredentials struct {
	Data []struct {
		Credential struct {
			ClientID     string     `json:"client_id"`
			ClientSecret string     `json:"client_secret"`
			AccessToken  string     `json:"access_token"`
			RefreshToken string     `json:"refresh_token"`
			TokenExpiry  *time.Time `json:"token_expiry"`
		} `json:"credential"`
		Key struct {
			Account string `json:"account"`
			Scope   string `json:"scope"`
		} `json:"key"`
	}
}

// An SDKConfig provides access to tokens from an account already
// authorized via the Google Cloud SDK.
type SDKConfig struct {
	conf         oauth2.Config
	initialToken *oauth2.Token
}

// NewSDKConfig creates an SDKConfig for the given Google Cloud SDK
// account. If account is empty, the account currently active in
// Google Cloud SDK properties is used.
// Google Cloud SDK credentials must be created by running `gcloud auth`
// before using this function.
// The Google Cloud SDK is available at https://cloud.google.com/sdk/.
func NewSDKConfig(account string) (*SDKConfig, error) {
	configPath, err := sdkConfigPath()
	if err != nil {
		return nil, fmt.Errorf("oauth2/google: error getting SDK config path: %v", err)
	}
	credentialsPath := filepath.Join(configPath, "credentials")
	f, err := os.Open(credentialsPath)
	if err != nil {
		return nil, fmt.Errorf("oauth2/google: failed to load SDK credentials: %v", err)
	}
	defer f.Close()

	var c sdkCredentials
	if err := json.NewDecoder(f).Decode(&c); err != nil {
		return nil, fmt.Errorf("oauth2/google: failed to decode SDK credentials from %q: %v", credentialsPath, err)
	}
	if len(c.Data) == 0 {
		return nil, fmt.Errorf("oauth2/google: no credentials found in %q, run `gcloud auth login` to create one", credentialsPath)
	}
	if account == "" {
		propertiesPath := filepath.Join(configPath, "properties")
		f, err := os.Open(propertiesPath)
		if err != nil {
			return nil, fmt.Errorf("oauth2/google: failed to load SDK properties: %v", err)
		}
		defer f.Close()
		ini, err := parseINI(f)
		if err != nil {
			return nil, fmt.Errorf("oauth2/google: failed to parse SDK properties %q: %v", propertiesPath, err)
		}
		core, ok := ini["core"]
		if !ok {
			return nil, fmt.Errorf("oauth2/google: failed to find [core] section in %v", ini)
		}
		active, ok := core["account"]
		if !ok {
			return nil, fmt.Errorf("oauth2/google: failed to find %q attribute in %v", "account", core)
		}
		account = active
	}

	for _, d := range c.Data {
		if account == "" || d.Key.Account == account {
			if d.Credential.AccessToken == "" && d.Credential.RefreshToken == "" {
				return nil, fmt.Errorf("oauth2/google: no token available for account %q", account)
			}
			var expiry time.Time
			if d.Credential.TokenExpiry != nil {
				expiry = *d.Credential.TokenExpiry
			}
			return &SDKConfig{
				conf: oauth2.Config{
					ClientID:     d.Credential.ClientID,
					ClientSecret: d.Credential.ClientSecret,
					Scopes:       strings.Split(d.Key.Scope, " "),
					Endpoint:     Endpoint,
					RedirectURL:  "oob",
				},
				initialToken: &oauth2.Token{
					AccessToken:  d.Credential.AccessToken,
					RefreshToken: d.Credential.RefreshToken,
					Expiry:       expiry,
				},
			}, nil
		}
	}
	return nil, fmt.Errorf("oauth2/google: no such credentials for account %q", account)
}

// Client returns an HTTP client using Google Cloud SDK credentials to
// authorize requests. The token will auto-refresh as necessary. The
// underlying http.RoundTripper will be obtained using the provided
// context. The returned client and its Transport should not be
// modified.
func (c *SDKConfig) Client(ctx context.Context) *http.Client {
	return &http.Client{
		Transport: &oauth2.Transport{
			Source: c.TokenSource(ctx),
		},
	}
}

// TokenSource returns an oauth2.TokenSource that retrieve tokens from
// Google Cloud SDK credentials using the provided context.
// It will returns the current access token stored in the credentials,
// and refresh it when it expires, but it won't update the credentials
// with the new access token.
func (c *SDKConfig) TokenSource(ctx context.Context) oauth2.TokenSource {
	return c.conf.TokenSource(ctx, c.initialToken)
}

// Scopes are the OAuth 2.0 scopes the current account is authorized for.
func (c *SDKConfig) Scopes() []string {
	return c.conf.Scopes
}

func parseINI(ini io.Reader) (map[string]map[string]string, error) {
	result := map[string]map[string]string{
		"": {}, // root section
	}
	scanner := bufio.NewScanner(ini)
	currentSection := ""
	for scanner.Scan() {
		line := strings.TrimSpace(scanner.Text())
		if strings.HasPrefix(line, ";") {
			// comment.
			continue
		}
		if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") {
			currentSection = strings.TrimSpace(line[1 : len(line)-1])
			result[currentSection] = map[string]string{}
			continue
		}
		parts := strings.SplitN(line, "=", 2)
		if len(parts) == 2 && parts[0] != "" {
			result[currentSection][strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1])
		}
	}
	if err := scanner.Err(); err != nil {
		return nil, fmt.Errorf("error scanning ini: %v", err)
	}
	return result, nil
}

// sdkConfigPath tries to guess where the gcloud config is located.
// It can be overridden during tests.
var sdkConfigPath = func() (string, error) {
	if runtime.GOOS == "windows" {
		return filepath.Join(os.Getenv("APPDATA"), "gcloud"), nil
	}
	homeDir := guessUnixHomeDir()
	if homeDir == "" {
		return "", errors.New("unable to get current user home directory: os/user lookup failed; $HOME is empty")
	}
	return filepath.Join(homeDir, ".config", "gcloud"), nil
}

func guessUnixHomeDir() string {
	// Prefer $HOME over user.Current due to glibc bug: golang.org/issue/13470
	if v := os.Getenv("HOME"); v != "" {
		return v
	}
	// Else, fall back to user.Current:
	if u, err := user.Current(); err == nil {
		return u.HomeDir
	}
	return ""
}