package records

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

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

// Deleting a definition is a permanent lifecycle operation, not an undeletable
// collection of mixed soft/physical children. Keep this disposition at the
// aggregate owner: changing an individual grant retains its existing policy,
// and an already-published Catalog does not need a new DeleteMode projection.
func permissionSetRootDeleteContext(ctx context.Context, objectName, id string) (context.Context, error) {
	relation, found, err := managedPermissionSetDefinitionRelation(objectName)
	if err != nil {
		return ctx, appErrors.NewInternalError("permission definition delete inventory is unavailable", err)
	}
	if found && relation.Role == schemacatalog.PermissionSetDefinitionRoleRoot {
		ctx = withRecordHardDeleteTarget(ctx, objectName, id)
	}
	return ctx, nil
}

// The same catalog owns definition identity, parent locking, and explicit
// Runtime delete actions. Physical FK CASCADE is never treated as permission
// to soft-delete a parent. References without an action remain restrictive.
func runtimeSoftDeleteReferences(objectName string) ([]softDeleteReferenceGuardEdge, error) {
	edges := managedSoftDeleteReferenceGuardEdges(objectName)
	relation, found, err := managedPermissionSetDefinitionRelation(objectName)
	if err != nil {
		return nil, appErrors.NewInternalError("permission definition delete inventory is unavailable", err)
	}
	if !found || relation.Role != schemacatalog.PermissionSetDefinitionRoleRoot {
		return edges, nil
	}
	for _, child := range managedPermissionSetDefinitionInventory.Relations() {
		if child.Role == schemacatalog.PermissionSetDefinitionRoleRoot {
			continue
		}
		edge := softDeleteReferenceGuardEdge{
			childObjectName: child.TableName, fieldName: child.ParentField,
			cascade: child.OnRootDelete == schemacatalog.PermissionSetRootDeleteCascade,
		}
		replaced := false
		for i, existing := range edges {
			if strings.EqualFold(existing.childObjectName, edge.childObjectName) && strings.EqualFold(existing.fieldName, edge.fieldName) {
				edges[i], replaced = edge, true
				break
			}
		}
		if !replaced {
			edges = append(edges, edge)
		}
	}
	sort.Slice(edges, func(i, j int) bool {
		return edges[i].childObjectName+"."+edges[i].fieldName < edges[j].childObjectName+"."+edges[j].fieldName
	})
	return edges, nil
}

