package auth

import (
	"errors"
	"net/url"
	"time"

	"code.secondbit.org/uuid"
)

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")
)

// 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
	Secret  string
	OwnerID uuid.ID
	Name    string
	Logo    string
	Website string
	Type    string
}

// 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
}

// 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
	ClientID uuid.ID
	URI      url.URL
	Added    time.Time
}

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)

	addEndpoint(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) addEndpoint(client uuid.ID, endpoint Endpoint) error {
	m.endpointLock.Lock()
	defer m.endpointLock.Unlock()
	m.endpoints[client.String()] = append(m.endpoints[client.String()], endpoint)
	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.URI.String() {
			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
}
