package records

import (
	"context"
	"fmt"
	"sort"
	"strings"
	"sync"

	appErrors "github.com/nexuscrm/kernel/pkg/errors"
	"github.com/nexuscrm/kernel/pkg/recordlocks"
	"github.com/nexuscrm/kernel/pkg/schemacatalog"
	"github.com/nexuscrm/persistence"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/contract"
)

const packManagedPermissionSetDefinitionLockedMessage = "Pack-owned Permission Set definitions are immutable via the standard record write surface."

var (
	managedPermissionSetDefinitionInventoryOnce sync.Once
	managedPermissionSetDefinitionInventory     schemacatalog.PermissionSetDefinitionInventory
	managedPermissionSetDefinitionInventoryErr  error
)

func managedPermissionSetDefinitionWriteGuardRequiresPessimisticTransaction(tableName string) bool {
	relation, found, err := managedPermissionSetDefinitionRelation(tableName)
	return err != nil || found && relation.Role != schemacatalog.PermissionSetDefinitionRoleSeparateAssignment
}

func managedPermissionSetDefinitionRelation(tableName string) (schemacatalog.PermissionSetDefinitionRelation, bool, error) {
	managedPermissionSetDefinitionInventoryOnce.Do(func() {
		managedPermissionSetDefinitionInventory, managedPermissionSetDefinitionInventoryErr =
			schemacatalog.ManagedPermissionSetDefinitionInventory()
	})
	if managedPermissionSetDefinitionInventoryErr != nil {
		return schemacatalog.PermissionSetDefinitionRelation{}, false, managedPermissionSetDefinitionInventoryErr
	}
	relation, found := managedPermissionSetDefinitionInventory.RelationForTable(tableName)
	return relation, found, nil
}

func managedPermissionSetDefinitionPhysicalDeleteContract(tableName string) (parentField string, root bool, protected bool, err error) {
	relation, found, err := managedPermissionSetDefinitionRelation(tableName)
	if err != nil || !found {
		return "", false, false, err
	}
	switch relation.Role {
	case schemacatalog.PermissionSetDefinitionRoleRoot:
		return "", true, true, nil
	case schemacatalog.PermissionSetDefinitionRoleModeledGrant:
		return relation.ParentField, false, true, nil
	default:
		return "", false, false, nil
	}
}

func (ps *PersistenceService) validateManagedPermissionSetDefinitionWriteContract(ctx context.Context, event contract.RecordWriteValidationEvent) error {
	relation, found, err := managedPermissionSetDefinitionRelation(event.ObjectName)
	if err != nil {
		return appErrors.NewInternalError("managed Permission Set definition inventory is unavailable", err)
	}
	if !found || relation.Role == schemacatalog.PermissionSetDefinitionRoleSeparateAssignment {
		return appErrors.NewInternalError(
			fmt.Sprintf("managed Permission Set definition capability is not classified for %q", event.ObjectName),
			nil,
		)
	}
	existing := event.OldRecord
	final := event.Record
	if relation.Role == schemacatalog.PermissionSetDefinitionRoleRoot {
		if permissionSetDefinitionIsPackOwned(existing) || permissionSetDefinitionIsPackOwned(final) {
			return appErrors.NewForbiddenError(packManagedPermissionSetDefinitionLockedMessage)
		}
		return nil
	}

	targets := permissionSetDefinitionWriteTargets(relation, existing, final, event.PendingCreates)
	if len(targets) == 0 {
		return nil
	}
	if ps == nil || ps.repo == nil {
		return appErrors.NewInternalError("managed Permission Set definition guard is unavailable", nil)
	}
	lockCtx := authoritativePermissionLockReadContext(ctx)
	targetRecords := make([]contract.Record, 0, len(targets))
	for _, target := range targets {
		targetRecords = append(targetRecords, contract.Record{relation.ParentField: target.permissionSetID})
	}
	keys, err := ps.managedPermissionSetDefinitionParentKeys(lockCtx, event.ObjectName, targetRecords...)
	if err != nil {
		return err
	}
	if len(keys) == 0 {
		return nil
	}
	scope := recordlocks.ScopeFromContext(lockCtx)
	if scope == nil {
		return appErrors.NewInternalError("managed Permission Set definition guard requires parent-lock coordination", recordlocks.ErrMissingParentLockScope)
	}
	var missing []recordlocks.ParentKey
	for _, key := range keys {
		if !scope.HasAcquired(key) {
			missing = append(missing, key)
		}
	}
	if len(missing) > 0 {
		return &parentLockPrelockRetryError{
			keys:   missing,
			reason: "managed Permission Set definition parent",
		}
	}
	rejectByID := make(map[string]bool, len(targets))
	for _, target := range targets {
		rejectByID[target.permissionSetID] = target.rejectManaged
	}
	for _, key := range keys {
		permissionSet, err := ps.repo.FindAny(persistence.WithPhysicalReadOnly(lockCtx), constants.TablePermissionSet, key.RecordID)
		if err != nil {
			return appErrors.NewInternalError(fmt.Sprintf("failed to read locked Permission Set %q for managed definition validation", key.RecordID), err)
		}
		if rejectByID[key.RecordID] && permissionSetDefinitionIsPackOwned(permissionSet) {
			return appErrors.NewForbiddenError(packManagedPermissionSetDefinitionLockedMessage)
		}
	}
	return nil
}

