package runtimeassembly

import (
	"context"
	"errors"
	"fmt"
	"reflect"
	"strings"

	layoutdocument "github.com/nexuscrm/backend/internal/layoutauthoring"
	database "github.com/nexuscrm/db"
	apperrors "github.com/nexuscrm/kernel/pkg/errors"
	"github.com/nexuscrm/metadata"
	"github.com/nexuscrm/permissions"
	"github.com/nexuscrm/persistence"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
	semantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/layoutauthoringv2"
	"github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/permissionstatev1"
)

// LayoutAuthoringRuntime is the process-local Runtime graph for synchronous
// Layout administration. It owns no scheduler, queue, or Catalog publisher.
type LayoutAuthoringRuntime struct {
	Engine *LayoutAuthoringEngine
}

type LayoutAuthoringEngine struct {
	metadata     *metadata.MetadataService
	permissions  *permissions.PermissionService
	policy       *permissions.TransactionCurrentPermissionAuthoringPolicy
	transactions *persistence.TransactionManager
}

func NewLayoutAuthoringRuntime(
	ctx context.Context,
	connection *database.TiDBConnection,
	permissionService *permissions.PermissionService,
) (*LayoutAuthoringRuntime, error) {
	if ctx == nil || connection == nil || connection.DB() == nil || permissionService == nil {
		return nil, errors.New("Layout authoring runtime requires database and permission owners")
	}
	if err := persistence.VerifyLayoutAuthoringSchema(ctx, connection.DB()); err != nil {
		return nil, err
	}
	transactions := persistence.NewTransactionManager(connection)
	authority, err := persistence.NewPermissionStateAuthority(transactions)
	if err != nil {
		return nil, err
	}
	repository := persistence.NewMetadataRepository(connection.DB())
	directWriters, err := persistence.NewPermissionStateDirectWriterCoordinator(transactions, authority)
	if err != nil {
		return nil, err
	}
	if err := repository.SetPermissionStateDirectWriterCoordinator(directWriters); err != nil {
		return nil, err
	}
	if err := repository.CheckLayoutAuthoringMutationReady(transactions, authority); err != nil {
		return nil, err
	}
	metadataService := metadata.NewMetadataService(repository, nil, nil)
	metadataService.SetTransactionManager(transactions)
	if err := metadataService.CheckLayoutAuthoringAdministrationReady(); err != nil {
		return nil, err
	}
	policy, err := permissions.NewTransactionCurrentPermissionAuthoringPolicy(permissionService, authority)
	if err != nil {
		return nil, err
	}
	return &LayoutAuthoringRuntime{Engine: &LayoutAuthoringEngine{
		metadata: metadataService, permissions: permissionService, policy: policy, transactions: transactions,
	}}, nil
}

func (engine *LayoutAuthoringEngine) ReadWorkspace(
	ctx context.Context,
	objectID string,
) (metadata.LayoutAuthoringAggregateSnapshot, error) {
	if engine == nil || engine.metadata == nil {
		return metadata.LayoutAuthoringAggregateSnapshot{}, errors.New("Layout authoring runtime is unavailable")
	}
	return engine.metadata.ReadLayoutAuthoringAggregate(ctx, objectID)
}

func (engine *LayoutAuthoringEngine) ReadStoredLayout(
	ctx context.Context,
	objectID string,
	layoutID string,
) (*models.PageLayout, metadata.LayoutAuthoringAggregateSnapshot, error) {
	snapshot, err := engine.ReadWorkspace(ctx, objectID)
	if err != nil {
		return nil, metadata.LayoutAuthoringAggregateSnapshot{}, err
	}
	layout := layoutInAggregate(snapshot, layoutID)
	if layout == nil {
		return nil, metadata.LayoutAuthoringAggregateSnapshot{}, apperrors.NewNotFoundError("PageLayout", layoutID)
	}
	return layout, snapshot, nil
}

