package auth

import (
	"crypto/rand"
	"encoding/hex"
	"encoding/json"
	"errors"
	"log"
	"net/http"
	"net/url"
	"strconv"
	"time"

	"github.com/PuerkitoBio/purell"
	"github.com/gorilla/mux"

	"code.secondbit.org/uuid.hg"
)

func init() {
	RegisterGrantType("client_credentials", GrantType{
		Validate:      clientCredentialsValidate,
		Invalidate:    nil,
		IssuesRefresh: true,
		ReturnToken:   RenderJSONToken,
		AllowsPublic:  false,
	})
}

var (
	// ErrNoClientStore is returned when a Context tries to act on a clientStore without setting one first.
	ErrNoClientStore = errors.New("no clientStore was specified for the Context")
	// ErrClientNotFound is returned when a Client is requested but not found in a clientStore.
	ErrClientNotFound = errors.New("client not found in clientStore")
	// ErrClientAlreadyExists is returned when a Client is added to a clientStore, but another Client with
	// the same ID already exists in the clientStore.
	ErrClientAlreadyExists = errors.New("client already exists in clientStore")

	// ErrEmptyChange is returned when a Change has all its properties set to nil.
	ErrEmptyChange = errors.New("change must have at least one property set")
	// ErrClientNameTooShort is returned when a Client's Name property is too short.
	ErrClientNameTooShort = errors.New("client name must be at least 2 characters")
	// ErrClientNameTooLong is returned when a Client's Name property is too long.
	ErrClientNameTooLong = errors.New("client name must be at most 32 characters")
	// ErrClientLogoTooLong is returned when a Client's Logo property is too long.
	ErrClientLogoTooLong = errors.New("client logo must be at most 1024 characters")
	// ErrClientLogoNotURL is returned when a Client's Logo property is not a valid absolute URL.
	ErrClientLogoNotURL = errors.New("client logo must be a valid absolute URL")
	// ErrClientWebsiteTooLong is returned when a Client's Website property is too long.
	ErrClientWebsiteTooLong = errors.New("client website must be at most 1024 characters")
	// ErrClientWebsiteNotURL is returned when a Client's Website property is not a valid absolute URL.
	ErrClientWebsiteNotURL = errors.New("client website must be a valid absolute URL")
	// ErrEndpointURINotURL is returned when an Endpoint's URI property is not a valid absolute URL.
	ErrEndpointURINotURL = errors.New("endpoint URI must be a valid absolute URL")
)

const (
	clientTypePublic       = "public"
	clientTypeConfidential = "confidential"
	minClientNameLen       = 2
	maxClientNameLen       = 24
)

// Client represents a client that grants access
// to the auth server, exchanging grants for tokens,
// and tokens for access.
type Client struct {
	ID      uuid.ID `json:"id,omitempty"`
	Secret  string  `json:"secret,omitempty"`
	OwnerID uuid.ID `json:"owner_id,omitempty"`
	Name    string  `json:"name,omitempty"`
	Logo    string  `json:"logo,omitempty"`
	Website string  `json:"website,omitempty"`
	Type    string  `json:"type,omitempty"`
}

// ApplyChange applies the properties of the passed
// ClientChange to the Client object it is called on.
func (c *Client) ApplyChange(change ClientChange) {
	if change.Secret != nil {
		c.Secret = *change.Secret
	}
	if change.OwnerID != nil {
		c.OwnerID = change.OwnerID
	}
	if change.Name != nil {
		c.Name = *change.Name
	}
	if change.Logo != nil {
		c.Logo = *change.Logo
	}
	if change.Website != nil {
		c.Website = *change.Website
	}
}

// ClientChange represents a bundle of options for
// updating a Client's mutable data.
type ClientChange struct {
	Secret  *string
	OwnerID uuid.ID
	Name    *string
	Logo    *string
	Website *string
}