func (ps *PersistenceService) managedPermissionSetDefinitionParentKeys(
	ctx context.Context,
	objectName string,
	records ...contract.Record,
) ([]recordlocks.ParentKey, error) {
	relation, found, err := managedPermissionSetDefinitionRelation(objectName)
	if err != nil {
		return nil, appErrors.NewInternalError("managed Permission Set definition inventory is unavailable", err)
	}
	if !found || relation.Role == schemacatalog.PermissionSetDefinitionRoleRoot {
		return nil, nil
	}
	if ps == nil || ps.repo == nil {
		return nil, appErrors.NewInternalError("managed Permission Set definition parent resolver is unavailable", nil)
	}
	unique := map[string]struct{}{}
	for _, record := range records {
		if id := strings.TrimSpace(permissionSetDefinitionStringField(record, relation.ParentField)); id != "" {
			unique[id] = struct{}{}
		}
	}
	ids := make([]string, 0, len(unique))
	for id := range unique {
		ids = append(ids, id)
	}
	sort.Strings(ids)
	keys := make([]recordlocks.ParentKey, 0, len(ids))
	readCtx := persistence.WithPhysicalReadOnly(authoritativePermissionLockReadContext(ctx))
	for _, id := range ids {
		permissionSet, err := ps.repo.FindAny(readCtx, constants.TablePermissionSet, id)
		if err != nil {
			return nil, appErrors.NewInternalError(fmt.Sprintf("failed to resolve Permission Set %q for managed definition validation", id), err)
		}
		if permissionSet != nil {
			keys = append(keys, recordlocks.ParentKey{ObjectName: constants.TablePermissionSet, RecordID: id})
		}
	}
	return keys, nil
}

type permissionSetDefinitionWriteTarget struct {
	permissionSetID string
	rejectManaged   bool
}

func permissionSetDefinitionWriteTargets(
	relation schemacatalog.PermissionSetDefinitionRelation,
	existing contract.Record,
	final contract.Record,
	pending []contract.RecordWriteValidationPendingCreate,
) []permissionSetDefinitionWriteTarget {
	rejectByID := map[string]bool{}
	add := func(record contract.Record, rejectManaged bool) {
		if id := strings.TrimSpace(permissionSetDefinitionStringField(record, relation.ParentField)); id != "" {
			rejectByID[id] = rejectByID[id] || rejectManaged
		}
	}
	switch relation.Role {
	case schemacatalog.PermissionSetDefinitionRoleModeledGrant:
		add(existing, true)
	case schemacatalog.PermissionSetDefinitionRoleNegativeEdge:
		add(existing, false)
	}
	add(final, true)
	for _, create := range pending {
		add(create.Record, false)
	}
	targets := make([]permissionSetDefinitionWriteTarget, 0, len(rejectByID))
	for id, rejectManaged := range rejectByID {
		targets = append(targets, permissionSetDefinitionWriteTarget{
			permissionSetID: id,
			rejectManaged:   rejectManaged,
		})
	}
	sort.Slice(targets, func(i, j int) bool {
		return targets[i].permissionSetID < targets[j].permissionSetID
	})
	return targets
}

func permissionSetDefinitionIsPackOwned(permissionSet map[string]interface{}) bool {
	if permissionSet == nil {
		return false
	}
	if value, present := permissionSet[constants.FieldPermissionSet_OwnerPack]; present {
		return value != nil
	}
	for field, value := range permissionSet {
		if strings.EqualFold(field, constants.FieldPermissionSet_OwnerPack) {
			return value != nil
		}
	}
	return false
}

func permissionSetDefinitionStringField(record map[string]interface{}, field string) string {
	value, _ := record[field].(string)
	return value
}