func (engine *LayoutAuthoringEngine) Mutate(
	ctx context.Context,
	actor models.UserSession,
	permissionFloor permissionstatev1.Version,
	request semantic.Request,
) (semantic.Response, error) {
	if engine == nil || engine.metadata == nil || engine.policy == nil || engine.transactions == nil {
		return semantic.Response{}, errors.New("Layout authoring runtime is unavailable")
	}
	var response semantic.Response
	err := engine.transactions.WithPessimisticRetry(ctx, func(txCtx context.Context) error {
		response = semantic.Response{}
		before, err := engine.metadata.LockLayoutAuthoringAggregate(
			txCtx, request.ObjectID,
			metadata.LayoutAuthoringVersionPreconditions{WorkspaceVersion: &request.ExpectedLayoutRevision},
		)
		if err != nil {
			return err
		}
		_, authorized, err := engine.policy.AuthorizeSystemPermissionAuthoring(
			txCtx, permissionFloor, &actor, string(constants.PermCustomizeApplication),
		)
		if err != nil {
			return err
		}
		if !authorized {
			return apperrors.NewForbiddenError("CustomizeApplication permission is required")
		}
		changed, layoutID, assignmentID, validateLayoutID, validateListFields, err := engine.apply(
			txCtx, actor.ID, request, before,
		)
		if err != nil {
			return err
		}
		if !changed {
			response = semantic.Response{Changed: false, LayoutRevision: before.Versions.WorkspaceVersion, LayoutID: layoutID, AssignmentID: assignmentID}
			return nil
		}
		profiles, recordTypes, err := engine.lockSelectorAuthorities(txCtx, request.ObjectID, request.Operation)
		if err != nil {
			return err
		}
		after, err := engine.metadata.LockLayoutAuthoringAggregate(txCtx, request.ObjectID, metadata.LayoutAuthoringVersionPreconditions{})
		if err != nil {
			return err
		}
		if err := validateChangedLayoutRouting(
			after, request.Operation, profiles, recordTypes, engine.permissions, txCtx,
		); err != nil {
			return err
		}
		if validateLayoutID != "" {
			layout := layoutInAggregate(after, validateLayoutID)
			if layout == nil {
				return errors.New("Layout mutation postimage is missing")
			}
			document, err := layoutdocument.DocumentFromPageLayout(*layout)
			if err != nil {
				return err
			}
			if err := engine.metadata.ValidateAndLockLayoutAuthoringReferences(
				txCtx, document, after.OrderedListFields, validateListFields, request.HistoryRelatedLists...,
			); err != nil {
				return err
			}
		}
		response = semantic.Response{Changed: true, LayoutRevision: after.Versions.WorkspaceVersion, LayoutID: layoutID, AssignmentID: assignmentID}
		return nil
	}, 3)
	if err != nil {
		return semantic.Response{}, err
	}
	return response, nil
}

