package cfclient

import (
	"bytes"
	"crypto/tls"
	"encoding/json"
	"fmt"
	"io"
	"io/ioutil"
	"net/http"
	"net/url"
	"os"
	"path/filepath"
	"strings"

	"time"

	"github.com/pkg/errors"
	"golang.org/x/net/context"
	"golang.org/x/oauth2"
	"golang.org/x/oauth2/clientcredentials"
)

// Client used to communicate with Cloud Foundry
type Client struct {
	Config   Config
	Endpoint Endpoint
}

type Endpoint struct {
	DopplerEndpoint   string `json:"doppler_logging_endpoint"`
	LoggingEndpoint   string `json:"logging_endpoint"`
	AuthEndpoint      string `json:"authorization_endpoint"`
	TokenEndpoint     string `json:"token_endpoint"`
	AppSSHEndpoint    string `json:"app_ssh_endpoint"`
	AppSSHOauthClient string `json:"app_ssh_oauth_client"`
}

// Config is used to configure the creation of a client
type Config struct {
	ApiAddress          string `json:"api_url"`
	Username            string `json:"user"`
	Password            string `json:"password"`
	ClientID            string `json:"client_id"`
	ClientSecret        string `json:"client_secret"`
	SkipSslValidation   bool   `json:"skip_ssl_validation"`
	HttpClient          *http.Client
	Token               string `json:"auth_token"`
	TokenSource         oauth2.TokenSource
	tokenSourceDeadline *time.Time
	UserAgent           string `json:"user_agent"`
	Origin              string `json:"-"`
}

type LoginHint struct {
	Origin string `json:"origin"`
}

// Request is used to help build up a request
type Request struct {
	method string
	url    string
	params url.Values
	body   io.Reader
	obj    interface{}
}

type cfHomeConfig struct {
	AccessToken           string
	RefreshToken          string
	Target                string
	AuthorizationEndpoint string
	OrganizationFields    struct {
		Name string
	}
	SpaceFields struct {
		Name string
	}
	SSLDisabled bool
}

func NewConfigFromCF() (*Config, error) {
	return NewConfigFromCFHome("")
}

func NewConfigFromCFHome(cfHomeDir string) (*Config, error) {
	var err error

	if cfHomeDir == "" {
		cfHomeDir = os.Getenv("CF_HOME")
		if cfHomeDir == "" {
			cfHomeDir, err = os.UserHomeDir()
			if err != nil {
				return nil, err
			}
		}
	}

	cfHomeConfig, err := loadCFHomeConfig(cfHomeDir)
	if err != nil {
		return nil, err
	}

	cfg := DefaultConfig()
	cfg.Token = cfHomeConfig.AccessToken
	cfg.ApiAddress = cfHomeConfig.Target
	cfg.SkipSslValidation = cfHomeConfig.SSLDisabled

	return cfg, nil
}

func loadCFHomeConfig(cfHomeDir string) (*cfHomeConfig, error) {
	cfConfigDir := filepath.Join(cfHomeDir, ".cf")
	cfJSON, err := ioutil.ReadFile(filepath.Join(cfConfigDir, "config.json"))
	if err != nil {
		return nil, err
	}

	var cfg cfHomeConfig
	err = json.Unmarshal(cfJSON, &cfg)
	if err == nil {
		if len(cfg.AccessToken) > len("bearer ") {
			cfg.AccessToken = cfg.AccessToken[len("bearer "):]
		}
	}

	return &cfg, nil
}

// DefaultConfig creates a default config object used by CF client
func DefaultConfig() *Config {
	return &Config{
		ApiAddress:        "http://api.bosh-lite.com",
		Username:          "admin",
		Password:          "admin",
		Token:             "",
		SkipSslValidation: false,
		HttpClient:        http.DefaultClient,
		UserAgent:         "Go-CF-client/1.1",
	}
}

func DefaultEndpoint() *Endpoint {
	return &Endpoint{
		DopplerEndpoint: "wss://doppler.10.244.0.34.xip.io:443",
		LoggingEndpoint: "wss://loggregator.10.244.0.34.xip.io:443",
		TokenEndpoint:   "https://uaa.10.244.0.34.xip.io",
		AuthEndpoint:    "https://login.10.244.0.34.xip.io",
	}
}

