package middleware

import (
	"context"
	"strings"
	"sync"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/rs/zerolog/log"
	"golang.org/x/sync/singleflight"

	"github.com/pingcap-inc/nexuscrm-api-host/hostruntime"
)

// permCacheEntry holds cached permission maps with an expiration time.
type permCacheEntry struct {
	systemPerms map[string]bool
	customPerms map[string]bool
	expiresAt   time.Time
}

// PermissionCache provides an in-memory TTL cache for effective permissions,
// following Salesforce's pattern of session-level permission caching.
// It caches only system and custom permissions keyed by (userID,
// JWT-baked profileID). Object and field authorization stays request-scoped in
// the permission engine so cache misses cannot trigger all-object projections.
// The session-bound SSoT model means profile/role IDs come from the JWT and are
// never refreshed mid-session — admin reassignment / rename triggers session revocation in
// records/persistence_service.go:Update, the user re-logs in, and the new
// JWT carries fresh IDs.
type PermissionCache struct {
	mu      sync.RWMutex
	entries map[string]permCacheEntry // key: "userID|profileID"

	ttl time.Duration
	sf  singleflight.Group // coalesce concurrent loads for the same user

	epochMu          sync.RWMutex
	epoch            uint64
	userEpochs       map[string]uint64
	asyncWarmMu      sync.Mutex
	asyncWarmSlots   chan struct{}
	asyncWarmTimeout time.Duration
	warming          sync.Map // key: "userID|profileID" -> struct{}{}
}

// DefaultPermCacheTTL is the default cache lifetime for permission maps.
const DefaultPermCacheTTL = 5 * time.Minute
const defaultPermCacheAsyncWarmLimit = 32
const defaultPermCacheAsyncWarmTimeout = 5 * time.Second

// NewPermissionCache creates a cache with the given TTL.
func NewPermissionCache(ttl time.Duration) *PermissionCache {
	return &PermissionCache{
		entries:          make(map[string]permCacheEntry),
		userEpochs:       make(map[string]uint64),
		asyncWarmSlots:   make(chan struct{}, defaultPermCacheAsyncWarmLimit),
		asyncWarmTimeout: defaultPermCacheAsyncWarmTimeout,
		ttl:              ttl,
	}
}

func (pc *PermissionCache) cacheKey(userID, profileID string) string {
	return userID + "|" + profileID
}

func (pc *PermissionCache) get(userID, profileID string) (map[string]bool, map[string]bool, bool) {
	pc.mu.RLock()
	defer pc.mu.RUnlock()
	entry, ok := pc.entries[pc.cacheKey(userID, profileID)]
	if !ok || time.Now().After(entry.expiresAt) {
		return nil, nil, false
	}
	return entry.systemPerms, entry.customPerms, true
}

func (pc *PermissionCache) set(userID, profileID string, systemPerms, customPerms map[string]bool) {
	pc.mu.Lock()
	defer pc.mu.Unlock()
	pc.entries[pc.cacheKey(userID, profileID)] = permCacheEntry{
		systemPerms: systemPerms,
		customPerms: customPerms,
		expiresAt:   time.Now().Add(pc.ttl),
	}
}

type permissionCacheWarmEpoch struct {
	global uint64
	user   uint64
}

func (pc *PermissionCache) captureWarmEpoch(userID string) permissionCacheWarmEpoch {
	pc.epochMu.RLock()
	defer pc.epochMu.RUnlock()
	return permissionCacheWarmEpoch{
		global: pc.epoch,
		user:   pc.userEpochs[strings.TrimSpace(userID)],
	}
}

func (pc *PermissionCache) epochCurrentLocked(userID string, epoch permissionCacheWarmEpoch) bool {
	return pc.epoch == epoch.global && pc.userEpochs[strings.TrimSpace(userID)] == epoch.user
}

func (pc *PermissionCache) publishIfWarmEpoch(userID string, epoch permissionCacheWarmEpoch, publish func()) bool {
	pc.epochMu.RLock()
	defer pc.epochMu.RUnlock()
	if !pc.epochCurrentLocked(userID, epoch) {
		return false
	}
	publish()
	return true
}

func (pc *PermissionCache) setIfWarmEpoch(userID, profileID string, epoch permissionCacheWarmEpoch, systemPerms, customPerms map[string]bool) bool {
	return pc.publishIfWarmEpoch(userID, epoch, func() {
		pc.set(userID, profileID, systemPerms, customPerms)
	})
}

