package permissions

import (
	"context"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"strings"
	"time"

	"github.com/google/uuid"
	"github.com/nexuscrm/kernel/pkg/ports"
	"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"
)

const (
	defaultPermissionAuthoringAdmissionTTL     = 15 * time.Minute
	defaultPermissionAuthoringOutcomeRetention = 7 * 24 * time.Hour
)

// PermissionAuthoringPolicy owns transaction-current authorization and target
// mutability decisions. CheckTarget runs only after the command has durably
// bound one canonical PermissionSet.
type PermissionAuthoringPolicy interface {
	AuthorizePermissionSetAuthoring(
		ctx context.Context,
		admittedFloor permissionstatev1.Version,
		actor *models.UserSession,
	) (*PermissionAuthoringAuthorization, bool, error)
	CheckPermissionSetAuthorable(ctx context.Context, permissionSetID string) (permissionauthoringv1.RejectionCode, error)
}

// PermissionAuthoringAuthorization combines two deliberately distinct facts:
// transaction-current effective permissions after the durable Permission
// State fence, and the immutable authenticated session actor used by Records
// formula globals. The aggregate installs both over the request context so
// lifecycle checks cannot fall back to an older permission cache or construct
// a second $User/$Profile/$UserRole read path.
type PermissionAuthoringAuthorization struct {
	FormulaActor      *models.UserSession
	SystemPermissions map[string]bool
	CustomPermissions map[string]bool
}

// PermissionAuthoringInvocation contains trusted Runtime authority inputs that
// are deliberately outside the caller-authored semantic request. The admitted
// Permission State floor is checked only for fresh/nonterminal execution; an
// exact terminal replay skips mutable business authorization.
type PermissionAuthoringInvocation struct {
	AuthorityScope               string
	Actor                        *models.UserSession
	AdmittedPermissionStateFloor permissionstatev1.Version
}

// PermissionAuthoringMutationPlan is a validation-complete final-image plan.
// Plan must perform every business validation before Apply starts writing, so
// a durable business rejection never has to coexist with rolled-back partial
// family mutations.
type PermissionAuthoringMutationPlan interface {
	Changed() bool
	FinalChanges() permissionauthoringv1.Changes
	Apply(ctx context.Context) error
}

// PermissionAuthoringMutationPlanner composes the five permission families
// under the aggregate root lock. The plan may use transaction-local Records
// and Permission Admin ports, but neither child may open or commit its own
// transaction.
type PermissionAuthoringMutationPlanner interface {
	PlanPermissionAuthoringMutation(
		ctx context.Context,
		aggregate ports.LockedPermissionAuthoringAggregate,
		changes permissionauthoringv1.Changes,
		actor *models.UserSession,
	) (PermissionAuthoringMutationPlan, error)
}

type PermissionAuthoringAuditEvent struct {
	CommandID              string
	CommandDigest          string
	ActorID                string
	SelectedTargetKind     permissionauthoringv1.TargetKind
	SelectedTargetID       string
	CanonicalPermissionSet string
	AuthoringRevision      uint64
	FinalChanges           permissionauthoringv1.Changes
}

// PermissionAuthoringAuditWriter is a strict transaction-local security audit
// writer. Returning an error aborts the aggregate; best-effort SetupAuditor
// callbacks are not valid implementations of this port.
type PermissionAuthoringAuditWriter interface {
	WritePermissionAuthoringAudit(ctx context.Context, event PermissionAuthoringAuditEvent) error
}

type PermissionAuthoringStateAdvancer interface {
	RequestAdvanceIfChanged(ctx context.Context, changed bool) error
}

type PermissionAuthoringReceiptSource interface {
	PermissionAuthoringReceipts(ctx context.Context) (permissionauthoringv1.Receipts, error)
}

type PermissionAuthoringTransactions interface {
	WithCurrentReadPermissionAuthoringTransaction(ctx context.Context, fn func(context.Context) error) error
}

