package middleware

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

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

	"github.com/nexuscrm/backend/internal/features/permissions"
	"github.com/nexuscrm/backend/pkg/auth"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
)

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

// objectPermCacheEntry holds cached object-level permissions for all objects.
// key: lowercase(objectAPIName) -> *models.ObjectPermissions
type objectPermCacheEntry struct {
	perms     map[string]*models.ObjectPermissions
	expiresAt time.Time
}

// fieldPermCacheEntry holds cached field-level permissions for all objects/fields.
// key: lowercase(objectAPIName) + "|" + lowercase(fieldAPIName) -> *models.FieldPermissions
type fieldPermCacheEntry struct {
	perms     map[string]*models.FieldPermissions
	expiresAt time.Time
}

// profileCacheEntry caches the DB-resolved profile and role IDs for a user
// so we don't hit the database on every single request. Both profile and role
// reassignments must take effect without re-login (Salesforce semantics).
type profileCacheEntry struct {
	profileID string
	roleID    *string
	expiresAt time.Time
}

// PermissionCache provides an in-memory TTL cache for effective permissions,
// following Salesforce's pattern of session-level permission caching.
// It caches system permissions, custom permissions, object permissions, field permissions,
// and the current profile ID (resolved from DB).
type PermissionCache struct {
	mu      sync.RWMutex
	entries map[string]permCacheEntry // key: "userID|profileID"

	objMu      sync.RWMutex
	objEntries map[string]objectPermCacheEntry // key: "userID|profileID"

	fieldMu      sync.RWMutex
	fieldEntries map[string]fieldPermCacheEntry // key: "userID|profileID"

	profileMu    sync.RWMutex
	profileCache map[string]profileCacheEntry // key: userID

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

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

// profileCacheTTL is shorter than permission TTL so profile reassignments
// propagate quickly without requiring a re-login.
const profileCacheTTL = 60 * time.Second

// NewPermissionCache creates a cache with the given TTL.
func NewPermissionCache(ttl time.Duration) *PermissionCache {
	return &PermissionCache{
		entries:      make(map[string]permCacheEntry),
		objEntries:   make(map[string]objectPermCacheEntry),
		fieldEntries: make(map[string]fieldPermCacheEntry),
		ttl:          ttl,
		profileCache: make(map[string]profileCacheEntry),
	}
}

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),
	}
}

// GetObjectPermission returns a cached object permission for a user, or (nil, false) on miss.
func (pc *PermissionCache) GetObjectPermission(userID, profileID, objectAPIName string) (*models.ObjectPermissions, bool) {
	pc.objMu.RLock()
	defer pc.objMu.RUnlock()
	entry, ok := pc.objEntries[pc.cacheKey(userID, profileID)]
	if !ok || time.Now().After(entry.expiresAt) {
		return nil, false
	}
	perm, ok := entry.perms[strings.ToLower(objectAPIName)]
	return perm, ok
}

// GetFieldPermission returns a cached field permission for a user, or (nil, false) on miss.
func (pc *PermissionCache) GetFieldPermission(userID, profileID, objectAPIName, fieldAPIName string) (*models.FieldPermissions, bool) {
	pc.fieldMu.RLock()
	defer pc.fieldMu.RUnlock()
	entry, ok := pc.fieldEntries[pc.cacheKey(userID, profileID)]
	if !ok || time.Now().After(entry.expiresAt) {
		return nil, false
	}
	key := strings.ToLower(objectAPIName) + "|" + strings.ToLower(fieldAPIName)
	perm, ok := entry.perms[key]
	return perm, ok
}

// HasObjectPermsLoaded checks whether object permissions have been bulk-loaded for this user.
func (pc *PermissionCache) HasObjectPermsLoaded(userID, profileID string) bool {
	pc.objMu.RLock()
	defer pc.objMu.RUnlock()
	entry, ok := pc.objEntries[pc.cacheKey(userID, profileID)]
	return ok && time.Now().Before(entry.expiresAt)
}

