package permissions

import (
	"context"
	"log"
	"time"

	apperrors "github.com/nexuscrm/kernel/pkg/errors"
)

// --- Role Hierarchy ---

// roleHierarchyTTL bounds cache staleness for reads that can't detect missing
// roles via cache-miss alone (e.g. GetRoleDescendants, where the parent role
// is already in cache but newly-added children are not). Role writes that go
// through PermissionService still trigger an immediate refresh — this TTL only
// kicks in for out-of-band mutations (direct SQL, seed scripts, cross-instance
// staleness in multi-replica deployments).
const roleHierarchyTTL = 60 * time.Second

// refreshRoleHierarchy loads the role hierarchy from the database into cache
func (ps *PermissionService) refreshRoleHierarchy(ctx context.Context) {
	// 1. Fetch data without lock
	roles, err := ps.repo.GetAllRoles(ctx)
	if err != nil {
		log.Printf("Warning: Failed to load role hierarchy: %v", err)
		return
	}

	// 2. Build new cache properly
	newCache := make(map[string]*string)
	for _, role := range roles {
		newCache[role.ID] = role.ParentRoleID
	}

	// 3. Swap with lock
	ps.roleHierarchyMu.Lock()
	defer ps.roleHierarchyMu.Unlock()
	ps.roleHierarchyCache = newCache
	ps.roleHierarchyLoadedAt = time.Now()
}

// ensureRoleHierarchyFresh is a best-effort lazy refresh used by read paths.
// It reloads the cache when either:
//   - triggerRoleID is non-empty and not present in the cache (strong signal
//     the role was added out-of-band since the last refresh), or
//   - the cache is older than roleHierarchyTTL (bounds staleness for new
//     children added under an already-cached parent, which cache-miss alone
//     cannot detect).
//
// Safe to call on hot paths: the fast path is an RLock-bounded timestamp
// check; only the slow path triggers a DB load.
func (ps *PermissionService) ensureRoleHierarchyFresh(ctx context.Context, triggerRoleID string) {
	ps.roleHierarchyMu.RLock()
	_, present := ps.roleHierarchyCache[triggerRoleID]
	loadedAt := ps.roleHierarchyLoadedAt
	ps.roleHierarchyMu.RUnlock()

	// Skip when the cache was populated by a caller bypassing refreshRoleHierarchy
	// (unit tests inject the cache directly). Production always stamps loadedAt
	// on init and on each refresh, so this only gates synthetic test setups.
	if loadedAt.IsZero() {
		return
	}
	stale := time.Since(loadedAt) > roleHierarchyTTL
	needsRefresh := stale || (triggerRoleID != "" && !present)
	if !needsRefresh {
		return
	}
	ps.refreshRoleHierarchy(ctx)
}

// GetRoleAncestors returns all ancestor role IDs for a given role (parent, grandparent, etc.)
// Used by layout resolution to match assignments on ancestor roles in the hierarchy.
func (ps *PermissionService) GetRoleAncestors(roleID string) []string {
	// Reload on cache miss so ancestors resolve correctly for roles added
	// out-of-band (SQL seed, migrations, other backend replicas).
	ps.ensureRoleHierarchyFresh(context.Background(), roleID)

	ps.roleHierarchyMu.RLock()
	defer ps.roleHierarchyMu.RUnlock()

	ancestors := make([]string, 0)
	visited := make(map[string]bool) // Prevent infinite loops

	currentID := roleID
	for {
		parentID, exists := ps.roleHierarchyCache[currentID]
		if !exists || parentID == nil {
			break
		}
		if visited[*parentID] {
			// Circular reference detected - should not happen, but handle gracefully
			log.Printf("Warning: Circular role hierarchy detected at %s", *parentID)
			break
		}
		visited[*parentID] = true
		ancestors = append(ancestors, *parentID)
		currentID = *parentID
	}

	return ancestors
}

