package permissions

import (
	"context"
	"errors"
	"fmt"

	appErrors "github.com/nexuscrm/kernel/pkg/errors"
	"github.com/nexuscrm/kernel/pkg/ports"
	"github.com/nexuscrm/shared/pkg/models"
)

// PermissionSetNotFoundError reports that a permission-set admin mutation
// targeted a missing PermissionSet.
type PermissionSetNotFoundError struct {
	PermissionSetID string
}

func (e *PermissionSetNotFoundError) Error() string {
	return "PermissionSet not found: " + e.PermissionSetID
}

// IsPermissionSetNotFound reports whether err came from a missing
// PermissionSet during an admin mutation.
func IsPermissionSetNotFound(err error) bool {
	var target *PermissionSetNotFoundError
	return errors.As(err, &target)
}

// ApplyError marks the portion of a permission-admin mutation that validates
// and applies the requested grant changes. REST keeps existing wire behavior:
// non-internal apply errors are INVALID_FIELD, while lock/read/transaction
// errors remain SERVER_ERROR.
type ApplyError struct {
	Err error
}

func (e *ApplyError) Error() string {
	if e == nil || e.Err == nil {
		return ""
	}
	return e.Err.Error()
}

func (e *ApplyError) Unwrap() error {
	if e == nil {
		return nil
	}
	return e.Err
}

// IsApplyError reports whether err happened during permission change
// validation/application rather than lock/read/transaction setup.
func IsApplyError(err error) bool {
	var target *ApplyError
	return errors.As(err, &target)
}

// RecordTypeGrantChange is the normalized service input for PermissionSet
// record-type visibility updates.
type RecordTypeGrantChange struct {
	IsVisible bool
}

// AdminService owns permission-admin mutation orchestration: transaction
// boundaries, PermissionSet row locks, lower-level permission writes, and
// cache invalidation. Transport handlers should only parse/project wire.
type AdminService struct {
	permissions                       *PermissionService
	tx                                ports.TransactionManager
	invalidateMetadataProjectionCache func()
}

// AdminServiceOptions configures optional post-commit invalidation hooks.
type AdminServiceOptions struct {
	InvalidateMetadataProjectionCache func()
}

// NewAdminService creates a PermissionSet admin mutation service.
func NewAdminService(permissions *PermissionService, tx ports.TransactionManager, opts AdminServiceOptions) *AdminService {
	return &AdminService{
		permissions:                       permissions,
		tx:                                tx,
		invalidateMetadataProjectionCache: opts.InvalidateMetadataProjectionCache,
	}
}

// SaveSystemGrants validates and writes system-permission grants for a
// PermissionSet under a row lock, then invalidates permission cache after
// successful commit.
func (s *AdminService) SaveSystemGrants(ctx context.Context, permSetID string, grants map[string]bool, actor *models.UserSession) error {
	err := s.withLockedPermissionSet(ctx, permSetID, func(txCtx context.Context) error {
		currentGrants, err := s.permissions.GetPermissionSetGrants(txCtx, permSetID)
		if err != nil {
			return err
		}
		if err := s.permissions.ApplyPermissionChanges(txCtx, permSetID, currentGrants, grants, actor); err != nil {
			return &ApplyError{Err: err}
		}
		return nil
	})
	if err != nil {
		return err
	}
	s.permissions.InvalidateCache()
	return nil
}

// SaveRecordTypeGrants validates and writes record-type visibility grants for
// a PermissionSet under a row lock, then invalidates permission and UI API
// caches after successful commit.
func (s *AdminService) SaveRecordTypeGrants(ctx context.Context, permSetID string, grants map[string]RecordTypeGrantChange) error {
	err := s.withLockedPermissionSet(ctx, permSetID, func(txCtx context.Context) error {
		if err := s.permissions.SavePermissionSetRecordTypeGrants(txCtx, permSetID, grants); err != nil {
			return &ApplyError{Err: err}
		}
		return nil
	})
	if err != nil {
		return err
	}
	s.permissions.InvalidateCache()
	if s.invalidateMetadataProjectionCache != nil {
		s.invalidateMetadataProjectionCache()
	}
	return nil
}

// SaveCustomPermissionGrants validates and writes custom-permission grants for
// a PermissionSet under a row lock, then invalidates permission cache after
// successful commit.
func (s *AdminService) SaveCustomPermissionGrants(ctx context.Context, permSetID string, grants map[string]bool) error {
	err := s.withLockedPermissionSet(ctx, permSetID, func(txCtx context.Context) error {
		if err := s.permissions.SavePermissionSetCustomPermGrants(txCtx, permSetID, grants); err != nil {
			return &ApplyError{Err: err}
		}
		return nil
	})
	if err != nil {
		return err
	}
	s.permissions.InvalidateCache()
	return nil
}

func (s *AdminService) withLockedPermissionSet(ctx context.Context, permSetID string, fn func(context.Context) error) error {
	if s == nil || s.permissions == nil || s.tx == nil {
		return fmt.Errorf("permission admin service is not configured")
	}
	return s.tx.WithPessimisticTransaction(ctx, func(txCtx context.Context) error {
		exists, err := s.permissions.LockPermissionSet(txCtx, permSetID)
		if err != nil {
			return err
		}
		if !exists {
			return &PermissionSetNotFoundError{PermissionSetID: permSetID}
		}
		// System-profile-owned permission sets are system-managed: reject grant
		// mutations before any write, mirroring the sobject-surface guard.
		owned, err := s.permissions.IsSystemProfileOwnedPermissionSet(txCtx, permSetID)
		if err != nil {
			return err
		}
		if owned {
			return appErrors.NewForbiddenError(systemProfileOwnedPermissionSetImmutableMessage)
		}
		return fn(txCtx)
	})
}