// WarmUser pre-loads system and custom permissions for a user.
// Uses singleflight to coalesce concurrent warm requests for the same user.
func (pc *PermissionCache) WarmUser(ctx context.Context, userID, profileID string, permSvc hostruntime.PermissionProvider, requestContext hostruntime.PermissionRequestContext) {
	pc.warmUserWithEpoch(ctx, userID, profileID, permSvc, hostruntime.NormalizePermissionRequestContext(requestContext), pc.captureWarmEpoch(userID))
}

func (pc *PermissionCache) warmUserWithEpoch(ctx context.Context, userID, profileID string, permSvc hostruntime.PermissionProvider, requestContext hostruntime.PermissionRequestContext, epoch permissionCacheWarmEpoch) {
	requestContext = hostruntime.NormalizePermissionRequestContext(requestContext)
	ctx = requestContext.Install(ctx)
	key := pc.cacheKey(userID, profileID)
	requestID := hostruntime.RequestLineageFromContext(ctx).RequestID

	// Use singleflight to avoid thundering herd when many requests
	// arrive for the same user before the cache is populated.
	pc.sf.Do("warm:"+key, func() (interface{}, error) {
		start := time.Now()

		phaseStart := time.Now()
		sysPerms, err := permSvc.GetEffectiveSystemPermissions(ctx, userID, profileID)
		if err != nil {
			log.Warn().Err(err).Str("user_id", userID).Msg("WarmUser: failed to load system permissions")
			logWarmPhaseTiming(requestID, userID, profileID, "system_custom", 0, phaseStart, err)
			return nil, nil
		}
		custPerms, err := permSvc.GetEffectiveCustomPermissions(ctx, userID, profileID)
		if err != nil {
			log.Warn().Err(err).Str("user_id", userID).Msg("WarmUser: failed to load custom permissions")
			logWarmPhaseTiming(requestID, userID, profileID, "system_custom", 0, phaseStart, err)
			return nil, nil
		}
		if !pc.setIfWarmEpoch(userID, profileID, epoch, sysPerms, custPerms) {
			log.Debug().Str("user_id", userID).Msg("WarmUser: skipped stale system/custom permissions publish after invalidation")
			return nil, nil
		}
		logWarmPhaseTiming(requestID, userID, profileID, "system_custom", len(sysPerms)+len(custPerms), phaseStart, nil)

		log.Info().
			Str("event", "perm_warm_total").
			Str("request_id", requestID).
			Str("user_id", userID).
			Str("profile_id", profileID).
			Int64("elapsed_ms", time.Since(start).Milliseconds()).
			Msg("Permission cache warmed")
		return nil, nil
	})
}

func logWarmPhaseTiming(requestID, userID, profileID, phase string, rows int, started time.Time, err error) {
	evt := log.Info()
	status := "success"
	if err != nil {
		evt = log.Error().Err(err)
		status = "error"
	}
	evt.
		Str("event", "perm_warm_phase_timing").
		Str("request_id", requestID).
		Str("user_id", userID).
		Str("profile_id", profileID).
		Str("phase", phase).
		Str("status", status).
		Int("rows", rows).
		Int64("elapsed_ms", time.Since(started).Milliseconds()).
		Msg("Permission warm phase timing")
}

func (pc *PermissionCache) WarmUserAsync(sourceCtx context.Context, requestID, userID, profileID string, permSvc hostruntime.PermissionProvider, requestContext hostruntime.PermissionRequestContext) {
	key := pc.cacheKey(userID, profileID)
	if _, inFlight := pc.warming.LoadOrStore(key, struct{}{}); inFlight {
		return
	}
	release, ok := pc.acquireAsyncWarmSlot()
	if !ok {
		pc.warming.Delete(key)
		log.Warn().Str("user_id", userID).Msg("WarmUserAsync: async permission warm gate full; dropping warm request")
		return
	}
	epoch := pc.captureWarmEpoch(userID)
	timeout := pc.asyncWarmTimeoutDuration()
	go func() {
		defer pc.warming.Delete(key)
		defer release()
		defer recoverPermissionWarmPanic(requestID, userID, profileID, "async_root")
		ctx, cancel := permissionWarmAsyncContext(sourceCtx, requestID, timeout)
		defer cancel()
		pc.warmUserWithEpoch(ctx, userID, profileID, permSvc, requestContext, epoch)
	}()
}