func (engine *LayoutAuthoringEngine) apply(
	ctx context.Context,
	actorID string,
	request semantic.Request,
	before metadata.LayoutAuthoringAggregateSnapshot,
) (changed bool, layoutID, assignmentID *string, validateLayoutID string, validateListFields bool, err error) {
	op := request.Operation
	switch {
	case op.Create != nil:
		document := op.Create.Document
		layout, convertErr := layoutdocument.PageLayoutFromDocument(document)
		if convertErr != nil {
			err = convertErr
			return
		}
		layout.Source, layout.IsCustomized = "admin_created", true
		listFields, applyErr := layoutdocument.ApplyListFields(before.OrderedListFields, op.Create.ListFields)
		if applyErr != nil {
			err = applyErr
			return
		}
		var listPointer *[]string
		if op.Create.ListFields.Mode != "PRESERVE" {
			listPointer = &listFields
		}
		err = engine.metadata.CreateLayoutForAdministration(ctx, layout, listPointer, "admin_create", &actorID)
		if err == nil {
			changed = true
			layoutID = stringPointer(document.LayoutID)
			validateLayoutID = document.LayoutID
			validateListFields = listPointer != nil
		}
		return
	case op.Update != nil:
		current := layoutInAggregate(before, op.Update.LayoutID)
		if current == nil {
			err = apperrors.NewNotFoundError("PageLayout", op.Update.LayoutID)
			return
		}
		document, convertErr := layoutdocument.DocumentFromPageLayout(*current)
		if convertErr != nil {
			err = convertErr
			return
		}
		originalDocument := document
		document, applyErr := layoutdocument.ApplyDocumentChanges(document, op.Update.Changes)
		if applyErr != nil {
			err = applyErr
			return
		}
		listFields, applyErr := layoutdocument.ApplyListFields(before.OrderedListFields, op.Update.ListFields)
		if applyErr != nil {
			err = applyErr
			return
		}
		documentChanged := !reflect.DeepEqual(originalDocument, document)
		listFieldsChanged := !reflect.DeepEqual(before.OrderedListFields, listFields)
		if !documentChanged && !listFieldsChanged {
			layoutID = stringPointer(current.ID)
			return
		}
		if !documentChanged {
			err = engine.metadata.SaveLayoutListFieldsForAdministration(ctx, request.ObjectID, listFields)
			if err == nil {
				changed = true
				layoutID = stringPointer(current.ID)
				validateLayoutID = current.ID
				validateListFields = true
			}
			return
		}
		updated, convertErr := layoutdocument.PageLayoutFromDocument(document)
		if convertErr != nil {
			err = convertErr
			return
		}
		copyLayoutLifecycle(updated, current)
		var listPointer *[]string
		if listFieldsChanged {
			listPointer = &listFields
		}
		err = engine.metadata.SaveLayoutForAdministration(ctx, updated, before.Versions.LayoutAggregateVersions[current.ID], listPointer, "admin_edit", &actorID)
		if err == nil {
			changed = true
			layoutID = stringPointer(current.ID)
			validateLayoutID = current.ID
			validateListFields = listPointer != nil
		}
		return
	case op.Clone != nil:
		current := layoutInAggregate(before, op.Clone.SourceLayoutID)
		if current == nil {
			err = apperrors.NewNotFoundError("PageLayout", op.Clone.SourceLayoutID)
			return
		}
		document, convertErr := layoutdocument.DocumentFromPageLayout(*current)
		if convertErr != nil {
			err = convertErr
			return
		}
		document.LayoutID, document.Name = op.Clone.TargetLayoutID, op.Clone.TargetName
		cloned, convertErr := layoutdocument.PageLayoutFromDocument(document)
		if convertErr != nil {
			err = convertErr
			return
		}
		cloned.Source, cloned.IsCustomized = "cloned", true
		err = engine.metadata.CreateLayoutForAdministration(ctx, cloned, nil, "admin_clone", &actorID)
		if err == nil {
			changed = true
			layoutID = stringPointer(cloned.ID)
			validateLayoutID = cloned.ID
		}
		return
	case op.SetDefault != nil:
		current := layoutInAggregate(before, op.SetDefault.LayoutID)
		if current == nil {
			err = apperrors.NewNotFoundError("PageLayout", op.SetDefault.LayoutID)
			return
		}
		if current.IsDefault {
			layoutID = stringPointer(current.ID)
			return
		}
		err = engine.metadata.SetDefaultLayout(ctx, request.ObjectID, current.ID, &actorID)
		if err == nil {
			changed = true
			layoutID = stringPointer(current.ID)
		}
		return
	case op.Delete != nil:
		if layoutInAggregate(before, op.Delete.LayoutID) == nil {
			err = apperrors.NewNotFoundError("PageLayout", op.Delete.LayoutID)
			return
		}
		err = engine.metadata.DeleteLayoutForAdministration(ctx, op.Delete.LayoutID)
		if err == nil {
			changed = true
			layoutID = stringPointer(op.Delete.LayoutID)
		}
		return
	case op.Restore != nil:
		if layoutInAggregate(before, op.Restore.LayoutID) == nil {
			err = apperrors.NewNotFoundError("PageLayout", op.Restore.LayoutID)
			return
		}
		err = engine.metadata.RestoreLayoutForAdministration(ctx, op.Restore.LayoutID, int(op.Restore.HistoryVersion), actorID)
		if err == nil {
			changed = true
			layoutID = stringPointer(op.Restore.LayoutID)
			validateLayoutID = op.Restore.LayoutID
		}
		return
	case op.AssignmentUpsert != nil:
		if layoutInAggregate(before, op.AssignmentUpsert.LayoutID) == nil {
			err = apperrors.NewNotFoundError("PageLayout", op.AssignmentUpsert.LayoutID)
			return
		}
		current := matchingAssignment(before, op.AssignmentUpsert.Selectors)
		if current != nil && strings.EqualFold(current.LayoutID, op.AssignmentUpsert.LayoutID) {
			assignmentID = stringPointer(current.ID)
			layoutID = stringPointer(op.AssignmentUpsert.LayoutID)
			return
		}
		routeSource := models.LayoutAssignmentRouteSourceAdminAuthored
		assignment := &models.LayoutAssignment{ObjectAPIName: request.ObjectID, LayoutID: op.AssignmentUpsert.LayoutID, ProfileID: cloneString(op.AssignmentUpsert.Selectors.ProfileID), RoleID: cloneString(op.AssignmentUpsert.Selectors.RoleID), RecordTypeID: cloneString(op.AssignmentUpsert.Selectors.RecordTypeID), RouteSource: &routeSource, CreatedByID: &actorID}
		if current != nil {
			assignment.ID = current.ID
		}
		err = engine.metadata.CreateAssignment(ctx, assignment)
		if err == nil {
			changed = true
			assignmentID = stringPointer(assignment.ID)
			layoutID = stringPointer(assignment.LayoutID)
		}
		return
	case op.AssignmentDelete != nil:
		assignment := assignmentInAggregate(before, op.AssignmentDelete.AssignmentID)
		if assignment == nil {
			err = apperrors.NewNotFoundError("LayoutAssignment", op.AssignmentDelete.AssignmentID)
			return
		}
		err = engine.metadata.DeleteAssignmentForAdministration(ctx, assignment.ID)
		if err == nil {
			changed = true
			assignmentID = stringPointer(assignment.ID)
		}
		return
	default:
		err = errors.New("Layout mutation operation is unavailable")
		return
	}
}

