package accountserver

import (
	"errors"
	"fmt"
	"log"
)

// GetResourceRequest requests a specific resource.
type GetResourceRequest struct {
	AdminResourceRequestBase
}

// GetResourceResponse is the response type for GetResourceRequest.
type GetResourceResponse struct {
	Resource *Resource `json:"resource"`
	Owner    string    `json:"owner"`
}

// Serve the request.
func (r *GetResourceRequest) Serve(rctx *RequestContext) (interface{}, error) {
	resp := GetResourceResponse{
		Resource: rctx.Resource,
	}
	if rctx.User != nil {
		resp.Owner = rctx.User.Name
	}
	return &resp, nil
}

// SearchResourceRequest searches for resources matching a pattern.
type SearchResourceRequest struct {
	AdminRequestBase

	Pattern string `json:"pattern"`
	Limit   int    `json:"limit"`
}

// Validate the request.
func (r *SearchResourceRequest) Validate(rctx *RequestContext) error {
	if r.Pattern == "" {
		return newValidationError(nil, "pattern", "empty pattern")
	}
	return nil
}

// SearchResourceResponse is the response type for SearchResourceRequest.
type SearchResourceResponse struct {
	Results []*RawResource `json:"results"`
}

// Serve the request.
func (r *SearchResourceRequest) Serve(rctx *RequestContext) (interface{}, error) {
	results, err := rctx.TX.SearchResource(rctx.Context, r.Pattern, r.Limit)
	if err != nil {
		return nil, err
	}
	return &SearchResourceResponse{Results: results}, nil
}

// setResourceStatus sets the status of a single resource (shared
// logic between enable / disable resource methods).
func setResourceStatus(rctx *RequestContext, status string) error {
	rsrc := rctx.Resource
	rsrc.Status = status
	if err := rctx.TX.UpdateResource(rctx.Context, rsrc); err != nil {
		return err
	}
	rctx.audit.Log(rctx, rsrc, fmt.Sprintf("status set to %s", status))
	return nil
}

// SetResourceStatusRequest modifies the status of a resource
// belonging to the user (admin-only).
type SetResourceStatusRequest struct {
	AdminResourceRequestBase

	Status string `json:"status"`
}

// Validate the request.
func (r *SetResourceStatusRequest) Validate(rctx *RequestContext) error {
	if !isValidStatusByResourceType(rctx.Resource.Type, r.Status) {
		return newValidationError(nil, "status", "invalid or unknown status")
	}
	return nil
}

// Serve the request.
func (r *SetResourceStatusRequest) Serve(rctx *RequestContext) (interface{}, error) {
	return nil, setResourceStatus(rctx, r.Status)
}

// AdminUpdateResourceRequest updates arbitrary fields on a Resource
// (privileged management operation).
type AdminUpdateResourceRequest struct {
	AdminResourceRequestBase

	Resource *Resource `json:"resource"`
}

func replaceResource(rsrcs []*Resource, r *Resource) []*Resource {
	var out []*Resource
	for _, rsrc := range rsrcs {
		if rsrc.ID != r.ID {
			out = append(out, rsrc)
		}
	}
	out = append(out, r)
	return out
}

func (r *AdminUpdateResourceRequest) Validate(rctx *RequestContext) error {
	// Verify some invariants (resources can't be renamed, type
	// can't be changed, etc). Prevent updates that would require
	// re-templating, since we don't really support derived fields.
	if r.Resource.ID != rctx.Resource.ID {
		return newValidationError(nil, "id", "can't update resource ID")
	}
	if r.Resource.Type != rctx.Resource.Type {
		return newValidationError(nil, "type", "can't update resource type")
	}
	if r.Resource.ParentID != rctx.Resource.ParentID {
		return newValidationError(nil, "parent_id", "can't update resource parent ID")
	}

	// The logic here mirrors somewhat that in
	// CreateResourcesRequest.Validate, where we assemble a fake
	// User with the "new" resources, and use it for user-level
	// validation.
	var tplUser *User
	if rctx.User != nil {
		userCopy := rctx.User.User
		userCopy.Resources = replaceResource(userCopy.Resources, r.Resource)
		tplUser = &userCopy
	}

	// Validate the resource.
	if err := rctx.resourceValidator.validateResource(rctx, r.Resource, tplUser, false); err != nil {
		return err
	}

	// Validate the user.
	if tplUser != nil {
		// If the resource has an owner, validate it (checks that the new
		// resources do not violate user invariants).
		if err := checkUserInvariants(tplUser); err != nil {
			log.Printf("validation error while updating resources: %v", err)
			return newValidationError(nil, "global", err.Error())
		}
	}

	return nil
}