// Validate checks the ClientChange it is called on
// and asserts its internal validity, or lack thereof.
func (c ClientChange) Validate() error {
	if c.Secret == nil && c.OwnerID == nil && c.Name == nil && c.Logo == nil && c.Website == nil {
		return ErrEmptyChange
	}
	if c.Name != nil && len(*c.Name) < 2 {
		return ErrClientNameTooShort
	}
	if c.Name != nil && len(*c.Name) > 32 {
		return ErrClientNameTooLong
	}
	if c.Logo != nil && *c.Logo != "" {
		if len(*c.Logo) > 1024 {
			return ErrClientLogoTooLong
		}
		u, err := url.Parse(*c.Logo)
		if err != nil || !u.IsAbs() {
			return ErrClientLogoNotURL
		}
	}
	if c.Website != nil && *c.Website != "" {
		if len(*c.Website) > 140 {
			return ErrClientWebsiteTooLong
		}
		u, err := url.Parse(*c.Website)
		if err != nil || !u.IsAbs() {
			return ErrClientWebsiteNotURL
		}
	}
	return nil
}

func getClientAuth(w http.ResponseWriter, r *http.Request, allowPublic bool) (uuid.ID, string, bool) {
	enc := json.NewEncoder(w)
	clientIDStr, clientSecret, fromAuthHeader := r.BasicAuth()
	if !fromAuthHeader {
		clientIDStr = r.PostFormValue("client_id")
	}
	if clientIDStr == "" {
		w.WriteHeader(http.StatusUnauthorized)
		if fromAuthHeader {
			w.Header().Set("WWW-Authenticate", "Basic")
		}
		renderJSONError(enc, "invalid_client")
		return nil, "", false
	}
	clientID, err := uuid.Parse(clientIDStr)
	if err != nil {
		log.Println("Error decoding client ID:", err)
		w.WriteHeader(http.StatusUnauthorized)
		if fromAuthHeader {
			w.Header().Set("WWW-Authenticate", "Basic")
		}
		renderJSONError(enc, "invalid_client")
		return nil, "", false
	}
	if !allowPublic && !fromAuthHeader {
		w.WriteHeader(http.StatusBadRequest)
		renderJSONError(enc, "unauthorized_client")
		return nil, "", false
	}
	return clientID, clientSecret, true
}

func verifyClient(w http.ResponseWriter, r *http.Request, allowPublic bool, context Context) (uuid.ID, bool) {
	enc := json.NewEncoder(w)
	clientID, clientSecret, ok := getClientAuth(w, r, allowPublic)
	if !ok {
		return nil, false
	}
	_, _, fromAuthHeader := r.BasicAuth()
	client, err := context.GetClient(clientID)
	if err == ErrClientNotFound {
		w.WriteHeader(http.StatusUnauthorized)
		if fromAuthHeader {
			w.Header().Set("WWW-Authenticate", "Basic")
		}
		renderJSONError(enc, "invalid_client")
		return nil, false
	} else if err != nil {
		w.WriteHeader(http.StatusInternalServerError)
		renderJSONError(enc, "server_error")
		return nil, false
	}
	if client.Secret != clientSecret { // it's important that any client deemed "public" is not issued a client secret.
		w.WriteHeader(http.StatusUnauthorized)
		if fromAuthHeader {
			w.Header().Set("WWW-Authenticate", "Basic")
		}
		renderJSONError(enc, "invalid_client")
		return nil, false
	}
	return clientID, true
}

// Endpoint represents a single URI that a Client
// controls. Users will be redirected to these URIs
// following successful authorization grants and
// exchanges for access tokens.
type Endpoint struct {
	ID            uuid.ID   `json:"id,omitempty"`
	ClientID      uuid.ID   `json:"client_id,omitempty"`
	URI           string    `json:"uri,omitempty"`
	NormalizedURI string    `json:"-"`
	Added         time.Time `json:"added,omitempty"`
}

func normalizeURIString(in string) (string, error) {
	n, err := purell.NormalizeURLString(in, purell.FlagsUsuallySafeNonGreedy|purell.FlagSortQuery)
	if err != nil {
		log.Println(err)
		return in, ErrEndpointURINotURL
	}
	return n, nil
}

func normalizeURI(in *url.URL) string {
	return purell.NormalizeURL(in, purell.FlagsUsuallySafeNonGreedy|purell.FlagSortQuery)
}

type sortedEndpoints []Endpoint

func (s sortedEndpoints) Len() int {
	return len(s)
}

func (s sortedEndpoints) Less(i, j int) bool {
	return s[i].Added.Before(s[j].Added)
}

func (s sortedEndpoints) Swap(i, j int) {
	s[i], s[j] = s[j], s[i]
}