func (engine *LayoutAuthoringEngine) lockSelectorAuthorities(ctx context.Context, objectID string, operation semantic.Operation) (map[string]bool, map[string]bool, error) {
	profileIDs, recordTypeIDs := []string{}, []string{}
	if operation.AssignmentUpsert != nil {
		if operation.AssignmentUpsert.Selectors.ProfileID != nil {
			profileIDs = append(profileIDs, *operation.AssignmentUpsert.Selectors.ProfileID)
		}
		if operation.AssignmentUpsert.Selectors.RecordTypeID != nil {
			recordTypeIDs = append(recordTypeIDs, *operation.AssignmentUpsert.Selectors.RecordTypeID)
		}
	}
	profiles := map[string]bool{}
	if len(profileIDs) != 0 {
		values, err := engine.permissions.LockProfileLifecycles(ctx, profileIDs)
		if err != nil {
			return nil, nil, err
		}
		for _, value := range values {
			if value != nil && !value.IsDeleted {
				profiles[strings.ToLower(value.ID)] = true
			}
		}
	}
	recordTypes := map[string]bool{}
	if len(recordTypeIDs) != 0 {
		values, err := engine.metadata.LockLayoutAuthoringRecordTypes(ctx, recordTypeIDs)
		if err != nil {
			return nil, nil, err
		}
		for _, value := range values {
			if value != nil && value.IsActive && strings.EqualFold(value.ObjectAPIName, objectID) {
				recordTypes[strings.ToLower(value.ID)] = true
			}
		}
	}
	return profiles, recordTypes, nil
}