// PermissionAuthoringRejection is a validation-complete business disposition
// returned by policy or planning before Apply writes anything. Infrastructure
// failures must remain ordinary errors so the transaction rolls back and the
// accepted command remains retryable.
type PermissionAuthoringRejection struct {
	Code  permissionauthoringv1.RejectionCode
	Cause error
}

func (r *PermissionAuthoringRejection) Error() string {
	if r == nil || r.Cause == nil {
		return string(r.Code)
	}
	return r.Cause.Error()
}

func (r *PermissionAuthoringRejection) Unwrap() error {
	if r == nil {
		return nil
	}
	return r.Cause
}

type PermissionAuthoringServiceOptions struct {
	AdmissionTTL     time.Duration
	OutcomeRetention time.Duration
	NewRowID         func() string
}

// PermissionAuthoringService is the single owner of one user-visible atomic
// Permission Set Authoring Save. It owns acceptance/replay, canonical target
// binding, one pessimistic mutation transaction, aggregate revision, strict
// audit, Permission State disposition, and recoverable post-commit convergence.
type PermissionAuthoringService struct {
	transactions PermissionAuthoringTransactions
	commands     ports.PermissionAuthoringCommandStore
	targets      ports.PermissionAuthoringTargetStore
	policy       PermissionAuthoringPolicy
	planner      PermissionAuthoringMutationPlanner
	audit        PermissionAuthoringAuditWriter
	state        PermissionAuthoringStateAdvancer
	convergence  ports.PermissionAuthoringConvergence
	receipts     PermissionAuthoringReceiptSource
	admissionTTL time.Duration
	retention    time.Duration
	newRowID     func() string
}

func NewPermissionAuthoringService(
	transactions PermissionAuthoringTransactions,
	commands ports.PermissionAuthoringCommandStore,
	targets ports.PermissionAuthoringTargetStore,
	policy PermissionAuthoringPolicy,
	planner PermissionAuthoringMutationPlanner,
	audit PermissionAuthoringAuditWriter,
	state PermissionAuthoringStateAdvancer,
	convergence ports.PermissionAuthoringConvergence,
	receipts PermissionAuthoringReceiptSource,
	opts PermissionAuthoringServiceOptions,
) (*PermissionAuthoringService, error) {
	if transactions == nil || commands == nil || targets == nil || policy == nil || planner == nil ||
		audit == nil || state == nil || convergence == nil || receipts == nil {
		return nil, errors.New("permission authoring service dependencies are incomplete")
	}
	if opts.OutcomeRetention <= 0 {
		opts.OutcomeRetention = defaultPermissionAuthoringOutcomeRetention
	}
	if opts.AdmissionTTL <= 0 {
		opts.AdmissionTTL = defaultPermissionAuthoringAdmissionTTL
	}
	if opts.NewRowID == nil {
		opts.NewRowID = uuid.NewString
	}
	return &PermissionAuthoringService{
		transactions: transactions, commands: commands, targets: targets,
		policy: policy, planner: planner, audit: audit, state: state,
		convergence: convergence, receipts: receipts,
		admissionTTL: opts.AdmissionTTL, retention: opts.OutcomeRetention, newRowID: opts.NewRowID,
	}, nil
}

