package authorizer

import (
	"fmt"
	"path"
	"strings"

	"k8s.io/kubernetes/pkg/util/sets"

	authorizationapi "github.com/openshift/origin/pkg/authorization/api"
)

type DefaultAuthorizationAttributes struct {
	Verb              string
	APIVersion        string
	APIGroup          string
	Resource          string
	ResourceName      string
	RequestAttributes interface{}
	NonResourceURL    bool
	URL               string
}

// ToDefaultAuthorizationAttributes coerces Action to DefaultAuthorizationAttributes.  Namespace is not included
// because the authorizer takes that information on the context
func ToDefaultAuthorizationAttributes(in authorizationapi.Action) DefaultAuthorizationAttributes {
	return DefaultAuthorizationAttributes{
		Verb:         in.Verb,
		APIGroup:     in.Group,
		APIVersion:   in.Version,
		Resource:     in.Resource,
		ResourceName: in.ResourceName,
	}
}

func (a DefaultAuthorizationAttributes) RuleMatches(rule authorizationapi.PolicyRule) (bool, error) {
	if a.IsNonResourceURL() {
		if a.nonResourceMatches(rule) {
			if a.verbMatches(rule.Verbs) {
				return true, nil
			}
		}

		return false, nil
	}

	if a.verbMatches(rule.Verbs) {
		if a.apiGroupMatches(rule.APIGroups) {

			allowedResourceTypes := authorizationapi.NormalizeResources(rule.Resources)
			if a.resourceMatches(allowedResourceTypes) {
				if a.nameMatches(rule.ResourceNames) {
					// this rule matches the request, so we should check the additional restrictions to be sure that it's allowed
					if rule.AttributeRestrictions != nil {
						switch rule.AttributeRestrictions.(type) {
						case (*authorizationapi.IsPersonalSubjectAccessReview):
							return IsPersonalAccessReview(a)
						default:
							return false, fmt.Errorf("unable to interpret: %#v", rule.AttributeRestrictions)
						}
					}

					return true, nil
				}
			}
		}
	}

	return false, nil
}

func (a DefaultAuthorizationAttributes) apiGroupMatches(allowedGroups []string) bool {
	// allowedGroups is expected to be small, so I don't feel bad about this.
	for _, allowedGroup := range allowedGroups {
		if allowedGroup == authorizationapi.APIGroupAll {
			return true
		}

		if strings.ToLower(allowedGroup) == strings.ToLower(a.GetAPIGroup()) {
			return true
		}
	}

	return false
}

func (a DefaultAuthorizationAttributes) verbMatches(verbs sets.String) bool {
	return verbs.Has(authorizationapi.VerbAll) || verbs.Has(strings.ToLower(a.GetVerb()))
}

func (a DefaultAuthorizationAttributes) resourceMatches(allowedResourceTypes sets.String) bool {
	return allowedResourceTypes.Has(authorizationapi.ResourceAll) || allowedResourceTypes.Has(strings.ToLower(a.GetResource()))
}

// nameMatches checks to see if the resourceName of the action is in a the specified whitelist.  An empty whitelist indicates that any name is allowed.
// An empty string in the whitelist should only match the action's resourceName if the resourceName itself is empty string.  This behavior allows for the
// combination of a whitelist for gets in the same rule as a list that won't have a resourceName.  I don't recommend writing such a rule, but we do
// handle it like you'd expect: white list is respected for gets while not preventing the list you explicitly asked for.
func (a DefaultAuthorizationAttributes) nameMatches(allowedResourceNames sets.String) bool {
	if len(allowedResourceNames) == 0 {
		return true
	}

	return allowedResourceNames.Has(a.GetResourceName())
}

func (a DefaultAuthorizationAttributes) GetVerb() string {
	return a.Verb
}

// nonResourceMatches take the remainer of a URL and attempts to match it against a series of explicitly allowed steps that can end in a wildcard
func (a DefaultAuthorizationAttributes) nonResourceMatches(rule authorizationapi.PolicyRule) bool {
	for allowedNonResourcePath := range rule.NonResourceURLs {
		// if the allowed resource path ends in a wildcard, check to see if the URL starts with it
		if strings.HasSuffix(allowedNonResourcePath, "*") {
			if strings.HasPrefix(a.GetURL(), allowedNonResourcePath[0:len(allowedNonResourcePath)-1]) {
				return true
			}
		}

		// if we have an exact match, return true
		if a.GetURL() == allowedNonResourcePath {
			return true
		}
	}

	return false
}

// splitPath returns the segments for a URL path.
func splitPath(thePath string) []string {
	thePath = strings.Trim(path.Clean(thePath), "/")
	if thePath == "" {
		return []string{}
	}
	return strings.Split(thePath, "/")
}

// DefaultAuthorizationAttributes satisfies the Action interface
var _ Action = DefaultAuthorizationAttributes{}

func (a DefaultAuthorizationAttributes) GetAPIVersion() string {
	return a.APIVersion
}

func (a DefaultAuthorizationAttributes) GetAPIGroup() string {
	return a.APIGroup
}

func (a DefaultAuthorizationAttributes) GetResource() string {
	return a.Resource
}

func (a DefaultAuthorizationAttributes) GetResourceName() string {
	return a.ResourceName
}

func (a DefaultAuthorizationAttributes) GetRequestAttributes() interface{} {
	return a.RequestAttributes
}

func (a DefaultAuthorizationAttributes) IsNonResourceURL() bool {
	return a.NonResourceURL
}

func (a DefaultAuthorizationAttributes) GetURL() string {
	return a.URL
}