package metadata

import (
	"context"
	"time"

	"github.com/nexuscrm/kernel/pkg/actionvalidate"
	apperrors "github.com/nexuscrm/kernel/pkg/errors"
	"github.com/nexuscrm/shared/pkg/constants"
	sharedEvents "github.com/nexuscrm/shared/pkg/events"
	"github.com/nexuscrm/shared/pkg/models"
)

// --- Action CRUD ---

// CreateAction creates a new action
func (ms *MetadataService) CreateAction(ctx context.Context, action *models.ActionMetadata) error {
	ms.mu.Lock()
	defer ms.mu.Unlock()

	// Validate required fields
	if action.ID == "" {
		action.ID = GenerateID()
	}
	if action.Name == "" {
		return apperrors.NewValidationError("name", "action name is required")
	}
	if action.Label == "" {
		return apperrors.NewValidationError("label", "action label is required")
	}
	if action.Type == "" {
		return apperrors.NewValidationError("type", "action type is required")
	}

	// Class-1 contract closure: reject action Configs that carry
	// workflow.childCopy keys the action-handler runtime silently
	// drops (sourceIdField / hydrateSource — object-level
	// ChildCloneRules territory, not action-level). The pack-apply
	// writers hit the same check; the admin/API writer path must
	// enforce it too or the three-way contract stays broken for
	// admin-authored actions (round-5 reviewer finding #1).
	if err := actionvalidate.StrictChildCopyInConfig(action.Name, action.Config); err != nil {
		return apperrors.NewValidationError("config", err.Error())
	}

	// Check for duplicate ID
	existing, _ := ms.repo.GetAction(ctx, action.ID)
	if existing != nil {
		return apperrors.NewConflictError("ActionMetadata", constants.FieldID, action.ID)
	}

	// Check for duplicate (object_api_name, name) - this is the unique constraint
	exists, err := ms.repo.CheckActionExists(ctx, action.ObjectAPIName, action.Name)
	if err != nil {
		return apperrors.NewInternalError("failed to check for existing action", err)
	}
	if exists {
		return apperrors.NewConflictError("ActionMetadata", constants.FieldName, action.Name)
	}

	// Insert into database via Repo
	if err := ms.repo.CreateAction(ctx, action); err != nil {
		return apperrors.NewInternalError("failed to insert action", err)
	}

	// Publish Event
	ms.publishMetadataEvent(ctx, sharedEvents.ActionCreated, sharedEvents.ActionEventPayload{
		EventType:     sharedEvents.ActionCreated,
		ObjectAPIName: action.ObjectAPIName,
		ActionID:      action.ID,
		ActionName:    action.Name,
		Timestamp:     time.Now().Format(time.RFC3339),
	})

	return nil
}

// UpdateAction updates an existing action
func (ms *MetadataService) UpdateAction(ctx context.Context, actionID string, updates *models.ActionMetadata) error {
	ms.mu.Lock()
	defer ms.mu.Unlock()

	// Find existing action
	existing, err := ms.repo.GetAction(ctx, actionID)
	if err != nil || existing == nil {
		return apperrors.NewNotFoundError("ActionMetadata", actionID)
	}

	// Same contract closure as CreateAction — admin-authored updates
	// must not smuggle in the runtime-dropped keys either. Use the
	// existing action's Name when the update omits it (common for
	// targeted Config updates) so the error message still points at
	// a recognisable row.
	strictName := updates.Name
	if strictName == "" {
		strictName = existing.Name
	}
	if err := actionvalidate.StrictChildCopyInConfig(strictName, updates.Config); err != nil {
		return apperrors.NewValidationError("config", err.Error())
	}

	// If name is changing, check for conflicts
	if updates.Name != "" && updates.Name != existing.Name {
		exists, err := ms.repo.CheckActionExists(ctx, existing.ObjectAPIName, updates.Name)
		if err != nil {
			return apperrors.NewInternalError("failed to check for existing action", err)
		}
		if exists {
			return apperrors.NewConflictError("ActionMetadata", constants.FieldName, updates.Name)
		}
	}

	// Preserve ID
	updates.ID = actionID

	// Update in database via Repo
	if err := ms.repo.UpdateAction(ctx, actionID, updates); err != nil {
		return apperrors.NewInternalError("failed to update action", err)
	}

	// Publish Event
	ms.publishMetadataEvent(ctx, sharedEvents.ActionUpdated, sharedEvents.ActionEventPayload{
		EventType:     sharedEvents.ActionUpdated,
		ObjectAPIName: existing.ObjectAPIName, // actions shouldn't change objects
		ActionID:      actionID,
		ActionName:    updates.Name, // This might be empty if name didn't change, but ID is adequate
		Timestamp:     time.Now().Format(time.RFC3339),
	})

	return nil
}

// DeleteAction deletes an action
func (ms *MetadataService) DeleteAction(ctx context.Context, actionID string) error {
	ms.mu.Lock()
	defer ms.mu.Unlock()

	if err := ms.repo.DeleteAction(ctx, actionID); err != nil {
		return err
	}

	// Publish Event
	ms.publishMetadataEvent(ctx, sharedEvents.ActionDeleted, sharedEvents.ActionEventPayload{
		EventType: sharedEvents.ActionDeleted,
		ActionID:  actionID,
		Timestamp: time.Now().Format(time.RFC3339),
	})

	return nil
}