func validateChangedLayoutRouting(
	snapshot metadata.LayoutAuthoringAggregateSnapshot,
	operation semantic.Operation,
	profiles map[string]bool,
	recordTypes map[string]bool,
	permissionService *permissions.PermissionService,
	ctx context.Context,
) error {
	var changedLayoutID string
	switch {
	case operation.SetDefault != nil:
		changedLayoutID = operation.SetDefault.LayoutID
	case operation.Update != nil:
		changedLayoutID = operation.Update.LayoutID
	case operation.Restore != nil:
		changedLayoutID = operation.Restore.LayoutID
	case operation.AssignmentUpsert != nil:
		changedLayoutID = operation.AssignmentUpsert.LayoutID
	}
	if changedLayoutID != "" {
		layout := layoutInAggregate(snapshot, changedLayoutID)
		if layout == nil {
			return apperrors.NewValidationError("layoutId", "Layout Assignment target is absent")
		}
		if strings.EqualFold(strings.TrimSpace(layout.Type), "Create") {
			for _, assignment := range snapshot.Assignments {
				if assignment != nil && strings.EqualFold(assignment.LayoutID, changedLayoutID) {
					return apperrors.NewValidationError("layoutId", "Type=Create Layout cannot own effective routing")
				}
			}
		}
	}
	if operation.AssignmentUpsert == nil {
		return nil
	}
	selectors := operation.AssignmentUpsert.Selectors
	if selectors.ProfileID != nil && !profiles[strings.ToLower(*selectors.ProfileID)] {
		return apperrors.NewValidationError("profileId", "Layout Assignment Profile is absent or deleted")
	}
	if selectors.RecordTypeID != nil && !recordTypes[strings.ToLower(*selectors.RecordTypeID)] {
		return apperrors.NewValidationError("recordTypeId", "Layout Assignment RecordType is absent or inactive")
	}
	if selectors.RoleID == nil {
		return nil
	}
	role, err := permissionService.LockRoleLifecycle(ctx, *selectors.RoleID)
	if err != nil {
		return err
	}
	if role == nil {
		return apperrors.NewValidationError("roleId", "Layout Assignment Role is absent")
	}
	return nil
}

func layoutInAggregate(snapshot metadata.LayoutAuthoringAggregateSnapshot, id string) *models.PageLayout {
	for _, value := range snapshot.Layouts {
		if value != nil && strings.EqualFold(value.ID, id) {
			return value
		}
	}
	return nil
}

func assignmentInAggregate(snapshot metadata.LayoutAuthoringAggregateSnapshot, id string) *models.LayoutAssignment {
	for _, value := range snapshot.Assignments {
		if value != nil && strings.EqualFold(value.ID, id) {
			return value
		}
	}
	return nil
}

func matchingAssignment(snapshot metadata.LayoutAuthoringAggregateSnapshot, selectors semantic.AssignmentSelectors) *models.LayoutAssignment {
	for _, value := range snapshot.Assignments {
		if value != nil && sameOptionalString(value.ProfileID, selectors.ProfileID) && sameOptionalString(value.RoleID, selectors.RoleID) && sameOptionalString(value.RecordTypeID, selectors.RecordTypeID) {
			return value
		}
	}
	return nil
}

func sameOptionalString(left, right *string) bool {
	if left == nil || right == nil {
		return left == nil && right == nil
	}
	return strings.EqualFold(*left, *right)
}

func copyLayoutLifecycle(target, source *models.PageLayout) {
	target.IsDefault, target.IsDeleted = source.IsDefault, source.IsDeleted
	target.Source, target.IsCustomized, target.GenerationVersion = source.Source, source.IsCustomized, source.GenerationVersion
	target.CreatedDate, target.LastModifiedDate = source.CreatedDate, source.LastModifiedDate
}

func cloneString(value *string) *string {
	if value == nil {
		return nil
	}
	result := *value
	return &result
}
func stringPointer(value string) *string { return &value }

func (runtime *LayoutAuthoringRuntime) Prepare(context.Context) error {
	if runtime == nil || runtime.Engine == nil {
		return fmt.Errorf("Layout authoring runtime is incomplete")
	}
	return nil
}
