package s3tables

import (
	"context"
	"fmt"
	"net/http"
	"reflect"
	"strings"

	"github.com/seaweedfs/seaweedfs/weed/glog"
	"github.com/seaweedfs/seaweedfs/weed/iam/integration"
	"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
)

// IAMAuthorizer allows s3tables handlers to evaluate IAM policies without importing s3api.
type IAMAuthorizer interface {
	IsActionAllowed(ctx context.Context, request *integration.ActionRequest) (bool, error)
}

// SetIAMAuthorizer injects the IAM authorizer for policy-based access checks.
func (h *S3TablesHandler) SetIAMAuthorizer(authorizer IAMAuthorizer) {
	h.iamAuthorizer = authorizer
}

func (h *S3TablesHandler) shouldUseIAM(r *http.Request, identityActions, identityPolicyNames []string) bool {
	if h.iamAuthorizer == nil || r == nil {
		return false
	}
	if s3_constants.GetIdentityFromContext(r) == nil {
		return false
	}
	// When default-allow is enabled, keep anonymous requests on the legacy path
	// to preserve zero-config behavior (IAM policies are not available for anonymous).
	if h.defaultAllow && isAnonymousIdentity(r) {
		return false
	}
	// An empty inline `identityActions` slice doesn't mean the identity has no
	// permissions—it just means authorization lives in IAM policies or session
	// tokens instead of static action lists. We therefore prefer the IAM path
	// whenever inline actions are absent and fall back to default policy names
	// or session tokens.
	if hasSessionToken(r) {
		return true
	}
	if len(identityActions) == 0 {
		return true
	}
	return len(identityPolicyNames) > 0
}

func isAnonymousIdentity(r *http.Request) bool {
	val, ok := getIdentityStructValue(r)
	if !ok {
		return false
	}
	if nameField := val.FieldByName("Name"); nameField.IsValid() && nameField.Kind() == reflect.String {
		if nameField.String() == s3_constants.AccountAnonymousId {
			return true
		}
	}
	accountField := val.FieldByName("Account")
	if accountField.IsValid() && !accountField.IsNil() {
		if accountField.Kind() == reflect.Ptr {
			accountField = accountField.Elem()
		}
		if accountField.Kind() == reflect.Struct {
			if idField := accountField.FieldByName("Id"); idField.IsValid() && idField.Kind() == reflect.String {
				if idField.String() == s3_constants.AccountAnonymousId {
					return true
				}
			}
		}
	}
	return false
}

func hasSessionToken(r *http.Request) bool {
	return extractSessionToken(r) != ""
}

func extractSessionToken(r *http.Request) string {
	if token := r.Header.Get(s3_constants.SeaweedFSSessionTokenHeader); token != "" {
		return token
	}
	if token := r.Header.Get("X-Amz-Security-Token"); token != "" {
		return token
	}
	return r.URL.Query().Get("X-Amz-Security-Token")
}

func (h *S3TablesHandler) authorizeIAMAction(r *http.Request, identityPolicyNames []string, action string, resources ...string) (bool, error) {
	if h.iamAuthorizer == nil {
		err := fmt.Errorf("nil iamAuthorizer in authorizeIAMAction")
		glog.V(2).Infof("S3Tables: %v", err)
		return false, err
	}
	principal := r.Header.Get(s3_constants.SeaweedFSPrincipalHeader)
	if principal == "" {
		principal = getIdentityPrincipalArn(r)
	}
	if principal == "" {
		return false, fmt.Errorf("missing principal for IAM authorization")
	}

	if !strings.Contains(action, ":") {
		action = "s3tables:" + action
	}

	sessionToken := extractSessionToken(r)

	requestContext := buildIAMRequestContext(r, getIdentityClaims(r))
	policyNames := identityPolicyNames
	if len(policyNames) == 0 {
		policyNames = getIdentityPolicyNames(r)
	}

	if len(resources) == 0 {
		return false, fmt.Errorf("no resources provided to authorizeIAMAction")
	}
	checkedResource := false
	for _, resource := range resources {
		if resource == "" {
			continue
		}
		checkedResource = true
		allowed, err := h.iamAuthorizer.IsActionAllowed(r.Context(), &integration.ActionRequest{
			Principal:      principal,
			Action:         action,
			Resource:       resource,
			SessionToken:   sessionToken,
			RequestContext: requestContext,
			PolicyNames:    policyNames,
		})
		if err != nil {
			glog.V(2).Infof("S3Tables: IAM authorization error action=%s resource=%s principal=%s: %v", action, resource, principal, err)
			return false, err
		}
		if !allowed {
			err := fmt.Errorf("access denied by IAM for resource %s", resource)
			return false, err
		}
	}
	if !checkedResource {
		return false, fmt.Errorf("no non-empty resources provided to authorizeIAMAction")
	}
	return true, nil
}

