package v2

import (
	"bytes"
	"crypto/tls"
	"crypto/x509"
	"encoding/json"
	"errors"
	"fmt"
	"io/ioutil"
	"net/http"
	"net/url"
	"time"

	log "github.com/Sirupsen/logrus"
	"github.com/docker/orca"
	"github.com/docker/orca/auth"
)

var (
	ErrNotFound        = errors.New("Not found")
	defaultHTTPTimeout = 30 * time.Second
)

type (
	AuthToken struct {
		Token string `json:"token"`
	}

	V2Registry struct {
		orca.RegistryConfig
		client *orca.RegistryClient
	}
)

func NewRegistry(reg *orca.RegistryConfig, swarmTLSConfig *tls.Config) (orca.Registry, error) {
	// sanity check the registry settings
	u, err := url.Parse(reg.URL)
	if err != nil {
		return nil, fmt.Errorf("The provided Docker Trusted Registry URL was malformed and could not be parsed")
	}

	// Create a new TLS config for the registry, based on swarm's
	// This will allow us not to mess with the Swarm RootCAs
	tlsConfig := *swarmTLSConfig
	tlsConfig.InsecureSkipVerify = reg.Insecure
	if reg.CACert != "" {
		// If the user specified a CA, create a new RootCA pool containing only that CA cert.
		log.Debugf("cert: %s", reg.CACert)
		certPool := x509.NewCertPool()
		certPool.AppendCertsFromPEM([]byte(reg.CACert))
		tlsConfig.RootCAs = certPool
		log.Debug("Connecting to Registry with user-provided CA")
	} else {
		// If the user did not specify a CA, fall back to the system's Root CAs
		tlsConfig.RootCAs = nil
		log.Debug("Connecting to Registry with system Root CAs")
	}

	httpClient := &http.Client{
		Transport: &http.Transport{TLSClientConfig: &tlsConfig},
		Timeout:   defaultHTTPTimeout,
	}

	rClient := &orca.RegistryClient{
		URL:        u,
		HttpClient: httpClient,
	}

	return &V2Registry{
		RegistryConfig: *reg,
		client:         rClient,
	}, nil
}

func (r *V2Registry) doRequest(method string, path string, body []byte, headers map[string]string, username string) ([]byte, error) {
	b := bytes.NewBuffer(body)

	req, err := http.NewRequest(method, path, b)
	if err != nil {
		log.Errorf("couldn't create request: %s", err)
		return nil, err
	}

	// The DTR Auth server will validate the UCP client cert and will grant access to whatever
	// username is passed to it.
	// However, DTR 1.4.3 rejects empty password strings under LDAP, in order to disallow anonymous users.
	req.SetBasicAuth(username, "really?")

	if headers != nil {
		for header, value := range headers {
			req.Header.Add(header, value)
		}
	}

	resp, err := r.client.HttpClient.Do(req)
	if err != nil {
		if err == http.ErrHandlerTimeout {
			log.Error("Login timed out to Docker Trusted Registry")
			return nil, err
		}
		log.Errorf("There was an error while authenticating: %s", err)
		return nil, err
	}
	defer resp.Body.Close()

	if resp.StatusCode == 401 {
		// Unauthorized
		log.Warnf("Unauthorized")
		return nil, auth.ErrUnauthorized
	} else if resp.StatusCode >= 400 {
		log.Errorf("Docker Trusted Registry returned an unexpected status code while authenticating: %s", resp.Status)
		return nil, auth.ErrUnknown
	}

	rBody, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		log.Errorf("couldn't read body: %s", err)
		return nil, err
	}

	return rBody, nil
}

func (r *V2Registry) GetAuthToken(username, accessType, hostname, reponame string) (string, error) {
	uri := fmt.Sprintf("%s/auth/token?scope=repository:%s:%s&service=%s", r.RegistryConfig.URL, reponame, accessType, hostname)

	log.Debugf("contacting DTR for auth token: %s", uri)

	data, err := r.doRequest("GET", uri, nil, nil, username)
	if err != nil {
		return "", err
	}

	var token AuthToken
	if err := json.Unmarshal(data, &token); err != nil {
		return "", err
	}

	return token.Token, nil
}

func (r *V2Registry) GetConfig() *orca.RegistryConfig {
	return &r.RegistryConfig
}

func (r *V2Registry) GetTransport() http.RoundTripper {
	return r.client.HttpClient.Transport
}