// HasFieldPermsLoaded checks whether field permissions have been bulk-loaded for this user.
func (pc *PermissionCache) HasFieldPermsLoaded(userID, profileID string) bool {
	pc.fieldMu.RLock()
	defer pc.fieldMu.RUnlock()
	entry, ok := pc.fieldEntries[pc.cacheKey(userID, profileID)]
	return ok && time.Now().Before(entry.expiresAt)
}

// SetObjectPermissions bulk-sets all object permissions for a user.
func (pc *PermissionCache) SetObjectPermissions(userID, profileID string, perms []models.ObjectPermissions) {
	m := make(map[string]*models.ObjectPermissions, len(perms))
	for i := range perms {
		p := perms[i]
		m[strings.ToLower(p.SobjectType)] = &p
	}
	pc.objMu.Lock()
	defer pc.objMu.Unlock()
	pc.objEntries[pc.cacheKey(userID, profileID)] = objectPermCacheEntry{
		perms:     m,
		expiresAt: time.Now().Add(pc.ttl),
	}
}

// SetFieldPermissions bulk-sets all field permissions for a user.
func (pc *PermissionCache) SetFieldPermissions(userID, profileID string, perms []models.FieldPermissions) {
	m := make(map[string]*models.FieldPermissions, len(perms))
	for i := range perms {
		p := perms[i]
		key := strings.ToLower(p.SobjectType) + "|" + strings.ToLower(p.Field)
		m[key] = &p
	}
	pc.fieldMu.Lock()
	defer pc.fieldMu.Unlock()
	pc.fieldEntries[pc.cacheKey(userID, profileID)] = fieldPermCacheEntry{
		perms:     m,
		expiresAt: time.Now().Add(pc.ttl),
	}
}

// WarmUser pre-loads all permission types for a non-super user.
// Uses singleflight to coalesce concurrent warm requests for the same user.
func (pc *PermissionCache) WarmUser(ctx context.Context, userID, profileID string, permSvc *permissions.PermissionService) {
	if constants.IsSuperUser(profileID) {
		return // super users bypass all checks; nothing to cache
	}

	key := pc.cacheKey(userID, profileID)

	// 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()

		// Load all three permission types concurrently
		var wg sync.WaitGroup
		wg.Add(3)

		go func() {
			defer wg.Done()
			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")
				sysPerms = make(map[string]bool)
			}
			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")
				custPerms = make(map[string]bool)
			}
			pc.set(userID, profileID, sysPerms, custPerms)
		}()

		go func() {
			defer wg.Done()
			objPerms, err := permSvc.ListEffectiveObjectPermissionsForUser(ctx, userID, profileID)
			if err != nil {
				log.Warn().Err(err).Str("user_id", userID).Msg("WarmUser: failed to load object permissions")
				return
			}
			pc.SetObjectPermissions(userID, profileID, objPerms)
		}()

		go func() {
			defer wg.Done()
			fieldPerms, err := permSvc.ListEffectiveFieldPermissionsForUser(ctx, userID, profileID)
			if err != nil {
				log.Warn().Err(err).Str("user_id", userID).Msg("WarmUser: failed to load field permissions")
				return
			}
			pc.SetFieldPermissions(userID, profileID, fieldPerms)
		}()

		wg.Wait()
		log.Info().Str("user_id", userID).Dur("duration_ms", time.Since(start)).Msg("Permission cache warmed")
		return nil, nil
	})
}

// getUserAssignment returns the cached current profile and role IDs for a user.
func (pc *PermissionCache) getUserAssignment(userID string) (string, *string, bool) {
	pc.profileMu.RLock()
	defer pc.profileMu.RUnlock()
	entry, ok := pc.profileCache[userID]
	if !ok || time.Now().After(entry.expiresAt) {
		return "", nil, false
	}
	return entry.profileID, entry.roleID, true
}