func recoverPermissionWarmPanic(requestID, userID, profileID, phase string) {
	if recovered := recover(); recovered != nil {
		log.Error().
			Interface("panic", recovered).
			Str("event", "perm_warm_panic").
			Str("request_id", requestID).
			Str("user_id", userID).
			Str("profile_id", profileID).
			Str("phase", phase).
			Msg("Recovered from permission warm panic")
	}
}

func permissionWarmAsyncContext(sourceCtx context.Context, requestID string, timeout time.Duration) (context.Context, context.CancelFunc) {
	ctx, cancel := context.WithTimeout(context.Background(), timeout)
	if sourceCtx == nil {
		sourceCtx = context.Background()
	}
	lineage := hostruntime.RequestLineageFromContext(sourceCtx)
	if requestID == "" {
		requestID = lineage.RequestID
	}
	if requestID != "" {
		lineage.RequestID = requestID
	}
	ctx = hostruntime.ContextWithRequestLineage(ctx, lineage)
	return ctx, cancel
}

func (pc *PermissionCache) acquireAsyncWarmSlot() (func(), bool) {
	slots, _ := pc.asyncWarmConfig()
	select {
	case slots <- struct{}{}:
		return func() { <-slots }, true
	default:
		return nil, false
	}
}

func (pc *PermissionCache) asyncWarmTimeoutDuration() time.Duration {
	_, timeout := pc.asyncWarmConfig()
	return timeout
}

func (pc *PermissionCache) asyncWarmConfig() (chan struct{}, time.Duration) {
	pc.asyncWarmMu.Lock()
	defer pc.asyncWarmMu.Unlock()
	if pc.asyncWarmSlots == nil {
		pc.asyncWarmSlots = make(chan struct{}, defaultPermCacheAsyncWarmLimit)
	}
	if pc.asyncWarmTimeout <= 0 {
		pc.asyncWarmTimeout = defaultPermCacheAsyncWarmTimeout
	}
	return pc.asyncWarmSlots, pc.asyncWarmTimeout
}

// Invalidate removes all cached entries. Call when permissions are modified
// (e.g., profile permission changes, permission set assignment changes).
func (pc *PermissionCache) Invalidate() {
	pc.epochMu.Lock()
	pc.epoch++
	pc.userEpochs = make(map[string]uint64)
	pc.mu.Lock()
	pc.entries = make(map[string]permCacheEntry)
	pc.mu.Unlock()
	pc.epochMu.Unlock()
}

// InvalidateUser removes cached entries for a specific user.
func (pc *PermissionCache) InvalidateUser(userID string) {
	userID = strings.TrimSpace(userID)
	pc.epochMu.Lock()
	if pc.userEpochs == nil {
		pc.userEpochs = make(map[string]uint64)
	}
	pc.userEpochs[userID]++
	pc.mu.Lock()
	for key := range pc.entries {
		if permissionCacheKeyHasUser(key, userID) {
			delete(pc.entries, key)
		}
	}
	pc.mu.Unlock()
	pc.epochMu.Unlock()
}

func permissionCacheKeyHasUser(key, userID string) bool {
	return userID != "" && len(key) > len(userID) && key[:len(userID)+1] == userID+"|"
}