func (ps *PersistenceService) cascadePermissionSetOwnedReferences(ctx context.Context, actor *models.UserSession, objectName, id string) error {
	relation, found, err := managedPermissionSetDefinitionRelation(objectName)
	if err != nil || !found || relation.Role != schemacatalog.PermissionSetDefinitionRoleRoot {
		return err
	}
	if ps.txManager == nil || !ps.txManager.IsTransactionActive(ctx) || !ps.txManager.IsPessimisticTx(ctx) {
		return appErrors.NewInternalError("permission definition deletion requires an active pessimistic transaction", nil)
	}
	edges, err := runtimeSoftDeleteReferences(objectName)
	if err != nil {
		return err
	}
	// Root authorization and all before-delete validators have already passed.
	// This privilege is limited to its catalog-owned children; their validators,
	// parent locks, revision authority, audit and lifecycle hooks still run.
	childCtx := constants.WithSecurityContext(ctx, constants.SystemContext, "permission_definition_owned_child_delete")
	for _, edge := range edges {
		if !edge.cascade {
			continue
		}
		children, err := ps.repo.GetChildren(childCtx, edge.childObjectName, edge.fieldName, id)
		if err != nil {
			return fmt.Errorf("read owned permission definition children: %w", err)
		}
		deletedChildren, err := ps.repo.GetDeletedChildren(childCtx, edge.childObjectName, edge.fieldName, id)
		if err != nil {
			return fmt.Errorf("read deleted permission definition children: %w", err)
		}
		children = append(children, deletedChildren...)
		ids := make([]string, 0, len(children))
		for _, child := range children {
			childID, _ := child[constants.FieldID].(string)
			if strings.TrimSpace(childID) == "" {
				return appErrors.NewInternalError("owned permission definition child has no identity", nil)
			}
			ids = append(ids, childID)
		}
		sort.Strings(ids)
		for _, childID := range ids {
			// The root is already locked; any additional metadata-declared
			// parents still use the normal coordinator before the child lock.
			preflight, err := ps.repo.FindAny(childCtx, edge.childObjectName, childID)
			if err != nil {
				return fmt.Errorf("read owned permission definition child: %w", err)
			}
			if preflight == nil {
				continue
			}
			if err := ps.prelockDeleteParentRows(childCtx, edge.childObjectName, childID, preflight); err != nil {
				return err
			}
			locked, err := ps.repo.GetLock(childCtx, edge.childObjectName, childID)
			if err != nil {
				return fmt.Errorf("lock owned permission definition child: %w", err)
			}
			if locked == nil {
				continue
			}
			if err := ps.ensureDeleteParentRowsAlreadyLocked(childCtx, edge.childObjectName, childID, locked); err != nil {
				return err
			}
			if !strings.EqualFold(permissionSetDefinitionStringField(locked, edge.fieldName), id) {
				return appErrors.NewConflictErrorWithMessage(objectName, "an owned permission row changed parent during deletion")
			}
			if isGuardFieldTruthy(locked[constants.FieldIsDeleted]) {
				// This row's logical delete already ran. Reuse the validated purge
				// seam rather than replaying its delete hooks/audit a second time.
				if err := ps.PhysicalDeleteRaw(childCtx, edge.childObjectName, childID); err != nil {
					return fmt.Errorf("purge deleted permission definition child: %w", err)
				}
				if err := ps.removePermissionDefinitionTombstones(childCtx, edge.childObjectName, childID); err != nil {
					return err
				}
				continue
			}
			deleteCtx := withRecordHardDeleteTarget(childCtx, edge.childObjectName, childID)
			if err := ps.deleteWithOutcomesAuthorized(deleteCtx, edge.childObjectName, childID, actor, nil, nil, nil, func(_ context.Context, locked models.Record) error {
				parent, _ := locked[edge.fieldName].(string)
				if !strings.EqualFold(parent, id) {
					return appErrors.NewConflictErrorWithMessage(objectName, "an owned permission row changed parent during deletion")
				}
				return nil
			}); err != nil {
				// Preserve domain errors: a child veto is not an infrastructure 500.
				return fmt.Errorf("delete owned permission definition child: %w", err)
			}
		}
	}
	return nil
}

// Only tombstones for the exact owned row may be removed. The parent and child
// locks remain held throughout physical purge and tombstone cleanup.
func (ps *PersistenceService) removePermissionDefinitionTombstones(ctx context.Context, objectName, id string) error {
	rows, err := ps.repo.GetChildren(ctx, constants.TableRecycleBin, constants.FieldRecordID, id)
	if err != nil {
		return fmt.Errorf("read permission definition tombstones: %w", err)
	}
	for _, row := range rows {
		binID, _ := row[constants.FieldID].(string)
		if strings.TrimSpace(binID) == "" {
			return appErrors.NewInternalError("permission definition tombstone has no identity", nil)
		}
		locked, err := ps.repo.GetLock(ctx, constants.TableRecycleBin, binID)
		if err != nil {
			return fmt.Errorf("lock permission definition tombstone: %w", err)
		}
		if locked == nil || permissionSetDefinitionStringField(locked, constants.FieldRecordID) != id ||
			!strings.EqualFold(permissionSetDefinitionStringField(locked, constants.FieldObjectAPIName), objectName) {
			continue
		}
		if err := ps.repo.PhysicalDelete(ctx, constants.TableRecycleBin, binID); err != nil {
			return fmt.Errorf("remove permission definition tombstone: %w", err)
		}
	}
	return nil
}

func (ps *PersistenceService) verifyPermissionSetDeleteClosure(ctx context.Context, objectName, id string) error {
	relation, found, err := managedPermissionSetDefinitionRelation(objectName)
	if err != nil || !found || relation.Role != schemacatalog.PermissionSetDefinitionRoleRoot {
		return err
	}
	// Observe the final image after synchronous lifecycle hooks, including
	// Object/Field children that have no physical FK.
	return ps.checkSoftDeleteReferences(ctx, objectName, id, true)
}