// Save executes or exactly replays one atomic authoring command. AuthorityScope
// is the immutable Runtime authority boundary (for example an installation or
// tenant scope); Actor.ID is always included independently so one principal can
// never observe or replay another principal's command identity.
func (s *PermissionAuthoringService) Save(
	ctx context.Context,
	invocation PermissionAuthoringInvocation,
	request permissionauthoringv1.Request,
) (permissionauthoringv1.Response, error) {
	if err := permissionauthoringv1.ValidateRequest(ctx, request); err != nil {
		return permissionauthoringv1.Response{}, err
	}
	invocation.AuthorityScope = strings.TrimSpace(invocation.AuthorityScope)
	if invocation.Actor == nil || strings.TrimSpace(invocation.Actor.ID) == "" || invocation.AuthorityScope == "" ||
		invocation.AdmittedPermissionStateFloor.Validate() != nil {
		return permissionauthoringv1.Response{}, &permissionauthoringv1.Failure{Category: permissionauthoringv1.FailureUnauthenticated}
	}
	digest, err := permissionauthoringv1.CommandDigest(request)
	if err != nil {
		return permissionauthoringv1.Response{}, err
	}
	scopeDigest := permissionAuthoringScopeDigest(invocation.AuthorityScope, invocation.Actor.ID)

	needsAcceptance := false
	row, err := s.commands.LoadActive(ctx, scopeDigest, request.CommandID)
	switch {
	case err == nil:
		if err := verifyPermissionAuthoringIdentity(row, request, digest); err != nil {
			return permissionauthoringv1.Response{}, err
		}
		if row.State == ports.PermissionAuthoringCommandCompleted {
			return decodePermissionAuthoringReplay(request, row)
		}
		if row.SemanticMappingRevision != permissionauthoringv1.SemanticMappingRevision {
			return permissionauthoringv1.Response{}, &permissionauthoringv1.Failure{Category: permissionauthoringv1.FailureIncompatible}
		}
	case errors.Is(err, ports.ErrPermissionAuthoringCommandMissing):
		needsAcceptance = true
	default:
		return permissionauthoringv1.Response{}, err
	}

	// MinCatalogRevision is a per-attempt serving fence. Command identity is
	// verified first so conflicts cannot probe mutable admission state; only an
	// exact frozen terminal replay may skip this fence. Every command that can
	// still execute, whether new or durably ACCEPTED, must satisfy it.
	admittedReceipts, receiptErr := s.receipts.PermissionAuthoringReceipts(ctx)
	if receiptErr != nil {
		return permissionauthoringv1.Response{}, receiptErr
	}
	if !permissionauthoringv1.MeetsFreshCommandCatalogFence(request, admittedReceipts.CatalogRevision) {
		return permissionauthoringv1.Response{}, &permissionauthoringv1.Failure{Category: permissionauthoringv1.FailureStaleState}
	}
	if needsAcceptance {
		row, err = s.accept(ctx, scopeDigest, digest, request)
		if err != nil {
			return permissionauthoringv1.Response{}, err
		}
		if row.State == ports.PermissionAuthoringCommandCompleted {
			return decodePermissionAuthoringReplay(request, row)
		}
		if row.SemanticMappingRevision != permissionauthoringv1.SemanticMappingRevision {
			return permissionauthoringv1.Response{}, &permissionauthoringv1.Failure{Category: permissionauthoringv1.FailureIncompatible}
		}
	}

	row, replay, err := s.bindCanonicalTarget(ctx, invocation, request, digest, row)
	if err != nil {
		return permissionauthoringv1.Response{}, err
	}
	if replay != nil {
		return *replay, nil
	}
	return s.executeBound(ctx, invocation, request, digest, row)
}

func (s *PermissionAuthoringService) accept(
	ctx context.Context,
	scopeDigest string,
	digest string,
	request permissionauthoringv1.Request,
) (ports.PermissionAuthoringCommand, error) {
	var accepted ports.PermissionAuthoringCommand
	err := s.transactions.WithCurrentReadPermissionAuthoringTransaction(ctx, func(txCtx context.Context) error {
		row, _, err := s.commands.Accept(txCtx, ports.PermissionAuthoringCommandAcceptance{
			RowID: s.newRowID(), ScopeDigest: scopeDigest, CommandID: request.CommandID,
			RequestDigest: digest, TargetKind: string(request.Target.Kind), TargetID: request.Target.ID,
			ExpectedAuthoringToken:  request.ExpectedAuthoringToken,
			SemanticMappingRevision: permissionauthoringv1.SemanticMappingRevision,
			AdmissionTTL:            s.admissionTTL, OutcomeRetention: s.retention,
		})
		if err != nil {
			if errors.Is(err, ports.ErrPermissionAuthoringCommandConflict) {
				return invalidPermissionAuthoringIdentity()
			}
			return err
		}
		if err := verifyPermissionAuthoringIdentity(row, request, digest); err != nil {
			return err
		}
		accepted = row
		return nil
	})
	return accepted, err
}

