package rbac

import (
	"errors"
	"fmt"
	"regexp"
	"strings"
)

const (
	EXACT_MATCH_FORMATTER = "^%s$"
	RESOURCE_EXPR_NO      = "(?U){.+}"
	PATH_EXPR_NO          = "(?U)\\(.+\\)"
)

const (
	PERMISSION_CACHE_KEY_FORMATTER = "%s:%s"

	PATH_METHOD_SEPARATOR = ":"
	PATH_METHOD_FORMATTER = "%s:%s"
	LOCAL_DEFAULT_METHOD  = "no_method"
)

type resourceUri struct {
	resourceMap map[string]string
}

type resolvedResource struct {
	reqUri      string
	resourceUri string
	params      map[string]string
}

func NewResourceUri(resourceMap map[string]string) (*resourceUri, error) {
	m, err := validateAndFormatResourceMap(resourceMap)
	if err != nil {
		return nil, err
	}

	return &resourceUri{
		resourceMap: m,
	}, nil
}

func (d *resourceUri) Get(path, method string) (string, error) {
	resolved, err := d.GetResolved(path, method)
	if err != nil || resolved == nil {
		return "", err
	}
	return resolved.reqUri, nil
}

func (d *resourceUri) GetResolved(path, method string) (*resolvedResource, error) {
	if len(d.resourceMap) == 0 {
		return nil, nil
	}

	for pathExpr, resourceExpr := range d.resourceMap {
		matched, resource, err := d.matchPath(pathExpr, path, method, resourceExpr)
		if err != nil {
			return nil, err
		}
		if matched {
			return resource, nil
		}
	}
	return nil, nil
}

func (d *resourceUri) matchPath(pathExpr, path, method, resourceExpr string) (bool, *resolvedResource, error) {
	p, m, err := ParsePathPattern(pathExpr)
	if err != nil {
		return false, nil, err
	}

	// method need match and method not match
	if m != LOCAL_DEFAULT_METHOD && m != strings.ToLower(method) {
		return false, nil, nil
	}
	compile := regexp.MustCompile(p)
	// check whether path match the pathExpr
	matched := compile.MatchString(path)
	if !matched {
		return false, nil, nil
	}
	// find path groups
	pathGroups := compile.FindStringSubmatch(path)
	resourceGroups := regexp.MustCompile(RESOURCE_EXPR_NO).FindAllString(resourceExpr, -1)
	// pathGroups[0] is the exact match group
	if len(pathGroups)-1 != len(resourceGroups) {
		return false, nil, errors.New("resource/path variable count not match")
	}
	retResource := resourceExpr
	params := make(map[string]string, len(resourceGroups))
	for i := 1; i < len(pathGroups); i++ {
		group := resourceGroups[i-1]
		value := pathGroups[i]
		params[group[1:len(group)-1]] = value
		retResource = strings.Replace(retResource, group, value, 1)
	}
	return true, &resolvedResource{
		reqUri:      retResource,
		resourceUri: resourceExpr,
		params:      params,
	}, nil
}

func validateAndFormatResourceMap(resourceMap map[string]string) (map[string]string, error) {
	if len(resourceMap) == 0 {
		return nil, errors.New("resourceMap is empty")
	}
	formatResourceMap := map[string]string{}
	for pathExpr, resourceExpr := range resourceMap {
		pathPattern, method, err := ParsePathPattern(pathExpr)
		if err != nil {
			return nil, err
		}
		// pathPattern: /api/v1/orgs/(\d+)/poc-applications
		// resourceExpr: /orgs/{org_id}/owner
		pathExactMatchExpr := fmt.Sprintf(EXACT_MATCH_FORMATTER, pathPattern)
		// check the pathPattern valid
		_, err = regexp.Compile(pathExactMatchExpr)
		if err != nil {
			return nil, err
		}
		// check the variable count in pathPattern and resourceExpr
		pathGroups := regexp.MustCompile(PATH_EXPR_NO).FindAllString(pathPattern, -1)
		resourceGroups := regexp.MustCompile(RESOURCE_EXPR_NO).FindAllString(resourceExpr, -1)
		if len(pathGroups) != len(resourceGroups) {
			return nil, errors.New("variable count not match in path_patten and resource_uri")
		}
		if method == "" {
			method = LOCAL_DEFAULT_METHOD
		}
		formatter := fmt.Sprintf(PATH_METHOD_FORMATTER, strings.ToLower(method), pathExactMatchExpr)
		formatResourceMap[formatter] = resourceExpr
	}
	return formatResourceMap, nil
}

func ParsePathPattern(pathPattern string) (path, method string, err error) {
	split := strings.Split(pathPattern, PATH_METHOD_SEPARATOR)
	if len(split) == 1 {
		path = split[0]
	} else if len(split) == 2 {
		method = split[0]
		path = split[1]
	} else {
		return "", "", fmt.Errorf("invalid path_pattern: %s", pathPattern)
	}
	return path, method, nil
}