// NewClient returns a new client
func NewClient(config *Config) (client *Client, err error) {
	// bootstrap the config
	defConfig := DefaultConfig()

	if len(config.ApiAddress) == 0 {
		config.ApiAddress = defConfig.ApiAddress
	}

	if len(config.Username) == 0 {
		config.Username = defConfig.Username
	}

	if len(config.Password) == 0 {
		config.Password = defConfig.Password
	}

	if len(config.Token) == 0 {
		config.Token = defConfig.Token
	}

	if len(config.UserAgent) == 0 {
		config.UserAgent = defConfig.UserAgent
	}

	if config.HttpClient == nil {
		config.HttpClient = defConfig.HttpClient
	}

	if config.HttpClient.Transport == nil {
		config.HttpClient.Transport = shallowDefaultTransport()
	}

	var tp *http.Transport

	switch t := config.HttpClient.Transport.(type) {
	case *http.Transport:
		tp = t
	case *oauth2.Transport:
		if bt, ok := t.Base.(*http.Transport); ok {
			tp = bt
		}
	}

	if tp != nil {
		if tp.TLSClientConfig == nil {
			tp.TLSClientConfig = &tls.Config{}
		}
		tp.TLSClientConfig.InsecureSkipVerify = config.SkipSslValidation
	}

	config.ApiAddress = strings.TrimRight(config.ApiAddress, "/")

	client = &Client{
		Config: *config,
	}

	if err := client.refreshEndpoint(); err != nil {
		return nil, err
	}

	return client, nil
}

func shallowDefaultTransport() *http.Transport {
	defaultTransport := http.DefaultTransport.(*http.Transport)
	return &http.Transport{
		Proxy:                 defaultTransport.Proxy,
		TLSHandshakeTimeout:   defaultTransport.TLSHandshakeTimeout,
		ExpectContinueTimeout: defaultTransport.ExpectContinueTimeout,
	}
}

func getUserAuth(ctx context.Context, config Config, endpoint *Endpoint) (Config, error) {
	authConfig := &oauth2.Config{
		ClientID: "cf",
		Scopes:   []string{""},
		Endpoint: oauth2.Endpoint{
			AuthURL:  endpoint.AuthEndpoint + "/oauth/auth",
			TokenURL: endpoint.TokenEndpoint + "/oauth/token",
		},
	}
	if config.Origin != "" {
		loginHint := LoginHint{config.Origin}
		origin, err := json.Marshal(loginHint)
		if err != nil {
			return config, errors.Wrap(err, "Error creating login_hint")
		}
		val := url.Values{}
		val.Set("login_hint", string(origin))
		authConfig.Endpoint.TokenURL = fmt.Sprintf("%s?%s", authConfig.Endpoint.TokenURL, val.Encode())
	}

	token, err := authConfig.PasswordCredentialsToken(ctx, config.Username, config.Password)
	if err != nil {
		return config, errors.Wrap(err, "Error getting token")
	}

	config.tokenSourceDeadline = &token.Expiry
	config.TokenSource = authConfig.TokenSource(ctx, token)
	config.HttpClient = oauth2.NewClient(ctx, config.TokenSource)

	return config, err
}

func getClientAuth(ctx context.Context, config Config, endpoint *Endpoint) Config {
	authConfig := &clientcredentials.Config{
		ClientID:     config.ClientID,
		ClientSecret: config.ClientSecret,
		TokenURL:     endpoint.TokenEndpoint + "/oauth/token",
	}

	config.TokenSource = authConfig.TokenSource(ctx)
	config.HttpClient = authConfig.Client(ctx)
	return config
}

