package client

import (
	"encoding/base64"
	"encoding/json"
	"errors"
	"io"
	"net/url"
	"reflect"
	"strings"

	"golang.org/x/crypto/bcrypt"

	"github.com/coreos/dex/repo"
	"github.com/coreos/go-oidc/oidc"
)

var (
	ErrorInvalidClientID = errors.New("not a valid client ID")

	ErrorInvalidClientSecret = errors.New("not a valid client Secret")

	ErrorDuplicateClientID = errors.New("client ID already exists")

	ErrorInvalidRedirectURL    = errors.New("not a valid redirect url for the given client")
	ErrorCantChooseRedirectURL = errors.New("must provide a redirect url; client has many")
	ErrorNoValidRedirectURLs   = errors.New("no valid redirect URLs for this client.")

	ErrorPublicClientRedirectURIs = errors.New("public clients cannot have redirect URIs")
	ErrorPublicClientMissingName  = errors.New("public clients must have a name")

	ErrorMissingRedirectURI = errors.New("no client redirect url given")

	ErrorNotFound = errors.New("no data found")
)

type ValidationError struct {
	Err error
}

func (v ValidationError) Error() string {
	return v.Err.Error()
}

const (
	bcryptHashCost = 10

	OOBRedirectURI = "urn:ietf:wg:oauth:2.0:oob"
)

func HashSecret(creds oidc.ClientCredentials) ([]byte, error) {
	secretBytes, err := base64.URLEncoding.DecodeString(creds.Secret)
	if err != nil {
		return nil, ErrorInvalidClientSecret
	}
	hashed, err := bcrypt.GenerateFromPassword([]byte(
		secretBytes),
		bcryptHashCost)
	if err != nil {
		return nil, err
	}
	return hashed, nil
}

type Client struct {
	Credentials oidc.ClientCredentials
	Metadata    oidc.ClientMetadata
	Admin       bool
	Public      bool
}

func (c Client) ValidRedirectURL(u *url.URL) (url.URL, error) {
	if c.Public {
		if u == nil {
			return url.URL{}, ErrorInvalidRedirectURL
		}
		if u.String() == OOBRedirectURI {
			return *u, nil
		}

		if u.Scheme != "http" {
			return url.URL{}, ErrorInvalidRedirectURL
		}

		hostPort := strings.Split(u.Host, ":")
		if len(hostPort) != 2 {
			return url.URL{}, ErrorInvalidRedirectURL
		}

		if hostPort[0] != "localhost" || u.Path != "" || u.RawPath != "" || u.RawQuery != "" || u.Fragment != "" {
			return url.URL{}, ErrorInvalidRedirectURL
		}

		return *u, nil
	}

	return ValidRedirectURL(u, c.Metadata.RedirectURIs)
}

type ClientRepo interface {
	Get(tx repo.Transaction, clientID string) (Client, error)

	// GetSecret returns the (base64 encoded) hashed client secret
	GetSecret(tx repo.Transaction, clientID string) ([]byte, error)

	// All returns all registered Clients
	All(tx repo.Transaction) ([]Client, error)

	// New registers a Client with the repo.
	// An unused ID must be provided. A corresponding secret will be returned
	// in a ClientCredentials struct along with the provided ID.
	New(tx repo.Transaction, client Client) (*oidc.ClientCredentials, error)

	Update(tx repo.Transaction, client Client) error

	// GetTrustedPeers returns the list of clients authorized to mint ID token for the given client.
	GetTrustedPeers(tx repo.Transaction, clientID string) ([]string, error)

	// SetTrustedPeers sets the list of clients authorized to mint ID token for the given client.
	SetTrustedPeers(tx repo.Transaction, clientID string, clientIDs []string) error
}

// ValidRedirectURL returns the passed in URL if it is present in the redirectURLs list, and returns an error otherwise.
// If nil is passed in as the rURL and there is only one URL in redirectURLs,
// that URL will be returned. If nil is passed but theres >1 URL in the slice,
// then an error is returned.
func ValidRedirectURL(rURL *url.URL, redirectURLs []url.URL) (url.URL, error) {
	if len(redirectURLs) == 0 {
		return url.URL{}, ErrorNoValidRedirectURLs
	}

	if rURL == nil {
		if len(redirectURLs) > 1 {
			return url.URL{}, ErrorCantChooseRedirectURL
		}

		return redirectURLs[0], nil
	}

	for _, ru := range redirectURLs {
		if reflect.DeepEqual(ru, *rURL) {
			return ru, nil
		}
	}
	return url.URL{}, ErrorInvalidRedirectURL
}

// LoadableClient contains sufficient information for creating a Client and its related entities.
type LoadableClient struct {
	Client       Client
	TrustedPeers []string
}

func ClientsFromReader(r io.Reader) ([]LoadableClient, error) {
	var c []struct {
		ID           string   `json:"id"`
		Secret       string   `json:"secret"`
		RedirectURLs []string `json:"redirectURLs"`
		Admin        bool     `json:"admin"`
		Public       bool     `json:"public"`
		TrustedPeers []string `json:"trustedPeers"`
	}
	if err := json.NewDecoder(r).Decode(&c); err != nil {
		return nil, err
	}
	clients := make([]LoadableClient, len(c))
	for i, client := range c {
		if client.ID == "" {
			return nil, errors.New("clients must have an ID")
		}
		if len(client.Secret) == 0 {
			return nil, errors.New("clients must have a Secret")
		}
		redirectURIs := make([]url.URL, len(client.RedirectURLs))
		for j, u := range client.RedirectURLs {
			uri, err := url.Parse(u)
			if err != nil {
				return nil, err
			}
			redirectURIs[j] = *uri
		}

		clients[i] = LoadableClient{
			Client: Client{
				Credentials: oidc.ClientCredentials{
					ID:     client.ID,
					Secret: client.Secret,
				},
				Metadata: oidc.ClientMetadata{
					RedirectURIs: redirectURIs,
				},
				Admin:  client.Admin,
				Public: client.Public,
			},
			TrustedPeers: client.TrustedPeers,
		}
	}
	return clients, nil
}
