package api

import (
	"errors"
	"fmt"
	"net/http"
	"net/http/cookiejar"
	"net/url"
	"os"
	"time"
)

const AuthCookieName = "token"

var (
	errRedirect = errors.New("redirect")
)

// Config is used to configure the creation of the client.
type Config struct {
	// Address is the address of the Vault server. This should be a complete
	// URL such as "http://vault.example.com". If you need a custom SSL
	// cert or want to enable insecure mode, you need to specify a custom
	// HttpClient.
	Address string

	// HttpClient is the HTTP client to use. http.DefaultClient will be
	// used if not specified. The HTTP client must have the cookie jar set
	// to be able to store cookies, otherwise authentication (login) will
	// not work properly. If the jar is nil, a default empty cookie jar
	// will be set.
	HttpClient *http.Client
}

// DefaultConfig returns a default configuration for the client. It is
// safe to modify the return value of this function.
//
// The default Address is https://127.0.0.1:8200, but this can be overridden by
// setting the `VAULT_ADDR` environment variable.
func DefaultConfig() *Config {
	config := &Config{
		Address:    "https://127.0.0.1:8200",
		HttpClient: &http.Client{},
	}

	if addr := os.Getenv("VAULT_ADDR"); addr != "" {
		config.Address = addr
	}

	return config
}

// Client is the client to the Vault API. Create a client with
// NewClient.
type Client struct {
	addr   *url.URL
	config *Config
}

// NewClient returns a new client for the given configuration.
//
// If the environment variable `VAULT_TOKEN` is present, the token will be
// automatically added to the client. Otherwise, you must manually call
// `SetToken()`.
func NewClient(c *Config) (*Client, error) {
	u, err := url.Parse(c.Address)
	if err != nil {
		return nil, err
	}

	// Make a copy of the HTTP client so we can configure it without
	// affecting the original
	//
	// If no cookie jar is set on the client, we set a default empty
	// cookie jar.
	if c.HttpClient.Jar == nil {
		jar, err := cookiejar.New(&cookiejar.Options{})
		if err != nil {
			return nil, err
		}

		c.HttpClient.Jar = jar
	}

	// Ensure redirects are not automatically followed
	c.HttpClient.CheckRedirect = func(req *http.Request, via []*http.Request) error {
		return errRedirect
	}

	client := &Client{
		addr:   u,
		config: c,
	}

	if token := os.Getenv("VAULT_TOKEN"); token != "" {
		client.SetToken(token)
	}

	return client, nil
}

// Token returns the access token being used by this client. It will
// return the empty string if there is no token set.
func (c *Client) Token() string {
	r := c.NewRequest("GET", "/")
	for _, cookie := range c.config.HttpClient.Jar.Cookies(r.URL) {
		if cookie.Name == AuthCookieName {
			return cookie.Value
		}
	}

	return ""
}

// SetToken sets the token directly. This won't perform any auth
// verification, it simply sets the cookie properly for future requests.
func (c *Client) SetToken(v string) {
	r := c.NewRequest("GET", "/")
	c.config.HttpClient.Jar.SetCookies(r.URL, []*http.Cookie{
		&http.Cookie{
			Name:    AuthCookieName,
			Value:   v,
			Path:    "/",
			Expires: time.Now().Add(365 * 24 * time.Hour),
		},
	})
}

// ClearToken deletes the token cookie if it is set or does nothing otherwise.
func (c *Client) ClearToken() {
	r := c.NewRequest("GET", "/")
	c.config.HttpClient.Jar.SetCookies(r.URL, []*http.Cookie{
		&http.Cookie{
			Name:    AuthCookieName,
			Value:   "",
			Expires: time.Now().Add(-1 * time.Hour),
		},
	})
}

// NewRequest creates a new raw request object to query the Vault server
// configured for this client. This is an advanced method and generally
// doesn't need to be called externally.
func (c *Client) NewRequest(method, path string) *Request {
	return &Request{
		Method: method,
		URL: &url.URL{
			Scheme: c.addr.Scheme,
			Host:   c.addr.Host,
			Path:   path,
		},
		Params: make(map[string][]string),
	}
}

// RawRequest performs the raw request given. This request may be against
// a Vault server not configured with this client. This is an advanced operation
// that generally won't need to be called externally.
func (c *Client) RawRequest(r *Request) (*Response, error) {
	redirectCount := 0
START:
	req, err := r.ToHTTP()
	if err != nil {
		return nil, err
	}

	var result *Response
	resp, err := c.config.HttpClient.Do(req)
	if resp != nil {
		result = &Response{Response: resp}
	}
	if err != nil {
		urlErr, ok := err.(*url.Error)
		if ok && urlErr.Err == errRedirect {
			err = nil
		}
	}
	if err != nil {
		return result, err
	}

	// Check for a redirect, only allowing for a single redirect
	if (resp.StatusCode == 302 || resp.StatusCode == 307) && redirectCount == 0 {
		// Parse the updated location
		respLoc, err := resp.Location()
		if err != nil {
			return result, err
		}

		// Ensure a protocol downgrade doesn't happen
		if req.URL.Scheme == "https" && respLoc.Scheme != "https" {
			return result, fmt.Errorf("redirect would cause protocol downgrade")
		}

		// Copy the cookies so that our client auth transfers
		cookies := c.config.HttpClient.Jar.Cookies(r.URL)
		c.config.HttpClient.Jar.SetCookies(respLoc, cookies)

		// Update the request
		r.URL = respLoc

		// Reset the request body if any
		if err := r.ResetJSONBody(); err != nil {
			return result, err
		}

		// Retry the request
		redirectCount++
		goto START
	}

	if err := result.Error(); err != nil {
		return result, err
	}

	return result, nil
}