func (s *PermissionAuthoringService) bindCanonicalTarget(
	ctx context.Context,
	invocation PermissionAuthoringInvocation,
	request permissionauthoringv1.Request,
	digest string,
	row ports.PermissionAuthoringCommand,
) (ports.PermissionAuthoringCommand, *permissionauthoringv1.Response, error) {
	if row.CanonicalPermissionSetID != "" {
		return row, nil, nil
	}
	var bound ports.PermissionAuthoringCommand
	var terminal *permissionauthoringv1.Response
	err := s.transactions.WithCurrentReadPermissionAuthoringTransaction(ctx, func(txCtx context.Context) error {
		locked, err := s.commands.LockCommand(txCtx, row.RowID)
		if err != nil {
			return err
		}
		if err := verifyPermissionAuthoringIdentity(locked, request, digest); err != nil {
			return err
		}
		if locked.State == ports.PermissionAuthoringCommandCompleted {
			response, err := decodePermissionAuthoringReplay(request, locked)
			terminal = &response
			return err
		}
		if locked.CanonicalPermissionSetID != "" {
			bound = locked
			return nil
		}

		permissionSetID, found, err := s.targets.ResolvePermissionAuthoringTarget(
			txCtx, string(request.Target.Kind), request.Target.ID,
		)
		if err != nil {
			return err
		}
		if !found {
			_, authorized, err := s.policy.AuthorizePermissionSetAuthoring(
				txCtx, invocation.AdmittedPermissionStateFloor, invocation.Actor,
			)
			if err != nil {
				return err
			}
			code := permissionauthoringv1.RejectionMissing
			if !authorized {
				code = permissionauthoringv1.RejectionDenied
			}
			response, payload, expiry, err := s.terminalResponse(txCtx, request, digest, permissionauthoringv1.OutcomeRejected, code, "", locked)
			if err != nil {
				return err
			}
			if err := s.commands.CompleteUnboundRejection(txCtx, locked.RowID, payload, expiry); err != nil {
				return err
			}
			terminal = &response
			return nil
		}
		if err := s.commands.BindCanonicalPermissionSet(txCtx, locked.RowID, permissionSetID); err != nil {
			return err
		}
		locked.CanonicalPermissionSetID = permissionSetID
		bound = locked
		return nil
	})
	return bound, terminal, err
}