// setUserAssignment caches the current profile and role IDs for a user.
func (pc *PermissionCache) setUserAssignment(userID, profileID string, roleID *string) {
	pc.profileMu.Lock()
	defer pc.profileMu.Unlock()
	pc.profileCache[userID] = profileCacheEntry{
		profileID: profileID,
		roleID:    roleID,
		expiresAt: time.Now().Add(profileCacheTTL),
	}
}

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

	pc.objMu.Lock()
	pc.objEntries = make(map[string]objectPermCacheEntry)
	pc.objMu.Unlock()

	pc.fieldMu.Lock()
	pc.fieldEntries = make(map[string]fieldPermCacheEntry)
	pc.fieldMu.Unlock()

	pc.profileMu.Lock()
	pc.profileCache = make(map[string]profileCacheEntry)
	pc.profileMu.Unlock()
}

// InvalidateUser removes cached entries for a specific user.
func (pc *PermissionCache) InvalidateUser(userID string) {
	pc.mu.Lock()
	for key := range pc.entries {
		if len(key) > len(userID) && key[:len(userID)+1] == userID+"|" {
			delete(pc.entries, key)
		}
	}
	pc.mu.Unlock()

	pc.objMu.Lock()
	for key := range pc.objEntries {
		if len(key) > len(userID) && key[:len(userID)+1] == userID+"|" {
			delete(pc.objEntries, key)
		}
	}
	pc.objMu.Unlock()

	pc.fieldMu.Lock()
	for key := range pc.fieldEntries {
		if len(key) > len(userID) && key[:len(userID)+1] == userID+"|" {
			delete(pc.fieldEntries, key)
		}
	}
	pc.fieldMu.Unlock()

	pc.profileMu.Lock()
	delete(pc.profileCache, userID)
	pc.profileMu.Unlock()
}