type clientStore interface {
	getClient(id uuid.ID) (Client, error)
	saveClient(client Client) error
	updateClient(id uuid.ID, change ClientChange) error
	deleteClient(id uuid.ID) error
	listClientsByOwner(ownerID uuid.ID, num, offset int) ([]Client, error)

	addEndpoints(client uuid.ID, endpoint []Endpoint) error
	removeEndpoint(client, endpoint uuid.ID) error
	checkEndpoint(client uuid.ID, endpoint string) (bool, error)
	listEndpoints(client uuid.ID, num, offset int) ([]Endpoint, error)
	countEndpoints(client uuid.ID) (int64, error)
}

func (m *memstore) getClient(id uuid.ID) (Client, error) {
	m.clientLock.RLock()
	defer m.clientLock.RUnlock()
	c, ok := m.clients[id.String()]
	if !ok {
		return Client{}, ErrClientNotFound
	}
	return c, nil
}

func (m *memstore) saveClient(client Client) error {
	m.clientLock.Lock()
	defer m.clientLock.Unlock()
	if _, ok := m.clients[client.ID.String()]; ok {
		return ErrClientAlreadyExists
	}
	m.clients[client.ID.String()] = client
	m.profileClientLookup[client.OwnerID.String()] = append(m.profileClientLookup[client.OwnerID.String()], client.ID)
	return nil
}

func (m *memstore) updateClient(id uuid.ID, change ClientChange) error {
	m.clientLock.Lock()
	defer m.clientLock.Unlock()
	c, ok := m.clients[id.String()]
	if !ok {
		return ErrClientNotFound
	}
	c.ApplyChange(change)
	m.clients[id.String()] = c
	return nil
}

func (m *memstore) deleteClient(id uuid.ID) error {
	client, err := m.getClient(id)
	if err != nil {
		return err
	}
	m.clientLock.Lock()
	defer m.clientLock.Unlock()
	delete(m.clients, id.String())
	pos := -1
	for p, item := range m.profileClientLookup[client.OwnerID.String()] {
		if item.Equal(id) {
			pos = p
			break
		}
	}
	if pos >= 0 {
		m.profileClientLookup[client.OwnerID.String()] = append(m.profileClientLookup[client.OwnerID.String()][:pos], m.profileClientLookup[client.OwnerID.String()][pos+1:]...)
	}
	return nil
}

func (m *memstore) listClientsByOwner(ownerID uuid.ID, num, offset int) ([]Client, error) {
	ids := m.lookupClientsByProfileID(ownerID.String())
	if len(ids) > num+offset {
		ids = ids[offset : num+offset]
	} else if len(ids) > offset {
		ids = ids[offset:]
	} else {
		return []Client{}, nil
	}
	clients := []Client{}
	for _, id := range ids {
		client, err := m.getClient(id)
		if err != nil {
			return []Client{}, err
		}
		clients = append(clients, client)
	}
	return clients, nil
}

func (m *memstore) addEndpoints(client uuid.ID, endpoints []Endpoint) error {
	m.endpointLock.Lock()
	defer m.endpointLock.Unlock()
	m.endpoints[client.String()] = append(m.endpoints[client.String()], endpoints...)
	return nil
}

func (m *memstore) removeEndpoint(client, endpoint uuid.ID) error {
	m.endpointLock.Lock()
	defer m.endpointLock.Unlock()
	pos := -1
	for p, item := range m.endpoints[client.String()] {
		if item.ID.Equal(endpoint) {
			pos = p
			break
		}
	}
	if pos >= 0 {
		m.endpoints[client.String()] = append(m.endpoints[client.String()][:pos], m.endpoints[client.String()][pos+1:]...)
	}
	return nil
}

func (m *memstore) checkEndpoint(client uuid.ID, endpoint string) (bool, error) {
	m.endpointLock.RLock()
	defer m.endpointLock.RUnlock()
	for _, candidate := range m.endpoints[client.String()] {
		if endpoint == candidate.NormalizedURI {
			return true, nil
		}
	}
	return false, nil
}

func (m *memstore) listEndpoints(client uuid.ID, num, offset int) ([]Endpoint, error) {
	m.endpointLock.RLock()
	defer m.endpointLock.RUnlock()
	return m.endpoints[client.String()], nil
}

func (m *memstore) countEndpoints(client uuid.ID) (int64, error) {
	m.endpointLock.RLock()
	defer m.endpointLock.RUnlock()
	return int64(len(m.endpoints[client.String()])), nil
}

