package permissions

import (
	"context"
	"encoding/json"
	"log"
	"math/big"
	"strings"
	"time"

	"github.com/nexuscrm/kernel/pkg/formula"
	"github.com/nexuscrm/kernel/pkg/schemautil"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
)

const sharingRuleCacheTTL = 5 * time.Minute

// getCachedSharingRules returns sharing rules for the given object, using the
// in-memory cache when valid and falling back to the metadata service on miss.
func (ps *PermissionService) getCachedSharingRules(ctx context.Context, objectName string) []*models.SharingRule {
	// RuntimeMetadataView is already immutable and request-local. Caching it by
	// object name would erase CatalogRevision from the cache key and permit a
	// later request to reuse rules from an older activation.
	if bound, ok := ps.metadata.(interface{ UsesRequestBoundRuntimeView() bool }); ok && bound.UsesRequestBoundRuntimeView() {
		if ps.metadata == nil {
			return nil
		}
		return ps.metadata.GetSharingRules(ctx, objectName)
	}
	ps.sharingRuleMu.RLock()
	if cached, ok := ps.sharingRuleCache[objectName]; ok && time.Now().Before(cached.expiresAt) {
		ps.sharingRuleMu.RUnlock()
		return cached.rules
	}
	ps.sharingRuleMu.RUnlock()

	// Cache miss — fetch from metadata service (which queries DB)
	if ps.metadata == nil {
		return nil
	}
	rules := ps.metadata.GetSharingRules(ctx, objectName)

	ps.sharingRuleMu.Lock()
	ps.sharingRuleCache[objectName] = &cachedSharingRules{
		rules:     rules,
		expiresAt: time.Now().Add(sharingRuleCacheTTL),
	}
	ps.sharingRuleMu.Unlock()

	return rules
}

// InvalidateSharingRuleCache flushes the entire sharing rule cache.
// Call this after creating, updating, or deleting sharing rules.
func (ps *PermissionService) InvalidateSharingRuleCache() {
	ps.sharingRuleMu.Lock()
	ps.sharingRuleCache = make(map[string]*cachedSharingRules)
	ps.sharingRuleMu.Unlock()
}

// --- Sharing Rules ---

// isUserInRoleOrAbove checks if the user's role matches the target role
// or is an ancestor (manager) of the target role.
// This enables standard hierarchical sharing: if you share with "Sales Rep",
// "Sales Manager" (above Sales Rep) also gets access.
func (ps *PermissionService) isUserInRoleOrAbove(userRoleID, targetRoleID *string) bool {
	if userRoleID == nil || targetRoleID == nil {
		return false
	}

	// Exact match
	if *userRoleID == *targetRoleID {
		return true
	}

	// Check if user is an ancestor of target role (user is above target)
	return ps.isUserAboveInHierarchy(userRoleID, targetRoleID)
}

// checkSharingRuleAccess evaluates if a sharing rule grants access to a record
func (ps *PermissionService) checkSharingRuleAccess(
	ctx context.Context,
	record models.SObject,
	rule *models.SharingRule,
	user *models.UserSession,
	operation string,
) bool {
	matchesIdentity := false

	// 1. Check if user belongs to the target role/group
	// A. Role-based sharing
	if rule.ShareWithRoleID != nil {
		if ps.isUserInRoleOrAbove(user.RoleID, rule.ShareWithRoleID) {
			matchesIdentity = true
		}
	}

	// B. Group-based sharing — routed through request-scoped point memoization
	// (issue #1585) so repeated sharing-rule evaluation across a page reuses
	// the exact (group, user) answer without a full user-group preload.
	if !matchesIdentity && rule.ShareWithGroupID != nil {
		if ps.isUserInGroupForSession(ctx, *rule.ShareWithGroupID, user.ID, user.ID) {
			matchesIdentity = true
		}
	}

	if !matchesIdentity {
		return false
	}

	// 2. Check if operation is allowed by access level
	// AccessLevel: "Read" or "Edit" (Salesforce-native format)
	// "Edit" includes read, so Edit grants both read and edit
	switch rule.AccessLevel {
	case constants.PermRead:
		if operation != constants.PermRead {
			return false
		}
	case constants.PermEdit:
		if operation != constants.PermRead && operation != constants.PermEdit {
			return false
		}
	default:
		return false
	}

	// 3. Evaluate criteria (if any)
	if rule.Criteria == "" || rule.Criteria == "[]" {
		return ps.recordOwnerMatchesSharingRuleSource(ctx, record, rule)
	}

	// Parse and evaluate criteria
	return ps.evaluateSharingCriteria(ctx, record, rule.ObjectAPIName, rule.Criteria)
}

// evaluateSharingCriteria evaluates a formula expression against a record.
// Examples: state == "CA", amount > 1000, priority == "High" || region == "West"
func (ps *PermissionService) evaluateSharingCriteria(ctx context.Context, record models.SObject, objectName string, criteria string) bool {
	// Empty criteria = match all records
	if criteria == "" || criteria == "[]" {
		return true
	}

	var schema *models.ObjectMetadata
	if ps.metadata != nil && objectName != "" {
		schema = ps.metadata.GetSchema(ctx, objectName)
	}

	criteriaExpr, err := SharingRuleRecordAccessCriteriaExpression(criteria, schema)
	if err != nil {
		log.Printf("Warning: Failed to normalize sharing rule criteria for record access: %v", err)
		return false
	}

	if ps.formula == nil {
		log.Printf("Warning: Cannot evaluate formula criteria - formula engine not initialized")
		return false
	}

	// Build formula context from record
	formulaCtx := &formula.Context{
		Record: normalizeSharingCriteriaRecordForFormula(record, schema, sharingRuleCriteriaIsJSON(criteria)),
		Schema: schema,
	}

	result, err := ps.formula.Evaluate(criteriaExpr, formulaCtx)
	if err != nil {
		log.Printf("Warning: Failed to evaluate sharing rule criteria: %v", err)
		return false
	}

	// Result should be a boolean
	if boolResult, ok := result.(bool); ok {
		return boolResult
	}

	log.Printf("Warning: Sharing rule criteria did not evaluate to boolean: %v", result)
	return false
}

