package interceptor

import (
	"context"
	"fmt"
	"regexp"
	"sort"
	"strconv"
	"strings"
	"sync"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/pingcap/log"
	"github.com/tidbcloud/account/idl/pbgen/proto/account"
	"github.com/tidbcloud/observability/pkg/common/constants"
	"github.com/tidbcloud/tiinf/sdk/auth"
	"github.com/tidbcloud/tiinf/sdk/rbac"
	"github.com/tidbcloud/tiinf/sdk/rbac/common"
	"github.com/tidbcloud/tiinf/sdk/rbac/helperimpl"
	"github.com/tidbcloud/tiinf/sdk/rbac/useadapter"
	"go.uber.org/zap"
)

var (
	permissionCheckers sync.Map
)

const currentProjectIDHeader = "x-current-project-id"

var orgProjectClusterPathPattern = regexp.MustCompile(`^(/api/v1/external.*/orgs/[^/]+/projects/)([^/]+)(/clusters.*)$`)
var orgClusterGetPathPattern = regexp.MustCompile(`^(/api/v1/external/[^/]+/orgs/[^/]+/)clusters/([^/]+)/?$`)

var pathPatternResourceMap = map[string]string{
	"/api/v1/external.*/orgs/(.+)/projects/(.+)/clusters/(.+)/monitor-alertrules": "o11y/orgs/{org_id}/projects/{project_id}/tidbs/{cluster_id}/monitor-alertrules",
	"/api/v1/external.*/orgs/(.+)/projects/(.+)/clusters/(.+)/alerts":             "o11y/orgs/{org_id}/projects/{project_id}/tidbs/{cluster_id}/alerts",
	"get:/api/v1/external.*/orgs/(.+)/projects/(.+)/clusters/(.+)/":               "o11y/orgs/{org_id}/projects/{project_id}/tidbs/{cluster_id}/get",
	"/api/v1/external.*/orgs/([^/]+)/clusters/([^/]+)/monitor-alertrules":         "o11y/orgs/{org_id}/tidbs/{cluster_id}/monitor-alertrules",
	"/api/v1/external.*/orgs/([^/]+)/clusters/([^/]+)/alerts":                     "o11y/orgs/{org_id}/tidbs/{cluster_id}/alerts",
	"get:/api/v1/external.*/orgs/([^/]+)/clusters/([^/]+)/":                       "o11y/orgs/{org_id}/tidbs/{cluster_id}/get",
	"get:/api/v1/external/byoc/orgs/([^/]+)/byocVcpuPools/observabilityVcpu":      "o11y/orgs/{org_id}/byoc/observabilityVcpu",
	"get:/api/v1/external.*/orgs/([^/]+)/byocDataplane/[^/]+/?":                   "o11y/orgs/{org_id}/byoc/get",
}

var OpenAPIPathPatternResourceMap = map[string]string{
	"/orgs/(\\d+)/projects/(\\d+)/.+/v1beta1/clusters/(\\d+)/monitoringPipelines.*": "o11y/orgs/{org_id}/projects/{project_id}/tidbs/{cluster_id}/monitoringpipelines",
	"/orgs/(\\d+)/projects/\\d+/.+/v1beta2/tidbs/(\\d+)/monitoringPipelines.*":      "o11y/orgs/{org_id}/tidbs/{cluster_id}/monitoringpipelines",
}

var pathWhitelist = []string{}

func PermissionCheckerAdapter(c *gin.Context) {
	bizType := c.Param("bizType")
	checker, err := getPermissionChecker(bizType)
	if err != nil {
		log.Error("getPermissionChecker failed", zap.Error(err))
		authError(c, "Unauthorized")
		return
	}
	result, err := checker.Handle(c.Request.Context(), GenAccessParamWithCurrentProject(c))
	if err != nil {
		log.Error("useadapter.Handle failed", zap.Error(err))
	}
	if !useadapter.DefaultAccessDecideCompleteExecFunc(c.Writer, c.Request, result, err) {
		c.Abort()
		return
	}
	c.Next()
}

// GenAccessParamWithCurrentProject prefers x-current-project-id and rewrites them to org/project/cluster style paths for RBAC resource mapping.
// NOTE: x-current-project-id is a client-supplied header and is not validated against the identity claims.
// This means a caller can set this header to any project ID they have access to, potentially allowing the
// RBAC check to be performed against a different project than the one in the URL path. This is a known
// limitation and is intentionally accepted for the current use case.
func GenAccessParamWithCurrentProject(c *gin.Context) *common.AccessParam {
	urlPath := c.Request.URL.Path
	projectID := c.GetHeader(currentProjectIDHeader)
	if projectID != "" {
		urlPath = injectProjectIDToPath(urlPath, projectID)
	}
	urlPath = rewriteByocGetClusterPath(urlPath, c.Param("bizType"), c.Request.Method)

	param := common.AccessParam{
		Metadata: map[string]interface{}{
			common.METADATA_PATH:   urlPath,
			common.METADATA_METHOD: c.Request.Method,
		},
		Req: c,
	}
	return &param
}

func injectProjectIDToPath(path, projectID string) string {
	if projectID == "" || !orgProjectClusterPathPattern.MatchString(path) {
		return path
	}

	matches := orgProjectClusterPathPattern.FindStringSubmatch(path)
	if len(matches) != 4 {
		return path
	}
	return fmt.Sprintf("%s%s%s", matches[1], projectID, matches[3])
}