// getUserTokenAuth initializes client credentials from existing bearer token.
func getUserTokenAuth(ctx context.Context, config Config, endpoint *Endpoint) Config {
	authConfig := &oauth2.Config{
		ClientID: "cf",
		Scopes:   []string{""},
		Endpoint: oauth2.Endpoint{
			AuthURL:  endpoint.AuthEndpoint + "/oauth/auth",
			TokenURL: endpoint.TokenEndpoint + "/oauth/token",
		},
	}

	// Token is expected to have no "bearer" prefix
	token := &oauth2.Token{
		AccessToken: config.Token,
		TokenType:   "Bearer"}

	config.TokenSource = authConfig.TokenSource(ctx, token)
	config.HttpClient = oauth2.NewClient(ctx, config.TokenSource)

	return config
}

func getInfo(api string, httpClient *http.Client) (*Endpoint, error) {
	var endpoint Endpoint

	if api == "" {
		return DefaultEndpoint(), nil
	}

	resp, err := httpClient.Get(api + "/v2/info")
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	err = decodeBody(resp, &endpoint)
	if err != nil {
		return nil, err
	}

	return &endpoint, err
}

// NewRequest is used to create a new Request
func (c *Client) NewRequest(method, path string) *Request {
	r := &Request{
		method: method,
		url:    c.Config.ApiAddress + path,
		params: make(map[string][]string),
	}
	return r
}

// NewRequestWithBody is used to create a new request with
// arbigtrary body io.Reader.
func (c *Client) NewRequestWithBody(method, path string, body io.Reader) *Request {
	r := c.NewRequest(method, path)

	// Set request body
	r.body = body

	return r
}

// DoRequest runs a request with our client
func (c *Client) DoRequest(r *Request) (*http.Response, error) {
	req, err := r.toHTTP()
	if err != nil {
		return nil, err
	}
	return c.Do(req)
}

// DoRequestWithoutRedirects executes the request without following redirects
func (c *Client) DoRequestWithoutRedirects(r *Request) (*http.Response, error) {
	prevCheckRedirect := c.Config.HttpClient.CheckRedirect
	c.Config.HttpClient.CheckRedirect = func(httpReq *http.Request, via []*http.Request) error {
		return http.ErrUseLastResponse
	}
	defer func() {
		c.Config.HttpClient.CheckRedirect = prevCheckRedirect
	}()
	return c.DoRequest(r)
}

func (c *Client) Do(req *http.Request) (*http.Response, error) {
	req.Header.Set("User-Agent", c.Config.UserAgent)
	if req.Body != nil && req.Header.Get("Content-type") == "" {
		req.Header.Set("Content-type", "application/json")
	}

	resp, err := c.Config.HttpClient.Do(req)
	if err != nil {
		return nil, err
	}

	if resp.StatusCode >= http.StatusBadRequest {
		return c.handleError(resp)
	}

	return resp, nil
}

func (c *Client) handleError(resp *http.Response) (*http.Response, error) {
	body, err := ioutil.ReadAll(resp.Body)

	if err != nil {
		return resp, CloudFoundryHTTPError{
			StatusCode: resp.StatusCode,
			Status:     resp.Status,
			Body:       body,
		}
	}
	defer resp.Body.Close()

	// Unmarshal V2 error response
	if strings.HasPrefix(resp.Request.URL.Path, "/v2/") {
		var cfErr CloudFoundryError
		if err := json.Unmarshal(body, &cfErr); err != nil {
			return resp, CloudFoundryHTTPError{
				StatusCode: resp.StatusCode,
				Status:     resp.Status,
				Body:       body,
			}
		}
		return nil, cfErr
	}

	// Unmarshal a V3 error response and convert it into a V2 model
	var cfErrorsV3 CloudFoundryErrorsV3
	if err := json.Unmarshal(body, &cfErrorsV3); err != nil {
		return resp, CloudFoundryHTTPError{
			StatusCode: resp.StatusCode,
			Status:     resp.Status,
			Body:       body,
		}
	}
	return nil, NewCloudFoundryErrorFromV3Errors(cfErrorsV3)
}