func (s *PermissionAuthoringService) executeBound(
	ctx context.Context,
	invocation PermissionAuthoringInvocation,
	request permissionauthoringv1.Request,
	digest string,
	row ports.PermissionAuthoringCommand,
) (permissionauthoringv1.Response, error) {
	var terminal permissionauthoringv1.Response
	err := s.transactions.WithCurrentReadPermissionAuthoringTransaction(ctx, func(txCtx context.Context) error {
		locked, err := s.commands.LockCommand(txCtx, row.RowID)
		if err != nil {
			return err
		}
		if err := verifyPermissionAuthoringIdentity(locked, request, digest); err != nil {
			return err
		}
		if locked.State == ports.PermissionAuthoringCommandCompleted {
			terminal, err = decodePermissionAuthoringReplay(request, locked)
			return err
		}
		if locked.CanonicalPermissionSetID == "" {
			return errors.New("accepted permission authoring command lost canonical binding")
		}

		aggregate, found, err := s.targets.LockPermissionSetAuthoring(txCtx, locked.CanonicalPermissionSetID)
		if err != nil {
			return err
		}
		if !found {
			return s.completeBoundRejection(txCtx, request, digest, locked, permissionauthoringv1.RejectionMissing, &terminal)
		}
		if aggregate.CanonicalPermissionSetID != locked.CanonicalPermissionSetID || !permissionAuthoringTargetMatchesRole(request.Target.Kind, aggregate.Role) {
			return errors.New("permission authoring canonical aggregate role disagrees with selected target")
		}
		revision := aggregate.Revision
		normalized := permissionauthoringv1.NormalizeRequest(request)
		if len(normalized.Changes.Custom) != 0 {
			customPermissionIDs := make([]string, 0, len(normalized.Changes.Custom))
			for _, change := range normalized.Changes.Custom {
				customPermissionIDs = append(customPermissionIDs, change.CapabilityID)
			}
			if err := s.targets.LockPermissionAuthoringCustomPermissions(txCtx, customPermissionIDs); err != nil {
				return err
			}
		}
		authorization, authorized, err := s.policy.AuthorizePermissionSetAuthoring(
			txCtx, invocation.AdmittedPermissionStateFloor, invocation.Actor,
		)
		if err != nil {
			return err
		}
		if !authorized {
			return s.completeBoundRejection(txCtx, request, digest, locked, permissionauthoringv1.RejectionDenied, &terminal)
		}
		if authorization == nil || authorization.FormulaActor == nil {
			return errors.New("permission authoring policy returned incomplete current authority")
		}
		txCtx = WithEffectivePermissions(
			txCtx,
			authorization.FormulaActor.ID,
			authorization.FormulaActor.ProfileID,
			authorization.SystemPermissions,
			authorization.CustomPermissions,
		)
		if code, err := s.policy.CheckPermissionSetAuthorable(txCtx, locked.CanonicalPermissionSetID); err != nil {
			return err
		} else if code != permissionauthoringv1.RejectionNone {
			return s.completeBoundRejection(txCtx, request, digest, locked, code, &terminal)
		}
		if !permissionauthoringv1.AuthoringTokenMatches(
			request.ExpectedAuthoringToken,
			locked.CanonicalPermissionSetID,
			revision,
		) {
			return s.completeBoundRejection(txCtx, request, digest, locked, permissionauthoringv1.RejectionStaleAuthoringState, &terminal)
		}

		plan, err := s.planner.PlanPermissionAuthoringMutation(
			txCtx, aggregate, normalized.Changes, authorization.FormulaActor,
		)
		if err != nil {
			var rejection *PermissionAuthoringRejection
			if errors.As(err, &rejection) && rejection.Code.ValidFor(permissionauthoringv1.OutcomeRejected) {
				return s.completeBoundRejection(txCtx, request, digest, locked, rejection.Code, &terminal)
			}
			return err
		}
		if plan == nil {
			return errors.New("permission authoring planner returned no plan")
		}
		finalChanges := plan.FinalChanges()
		if plan.Changed() != (permissionAuthoringChangeCount(finalChanges) != 0) {
			return errors.New("permission authoring plan changed state disagrees with its final semantic delta")
		}
		if !plan.Changed() {
			token, err := permissionauthoringv1.NewAuthoringToken(
				locked.CanonicalPermissionSetID,
				revision,
			)
			if err != nil {
				return err
			}
			response, payload, expiry, err := s.terminalResponse(
				txCtx, request, digest, permissionauthoringv1.OutcomeNoop,
				permissionauthoringv1.RejectionNone, token, locked,
			)
			if err != nil {
				return err
			}
			if err := s.commands.CompleteBound(txCtx, locked.RowID, payload, expiry); err != nil {
				return err
			}
			terminal = response
			return nil
		}

		if err := plan.Apply(txCtx); err != nil {
			return err
		}
		advanced, err := s.targets.AdvancePermissionSetAuthoringRevision(txCtx, locked.CanonicalPermissionSetID, revision)
		if err != nil {
			return err
		}
		if err := s.audit.WritePermissionAuthoringAudit(txCtx, PermissionAuthoringAuditEvent{
			CommandID: request.CommandID, CommandDigest: digest, ActorID: authorization.FormulaActor.ID,
			SelectedTargetKind: request.Target.Kind, SelectedTargetID: request.Target.ID,
			CanonicalPermissionSet: locked.CanonicalPermissionSetID,
			AuthoringRevision:      advanced,
			FinalChanges:           finalChanges,
		}); err != nil {
			return err
		}
		if err := s.state.RequestAdvanceIfChanged(txCtx, true); err != nil {
			return err
		}
		convergence := ports.PermissionAuthoringConvergenceEvent{
			CommandRowID: locked.RowID,
			CommandID:    request.CommandID, CommandDigest: digest,
			CanonicalPermissionSet: locked.CanonicalPermissionSetID,
			AuthoringRevision:      advanced,
		}
		if err := s.convergence.EnqueuePermissionAuthoringConvergence(txCtx, convergence); err != nil {
			return err
		}
		token, err := permissionauthoringv1.NewAuthoringToken(
			locked.CanonicalPermissionSetID,
			advanced,
		)
		if err != nil {
			return err
		}
		response, payload, expiry, err := s.terminalResponse(
			txCtx, request, digest, permissionauthoringv1.OutcomeApplied,
			permissionauthoringv1.RejectionNone, token, locked,
		)
		if err != nil {
			return err
		}
		if err := s.commands.CompleteBound(txCtx, locked.RowID, payload, expiry); err != nil {
			return err
		}
		if !ports.RegisterAfterCommitHook(txCtx, s.convergence.WakePermissionAuthoringConvergence) {
			return errors.New("permission authoring transaction does not support after-commit convergence")
		}
		terminal = response
		return nil
	})
	return terminal, err
}