func (r *AdminUpdateResourceRequest) Serve(rctx *RequestContext) (interface{}, error) {
	if err := rctx.TX.UpdateResource(rctx.Context, r.Resource); err != nil {
		return nil, err
	}
	rctx.audit.Log(rctx, r.Resource, "resource updated by admin")
	return nil, nil
}

// CheckResourceAvailabilityRequest is an unauthenticated request that
// can tell if a given resource ID is available or not.
type CheckResourceAvailabilityRequest struct {
	Type string `json:"type"`
	Name string `json:"name"`
}

// CheckResourceAvailabilityResponse is the response type for
// CheckResourceAvailabilityRequest.
type CheckResourceAvailabilityResponse struct {
	Available bool `json:"available"`
}

// Authorize the request - this one requires no authentication.
func (r *CheckResourceAvailabilityRequest) Authorize(rctx *RequestContext) error {
	return nil
}

// PopulateContext is a no-op for this type.
func (r *CheckResourceAvailabilityRequest) PopulateContext(rctx *RequestContext) error {
	return nil
}

// Validate the request.
func (r *CheckResourceAvailabilityRequest) Validate(rctx *RequestContext) error {
	if r.Name == "" {
		return newValidationError(nil, "name", "name is unset")
	}
	return nil
}

// Serve the request.
func (r *CheckResourceAvailabilityRequest) Serve(rctx *RequestContext) (interface{}, error) {
	var check ValidatorFunc
	switch r.Type {
	case ResourceTypeEmail, ResourceTypeMailingList:
		check = rctx.validationCtx.isAvailableEmailAddr()
	case ResourceTypeDomain:
		check = rctx.validationCtx.isAvailableDomain()
	case ResourceTypeWebsite:
		check = rctx.validationCtx.isAvailableWebsite()
	case ResourceTypeDAV:
		check = rctx.validationCtx.isAvailableDAV()
	case ResourceTypeDatabase:
		check = rctx.validationCtx.isAvailableDatabase()
	default:
		return nil, errors.New("unknown resource type")
	}

	var resp CheckResourceAvailabilityResponse
	if err := check(rctx, r.Name); err == nil {
		resp.Available = true
	}
	return &resp, nil
}

// ResetResourcePasswordRequest will reset the password associated
// with a resource (if the resource type supports it). It will
// generate a random password and return it to the caller.
type ResetResourcePasswordRequest struct {
	ResourceRequestBase
}

// ResetResourcePasswordResponse is the response type for
// ResetResourcePasswordRequest.
type ResetResourcePasswordResponse struct {
	Password string `json:"password"`
}

// Validate the request.
func (r *ResetResourcePasswordRequest) Validate(rctx *RequestContext) error {
	switch rctx.Resource.Type {
	case ResourceTypeDAV, ResourceTypeDatabase, ResourceTypeMailingList:
	case ResourceTypeEmail:
		return newValidationError(nil, "type", "can't reset email passwords with this API")
	default:
		return newValidationError(nil, "type", "can't reset password on this resource type")
	}
	return nil
}

// Serve the request.
func (r *ResetResourcePasswordRequest) Serve(rctx *RequestContext) (interface{}, error) {
	// TODO: this needs a resource-type switch, because in some
	// cases we may want to call out to other backends in order to
	// reset credentials for certain resources that have their own
	// secondary authentication databases (lists, mysql).
	password, err := doResetResourcePassword(rctx.Context, rctx.TX, rctx.Resource)
	if err != nil {
		return nil, err
	}
	return &ResetResourcePasswordResponse{
		Password: password,
	}, nil
}

