package records

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

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

type softDeleteReferenceGuardEdge struct {
	childObjectName string
	fieldName       string
	cascade         bool
	retain          bool
}

var (
	managedSoftDeleteReferenceGuardEdgesOnce     sync.Once
	managedSoftDeleteReferenceGuardEdgesByObject map[string][]softDeleteReferenceGuardEdge
)

func (s *PersistenceService) validateSoftDeleteReferenceGuard(ctx context.Context, objectName string, recordID string) error {
	return s.checkSoftDeleteReferences(ctx, objectName, recordID, false)
}

func (s *PersistenceService) checkSoftDeleteReferences(ctx context.Context, objectName string, recordID string, afterCascade bool) error {
	objectName = strings.TrimSpace(objectName)
	recordID = strings.TrimSpace(recordID)
	if objectName == "" || recordID == "" {
		return nil
	}

	edges, err := runtimeSoftDeleteReferences(objectName)
	if err != nil {
		return err
	}
	relation, found, err := managedPermissionSetDefinitionRelation(objectName)
	if err != nil {
		return err
	}
	permanentDefinition := found && relation.Role == schemacatalog.PermissionSetDefinitionRoleRoot
	permanentDelete := permanentDefinition || recordHardDeleteAllowed(ctx, objectName, recordID)
	if s.metadata != nil {
		if object := s.metadata.GetSchema(ctx, objectName); object != nil && strings.EqualFold(object.DeleteMode, constants.DeleteModeHard) {
			permanentDelete = true
		}
	}
	for _, edge := range edges {
		// Explicit parent-owned retained configuration does not dangle: the
		// owning lifecycle gates activation on the parent's liveness, and a
		// restore reuses the same rows. Physical FK CASCADE alone grants no
		// such disposition, and external references remain restrictive.
		if edge.retain && !permanentDelete {
			continue
		}
		// Only an explicit Runtime declaration admits an owned child here.
		// The delete pipeline executes that child's complete lifecycle and
		// checks the closure again before committing parent deletion.
		if edge.cascade && !afterCascade {
			continue
		}
		children, err := s.repo.GetChildren(authoritativeSoftDeleteReferenceGuardReadContext(ctx), edge.childObjectName, edge.fieldName, recordID)
		if err != nil {
			return appErrors.NewInternalError(fmt.Sprintf("failed to resolve %s.%s references for %s delete guard", edge.childObjectName, edge.fieldName, objectName), err)
		}
		for _, child := range children {
			if child == nil {
				continue
			}
			if !isGuardFieldTruthy(child[constants.FieldIsDeleted]) {
				return appErrors.NewConflictErrorWithMessage(objectName, softDeleteReferenceGuardConflictMessage(objectName, edge))
			}
		}
		if permanentDefinition {
			// A physical parent removal must not silently FK-cascade historical
			// assignments or other external-owner records. Their owners must
			// purge/detach them first; owned grants are purged by this lifecycle.
			deleted, err := s.repo.GetDeletedChildren(authoritativeSoftDeleteReferenceGuardReadContext(ctx), edge.childObjectName, edge.fieldName, recordID)
			if err != nil {
				return appErrors.NewInternalError("failed to resolve historical permission definition references", err)
			}
			if len(deleted) > 0 {
				return appErrors.NewConflictErrorWithMessage(objectName, softDeleteReferenceGuardConflictMessage(objectName, edge)+" (including deleted references; purge them first)")
			}
		}
	}
	return nil
}

func managedSoftDeleteReferenceGuardEdges(objectName string) []softDeleteReferenceGuardEdge {
	managedSoftDeleteReferenceGuardEdgesOnce.Do(func() {
		managedSoftDeleteReferenceGuardEdgesByObject = softDeleteReferenceGuardEdgesFromDefinitions(managedTableDefinitions())
	})
	edges := managedSoftDeleteReferenceGuardEdgesByObject[securityWriteValidationObjectKey(objectName)]
	if len(edges) == 0 {
		return nil
	}
	out := make([]softDeleteReferenceGuardEdge, len(edges))
	copy(out, edges)
	return out
}

func softDeleteReferenceGuardEdgesFromDefinitions(definitions []schema.TableDefinition) map[string][]softDeleteReferenceGuardEdge {
	edgesByObject := make(map[string][]softDeleteReferenceGuardEdge)
	for _, definition := range definitions {
		childObjectName := strings.TrimSpace(definition.TableName)
		if childObjectName == "" {
			continue
		}
		for _, fk := range definition.ForeignKeys {
			if !foreignKeyBlocksSoftDelete(fk.OnDelete) {
				continue
			}
			parentObjectName, ok := foreignKeyParentObjectName(fk.References)
			if !ok {
				continue
			}
			fieldName := strings.TrimSpace(fk.Column)
			if fieldName == "" {
				continue
			}
			key := securityWriteValidationObjectKey(parentObjectName)
			if key == "" {
				continue
			}
			edgesByObject[key] = append(edgesByObject[key], softDeleteReferenceGuardEdge{
				childObjectName: childObjectName,
				fieldName:       fieldName,
				retain:          fk.RetainOnSoftDelete,
			})
		}
	}
	for key := range edgesByObject {
		sort.Slice(edgesByObject[key], func(i, j int) bool {
			if edgesByObject[key][i].childObjectName == edgesByObject[key][j].childObjectName {
				return edgesByObject[key][i].fieldName < edgesByObject[key][j].fieldName
			}
			return edgesByObject[key][i].childObjectName < edgesByObject[key][j].childObjectName
		})
	}
	return edgesByObject
}

// foreignKeyBlocksSoftDelete treats table-level FK metadata as a fail-closed
// soft-delete guard. `foreignKeys[].onDelete` remains physical DDL metadata; it
// is not a generic runtime cascade/set-null engine for soft deletes. Until a
// caller has an explicit runtime action path, live managed FK children must
// block soft delete so the parent cannot be tombstoned while children remain
// live and dangling.
func foreignKeyBlocksSoftDelete(onDelete string) bool {
	return true
}

func foreignKeyParentObjectName(ref string) (string, bool) {
	ref = strings.TrimSpace(ref)
	if ref == "" {
		return "", false
	}
	parent, _, ok := strings.Cut(ref, "(")
	parent = strings.TrimSpace(parent)
	return parent, ok && parent != ""
}

func softDeleteReferenceGuardConflictMessage(objectName string, edge softDeleteReferenceGuardEdge) string {
	return fmt.Sprintf("%s cannot be deleted while %s.%s references it; remove or reassign referencing records first", objectName, edge.childObjectName, edge.fieldName)
}

func authoritativeSoftDeleteReferenceGuardReadContext(ctx context.Context) context.Context {
	if constants.GetSecurityContext(ctx) >= constants.SystemContext {
		return ctx
	}
	return constants.WithSecurityContext(ctx, constants.SystemContext, "soft_delete_reference_guard_read")
}