// PermissionLoader loads user permissions on each request (Salesforce style).
// Uses an in-memory cache so the DB is queried at most once per user per TTL
// interval, matching Salesforce's session-level permission caching.
//
// SSoT MODEL — session-bound, refreshed only on re-login (CLAUDE.md
// §"Salesforce Boundary / Formula context SSoT"):
//
// ProfileId / UserRoleID / ProfileName / RoleName on the gin context's
// auth.UserSession are sourced ENTIRELY from the JWT decoded by RequireAuth.
// This middleware does NOT refresh those fields from the database mid-session.
// Doing so would make the IDs lead the names (PermissionLoader can refresh
// IDs but cannot atomically refresh ProfileName/RoleName under all error
// paths) and would let validation rules and flow formulas observe inconsistent
// session-bound facts within a single save transaction.
//
// Instead, admin writes that mutate session-bound facts trigger session
// revocation in records/persistence_service.go:Update — User Profile/Role
// reassignment, Profile.Name rename, UserRole.Name rename all revoke the
// affected sessions. The user's next request fails JWT validation (revoked
// session), they re-log in, and the new JWT carries fresh ProfileId/Name and
// UserRoleID/RoleName atomically.
//
// This middleware therefore only:
//  1. Loads system + custom permissions for (userID, JWT-baked profileID)
//  2. Caches those bounded permission maps
//
// This middleware should be applied globally after RequireAuth.
func PermissionLoaderWithSessionContext(permSvc hostruntime.PermissionProvider, cache *PermissionCache, requestContext hostruntime.PermissionRequestContext, sessionContext PermissionSessionContext) gin.HandlerFunc {
	requestContext = hostruntime.NormalizePermissionRequestContext(requestContext)
	sessionContext = NormalizePermissionSessionContext(sessionContext)
	return func(c *gin.Context) {
		session := sessionContext.ReadSession(c)
		if !session.Exists {
			c.Next()
			return
		}
		if !session.Valid {
			log.Warn().Msg("PermissionLoader: user context is not auth.UserSession type")
			c.Next()
			return
		}
		authUser := session.User

		// Install permission diagnostics plus request-scoped access/read caches
		// through the host-runtime protocol so this middleware no longer reaches
		// into kernel package context keys directly.
		c.Request = c.Request.WithContext(requestContext.Install(c.Request.Context()))
		requestID := sessionContext.RequestID(c)
		defer logPermissionDiagnostics(c, authUser, requestID, requestContext)

		// JWT-baked profile/role IDs are the SSoT for this request.
		// Reassignment → session revocation → re-login → fresh JWT.
		currentProfileID := authUser.ProfileID

		// Try cache first
		if sysPerms, custPerms, hit := cache.get(authUser.ID, currentProfileID); hit {
			authUser.SystemPermissions = sysPerms
			sessionContext.WriteCurrentSession(c, authUser)
			sessionContext.WritePermissions(c, sysPerms, custPerms)
			c.Next()
			return
		}

		// Cache miss — load system+custom permissions from DB
		systemPerms, err := permSvc.GetEffectiveSystemPermissions(c.Request.Context(), authUser.ID, currentProfileID)
		if err != nil {
			log.Warn().Err(err).Str("user_id", authUser.ID).Msg("Failed to load system permissions")
			systemPerms = make(map[string]bool)
		}

		customPerms, err := permSvc.GetEffectiveCustomPermissions(c.Request.Context(), authUser.ID, currentProfileID)
		if err != nil {
			log.Warn().Err(err).Str("user_id", authUser.ID).Msg("Failed to load custom permissions")
			customPerms = make(map[string]bool)
		}

		cache.set(authUser.ID, currentProfileID, systemPerms, customPerms)
		authUser.SystemPermissions = systemPerms
		sessionContext.WriteCurrentSession(c, authUser)

		sessionContext.WritePermissions(c, systemPerms, customPerms)
		c.Next()
	}
}

// PermissionWarmer binds a cache to its backend permission provider and request
// context installer for consumers, such as OAuth login, that should not know
// those wiring details.
type PermissionWarmer struct {
	cache          *PermissionCache
	provider       hostruntime.PermissionProvider
	requestContext hostruntime.PermissionRequestContext
}

func NewPermissionWarmer(cache *PermissionCache, provider hostruntime.PermissionProvider, requestContext hostruntime.PermissionRequestContext) *PermissionWarmer {
	return &PermissionWarmer{
		cache:          cache,
		provider:       provider,
		requestContext: hostruntime.NormalizePermissionRequestContext(requestContext),
	}
}

func (w *PermissionWarmer) WarmUserAsync(ctx context.Context, requestID, userID, profileID string) {
	if w == nil || w.cache == nil {
		return
	}
	w.cache.WarmUserAsync(ctx, requestID, userID, profileID, w.provider, w.requestContext)
}