// MoveResourceRequest is an administrative operation to move resources
// between shards. Resources that are part of a group are moved all at
// once regardless of which individual ResourceID is provided as long
// as it belongs to the group.
type MoveResourceRequest struct {
	AdminResourceRequestBase
	Shard string `json:"shard"`
}

// Validate the request.
func (r *MoveResourceRequest) Validate(rctx *RequestContext) error {
	// TODO: check shard
	return nil
}

// MoveResourceResponse is the response type for MoveResourceRequest.
type MoveResourceResponse struct {
	MovedIDs []string `json:"moved_ids"`
}

// Serve the request.
func (r *MoveResourceRequest) Serve(rctx *RequestContext) (interface{}, error) {
	resources := []*Resource{rctx.Resource}

	// If we have an associated user, collect all related
	// resources, as they should all be moved at once.
	if rctx.User != nil && rctx.Resource.Group != "" {
		resources = append(resources, rctx.User.GetResourcesByGroup(rctx.Resource.Group)...)
	}

	// We need to enforce consistency between email resources and
	// the user shard, so that temporary data can be colocated
	// with email storage.
	if rctx.Resource.Type == ResourceTypeEmail && rctx.User.Shard != r.Shard {
		rctx.User.Shard = r.Shard
		if err := rctx.TX.UpdateUser(rctx.Context, &rctx.User.User); err != nil {
			return nil, err
		}
	}

	var resp MoveResourceResponse
	for _, rsrc := range resources {
		rsrc.Shard = r.Shard
		if err := rctx.TX.UpdateResource(rctx.Context, rsrc); err != nil {
			return nil, err
		}
		resp.MovedIDs = append(resp.MovedIDs, rsrc.ID.String())
	}
	return &resp, nil
}

// AddEmailAliasRequest adds an alias (additional address) to an email resource.
type AddEmailAliasRequest struct {
	ResourceRequestBase
	Addr string `json:"addr"`
}

// Validate the request.
func (r *AddEmailAliasRequest) Validate(rctx *RequestContext) error {
	if rctx.Resource.Type != ResourceTypeEmail {
		return newValidationError(nil, "type", "this operation only works on email resources")
	}

	// Allow at most 5 aliases.
	if len(rctx.Resource.Email.Aliases) >= maxEmailAliases {
		return newValidationError(nil, "addr", "too many aliases")
	}

	if err := rctx.fieldValidators.newEmail(rctx, r.Addr); err != nil {
		return newValidationError(nil, "addr", err.Error())
	}
	return nil
}

const maxEmailAliases = 5

// Serve the request.
func (r *AddEmailAliasRequest) Serve(rctx *RequestContext) (interface{}, error) {
	rctx.Resource.Email.Aliases = append(rctx.Resource.Email.Aliases, r.Addr)
	if err := rctx.TX.UpdateResource(rctx.Context, rctx.Resource); err != nil {
		return nil, err
	}

	rctx.audit.Log(rctx, rctx.Resource, fmt.Sprintf("added alias %s", r.Addr))
	return nil, nil
}

// DeleteEmailAliasRequest removes an alias from an email resource.
type DeleteEmailAliasRequest struct {
	ResourceRequestBase
	Addr string `json:"addr"`
}

// Validate the request.
func (r *DeleteEmailAliasRequest) Validate(rctx *RequestContext) error {
	if rctx.Resource.Type != ResourceTypeEmail {
		return newValidationError(nil, "type", "this operation only works on email resources")
	}
	return nil
}

// Serve the request.
func (r *DeleteEmailAliasRequest) Serve(rctx *RequestContext) (interface{}, error) {
	var aliases []string
	for _, a := range rctx.Resource.Email.Aliases {
		if a != r.Addr {
			aliases = append(aliases, a)
		}
	}
	rctx.Resource.Email.Aliases = aliases
	if err := rctx.TX.UpdateResource(rctx.Context, rctx.Resource); err != nil {
		return nil, err
	}

	rctx.audit.Log(rctx, rctx.Resource, fmt.Sprintf("removed alias %s", r.Addr))
	return nil, nil
}