func normalizeSharingCriteriaRecordForFormula(record models.SObject, schema *models.ObjectMetadata, criteriaIsJSON bool) models.SObject {
	if schema == nil || len(record) == 0 {
		return record
	}

	normalized := record
	copied := false
	for _, field := range schema.Fields {
		raw, ok := normalized[field.APIName]
		if !ok {
			continue
		}
		switch field.Type {
		case constants.FieldTypeDate:
			dateValue, ok := raw.(time.Time)
			if !ok {
				continue
			}
			normalized, copied = cloneSharingCriteriaRecordIfNeeded(record, normalized, copied)
			// Preserve the hydrated DATE calendar day; DATE has no time-zone component.
			normalized[field.APIName] = dateValue.Format("2006-01-02")
		case constants.FieldTypeCurrency, constants.FieldTypeDouble:
			number, ok := sharingCriteriaExactDecimalNumber(raw)
			if !ok {
				continue
			}
			normalized, copied = cloneSharingCriteriaRecordIfNeeded(record, normalized, copied)
			normalized[field.APIName] = number
		case constants.FieldTypePercent:
			if !criteriaIsJSON {
				continue
			}
			number, ok := sharingCriteriaExactDecimalNumberFromRecord(normalized, field.APIName, raw)
			if !ok {
				normalized, copied = cloneSharingCriteriaRecordIfNeeded(record, normalized, copied)
				normalized[field.APIName] = struct{}{}
				continue
			}
			normalized, copied = cloneSharingCriteriaRecordIfNeeded(record, normalized, copied)
			normalized[field.APIName] = number
		}
	}
	return normalized
}

func sharingRuleCriteriaIsJSON(criteria string) bool {
	trimmed := strings.TrimSpace(criteria)
	return strings.HasPrefix(trimmed, "[")
}

func cloneSharingCriteriaRecordIfNeeded(original models.SObject, normalized models.SObject, copied bool) (models.SObject, bool) {
	if copied {
		return normalized, true
	}
	clone := make(models.SObject, len(original))
	for key, value := range original {
		clone[key] = value
	}
	return clone, true
}

func sharingCriteriaExactDecimalNumberFromRecord(record models.SObject, fieldName string, value interface{}) (json.Number, bool) {
	if decimal, ok := schemautil.ExactDecimalForField(record, fieldName); ok {
		if sharingCriteriaDecimalStringValid(decimal.String()) {
			return json.Number(decimal.String()), true
		}
	}
	return sharingCriteriaExactDecimalNumber(value)
}

func sharingCriteriaExactDecimalNumber(value interface{}) (json.Number, bool) {
	switch v := value.(type) {
	case schemautil.ExactDecimal:
		if sharingCriteriaDecimalStringValid(v.String()) {
			return json.Number(v.String()), true
		}
	case json.Number:
		if sharingCriteriaDecimalStringValid(v.String()) {
			return v, true
		}
	case string:
		trimmed := strings.TrimSpace(v)
		if sharingCriteriaDecimalStringValid(trimmed) {
			return json.Number(trimmed), true
		}
	case []byte:
		trimmed := strings.TrimSpace(string(v))
		if sharingCriteriaDecimalStringValid(trimmed) {
			return json.Number(trimmed), true
		}
	}
	return "", false
}

func sharingCriteriaDecimalStringValid(value string) bool {
	if value == "" || strings.Contains(value, "/") {
		return false
	}
	_, ok := new(big.Rat).SetString(value)
	return ok
}

func (ps *PermissionService) recordOwnerMatchesSharingRuleSource(
	ctx context.Context,
	record models.SObject,
	rule *models.SharingRule,
) bool {
	ownerID, ok := record[constants.FieldOwnerID]
	if !ok {
		return false
	}

	ownerIDStr, ok := ownerID.(string)
	if !ok || ownerIDStr == "" {
		return false
	}

	if rule.SharedFromGroupID != nil && *rule.SharedFromGroupID != "" {
		// The member here is the record OWNER, not the session user, so this uses
		// the point-memoized membership path (issue #1585): each distinct
		// (group, owner) pair is resolved at most once per request instead of per
		// record during sharing-rule source evaluation across a page.
		if ps.cachedIsMemberOfGroup(ctx, *rule.SharedFromGroupID, ownerIDStr) {
			return true
		}
	}

	if rule.SharedFromRoleID != nil && *rule.SharedFromRoleID != "" {
		ownerRoleID := ps.getRecordOwnerRoleID(ctx, ownerIDStr)
		if ownerRoleID != nil && ps.isUserInRoleOrAbove(rule.SharedFromRoleID, ownerRoleID) {
			return true
		}
	}

	return false
}