func logPermissionDiagnostics(c *gin.Context, user hostruntime.UserSession, requestID string, requestContext hostruntime.PermissionRequestContext) {
	snapshot := requestContext.Snapshot(c.Request.Context())
	if !snapshot.HasWork() {
		return
	}

	path := c.FullPath()
	if path == "" {
		path = c.Request.URL.Path
	}

	event := log.Info().
		Str("request_id", requestID).
		Int("status", c.Writer.Status()).
		Str("method", c.Request.Method).
		Str("path", c.Request.URL.Path).
		Str("route", path).
		Str("user_id", user.ID).
		Int("perm_system_calls", snapshot.SystemCalls).
		Float64("perm_system_ms", durationMillis(snapshot.SystemDuration)).
		Int("perm_custom_calls", snapshot.CustomCalls).
		Float64("perm_custom_ms", durationMillis(snapshot.CustomDuration)).
		Int("perm_recordtype_calls", snapshot.RecordTypeCalls).
		Int("perm_recordtype_rows", snapshot.RecordTypeRows).
		Float64("perm_recordtype_ms", durationMillis(snapshot.RecordTypeDur)).
		Int("perm_object_db_fallbacks", snapshot.ObjectDBFallbacks).
		Float64("perm_object_db_ms", durationMillis(snapshot.ObjectDBDuration)).
		Int("perm_object_cache_hits", snapshot.ObjectCacheHits).
		Int("perm_field_db_fallbacks", snapshot.FieldDBFallbacks).
		Float64("perm_field_db_ms", durationMillis(snapshot.FieldDBDuration)).
		Int("perm_field_cache_hits", snapshot.FieldCacheHits).
		Int("perm_object_bulk_calls", snapshot.ObjectBulkCalls).
		Int("perm_object_bulk_rows", snapshot.ObjectBulkRows).
		Float64("perm_object_bulk_ms", durationMillis(snapshot.ObjectBulkDuration)).
		Int("perm_field_bulk_calls", snapshot.FieldBulkCalls).
		Int("perm_field_bulk_rows", snapshot.FieldBulkRows).
		Float64("perm_field_bulk_ms", durationMillis(snapshot.FieldBulkDuration)).
		Int("perm_field_bulk_rejected", snapshot.FieldBulkRejected).
		Int("perm_effective_access_calls", snapshot.EffectiveAccessCalls).
		Int("perm_effective_access_fields", snapshot.EffectiveAccessFields).
		Float64("perm_effective_access_ms", durationMillis(snapshot.EffectiveAccessDuration))

	if email := strings.TrimSpace(user.Email); email != "" {
		event = event.Str("email", email)
	}

	event.Msg("Permission diagnostics")
}

func durationMillis(d time.Duration) float64 {
	return float64(d.Microseconds()) / 1000
}

func RequirePermissionWithSessionContext(sessionContext PermissionSessionContext, perm string) gin.HandlerFunc {
	sessionContext = NormalizePermissionSessionContext(sessionContext)
	perm = strings.TrimSpace(perm)
	return func(c *gin.Context) {
		session := sessionContext.ReadSession(c)
		if !session.Exists {
			c.AbortWithStatusJSON(403, gin.H{"error": "USER_NOT_FOUND"})
			return
		}

		if !session.Valid {
			c.AbortWithStatusJSON(500, gin.H{"error": "INTERNAL_ERROR"})
			return
		}

		permLookup := sessionContext.SystemPermissions(c)
		if !permLookup.Exists {
			c.AbortWithStatusJSON(403, gin.H{"error": "PERMISSIONS_NOT_LOADED"})
			return
		}
		if !permLookup.Valid {
			c.AbortWithStatusJSON(500, gin.H{"error": "INTERNAL_ERROR"})
			return
		}
		if !permLookup.Permissions[perm] {
			c.AbortWithStatusJSON(403, gin.H{
				"error":      "MISSING_PERMISSION",
				"permission": perm,
			})
			return
		}

		c.Next()
	}
}

func RequireAnyPermissionWithSessionContext(sessionContext PermissionSessionContext, perms ...string) gin.HandlerFunc {
	sessionContext = NormalizePermissionSessionContext(sessionContext)
	normalized := make([]string, 0, len(perms))
	for _, perm := range perms {
		if perm = strings.TrimSpace(perm); perm != "" {
			normalized = append(normalized, perm)
		}
	}
	return func(c *gin.Context) {
		session := sessionContext.ReadSession(c)
		if !session.Exists {
			c.AbortWithStatusJSON(403, gin.H{"error": "USER_NOT_FOUND"})
			return
		}

		if !session.Valid {
			c.AbortWithStatusJSON(500, gin.H{"error": "INTERNAL_ERROR"})
			return
		}

		permLookup := sessionContext.SystemPermissions(c)
		if !permLookup.Exists {
			c.AbortWithStatusJSON(403, gin.H{"error": "PERMISSIONS_NOT_LOADED"})
			return
		}
		if !permLookup.Valid {
			c.AbortWithStatusJSON(500, gin.H{"error": "INTERNAL_ERROR"})
			return
		}
		for _, perm := range normalized {
			if permLookup.Permissions[perm] {
				c.Next()
				return
			}
		}

		c.AbortWithStatusJSON(403, gin.H{
			"error":       "MISSING_PERMISSION",
			"permissions": normalized,
		})
	}
}
