package auth

import (
	"context"
	"errors"
	"fmt"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
	"regexp"
	"strings"
)

type mapStoreResourcePlugin struct {
	ConfigPaths []ConfigPathVal
}

type ConfigPathVal struct {
	ExactMatchPath string
	Methods        []string
	ResourceUri    string
}

func NewMapStoreResourcePlugin(resourceMap map[string]string) (ResourcePlugin, error) {
	configPaths, err := validateAndFormatResourceMap(resourceMap)
	if err != nil {
		return nil, err
	}
	return &mapStoreResourcePlugin{
		ConfigPaths: configPaths,
	}, nil
}

func NewMapStoreResourceFunc(resourceMap map[string]string) (GetResourceFunc, error) {
	helper, err := NewMapStoreResourcePlugin(resourceMap)
	if err != nil {
		return nil, err
	}
	return func(ctx context.Context, param *AccessParam) (*ResourceRet, error) {
		return helper.GetResource(ctx, param)
	}, nil
}

func (d *mapStoreResourcePlugin) GetResource(ctx context.Context, req *AccessParam) (*ResourceRet, error) {
	if d.ConfigPaths == nil || len(d.ConfigPaths) == 0 {
		return nil, errors.New("config path is empty: path not match resource_uri")
	}
	tilog.Info(ctx, "get client resource_uri", zap.Any("resource_uri size", len(d.ConfigPaths)))
	path := getRequestPath(req)
	method := getRequestMethod(req)
	for _, configPath := range d.ConfigPaths {
		matched, reqUri, err := matchPath(path, method, configPath)
		if err != nil {
			return nil, err
		}
		if matched {
			return &ResourceRet{
				ReqUri:      reqUri,
				ResourceUri: configPath.ResourceUri,
			}, nil
		}
	}
	return nil, errors.New(fmt.Sprintf("path not match resource_uri, path: %s, method: %s", path, method))
}

func matchPath(reqPath, reqMethod string, configPathVal ConfigPathVal) (bool, string, error) {
	pathExpr := configPathVal.ExactMatchPath
	compile := regexp.MustCompile(pathExpr)
	// check whether path match the pathExpr
	matched := compile.MatchString(reqPath)
	if !matched {
		return false, "", nil
	}
	var methodMatch = true
	if len(configPathVal.Methods) > 0 {
		methodMatch = false
		rm := strings.ToLower(reqMethod)
		for _, m := range configPathVal.Methods {
			if m == rm {
				methodMatch = true
				break
			}
		}
	}
	if !methodMatch {
		return false, "", nil
	}
	// find path groups
	pathGroups := compile.FindStringSubmatch(reqPath)
	resourceGroups := regexp.MustCompile(ResourceExprNO).FindAllString(configPathVal.ResourceUri, -1)
	// pathGroups[0] is the exact match group
	if len(pathGroups)-1 != len(resourceGroups) {
		return false, "", errors.New(fmt.Sprintf("pathExpr matched path, but variable count not match, pathExpr: %s, reqPath: %s, resourceUri: %s", pathExpr, reqPath, configPathVal.ResourceUri))
	}
	var retResource = configPathVal.ResourceUri
	for i := 1; i < len(pathGroups); i++ {
		retResource = strings.Replace(retResource, resourceGroups[i-1], pathGroups[i], 1)
	}
	return true, retResource, nil
}

