package auth

import (
	"html/template"
	"io"
	"log"
	"net/url"
	"time"

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

// Context wraps the different storage interfaces and should
// be used as the main point of interaction for the data storage
// layer.
type Context struct {
	template  *template.Template
	loginURI  *url.URL
	clients   clientStore
	authCodes authorizationCodeStore
	profiles  profileStore
	tokens    tokenStore
	sessions  sessionStore
	scopes    scopeStore
	config    Config
}

// NewContext takes a Config instance and uses it to bootstrap a Context
// using the information provided in the Config variable.
func NewContext(config Config) (Context, error) {
	if config.iterations == 0 {
		return Context{}, ErrConfigNotInitialized
	}
	context := Context{
		clients:   config.ClientStore,
		authCodes: config.AuthCodeStore,
		profiles:  config.ProfileStore,
		tokens:    config.TokenStore,
		sessions:  config.SessionStore,
		scopes:    config.ScopeStore,
		template:  config.Template,
		config:    config,
	}
	var err error
	context.loginURI, err = url.Parse(config.LoginURI)
	if err != nil {
		log.Println(err)
		return Context{}, ErrInvalidLoginURI
	}
	return context, nil
}

// Render uses the HTML templates associated with the Context to render the
// template specified by name to out using data to fill any template variables.
func (c Context) Render(out io.Writer, name string, data interface{}) {
	if c.template == nil {
		log.Println("No template set on Context, can't render anything!")
		return
	}
	err := c.template.ExecuteTemplate(out, name, data)
	if err != nil {
		log.Println("Error executing template", name, ":", err)
	}
}

// GetClient returns a single Client by its ID from the
// clientStore associated with the Context.
func (c Context) GetClient(id uuid.ID) (Client, error) {
	if c.clients == nil {
		return Client{}, ErrNoClientStore
	}
	return c.clients.getClient(id)
}

// SaveClient stores the passed Client in the clientStore
// associated with the Context.
func (c Context) SaveClient(client Client) error {
	if c.clients == nil {
		return ErrNoClientStore
	}
	return c.clients.saveClient(client)
}

// UpdateClient applies the specified ClientChange to the Client
// with the specified ID in the clientStore associated with the
// Context.
func (c Context) UpdateClient(id uuid.ID, change ClientChange) error {
	if c.clients == nil {
		return ErrNoClientStore
	}
	return c.clients.updateClient(id, change)
}

// ListClientsByOwner returns a slice of up to num Clients, starting at offset (inclusive)
// that have the specified OwnerID in the clientStore associated with the Context.
func (c Context) ListClientsByOwner(ownerID uuid.ID, num, offset int) ([]Client, error) {
	if c.clients == nil {
		return []Client{}, ErrNoClientStore
	}
	return c.clients.listClientsByOwner(ownerID, num, offset)
}

func (c Context) DeleteClientsByOwner(ownerID uuid.ID) error {
	if c.clients == nil {
		return ErrNoClientStore
	}
	return c.clients.deleteClientsByOwner(ownerID)
}

// AddEndpoints stores the specified Endpoints in the clientStore associated with the Context.
func (c Context) AddEndpoints(endpoints []Endpoint) error {
	if c.clients == nil {
		return ErrNoClientStore
	}
	for pos, endpoint := range endpoints {
		u, err := normalizeURIString(endpoint.URI)
		if err != nil {
			return err
		}
		endpoint.NormalizedURI = u
		endpoints[pos] = endpoint
	}
	return c.clients.addEndpoints(endpoints)
}

// GetEndpoint retrieves the Endpoint with the specified ID from the clientStore associated
// with the Context, if and only if it belongs to the Client with the specified ID.
func (c Context) GetEndpoint(client, endpoint uuid.ID) (Endpoint, error) {
	if c.clients == nil {
		return Endpoint{}, ErrNoClientStore
	}
	return c.clients.getEndpoint(client, endpoint)
}

// RemoveEndpoint deletes the Endpoint with the specified ID from the clientStore associated
// with the Context, and disassociates the Endpoint from the specified Client.
func (c Context) RemoveEndpoint(client, endpoint uuid.ID) error {
	if c.clients == nil {
		return ErrNoClientStore
	}
	return c.clients.removeEndpoint(client, endpoint)
}

// CheckEndpoint finds Endpoints in the clientStore associated with the Context that belong
// to the Client specified by the passed ID and match the URI passed. URI matches must be
// performed according to RFC 3986 Section 6.
func (c Context) CheckEndpoint(client uuid.ID, URI string) (bool, error) {
	if c.clients == nil {
		return false, ErrNoClientStore
	}
	u, err := normalizeURIString(URI)
	if err != nil {
		return false, err
	}
	return c.clients.checkEndpoint(client, u)
}

// ListEndpoints finds Endpoints in the clientStore associated with the Context that belong
// to the Client specified by the passed ID. It returns up to num endpoints, starting at offset,
// exclusive.
func (c Context) ListEndpoints(client uuid.ID, num, offset int) ([]Endpoint, error) {
	if c.clients == nil {
		return []Endpoint{}, ErrNoClientStore
	}
	return c.clients.listEndpoints(client, num, offset)
}

func (c Context) RemoveEndpointsByClientID(client uuid.ID) error {
	if c.clients == nil {
		return ErrNoClientStore
	}
	return c.clients.removeEndpointsByClientID(client)
}

// CountEndpoints returns the number of Endpoints the are associated with the Client specified by the
// passed ID in the clientStore associated with the Context.
func (c Context) CountEndpoints(client uuid.ID) (int64, error) {
	if c.clients == nil {
		return 0, ErrNoClientStore
	}
	return c.clients.countEndpoints(client)
}

// GetAuthorizationCode returns the AuthorizationCode specified by the provided code from the authorizationCodeStore associated with the
// Context.
func (c Context) GetAuthorizationCode(code string) (AuthorizationCode, error) {
	if c.authCodes == nil {
		return AuthorizationCode{}, ErrNoAuthorizationCodeStore
	}
	return c.authCodes.getAuthorizationCode(code)
}

// SaveAuthorizationCode stores the passed AuthorizationCode in the authorizationCodeStore associated with the Context.
func (c Context) SaveAuthorizationCode(authCode AuthorizationCode) error {
	if c.authCodes == nil {
		return ErrNoAuthorizationCodeStore
	}
	return c.authCodes.saveAuthorizationCode(authCode)
}

// DeleteAuthorizationCode removes the AuthorizationCode specified by the provided code from the authorizationCodeStore associated with
// the Context.
func (c Context) DeleteAuthorizationCode(code string) error {
	if c.authCodes == nil {
		return ErrNoAuthorizationCodeStore
	}
	return c.authCodes.deleteAuthorizationCode(code)
}

// DeleteAuthorizationCodesByProfileID removes the AuthorizationCodes associated with the Profile specified by the provided ID from the
// authorizationCodeStore associated with the Context.
func (c Context) DeleteAuthorizationCodesByProfileID(profileID uuid.ID) error {
	if c.authCodes == nil {
		return ErrNoAuthorizationCodeStore
	}
	return c.authCodes.deleteAuthorizationCodesByProfileID(profileID)
}

func (c Context) DeleteAuthorizationCodesByClientID(clientID uuid.ID) error {
	if c.authCodes == nil {
		return ErrNoAuthorizationCodeStore
	}
	return c.authCodes.deleteAuthorizationCodesByClientID(clientID)
}

// UseAuthorizationCode marks the AuthorizationCode specified by the provided code as used in the authorizationCodeStore associated with
// the Context. Once an AuthorizationCode is marked as used, its Used property will be set to true when retrieved from the authorizationCodeStore.
func (c Context) UseAuthorizationCode(code string) error {
	if c.authCodes == nil {
		return ErrNoAuthorizationCodeStore
	}
	return c.authCodes.useAuthorizationCode(code)
}

// GetProfileByID returns the Profile specified by the provided ID from the profileStore associated with
// the Context.
func (c Context) GetProfileByID(id uuid.ID) (Profile, error) {
	if c.profiles == nil {
		return Profile{}, ErrNoProfileStore
	}
	return c.profiles.getProfileByID(id)
}

// GetProfileByLogin returns the Profile associated with the specified Login from the profileStore associated
// with the Context.
func (c Context) GetProfileByLogin(value string) (Profile, error) {
	if c.profiles == nil {
		return Profile{}, ErrNoProfileStore
	}
	return c.profiles.getProfileByLogin(value)
}

// SaveProfile inserts the passed Profile into the profileStore associated with the Context.
func (c Context) SaveProfile(profile Profile) error {
	if c.profiles == nil {
		return ErrNoProfileStore
	}
	return c.profiles.saveProfile(profile)
}

// UpdateProfile applies the supplied ProfileChange to the Profile that matches the specified ID
// in the profileStore associated with the Context.
func (c Context) UpdateProfile(id uuid.ID, change ProfileChange) error {
	if c.profiles == nil {
		return ErrNoProfileStore
	}
	return c.profiles.updateProfile(id, change)
}

// UpdateProfiles applies the supplied BulkProfileChange to every Profile that matches one of the
// specified IDs in the profileStore associated with the Context.
func (c Context) UpdateProfiles(ids []uuid.ID, change BulkProfileChange) error {
	if c.profiles == nil {
		return ErrNoProfileStore
	}
	return c.profiles.updateProfiles(ids, change)
}

// DeleteProfile removes the specified Profile from the profileStore associated with the Context.
func (c Context) DeleteProfile(id uuid.ID) error {
	if c.profiles == nil {
		return ErrNoProfileStore
	}
	return c.profiles.deleteProfile(id)
}

// AddLogin stores the passed Login in the profileStore associated with the Context. It also associates
// the newly-created Login with the Orofile in login.ProfileID.
func (c Context) AddLogin(login Login) error {
	if c.profiles == nil {
		return ErrNoProfileStore
	}
	return c.profiles.addLogin(login)
}

// RemoveLogin removes the specified Login from the profileStore associated with the Context, provided
// the Login has a ProfileID property that matches the profile ID passed in. It also disassociates the
// deleted Login from the Profile in login.ProfileID.
func (c Context) RemoveLogin(value string, profile uuid.ID) error {
	if c.profiles == nil {
		return ErrNoProfileStore
	}
	return c.profiles.removeLogin(value, profile)
}

// RemoveLoginsByProfile removes all Logins connected to the specified Profile in the profileStore
// associated with the Context and disassociates them from the Profile.
func (c Context) RemoveLoginsByProfile(profile uuid.ID) error {
	if c.profiles == nil {
		return ErrNoProfileStore
	}
	return c.profiles.removeLoginsByProfile(profile)
}

// RecordLoginUse sets the LastUsed property of the Login specified in the profileStore associated with
// the Context to the value passed in as when.
func (c Context) RecordLoginUse(value string, when time.Time) error {
	if c.profiles == nil {
		return ErrNoProfileStore
	}
	return c.profiles.recordLoginUse(value, when)
}

// ListLogins returns a slice of up to num Logins associated with the specified Profile from the profileStore
// associated with the Context, skipping offset Profiles.
func (c Context) ListLogins(profile uuid.ID, num, offset int) ([]Login, error) {
	if c.profiles == nil {
		return []Login{}, ErrNoProfileStore
	}
	return c.profiles.listLogins(profile, num, offset)
}

// GetToken returns the Token specified from the tokenStore associated with the Context.
// If refresh is true, the token input should be compared against the refresh tokens, not the
// access tokens.
func (c Context) GetToken(token string, refresh bool) (Token, error) {
	if c.tokens == nil {
		return Token{}, ErrNoTokenStore
	}
	return c.tokens.getToken(token, refresh)
}

// SaveToken stores the passed Token in the tokenStore associated with the Context.
func (c Context) SaveToken(token Token) error {
	if c.tokens == nil {
		return ErrNoTokenStore
	}
	return c.tokens.saveToken(token)
}

// RevokeToken revokes the Token identfied by the passed token string from the tokenStore associated
// with the context. If refresh is true, the token input should be compared against the refresh tokens,
// not the access tokens.
func (c Context) RevokeToken(token string, refresh bool) error {
	if c.tokens == nil {
		return ErrNoTokenStore
	}
	return c.tokens.revokeToken(token, refresh)
}

// RevokeTokensByProfileID revokes the Tokens associated with the Profile identified by the passed ID in
// the tokenStore associated with the Context.
func (c Context) RevokeTokensByProfileID(profileID uuid.ID) error {
	if c.tokens == nil {
		return ErrNoTokenStore
	}
	return c.tokens.revokeTokensByProfileID(profileID)
}

func (c Context) RevokeTokensByClientID(clientID uuid.ID) error {
	if c.tokens == nil {
		return ErrNoTokenStore
	}
	return c.tokens.revokeTokensByClientID(clientID)
}

// GetTokensByProfileID returns a slice of up to num Tokens with a ProfileID that matches the specified
// profileID from the tokenStore associated with the Context, skipping offset Tokens.
func (c Context) GetTokensByProfileID(profileID uuid.ID, num, offset int) ([]Token, error) {
	if c.tokens == nil {
		return []Token{}, ErrNoTokenStore
	}
	return c.tokens.getTokensByProfileID(profileID, num, offset)
}

// CreateSession stores the passed Session in the sessionStore associated with the Context.
func (c Context) CreateSession(session Session) error {
	if c.sessions == nil {
		return ErrNoSessionStore
	}
	return c.sessions.createSession(session)
}

// GetSession returns the Session specified from the sessionStore associated with the Context.
func (c Context) GetSession(id string) (Session, error) {
	if c.sessions == nil {
		return Session{}, ErrNoSessionStore
	}
	return c.sessions.getSession(id)
}

// TerminateSession sets the Session identified by the passed ID as inactive in the sessionStore assocated
// with the Context.
func (c Context) TerminateSession(id string) error {
	if c.sessions == nil {
		return ErrNoSessionStore
	}
	return c.sessions.terminateSession(id)
}

// TerminateSessionsByProfile sets the Sessions associated with the passed Profile ID as inactive in the
// sessionStore associated with the Context.
func (c Context) TerminateSessionsByProfile(profile uuid.ID) error {
	if c.sessions == nil {
		return ErrNoSessionStore
	}
	return c.sessions.terminateSessionsByProfile(profile)
}

// RemoveSession removes the Session identified by the passed ID from the sessionStore associated with
// the Context.
func (c Context) RemoveSession(id string) error {
	if c.sessions == nil {
		return ErrNoSessionStore
	}
	return c.sessions.removeSession(id)
}

// ListSessions returns a slice of up to num Sessions from the sessionStore associated with the Context,
// ordered by the date they were created, descending. If before.IsZero() returns false, only Sessions
// that were created before that time will be returned. If profile is not nil, only Sessions belonging to
// that Profile will be returned.
func (c Context) ListSessions(profile uuid.ID, before time.Time, num int64) ([]Session, error) {
	if c.sessions == nil {
		return []Session{}, ErrNoSessionStore
	}
	return c.sessions.listSessions(profile, before, num)
}

func (c Context) CreateScopes(scopes []Scope) error {
	if c.scopes == nil {
		return ErrNoScopeStore
	}
	return c.scopes.createScopes(scopes)
}

func (c Context) GetScopes(ids []string) ([]Scope, error) {
	if c.scopes == nil {
		return []Scope{}, ErrNoScopeStore
	}
	return c.scopes.getScopes(ids)
}

func (c Context) UpdateScope(id string, change ScopeChange) error {
	if c.scopes == nil {
		return ErrNoScopeStore
	}
	return c.scopes.updateScope(id, change)
}

func (c Context) RemoveScopes(ids []string) error {
	if c.scopes == nil {
		return ErrNoScopeStore
	}
	return c.scopes.removeScopes(ids)
}

func (c Context) ListScopes() ([]Scope, error) {
	if c.scopes == nil {
		return []Scope{}, ErrNoScopeStore
	}
	return c.scopes.listScopes()
}