// isUserAboveInHierarchy checks if the user's role is an ancestor of the target role
// Returns true if userRoleID is above targetRoleID in the hierarchy
func (ps *PermissionService) isUserAboveInHierarchy(userRoleID, targetRoleID *string) bool {
	// Both must have roles for hierarchy check
	if userRoleID == nil || targetRoleID == nil {
		return false
	}

	// Same role - not above
	if *userRoleID == *targetRoleID {
		return false
	}

	// Get all ancestors of the target role
	ancestors := ps.GetRoleAncestors(*targetRoleID)

	// Check if user's role is in the ancestor list
	for _, ancestorID := range ancestors {
		if ancestorID == *userRoleID {
			return true
		}
	}

	return false
}

// getRecordOwnerRoleID retrieves the role_id of the record owner.
// Memoized per ownerID within the request (issue #1585) so a list/hydration
// page resolves each distinct owner's role at most once instead of per record.
func (ps *PermissionService) getRecordOwnerRoleID(ctx context.Context, ownerID string) *string {
	if roleID, ok := ps.cachedOwnerRoleID(ctx, ownerID); ok {
		return roleID
	}
	roleID, err := ps.userRepo.GetUserRoleID(ctx, ownerID)
	if err != nil {
		return nil // do not cache transient errors
	}
	ps.storeOwnerRoleID(ctx, ownerID, roleID)
	return roleID
}

// RefreshRoleHierarchy reloads the role hierarchy cache
// Call this when roles are modified
func (ps *PermissionService) RefreshRoleHierarchy(ctx context.Context) {
	ps.refreshRoleHierarchy(ctx)
}

// GetRoleDescendants returns all descendant role IDs (children, grandchildren, etc.) for a given role ID.
func (ps *PermissionService) GetRoleDescendants(roleID string) []string {
	// TTL-bounded refresh so newly-added children of an already-cached parent
	// become visible without requiring a server restart.
	ps.ensureRoleHierarchyFresh(context.Background(), roleID)

	ps.roleHierarchyMu.RLock()
	defer ps.roleHierarchyMu.RUnlock()

	descendants := []string{}
	for id, parentID := range ps.roleHierarchyCache {
		if parentID != nil {
			// Walk up from this id to see if roleID is an ancestor
			current := id
			visited := make(map[string]bool)
			for {
				if visited[current] {
					break // Cycle detected
				}
				visited[current] = true
				parent, exists := ps.roleHierarchyCache[current]
				if !exists || parent == nil {
					break
				}
				if *parent == roleID {
					descendants = append(descendants, id)
					break
				}
				current = *parent
			}
		}
	}
	return descendants
}

// GetSubordinateUserIDs returns the IDs of all users whose role is a descendant of roleID.
func (ps *PermissionService) GetSubordinateUserIDs(ctx context.Context, roleID string) ([]string, error) {
	descendantRoleIDs := ps.GetRoleDescendants(roleID)
	if len(descendantRoleIDs) == 0 {
		return []string{}, nil
	}
	return ps.userRepo.GetUserIDsByRoleIDs(ctx, descendantRoleIDs)
}

type securityEpochRoleHierarchyReader interface {
	GetRoleAncestorsAtSecurityEpoch(context.Context, string, string, uint64) ([]string, error)
}

// GetRoleAncestorsAtSecurityEpoch reads current role ancestry together with the
// org's admitted security epoch. This path never falls back to the TTL cache.
func (ps *PermissionService) GetRoleAncestorsAtSecurityEpoch(ctx context.Context, orgID, roleID string, securityEpoch uint64) ([]string, error) {
	reader, ok := ps.repo.(securityEpochRoleHierarchyReader)
	if !ok {
		return nil, apperrors.NewInternalError("Role hierarchy security authority is unavailable", nil)
	}
	return reader.GetRoleAncestorsAtSecurityEpoch(ctx, orgID, roleID, securityEpoch)
}