func validateAndFormatResourceMap(resourceMap map[string]string) ([]ConfigPathVal, error) {
	if resourceMap == nil || len(resourceMap) == 0 {
		return nil, errors.New("resource map is empty")
	}
	var formatPaths []ConfigPathVal
	for pathExpr, resourceExpr := range resourceMap {
		pathPattern, methods, err := parsePathPattern(pathExpr)
		if err != nil {
			return nil, err
		}
		// pathPattern: /api/v1/orgs/(\d+)/poc-applications
		// resourceExpr: /orgs/{org_id}/owner
		pathExactMatchExpr := fmt.Sprintf(ExactMatchFormatter, 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(PathExprNO).FindAllString(pathPattern, -1)
		resourceGroups := regexp.MustCompile(ResourceExprNO).FindAllString(resourceExpr, -1)
		if len(pathGroups) != len(resourceGroups) {
			return nil, errors.New(fmt.Sprintf("variable count not match in path_patten and resource_uri, path_patten: %s, resource_uri: %s", pathExpr, resourceExpr))
		}

		pathVal := ConfigPathVal{
			ExactMatchPath: pathExactMatchExpr,
			Methods:        methods,
			ResourceUri:    resourceExpr,
		}
		formatPaths = append(formatPaths, pathVal)
	}
	return formatPaths, nil
}

type metadataStoreResourcePlugin struct {
	MetadataResourceKey      string
	MetadataResourceParamKey string
}

func NewMetadataStoreResourcePlugin(metadataResourceKey, metadataResourceParamKey string) (ResourcePlugin, error) {
	if metadataResourceKey == "" {
		return nil, errors.New("metadataResourceKey is empty")
	}
	return &metadataStoreResourcePlugin{
		MetadataResourceKey:      metadataResourceKey,
		MetadataResourceParamKey: metadataResourceParamKey,
	}, nil
}

func (d *metadataStoreResourcePlugin) GetResource(_ context.Context, req *AccessParam) (*ResourceRet, error) {
	mResource, ok := req.Metadata[d.MetadataResourceKey]
	if !ok {
		return nil, errors.New("not found resource int metadata")
	}
	resource, ok := mResource.(string)
	if !ok {
		return nil, errors.New("resource type is not string")
	}

	resourceUri := resource

	resourceGroups := regexp.MustCompile(ResourceExprNO).FindAllString(resource, -1)
	if resourceGroups != nil && len(resourceGroups) > 0 {
		mParams, ok := req.Metadata[d.MetadataResourceParamKey]
		if !ok {
			return nil, errors.New("resource params not found int metadata")
		}
		params, ok := mParams.(map[string]string)
		if !ok {
			return nil, errors.New("resource params' type is not map[string]string")
		}
		// find the variable in params and replace
		for _, group := range resourceGroups {
			variable := group[1 : len(group)-1]
			value, ok := params[variable]
			if !ok {
				return nil, errors.New(fmt.Sprintf("resource variable not found value in resource params, resource: %s, params: %v", resource, params))
			}
			resource = strings.Replace(resource, group, value, 1)
		}
	}

	return &ResourceRet{
		ReqUri:      resource,
		ResourceUri: resourceUri,
	}, nil
}

type metadataStoreAllResourcePlugin struct {
	MetadataResourceMapKey string
}

func NewMetadataStoreAllResourcePlugin(metadataResourceMapKey string) (ResourcePlugin, error) {
	if metadataResourceMapKey == "" {
		return nil, errors.New("metadataResourceMapKey is empty")
	}
	return &metadataStoreAllResourcePlugin{
		MetadataResourceMapKey: metadataResourceMapKey,
	}, nil
}

func (d *metadataStoreAllResourcePlugin) GetResource(ctx context.Context, req *AccessParam) (*ResourceRet, error) {
	resourceMapI, ok := req.Metadata[d.MetadataResourceMapKey]
	if !ok {
		return nil, errors.New("not found resource map in metadata")
	}
	resourceMap, ok := resourceMapI.(map[string]string)
	if !ok {
		return nil, errors.New("resource map type is not map[string]string")
	}

	if resourceMap == nil || len(resourceMap) == 0 {
		return nil, errors.New("resource map is empty: path not match resource_uri")
	}
	reqPath := getRequestPath(req)
	reqMethod := getRequestMethod(req)
	for pathExpr, resourceExpr := range resourceMap {
		pathPattern, methods, err := parsePathPattern(pathExpr)
		if err != nil {
			tilog.Warn(ctx, fmt.Sprintf("parse path pattern error: %s", pathExpr), zap.Any("err", err))
			continue
		}
		// pathPattern: /api/v1/orgs/(\d+)/poc-applications
		// resourceExpr: /orgs/{org_id}/owner
		pathExactMatchExpr := fmt.Sprintf(ExactMatchFormatter, pathPattern)
		// check the pathPattern valid
		_, err = regexp.Compile(pathExactMatchExpr)
		if err != nil {
			tilog.Warn(ctx, fmt.Sprintf("path pattern invalid: %s", pathExpr), zap.Any("err", err))
			continue
		}
		// check the variable count in pathPattern and resourceExpr
		pathGroups := regexp.MustCompile(PathExprNO).FindAllString(pathPattern, -1)
		resourceGroups := regexp.MustCompile(ResourceExprNO).FindAllString(resourceExpr, -1)
		if len(pathGroups) != len(resourceGroups) {
			tilog.Warn(ctx, fmt.Sprintf("variable count not match in path_patten and resource_uri, path_patten: %s, resource_uri: %s, ignore to use", pathExpr, resourceExpr))
			continue
		}
		configPath := ConfigPathVal{
			ExactMatchPath: pathExactMatchExpr,
			Methods:        methods,
			ResourceUri:    resourceExpr,
		}
		matched, reqUri, err := matchPath(reqPath, reqMethod, configPath)
		if err != nil {
			tilog.Warn(ctx, fmt.Sprintf("match path error, req_path: %s, req_method: %s, config_path: %v", reqPath, reqMethod, configPath))
			continue
		}
		if matched {
			return &ResourceRet{
				ReqUri:      reqUri,
				ResourceUri: configPath.ResourceUri,
			}, nil
		}
	}
	return nil, errors.New(fmt.Sprintf("path not match resource_uri, path: %s, method: %s", reqPath, reqMethod))
}