func (s *PermissionAuthoringService) completeBoundRejection(
	ctx context.Context,
	request permissionauthoringv1.Request,
	digest string,
	row ports.PermissionAuthoringCommand,
	code permissionauthoringv1.RejectionCode,
	destination *permissionauthoringv1.Response,
) error {
	response, payload, expiry, err := s.terminalResponse(
		ctx, request, digest, permissionauthoringv1.OutcomeRejected, code, "", row,
	)
	if err != nil {
		return err
	}
	if err := s.commands.CompleteBound(ctx, row.RowID, payload, expiry); err != nil {
		return err
	}
	*destination = response
	return nil
}

func (s *PermissionAuthoringService) terminalResponse(
	ctx context.Context,
	request permissionauthoringv1.Request,
	digest string,
	outcome permissionauthoringv1.Outcome,
	rejection permissionauthoringv1.RejectionCode,
	token string,
	row ports.PermissionAuthoringCommand,
) (permissionauthoringv1.Response, []byte, time.Time, error) {
	receipts, err := s.receipts.PermissionAuthoringReceipts(ctx)
	if err != nil {
		return permissionauthoringv1.Response{}, nil, time.Time{}, err
	}
	expires, err := s.commands.TerminalOutcomeExpiry(ctx, row.RowID)
	if err != nil {
		return permissionauthoringv1.Response{}, nil, time.Time{}, err
	}
	response := permissionauthoringv1.Response{
		CommandID: request.CommandID, CommandDigest: digest, Outcome: outcome,
		RejectionCode: rejection, AuthoringToken: token, Receipts: receipts,
		OutcomeExpiresAtUTC: expires.Format(time.RFC3339Nano),
	}
	if outcome != permissionauthoringv1.OutcomeRejected {
		response.CanonicalPermissionSetID = row.CanonicalPermissionSetID
	}
	if err := permissionauthoringv1.ValidateResponse(request, response); err != nil {
		return permissionauthoringv1.Response{}, nil, time.Time{}, fmt.Errorf("build permission authoring terminal response: %w", err)
	}
	payload, err := json.Marshal(response)
	if err != nil {
		return permissionauthoringv1.Response{}, nil, time.Time{}, fmt.Errorf("encode permission authoring terminal response: %w", err)
	}
	return response, payload, expires, nil
}