func rewriteByocGetClusterPath(path, bizType, method string) string {
	if !strings.EqualFold(method, "GET") || !strings.EqualFold(bizType, constants.DefaultByocBizType) {
		return path
	}

	matches := orgClusterGetPathPattern.FindStringSubmatch(path)
	if len(matches) != 3 {
		return path
	}
	return fmt.Sprintf("%sbyocDataplane/%s", matches[1], matches[2])
}

func getPermissionChecker(bizType string) (rbac.PermissionChecker, error) {
	if checker, ok := permissionCheckers.Load(bizType); ok {
		return checker.(rbac.PermissionChecker), nil
	}

	cli, err := getAccountProviderSvcClient(bizType)
	if err != nil {
		return nil, err
	}

	resourceH := helperimpl.NewMapStoreResourceHelper(pathPatternResourceMap)
	requestH := helperimpl.NewWhitelistRequestHelper(pathWhitelist...)
	permissionH := &o11yPermissionHelper{
		cli:           cli,
		readTimeout:   6 * time.Second,
		pureTemplates: buildPureTemplates(pathPatternResourceMap),
	}
	checker := rbac.NewPermissionChecker(common.GetIdentityFunc(ginGetIdentityFunc), resourceH, permissionH)
	checker.RegisterRequestHelper(requestH)

	permissionCheckers.Store(bizType, checker)
	return checker, nil
}

// nasty hack as tiinf do not support a multi-env permissionHelper for now.
// o11yPermissionHelper is a custom implementation of common.PermissionHelper
type o11yPermissionHelper struct {
	cli           account.AccountAPIServiceClient
	readTimeout   time.Duration
	pureTemplates []pureResourceTemplate
}

// pureResourceTemplate pairs a resource template (the pathPatternResourceMap value with
// {placeholders} intact, e.g. "o11y/orgs/{org_id}/tidbs/{cluster_id}/alerts") with a
// regex that matches its substituted form, so that the template can be recovered from a
// substituted resource uri and sent to the account service as PureResourceUri.
type pureResourceTemplate struct {
	expr     *regexp.Regexp
	template string
}

var resourcePlaceholder = regexp.MustCompile(`\{[^}]+\}`)

// buildPureTemplates compiles a matching regex for every resource template in the map.
// Templates are sorted by length descending so the most specific template wins when a
// substituted resource happens to match more than one.
func buildPureTemplates(m map[string]string) []pureResourceTemplate {
	seen := make(map[string]struct{}, len(m))
	out := make([]pureResourceTemplate, 0, len(m))
	for _, template := range m {
		if _, ok := seen[template]; ok {
			continue
		}
		seen[template] = struct{}{}
		out = append(out, pureResourceTemplate{expr: compileTemplateRegex(template), template: template})
	}
	sort.Slice(out, func(i, j int) bool { return len(out[i].template) > len(out[j].template) })
	return out
}

// compileTemplateRegex turns a resource template like
// "o11y/orgs/{org_id}/tidbs/{cluster_id}/alerts" into the anchored regex
// "^o11y/orgs/[^/]+/tidbs/[^/]+/alerts$" that matches its substituted form.
func compileTemplateRegex(template string) *regexp.Regexp {
	parts := resourcePlaceholder.Split(template, -1)
	for i, p := range parts {
		parts[i] = regexp.QuoteMeta(p)
	}
	return regexp.MustCompile("^" + strings.Join(parts, "[^/]+") + "$")
}

// pureResourceOf recovers the resource template (placeholders intact) from a substituted
// resource uri. It returns an empty string when no template matches.
func (h *o11yPermissionHelper) pureResourceOf(substituted string) string {
	for _, t := range h.pureTemplates {
		if t.expr.MatchString(substituted) {
			return t.template
		}
	}
	return ""
}

func (h *o11yPermissionHelper) HasPermission(ctx context.Context, identity, resource string) (bool, error) {
	identInt, err := strconv.ParseUint(identity, 10, 64)
	if err != nil {
		return false, fmt.Errorf("defaultPermissionHelper: %s", err.Error())
	}
	req := account.RbacPermissionRequest{
		UserId:          identInt,
		ResourceUri:     resource,
		PureResourceUri: h.pureResourceOf(resource),
	}

	timeout := h.readTimeout
	if timeout == 0 {
		timeout = 6 * time.Second
	}
	tctx, cf := context.WithTimeout(ctx, timeout)
	defer cf()

	rsp, err := h.cli.HasPermissionForUser(tctx, &req)
	if err != nil {
		return false, fmt.Errorf("defaultPermissionHelper: %s", err.Error())
	}
	return rsp.AuthResult, nil
}

// GenAccessParamWithPrefixOrgPrj is used for prefixing orgs and projects info to the openapi request path,
// in order to make account rbac client happy.
func GenAccessParamWithPrefixOrgPrj(c *gin.Context) (*auth.AccessParam, error) {
	orgId, projectId := c.GetString(authMetaOrgId), c.GetString(authMetaProjectId)
	urlPath := fmt.Sprintf("/orgs/%s/projects/%s%s", orgId, projectId, c.Request.URL.Path)
	param := auth.AccessParam{
		Metadata: map[string]interface{}{
			auth.AccessMetadataPath:   urlPath,
			auth.AccessMetadataMethod: c.Request.Method,
		},
		Req: c,
	}
	return &param, nil
}
