package permissions

import (
	"context"
	"errors"
	"strings"

	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
	"github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/permissionauthoringv1"
	"github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/permissionstatev1"
)

// PermissionAuthoringStateFence serializes a fresh authoring decision with the
// durable Permission State authority. Implementations must hold the current
// singleton lock until the caller-owned transaction completes.
type PermissionAuthoringStateFence interface {
	LockPermissionStateAtLeast(
		ctx context.Context,
		admittedFloor permissionstatev1.Version,
	) (permissionstatev1.Version, error)
}

// TransactionCurrentPermissionAuthoringPolicy owns the fresh-command
// authorization order. The current User row is locked first, its mutable
// Profile/Role assignment is checked against the authenticated session, the
// assigned Profile lifecycle row is locked and required active, then Permission
// State is fenced and effective permissions are recomputed from one
// transaction-current aggregate. The JWT-backed session remains the sole
// source for Records formula globals; request permission caches are never
// consulted.
type profileLifecycleLocker interface {
	LockProfileLifecycles(context.Context, []string) ([]*models.Profile, error)
}

type TransactionCurrentPermissionAuthoringPolicy struct {
	permissions      *PermissionService
	profileLifecycle profileLifecycleLocker
	state            PermissionAuthoringStateFence
}

func NewTransactionCurrentPermissionAuthoringPolicy(
	permissions *PermissionService,
	state PermissionAuthoringStateFence,
) (*TransactionCurrentPermissionAuthoringPolicy, error) {
	if permissions == nil || permissions.repo == nil || permissions.userRepo == nil || state == nil {
		return nil, errors.New("transaction-current permission authoring policy dependencies are incomplete")
	}
	profileLifecycle, ok := permissions.repo.(profileLifecycleLocker)
	if !ok {
		return nil, errors.New("transaction-current permission authoring Profile lifecycle authority is unavailable")
	}
	return &TransactionCurrentPermissionAuthoringPolicy{
		permissions: permissions, profileLifecycle: profileLifecycle, state: state,
	}, nil
}

func (policy *TransactionCurrentPermissionAuthoringPolicy) AuthorizePermissionSetAuthoring(
	ctx context.Context,
	admittedFloor permissionstatev1.Version,
	actor *models.UserSession,
) (*PermissionAuthoringAuthorization, bool, error) {
	return policy.AuthorizeSystemPermissionAuthoring(
		ctx, admittedFloor, actor, string(constants.PermManageProfilesPermSets),
	)
}

// AuthorizeSystemPermissionAuthoring is the shared transaction-current policy
// for durable setup mutations. Callers choose only the standard system
// permission; this owner still controls current-principal locking, Permission
// State fencing and uncached effective permission assembly.
func (policy *TransactionCurrentPermissionAuthoringPolicy) AuthorizeSystemPermissionAuthoring(
	ctx context.Context,
	admittedFloor permissionstatev1.Version,
	actor *models.UserSession,
	permission string,
) (*PermissionAuthoringAuthorization, bool, error) {
	if policy == nil || policy.permissions == nil || policy.profileLifecycle == nil || policy.state == nil ||
		actor == nil || strings.TrimSpace(actor.ID) == "" || admittedFloor.Validate() != nil || strings.TrimSpace(permission) == "" {
		return nil, false, errors.New("transaction-current permission authoring authority is unavailable")
	}
	formulaActor, err := permissionAuthoringFormulaActor(actor)
	if err != nil {
		return nil, false, err
	}

	// GetUserByID is a locking current read in a pessimistic grant transaction.
	// It prevents User deactivation or Profile reassignment from committing
	// between principal resolution and the final Permission State fence. The
	// assigned Profile row is locked separately below because Profile lifecycle
	// is an independent authorization fact.
	user, err := policy.permissions.userRepo.GetUserByID(ctx, actor.ID)
	if err != nil {
		return nil, false, err
	}
	if user == nil || !user.IsActive || strings.TrimSpace(user.ProfileID) == "" {
		return nil, false, nil
	}
	if user.ProfileID != formulaActor.ProfileID ||
		permissionAuthoringOptionalString(user.UserRoleID) != permissionAuthoringOptionalString(formulaActor.RoleID) {
		// Profile/Role reassignment revokes the session, but a request admitted
		// immediately before that commit may already be in flight. Do not combine
		// current authorization with stale session-bound formula identity.
		return nil, false, nil
	}
	profiles, err := policy.profileLifecycle.LockProfileLifecycles(ctx, []string{user.ProfileID})
	if err != nil {
		return nil, false, err
	}
	if len(profiles) != 1 || profiles[0] == nil || profiles[0].ID != user.ProfileID ||
		!profiles[0].IsActive || profiles[0].IsDeleted {
		return nil, false, nil
	}
	fencedState, err := policy.state.LockPermissionStateAtLeast(ctx, admittedFloor)
	if err != nil {
		return nil, false, err
	}
	subject, err := permissionstatev1.NewSubject(user.ID, user.ProfileID)
	if err != nil {
		return nil, false, err
	}
	snapshot, err := policy.permissions.LoadCurrentEffectivePermissionSnapshot(ctx, subject)
	if err != nil {
		return nil, false, err
	}
	comparison, err := snapshot.Version().Compare(fencedState)
	if err != nil || comparison != 0 {
		return nil, false, errors.New("transaction-current permission snapshot does not match the fenced Permission State")
	}
	systemPermissions := make(map[string]bool, len(snapshot.SystemPermissions()))
	for _, permission := range snapshot.SystemPermissions() {
		systemPermissions[permission] = true
	}
	customPermissions := make(map[string]bool, len(snapshot.CustomPermissions()))
	for _, permission := range snapshot.CustomPermissions() {
		customPermissions[permission] = true
	}
	return &PermissionAuthoringAuthorization{
		FormulaActor:      formulaActor,
		SystemPermissions: systemPermissions,
		CustomPermissions: customPermissions,
	}, systemPermissions[permission], nil
}

func permissionAuthoringFormulaActor(actor *models.UserSession) (*models.UserSession, error) {
	if actor == nil || strings.TrimSpace(actor.ID) == "" || strings.TrimSpace(actor.ProfileID) == "" ||
		strings.TrimSpace(actor.ProfileName) == "" {
		return nil, errors.New("authenticated permission authoring formula actor is incomplete")
	}
	roleID := permissionAuthoringOptionalString(actor.RoleID)
	roleName := permissionAuthoringOptionalString(actor.RoleName)
	if (roleID == "") != (roleName == "") {
		return nil, errors.New("authenticated permission authoring formula actor has inconsistent UserRole identity")
	}
	clone := *actor
	clone.Email = permissionAuthoringCloneString(actor.Email)
	clone.RoleID = permissionAuthoringCloneString(actor.RoleID)
	clone.RoleName = permissionAuthoringCloneString(actor.RoleName)
	return &clone, nil
}

func permissionAuthoringOptionalString(value *string) string {
	if value == nil {
		return ""
	}
	return strings.TrimSpace(*value)
}

func permissionAuthoringCloneString(value *string) *string {
	if value == nil {
		return nil
	}
	clone := *value
	return &clone
}

func (policy *TransactionCurrentPermissionAuthoringPolicy) CheckPermissionSetAuthorable(
	ctx context.Context,
	permissionSetID string,
) (permissionauthoringv1.RejectionCode, error) {
	if policy == nil || policy.permissions == nil {
		return permissionauthoringv1.RejectionNone, errors.New("transaction-current permission authoring policy is unavailable")
	}
	return policy.permissions.CheckPermissionSetAuthorable(ctx, permissionSetID)
}