// 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.
//
// IMPORTANT: The JWT's ProfileId is set at session-creation time and becomes
// stale when a user's profile is reassigned. This middleware resolves the
// CURRENT profile from the database (with a short-lived cache) and updates
// the gin context user, so all downstream handlers and permission checks
// use the correct profile.
//
// This middleware should be applied globally after RequireAuth.
func PermissionLoader(permSvc *permissions.PermissionService, cache *PermissionCache) gin.HandlerFunc {
	return func(c *gin.Context) {
		userInterface, exists := c.Get(constants.ContextKeyUser)
		if !exists {
			c.Next()
			return
		}

		authUser, ok := userInterface.(auth.UserSession)
		if !ok {
			log.Warn().Msg("PermissionLoader: user context is not auth.UserSession type")
			c.Next()
			return
		}

		// Resolve current profile and role from DB (cached for profileCacheTTL).
		// This ensures profile and role reassignments take effect without re-login.
		// Both are user-assignment metadata that downstream RLS, sharing rules,
		// and hierarchy access depend on.
		currentProfileID := authUser.ProfileId
		currentRoleID := authUser.RoleId
		if cachedProfile, cachedRole, hit := cache.getUserAssignment(authUser.ID); hit {
			currentProfileID = cachedProfile
			currentRoleID = cachedRole
		} else {
			if dbProfile, err := permSvc.GetUserProfileID(c.Request.Context(), authUser.ID); err == nil && dbProfile != "" {
				currentProfileID = dbProfile
			}
			if dbRole, err := permSvc.GetUserRoleID(c.Request.Context(), authUser.ID); err == nil {
				currentRoleID = dbRole
			}
			cache.setUserAssignment(authUser.ID, currentProfileID, currentRoleID)
		}

		// If profile or role changed since session creation, update the context
		// user so IsSuperUser(), GetUserFromContext(), and all downstream checks
		// (RLS hierarchy, sharing rules) use the correct values.
		changed := false
		if currentProfileID != authUser.ProfileId {
			authUser.ProfileId = currentProfileID
			changed = true
		}
		if (currentRoleID == nil) != (authUser.RoleId == nil) ||
			(currentRoleID != nil && authUser.RoleId != nil && *currentRoleID != *authUser.RoleId) {
			authUser.RoleId = currentRoleID
			changed = true
		}
		if changed {
			c.Set(constants.ContextKeyUser, authUser)
		}

		// Super-users bypass permission loading — they pass all RequirePermission checks
		if authUser.IsSuperUser() {
			c.Set(constants.ContextKeySystemPermissions, map[string]bool{})
			c.Set(constants.ContextKeyCustomPermissions, map[string]bool{})
			c.Next()
			return
		}

		// Try cache first
		if sysPerms, custPerms, hit := cache.get(authUser.ID, currentProfileID); hit {
			c.Set(constants.ContextKeySystemPermissions, sysPerms)
			c.Set(constants.ContextKeyCustomPermissions, 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)

		// Also trigger full cache warm (object + field perms) in background
		// if not yet loaded, so subsequent permission checks hit memory.
		if !cache.HasObjectPermsLoaded(authUser.ID, currentProfileID) {
			go cache.WarmUser(context.Background(), authUser.ID, currentProfileID, permSvc)
		}

		c.Set(constants.ContextKeySystemPermissions, systemPerms)
		c.Set(constants.ContextKeyCustomPermissions, customPerms)
		c.Next()
	}
}

// RequirePermission checks for a specific system permission.
// System admins (IsSuperUser) automatically have all permissions.
func RequirePermission(perm constants.Permission) gin.HandlerFunc {
	return func(c *gin.Context) {
		userInterface, exists := c.Get(constants.ContextKeyUser)
		if !exists {
			c.AbortWithStatusJSON(403, gin.H{"error": "USER_NOT_FOUND"})
			return
		}

		authUser, ok := userInterface.(auth.UserSession)
		if !ok {
			c.AbortWithStatusJSON(500, gin.H{"error": "INTERNAL_ERROR"})
			return
		}

		if authUser.IsSuperUser() {
			c.Next()
			return
		}

		permsInterface, exists := c.Get(constants.ContextKeySystemPermissions)
		if !exists {
			c.AbortWithStatusJSON(403, gin.H{"error": "PERMISSIONS_NOT_LOADED"})
			return
		}

		perms, ok := permsInterface.(map[string]bool)
		if !ok {
			c.AbortWithStatusJSON(500, gin.H{"error": "INTERNAL_ERROR"})
			return
		}
		if !perms[string(perm)] {
			c.AbortWithStatusJSON(403, gin.H{
				"error":      "MISSING_PERMISSION",
				"permission": perm,
			})
			return
		}

		c.Next()
	}
}

// RequireAnyPermission checks if user has at least one of the specified permissions.
func RequireAnyPermission(perms ...constants.Permission) gin.HandlerFunc {
	return func(c *gin.Context) {
		userInterface, exists := c.Get(constants.ContextKeyUser)
		if !exists {
			c.AbortWithStatusJSON(403, gin.H{"error": "USER_NOT_FOUND"})
			return
		}

		authUser, ok := userInterface.(auth.UserSession)
		if !ok {
			c.AbortWithStatusJSON(500, gin.H{"error": "INTERNAL_ERROR"})
			return
		}

		if authUser.IsSuperUser() {
			c.Next()
			return
		}

		permsInterface, exists := c.Get(constants.ContextKeySystemPermissions)
		if !exists {
			c.AbortWithStatusJSON(403, gin.H{"error": "PERMISSIONS_NOT_LOADED"})
			return
		}

		userPerms, ok := permsInterface.(map[string]bool)
		if !ok {
			c.AbortWithStatusJSON(500, gin.H{"error": "INTERNAL_ERROR"})
			return
		}
		for _, perm := range perms {
			if userPerms[string(perm)] {
				c.Next()
				return
			}
		}

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

