package permissions

import (
	"context"
	"fmt"
	"log"
	"strings"

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

// computeIsLockedWith is the single-probe variant of computeIsLocked.
// approvalLocked must be the result of isApprovalLockedForOperation(ctx, record, PermEdit, user)
// called by the caller before this function.
//
// Trusted contexts (ServiceContext+) bypass LockRule evaluation. System
// administrators and SuperUsers are subject to admin-authored LockRule by
// design — see canBypassLockRule rationale. This keeps the UI-facing IsLocked
// signal aligned with the backend mutation gate in checkRecordAccessInternal.
func (ps *PermissionService) computeIsLockedWith(
	ctx context.Context,
	schema *models.ObjectMetadata,
	record models.SObject,
	user *models.UserSession,
	approvalLocked bool,
) bool {
	if approvalLocked {
		return true
	}
	// Trusted ServiceContext+ callers bypass the LockRule formula, same as in
	// checkRecordAccessInternal. Without this, a system-internal caller sees
	// IsLocked=true (UI hides Edit) while the backend would accept the write —
	// a UI lie that contradicts the actual authorization outcome. Admins and
	// SuperUsers are intentionally NOT bypassed here.
	if canBypassLockRule(ctx, user) {
		return false
	}
	return ps.evaluateLockRule(schema, record, user)
}

// IsRecordLockedForGate reports whether the record is locked by an approval
// lock or by the admin-authored LockRule formula, using fail-closed semantics:
// if the formula engine returns an error the record is treated as locked
// (returns true) to prevent authorization bypasses.
//
// Use this for authoritative mutation gates such as Submit and
// _System_RecordShare inserts — paths that must not silently allow mutations
// when lock evaluation fails. Trusted contexts (ServiceContext+) bypass
// LockRule. System administrators and SuperUsers are subject to admin-authored
// LockRule by design — see canBypassLockRule rationale.
func (ps *PermissionService) IsRecordLockedForGate(
	ctx context.Context,
	schema *models.ObjectMetadata,
	record models.SObject,
	user *models.UserSession,
) bool {
	approvalLocked := ps.isApprovalLockedForOperation(ctx, record, constants.PermEdit, user)
	if approvalLocked {
		return true
	}
	if canBypassLockRule(ctx, user) {
		return false
	}
	locked, err := ps.evaluateLockRuleForGate(schema, record, user)
	if err != nil {
		return true // fail-closed: evaluation error counts as locked
	}
	return locked
}

// evaluateLockRule evaluates the ObjectMetadata.LockRule formula
// expression against the raw record. Returns false when no expression is
// declared, when the formula engine is unavailable, or when evaluation errors —
// failing open keeps the rest of the mutation UI usable when metadata is
// malformed; the authoritative write-side enforcement still lives in
// validation rules and sharing logic.
//
// Use this only for UI signal paths (IsLocked). For the authoritative mutation
// gate, use evaluateLockRuleForGate instead.
func (ps *PermissionService) evaluateLockRule(
	schema *models.ObjectMetadata,
	record models.SObject,
	user *models.UserSession,
) bool {
	locked, err := ps.evaluateLockRuleForGate(schema, record, user)
	if err != nil && schema != nil {
		// Log a warning for UI callers so operators see the misconfigured expression;
		// the UI path fails open (returns false) to avoid hiding the Edit affordance.
		log.Printf("Warning: LockRule for object %s could not be evaluated for UI lock signal: %v (treating as unlocked)", schema.APIName, err)
	}
	return locked
}

// evaluateLockRuleForGate evaluates the ObjectMetadata.LockRule
// formula expression against the raw record and returns both the lock decision
// and any evaluation error.
//
// Callers that enforce an authoritative write gate MUST call this function and
// treat a non-nil error as "record is locked" (fail closed), preventing an
// authorization bypass when the formula engine is unavailable or the expression
// is malformed.
//
// Supported formula context: Record fields and $User/$Profile/$UserRole session
// globals (populated from user when non-nil). Relationship traversal
// (e.g. Parent__r.Status) is NOT supported because no Fetcher is wired here;
// formulas using relationship paths are caught at save time by UpdateSchema's
// ValidateSyntaxWithSchema gate, which rejects them before they can reach runtime.
func (ps *PermissionService) evaluateLockRuleForGate(
	schema *models.ObjectMetadata,
	record models.SObject,
	user *models.UserSession,
) (locked bool, err error) {
	if schema == nil || schema.LockRule == nil {
		return false, nil
	}
	expr := strings.TrimSpace(*schema.LockRule)
	if expr == "" {
		return false, nil
	}
	if ps.formula == nil {
		log.Printf("Warning: PermissionService has no formula engine; LockRule gate denied for object %s", schema.APIName)
		return false, fmt.Errorf("formula engine unavailable; cannot evaluate LockRule for object %s", schema.APIName)
	}

	// Pass Schema so field-aware formula functions can resolve type information.
	// Pass User so $User/$Profile/$UserRole session globals (e.g. $Profile.Name == 'Sales')
	// are available to the formula engine. BuildFormulaUserContext is the single SSoT for
	// session-bound formula globals — see CLAUDE.md §"Formula context SSoT".
	// No Fetcher is wired: relationship traversal (Parent__r.Field) is not supported and is
	// rejected at metadata-save time by evaluateLockRuleForGate's sibling gate in
	// UpdateSchema.
	result, evalErr := ps.formula.Evaluate(expr, &formula.Context{
		Record: record,
		Schema: schema,
		User:   formula.BuildFormulaUserContext(user),
	})
	if evalErr != nil {
		log.Printf("Warning: Failed to evaluate LockRule for object %s: %v", schema.APIName, evalErr)
		return false, evalErr
	}
	boolResult, ok := result.(bool)
	if !ok {
		return false, fmt.Errorf("LockRule expression for object %s returned non-boolean type %T; expression must evaluate to true or false", schema.APIName, result)
	}
	return boolResult, nil
}