func (c *Client) refreshEndpoint() error {
	// we want to keep the Timeout value from config.HttpClient
	timeout := c.Config.HttpClient.Timeout

	ctx := context.Background()
	ctx = context.WithValue(ctx, oauth2.HTTPClient, c.Config.HttpClient)

	endpoint, err := getInfo(c.Config.ApiAddress, oauth2.NewClient(ctx, nil))

	if err != nil {
		return errors.Wrap(err, "Could not get api /v2/info")
	}

	switch {
	case c.Config.Token != "":
		c.Config = getUserTokenAuth(ctx, c.Config, endpoint)
	case c.Config.ClientID != "":
		c.Config = getClientAuth(ctx, c.Config, endpoint)
	default:
		c.Config, err = getUserAuth(ctx, c.Config, endpoint)
		if err != nil {
			return err
		}
	}
	// make sure original Timeout value will be used
	if c.Config.HttpClient.Timeout != timeout {
		c.Config.HttpClient.Timeout = timeout
	}

	c.Endpoint = *endpoint
	return nil
}

// toHTTP converts the request to an HTTP Request
func (r *Request) toHTTP() (*http.Request, error) {

	// Check if we should encode the body
	if r.body == nil && r.obj != nil {
		b, err := encodeBody(r.obj)
		if err != nil {
			return nil, err
		}
		r.body = b
	}

	// Create the HTTP Request
	return http.NewRequest(r.method, r.url, r.body)
}

// decodeBody is used to JSON decode a body
func decodeBody(resp *http.Response, out interface{}) error {
	defer resp.Body.Close()
	dec := json.NewDecoder(resp.Body)
	return dec.Decode(out)
}

// encodeBody is used to encode a request body
func encodeBody(obj interface{}) (io.Reader, error) {
	buf := bytes.NewBuffer(nil)
	enc := json.NewEncoder(buf)
	if err := enc.Encode(obj); err != nil {
		return nil, err
	}
	return buf, nil
}

func (c *Client) GetToken() (string, error) {
	if c.Config.tokenSourceDeadline != nil && c.Config.tokenSourceDeadline.Before(time.Now()) {
		if err := c.refreshEndpoint(); err != nil {
			return "", err
		}
	}

	token, err := c.Config.TokenSource.Token()
	if err != nil {
		return "", errors.Wrap(err, "Error getting bearer token")
	}
	return "bearer " + token.AccessToken, nil
}

var ErrPreventRedirect = errors.New("prevent-redirect")

func (c *Client) GetSSHCode() (string, error) {
	authorizeUrl, err := url.Parse(c.Endpoint.TokenEndpoint)
	if err != nil {
		return "", err
	}

	values := url.Values{}
	values.Set("response_type", "code")
	values.Set("grant_type", "authorization_code")
	values.Set("client_id", c.Endpoint.AppSSHOauthClient) // client_id，used by cf server

	authorizeUrl.Path = "/oauth/authorize"
	authorizeUrl.RawQuery = values.Encode()

	req, err := http.NewRequest("GET", authorizeUrl.String(), nil)
	if err != nil {
		return "", err
	}

	token, err := c.GetToken()
	if err != nil {
		return "", err
	}

	req.Header.Add("authorization", token)
	httpClient := &http.Client{
		CheckRedirect: func(req *http.Request, _ []*http.Request) error {
			return ErrPreventRedirect
		},
		Timeout: 30 * time.Second,
		Transport: &http.Transport{
			DisableKeepAlives: true,
			TLSClientConfig: &tls.Config{
				InsecureSkipVerify: c.Config.SkipSslValidation,
			},
			Proxy:               http.ProxyFromEnvironment,
			TLSHandshakeTimeout: 10 * time.Second,
		},
	}

	resp, err := httpClient.Do(req)
	if err == nil {
		return "", errors.New("authorization server did not redirect with one time code")
	}
	defer resp.Body.Close()
	if netErr, ok := err.(*url.Error); !ok || netErr.Err != ErrPreventRedirect {
		return "", errors.New(fmt.Sprintf("error requesting one time code from server: %s", err.Error()))
	}

	loc, err := resp.Location()
	if err != nil {
		return "", errors.New(fmt.Sprintf("error getting the redirected location:  %s", err.Error()))
	}

	codes := loc.Query()["code"]
	if len(codes) != 1 {
		return "", errors.New("unable to acquire one time code from authorization response")
	}

	return codes[0], nil
}