func decodePermissionAuthoringReplay(
	request permissionauthoringv1.Request,
	row ports.PermissionAuthoringCommand,
) (permissionauthoringv1.Response, error) {
	var response permissionauthoringv1.Response
	if row.State != ports.PermissionAuthoringCommandCompleted || len(row.ResultPayload) == 0 {
		return response, errors.New("permission authoring command has no terminal result")
	}
	if err := json.Unmarshal(row.ResultPayload, &response); err != nil {
		return response, fmt.Errorf("decode permission authoring terminal replay: %w", err)
	}
	if err := permissionauthoringv1.ValidateResponse(request, response); err != nil {
		return permissionauthoringv1.Response{}, fmt.Errorf("validate permission authoring terminal replay: %w", err)
	}
	return response, nil
}

func verifyPermissionAuthoringIdentity(
	row ports.PermissionAuthoringCommand,
	request permissionauthoringv1.Request,
	digest string,
) error {
	if row.RequestDigest != digest || row.TargetKind != string(request.Target.Kind) ||
		row.TargetID != request.Target.ID || row.ExpectedAuthoringToken != request.ExpectedAuthoringToken {
		return invalidPermissionAuthoringIdentity()
	}
	return nil
}

func invalidPermissionAuthoringIdentity() error {
	return &permissionauthoringv1.Failure{
		Category:         permissionauthoringv1.FailureInvalidRequest,
		InvalidComponent: permissionauthoringv1.InvalidComponentCommandData,
	}
}

func permissionAuthoringScopeDigest(authorityScope, actorID string) string {
	sum := sha256.Sum256([]byte("permission-authoring-v1\x00" + authorityScope + "\x00" + actorID))
	return hex.EncodeToString(sum[:])
}

// CheckPermissionSetAuthorable enforces both PermissionSet definition owner
// boundaries before any family plan is built. OwnerPack uses a NULL boundary:
// NULL is administrator-owned, while every non-NULL value (including blank or
// whitespace legacy values) remains pack-owned and immutable outside the
// component lifecycle provider.
func (ps *PermissionService) CheckPermissionSetAuthorable(ctx context.Context, permissionSetID string) (permissionauthoringv1.RejectionCode, error) {
	if ps == nil || ps.repo == nil {
		return permissionauthoringv1.RejectionNone, errors.New("permission service is unavailable")
	}
	permissionSet, err := ps.repo.GetRecordByID(ctx, constants.TablePermissionSet, permissionSetID)
	if err != nil {
		return permissionauthoringv1.RejectionNone, err
	}
	if _, present := permissionSet[constants.FieldPermissionSet_OwnerPack]; !present {
		return permissionauthoringv1.RejectionNone, errors.New("PermissionSet owner boundary is unavailable")
	}
	if permissionSetOwnerPackManaged(permissionSet) {
		return permissionauthoringv1.RejectionDenied, nil
	}
	owned, err := ps.IsSystemProfileOwnedPermissionSet(ctx, permissionSetID)
	if err != nil {
		return permissionauthoringv1.RejectionNone, err
	}
	if owned {
		return permissionauthoringv1.RejectionDenied, nil
	}
	return permissionauthoringv1.RejectionNone, nil
}

func permissionAuthoringTargetMatchesRole(target permissionauthoringv1.TargetKind, role ports.PermissionAuthoringAggregateRole) bool {
	return target == permissionauthoringv1.TargetProfile && role == ports.PermissionAuthoringProfileBaseline ||
		target == permissionauthoringv1.TargetPermissionSet && role == ports.PermissionAuthoringAdditivePermissionSet
}

func permissionSetOwnerPackManaged(permissionSet map[string]any) bool {
	owner, present := permissionSet[constants.FieldPermissionSet_OwnerPack]
	return present && owner != nil
}