func getIdentityPrincipalArn(r *http.Request) string {
	val, ok := getIdentityStructValue(r)
	if !ok {
		return ""
	}
	field := val.FieldByName("PrincipalArn")
	if field.IsValid() && field.Kind() == reflect.String {
		return field.String()
	}
	return ""
}

func getIdentityPolicyNames(r *http.Request) []string {
	val, ok := getIdentityStructValue(r)
	if !ok {
		return nil
	}
	field := val.FieldByName("PolicyNames")
	if !field.IsValid() || field.Kind() != reflect.Slice {
		return nil
	}
	policies := make([]string, 0, field.Len())
	for i := 0; i < field.Len(); i++ {
		item := field.Index(i)
		if item.Kind() == reflect.String {
			policies = append(policies, item.String())
		} else if item.CanInterface() {
			policies = append(policies, fmt.Sprint(item.Interface()))
		}
	}
	if len(policies) == 0 {
		return nil
	}
	return policies
}

func getIdentityClaims(r *http.Request) map[string]interface{} {
	val, ok := getIdentityStructValue(r)
	if !ok {
		return nil
	}
	field := val.FieldByName("Claims")
	if !field.IsValid() || field.Kind() != reflect.Map || field.IsNil() {
		return nil
	}
	if field.Type().Key().Kind() != reflect.String {
		return nil
	}
	claims := make(map[string]interface{}, field.Len())
	for _, key := range field.MapKeys() {
		if key.Kind() != reflect.String {
			continue
		}
		val := field.MapIndex(key)
		if !val.IsValid() {
			continue
		}
		claims[key.String()] = val.Interface()
	}
	if len(claims) == 0 {
		return nil
	}
	return claims
}

func buildIAMRequestContext(r *http.Request, claims map[string]interface{}) map[string]interface{} {
	ctx := make(map[string]interface{})
	if ua := r.Header.Get("User-Agent"); ua != "" {
		ctx["userAgent"] = ua
	}
	if referer := r.Header.Get("Referer"); referer != "" {
		ctx["referer"] = referer
	}
	for k, v := range claims {
		if strings.HasPrefix(k, "jwt:") {
			if _, exists := ctx[k]; !exists {
				ctx[k] = v
			}
		}
	}
	for k, v := range claims {
		if strings.HasPrefix(k, "jwt:") {
			continue
		}
		if _, exists := ctx[k]; !exists {
			ctx[k] = v
		}
		jwtKey := "jwt:" + k
		if _, exists := ctx[jwtKey]; !exists {
			ctx[jwtKey] = v
		}
	}
	if len(ctx) == 0 {
		return nil
	}
	return ctx
}

// getIdentityStructValue fetches the identity struct held in the request context.
// The identity is expected to be a pointer to a struct with the fields used by
// the reflection helpers (PrincipalArn string, PolicyNames []string,
// Claims map[string]interface{}).
// This helper centralizes the nil-check and ptr-deref logic so callers focus on
// reading the specific fields they need.
func getIdentityStructValue(r *http.Request) (reflect.Value, bool) {
	identityRaw := s3_constants.GetIdentityFromContext(r)
	if identityRaw == nil {
		return reflect.Value{}, false
	}
	val := reflect.ValueOf(identityRaw)
	if val.Kind() == reflect.Ptr {
		val = val.Elem()
	}
	if val.Kind() != reflect.Struct {
		return reflect.Value{}, false
	}
	return val, true
}