type newClientReq struct {
	Name      string   `json:"name"`
	Logo      string   `json:"logo"`
	Website   string   `json:"website"`
	Type      string   `json:"type"`
	Endpoints []string `json:"endpoints"`
}

func RegisterClientHandlers(r *mux.Router, context Context) {
	r.Handle("/clients", wrap(context, CreateClientHandler)).Methods("POST")
}

func CreateClientHandler(w http.ResponseWriter, r *http.Request, c Context) {
	errors := []requestError{}
	username, password, ok := r.BasicAuth()
	if !ok {
		errors = append(errors, requestError{Slug: requestErrAccessDenied})
		encode(w, r, http.StatusUnauthorized, response{Errors: errors})
		return
	}
	profile, err := authenticate(username, password, c)
	if err != nil {
		errors = append(errors, requestError{Slug: requestErrAccessDenied})
		encode(w, r, http.StatusUnauthorized, response{Errors: errors})
		return
	}
	var req newClientReq
	decoder := json.NewDecoder(r.Body)
	err = decoder.Decode(&req)
	if err != nil {
		encode(w, r, http.StatusBadRequest, invalidFormatResponse)
		return
	}
	if req.Type == "" {
		errors = append(errors, requestError{Slug: requestErrMissing, Field: "/type"})
	} else if req.Type != clientTypePublic && req.Type != clientTypeConfidential {
		errors = append(errors, requestError{Slug: requestErrInvalidValue, Field: "/type"})
	}
	if req.Name == "" {
		errors = append(errors, requestError{Slug: requestErrMissing, Field: "/name"})
	} else if len(req.Name) < minClientNameLen {
		errors = append(errors, requestError{Slug: requestErrInsufficient, Field: "/name"})
	} else if len(req.Name) > maxClientNameLen {
		errors = append(errors, requestError{Slug: requestErrOverflow, Field: "/name"})
	}
	if len(errors) > 0 {
		encode(w, r, http.StatusBadRequest, response{Errors: errors})
		return
	}
	client := Client{
		ID:      uuid.NewID(),
		OwnerID: profile.ID,
		Name:    req.Name,
		Logo:    req.Logo,
		Website: req.Website,
		Type:    req.Type,
	}
	if client.Type == clientTypeConfidential {
		secret := make([]byte, 32)
		_, err = rand.Read(secret)
		if err != nil {
			encode(w, r, http.StatusInternalServerError, actOfGodResponse)
			return
		}
		client.Secret = hex.EncodeToString(secret)
	}
	err = c.SaveClient(client)
	if err != nil {
		if err == ErrClientAlreadyExists {
			errors = append(errors, requestError{Slug: requestErrConflict, Field: "/id"})
			encode(w, r, http.StatusBadRequest, response{Errors: errors})
			return
		}
		encode(w, r, http.StatusInternalServerError, actOfGodResponse)
		return
	}
	endpoints := []Endpoint{}
	for pos, u := range req.Endpoints {
		uri, err := url.Parse(u)
		if err != nil {
			errors = append(errors, requestError{Slug: requestErrInvalidFormat, Field: "/endpoints/" + strconv.Itoa(pos)})
			continue
		}
		if !uri.IsAbs() {
			errors = append(errors, requestError{Slug: requestErrInvalidValue, Field: "/endpoints/" + strconv.Itoa(pos)})
			continue
		}
		endpoint := Endpoint{
			ID:       uuid.NewID(),
			ClientID: client.ID,
			URI:      uri.String(),
			Added:    time.Now(),
		}
		endpoints = append(endpoints, endpoint)
	}
	err = c.AddEndpoints(client.ID, endpoints)
	if err != nil {
		errors = append(errors, requestError{Slug: requestErrActOfGod})
		encode(w, r, http.StatusInternalServerError, response{Errors: errors, Clients: []Client{client}})
		return
	}
	resp := response{
		Clients:   []Client{client},
		Endpoints: endpoints,
		Errors:    errors,
	}
	encode(w, r, http.StatusCreated, resp)
}

func clientCredentialsValidate(w http.ResponseWriter, r *http.Request, context Context) (scope string, profileID uuid.ID, valid bool) {
	scope = r.PostFormValue("scope")
	valid = true
	return
}
