package automation

import (
	"context"
	"fmt"
	"strings"

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

type parentLockCoordinatorRunner interface {
	RunWithParentLockCoordinator(ctx context.Context, fn func(context.Context) error) error
}

type parentRowLocker interface {
	LockParentRows(ctx context.Context, keys []recordlocks.ParentKey) ([]recordlocks.ParentKey, error)
}

type preparedUpdateParentPrelocker interface {
	PrelockPreparedUpdateParents(ctx context.Context, tableName string, id string, prepared *models.PreparedRecordUpdate, extraUpdates models.SObject, user *models.UserSession) error
}

type preparedUpdateParentPlanner interface {
	PlanPreparedUpdateParentKeys(ctx context.Context, tableName string, id string, prepared *models.PreparedRecordUpdate, extraUpdates models.SObject, user *models.UserSession) ([]recordlocks.ParentKey, error)
}

func (s *ApprovalService) planLifecycleFieldUpdateParentKeys(ctx context.Context, actions []models.SObject, objectAPIName, targetObjectID string, user *models.UserSession) ([]recordlocks.ParentKey, error) {
	planner, ok := s.persistence.(preparedUpdateParentPlanner)
	if !ok {
		return nil, nil
	}
	planned, err := s.planOrderedFieldUpdatesFromActions(ctx, actions, objectAPIName, targetObjectID, user)
	if err != nil {
		return nil, err
	}
	keys := make([]recordlocks.ParentKey, 0)
	for _, action := range planned {
		if action.PreparedUpdate == nil {
			continue
		}
		actionKeys, planErr := planner.PlanPreparedUpdateParentKeys(ctx, action.UpdateObjectName, action.UpdateRecordID, action.PreparedUpdate.Clone(), nil, user)
		if planErr != nil {
			return nil, &ApprovalActionError{
				ActionName: action.ActionName, ActionType: constants.ProcessActionTypeFieldUpdate,
				TargetObject: action.UpdateObjectName, TargetID: action.UpdateRecordID, Field: action.UpdateFieldAPIName,
				Reason: planErr.Error(), Cause: planErr,
			}
		}
		keys = append(keys, actionKeys...)
	}
	return recordlocks.OrderedParentKeys(keys), nil
}

// withPlannedLifecycleParentPrelocks carries the complete parent-lock intent
// of the selected lifecycle batches to the first Records mutation that follows.
// That Records boundary unions these keys with its own parent set and acquires
// one globally ordered lock plan before any Approval runtime row can establish
// a higher transaction lock-watermark.
//
// This is intentionally only a lock-intent plan. The live mutation path derives
// and validates the authoritative parent set again under lock.
func (s *ApprovalService) withPlannedLifecycleParentPrelocks(ctx context.Context, user *models.UserSession, batches ...lifecycleActionBatch) (context.Context, error) {
	keys := recordlocks.ForcedParentPrelocksFromContext(ctx)
	for _, batch := range batches {
		batchKeys, err := s.planLifecycleFieldUpdateParentKeys(ctx, batch.actions, batch.objectApiName, batch.targetID, user)
		if err != nil {
			return nil, err
		}
		keys = append(keys, batchKeys...)
	}
	keys = recordlocks.OrderedParentKeys(keys)
	if len(keys) == 0 {
		return ctx, nil
	}
	return recordlocks.WithForcedParentPrelocks(ctx, keys), nil
}

func (s *ApprovalService) executeLiveFieldUpdateMutations(ctx context.Context, actions []models.SObject, objectApiName, targetObjectId string, user *models.UserSession) error {
	for _, action := range actions {
		actionType, _ := action["ActionType"].(string)
		if actionType != constants.ProcessActionTypeFieldUpdate {
			continue
		}

		name, _ := action["Name"].(string)
		fieldAPIName, _ := action["FieldApiName"].(string)
		fieldValue, _ := action["FieldValue"].(string)
		if strings.TrimSpace(fieldAPIName) == "" || strings.TrimSpace(objectApiName) == "" {
			return &ApprovalActionError{
				ActionName: name,
				ActionType: constants.ProcessActionTypeFieldUpdate,
				Reason:     fmt.Sprintf("missing fieldApiName or objectApiName (fieldApiName=%q objectApiName=%q)", fieldAPIName, objectApiName),
			}
		}

		updateObjectName, updateRecordID, updateFieldAPIName, resolveErr := s.resolveFieldUpdateTarget(ctx, objectApiName, targetObjectId, fieldAPIName)
		if resolveErr != nil {
			return &ApprovalActionError{ActionName: name, ActionType: constants.ProcessActionTypeFieldUpdate, Field: fieldAPIName, Reason: resolveErr.Error()}
		}

		updates := models.SObject{updateFieldAPIName: fieldValue}
		s.logger.Info(fmt.Sprintf("ProcessAction FieldUpdate '%s': setting %s.%s = %q on %s", name, updateObjectName, updateFieldAPIName, fieldValue, updateRecordID))
		if updateErr := s.persistence.Update(ctx, updateObjectName, updateRecordID, updates, user); updateErr != nil {
			updateErr = flowRecordMutationValidation(updateErr)
			s.logger.Error(fmt.Sprintf("APPROVAL_ACTION_FAILURE ProcessAction FieldUpdate '%s' failed for %s.%s id=%s", name, updateObjectName, updateFieldAPIName, updateRecordID), updateErr)
			return &ApprovalActionError{
				ActionName:   name,
				ActionType:   constants.ProcessActionTypeFieldUpdate,
				TargetObject: updateObjectName,
				TargetID:     updateRecordID,
				Field:        updateFieldAPIName,
				Reason:       updateErr.Error(),
				Cause:        updateErr,
			}
		}
	}
	return nil
}

func (s *ApprovalService) executeSubmitFieldUpdateMutationsBeforeRuntimeRows(ctx context.Context, actions []models.SObject, objectAPIName, targetObjectID string, user *models.UserSession) error {
	for _, action := range actions {
		actionType, _ := action["ActionType"].(string)
		if actionType != constants.ProcessActionTypeFieldUpdate {
			continue
		}
		planned, err := s.planOrderedFieldUpdatesFromActions(ctx, []models.SObject{action}, objectAPIName, targetObjectID, user)
		if err != nil {
			return err
		}
		if err := s.prelockPlannedFieldUpdateMutations(ctx, planned, user); err != nil {
			return err
		}
		if err := s.executePlannedFieldUpdateMutations(ctx, planned, user); err != nil {
			return err
		}
	}
	return nil
}

func (s *ApprovalService) runWithParentLockCoordinatorIfSupported(ctx context.Context, fn func(context.Context) error) error {
	if fn == nil {
		return nil
	}
	if runner, ok := s.persistence.(parentLockCoordinatorRunner); ok {
		return runner.RunWithParentLockCoordinator(ctx, fn)
	}
	return fn(ctx)
}

func (s *ApprovalService) prelockSubmitRuntimeParents(ctx context.Context, objectName, targetRecordID, processDefID, firstNodeID string, actorIDs []string, submitterID string, startPendingApproval bool) error {
	locker, ok := s.persistence.(parentRowLocker)
	if !ok {
		return appErrors.NewInternalError("approval submit requires coordinated row locking", nil)
	}
	keys := append(recordlocks.ForcedParentPrelocksFromContext(ctx),
		recordlocks.ParentKey{ObjectName: objectName, RecordID: targetRecordID},
		recordlocks.ParentKey{ObjectName: constants.TableProcessDefinition, RecordID: processDefID},
		recordlocks.ParentKey{ObjectName: constants.TableUser, RecordID: submitterID},
	)
	if startPendingApproval && firstNodeID != "" {
		keys = append(keys, recordlocks.ParentKey{ObjectName: constants.TableProcessNode, RecordID: firstNodeID})
		for _, actorID := range actorIDs {
			keys = append(keys, recordlocks.ParentKey{ObjectName: constants.TableUser, RecordID: actorID})
		}
	}
	_, err := locker.LockParentRows(processTableWriteContext(ctx, "ApprovalService: prelock approval runtime parents"), keys)
	return err
}

func (s *ApprovalService) prelockPlannedFieldUpdateMutations(ctx context.Context, actions []ApprovalActionReplayFieldUpdate, user *models.UserSession) error {
	for _, action := range actions {
		if err := s.prelockPlannedFieldUpdateParents(ctx, action, user); err != nil {
			s.logger.Error(fmt.Sprintf("APPROVAL_ACTION_FAILURE ProcessAction FieldUpdate '%s' failed to prelock update parents for %s.%s id=%s", action.ActionName, action.UpdateObjectName, action.UpdateFieldAPIName, action.UpdateRecordID), err)
			return &ApprovalActionError{
				ActionName:   action.ActionName,
				ActionType:   constants.ProcessActionTypeFieldUpdate,
				TargetObject: action.UpdateObjectName,
				TargetID:     action.UpdateRecordID,
				Field:        action.UpdateFieldAPIName,
				Reason:       err.Error(),
				Cause:        err,
			}
		}
	}
	return nil
}

func (s *ApprovalService) prelockPlannedFieldUpdateParents(ctx context.Context, action ApprovalActionReplayFieldUpdate, user *models.UserSession) error {
	if prelocker, ok := s.persistence.(preparedUpdateParentPrelocker); ok && action.PreparedUpdate != nil {
		return prelocker.PrelockPreparedUpdateParents(ctx, action.UpdateObjectName, action.UpdateRecordID, action.PreparedUpdate.Clone(), nil, user)
	}
	updates := models.SObject{
		action.UpdateFieldAPIName: action.FieldValue,
	}
	if action.PreparedUpdate != nil && len(action.PreparedUpdate.EffectiveUpdates) > 0 {
		updates = cloneAutomationSObject(action.PreparedUpdate.EffectiveUpdates)
	}
	return s.persistence.PrelockUpdateParents(ctx, action.UpdateObjectName, action.UpdateRecordID, updates, user)
}

func (s *ApprovalService) executePlannedFieldUpdateMutations(ctx context.Context, actions []ApprovalActionReplayFieldUpdate, user *models.UserSession) error {
	for _, action := range actions {
		s.logger.Info(fmt.Sprintf("ProcessAction FieldUpdate '%s': setting %s.%s = %q on %s", action.ActionName, action.UpdateObjectName, action.UpdateFieldAPIName, action.FieldValue, action.UpdateRecordID))
		updates := models.SObject{action.UpdateFieldAPIName: action.FieldValue}
		if action.PreparedUpdate != nil && s.submitPreparedCommitSupported(ctx, action.UpdateObjectName) {
			if commitErr := s.persistence.CommitPreparedUpdate(ctx, action.UpdateObjectName, action.UpdateRecordID, action.PreparedUpdate.Clone(), nil, user); commitErr != nil {
				commitErr = flowRecordMutationValidation(commitErr)
				s.logger.Error(fmt.Sprintf("APPROVAL_ACTION_FAILURE ProcessAction FieldUpdate '%s' failed prepared commit for %s.%s id=%s", action.ActionName, action.UpdateObjectName, action.UpdateFieldAPIName, action.UpdateRecordID), commitErr)
				return &ApprovalActionError{
					ActionName:   action.ActionName,
					ActionType:   constants.ProcessActionTypeFieldUpdate,
					TargetObject: action.UpdateObjectName,
					TargetID:     action.UpdateRecordID,
					Field:        action.UpdateFieldAPIName,
					Reason:       commitErr.Error(),
					Cause:        commitErr,
				}
			}
			continue
		}
		if updateErr := s.persistence.Update(ctx, action.UpdateObjectName, action.UpdateRecordID, updates, user); updateErr != nil {
			updateErr = flowRecordMutationValidation(updateErr)
			s.logger.Error(fmt.Sprintf("APPROVAL_ACTION_FAILURE ProcessAction FieldUpdate '%s' failed for %s.%s id=%s", action.ActionName, action.UpdateObjectName, action.UpdateFieldAPIName, action.UpdateRecordID), updateErr)
			return &ApprovalActionError{
				ActionName:   action.ActionName,
				ActionType:   constants.ProcessActionTypeFieldUpdate,
				TargetObject: action.UpdateObjectName,
				TargetID:     action.UpdateRecordID,
				Field:        action.UpdateFieldAPIName,
				Reason:       updateErr.Error(),
				Cause:        updateErr,
			}
		}
	}
	return nil
}

func (s *ApprovalService) submitPreparedCommitSupported(ctx context.Context, objectAPIName string) bool {
	if s == nil || s.persistence == nil {
		return false
	}
	if !s.persistence.IsTransactionActive(ctx) {
		return true
	}
	if s.schema == nil {
		return true
	}
	schema := s.schema.GetSchema(ctx, objectAPIName)
	return schema == nil || len(schema.ExclusiveSiblingFields) == 0
}

type fieldUpdatePlanningState struct {
	current map[string]models.SObject
	base    map[string]models.SObject
}

func newFieldUpdatePlanningState() *fieldUpdatePlanningState {
	return &fieldUpdatePlanningState{
		current: make(map[string]models.SObject),
		base:    make(map[string]models.SObject),
	}
}

func (s *ApprovalService) loadFieldUpdatePlanningRecord(ctx context.Context, state *fieldUpdatePlanningState, objectAPIName, recordID string, requireStoredRecord bool) (models.SObject, error) {
	if state == nil {
		return nil, fmt.Errorf("field update planning state is not configured")
	}
	key := objectAPIName + "/" + recordID
	if record, ok := state.current[key]; ok && record != nil {
		if !requireStoredRecord {
			return record, nil
		}
		if base := state.base[key]; base != nil {
			return record, nil
		}
	}
	if !requireStoredRecord {
		record := models.SObject{constants.FieldID: recordID}
		state.current[key] = record
		return record, nil
	}
	stored, err := s.persistence.GetRaw(ctx, objectAPIName, recordID)
	if err != nil {
		return nil, fmt.Errorf("failed to load %s %s for ordered FieldUpdate planning: %w", objectAPIName, recordID, err)
	}
	if stored == nil {
		return nil, fmt.Errorf("%s %s not found for ordered FieldUpdate planning", objectAPIName, recordID)
	}
	base := cloneAutomationSObject(stored)
	current := cloneAutomationSObject(base)
	if existing := state.current[key]; existing != nil {
		for field, value := range existing {
			current[field] = value
		}
	}
	state.base[key] = base
	state.current[key] = current
	return current, nil
}

func (s *ApprovalService) resolveFieldUpdateTargetFromState(ctx context.Context, state *fieldUpdatePlanningState, objectApiName, targetObjectId, fieldApiName string) (string, string, string, error) {
	fieldPath := strings.Split(fieldApiName, ".")
	if len(fieldPath) == 1 {
		if targetObjectId == "" {
			return "", "", "", fmt.Errorf("missing target object id")
		}
		return objectApiName, targetObjectId, fieldApiName, nil
	}

	currentObjectName := objectApiName
	currentRecordID := targetObjectId
	for _, lookupField := range fieldPath[:len(fieldPath)-1] {
		if currentObjectName == "" || currentRecordID == "" {
			return "", "", "", fmt.Errorf("missing target for lookup path %q", fieldApiName)
		}
		record, err := s.loadFieldUpdatePlanningRecord(ctx, state, currentObjectName, currentRecordID, true)
		if err != nil {
			return "", "", "", err
		}
		nextRecordID, _ := record[lookupField].(string)
		if strings.TrimSpace(nextRecordID) == "" {
			return "", "", "", fmt.Errorf("lookup field %q is empty on %s %s", lookupField, currentObjectName, currentRecordID)
		}
		nextObjectName, resolveErr := s.resolveLookupTargetObject(ctx, currentObjectName, lookupField)
		if resolveErr != nil {
			return "", "", "", resolveErr
		}
		currentObjectName = nextObjectName
		currentRecordID = nextRecordID
	}

	return currentObjectName, currentRecordID, fieldPath[len(fieldPath)-1], nil
}

func (s *ApprovalService) resolveFieldUpdateTarget(ctx context.Context, objectApiName, targetObjectId, fieldApiName string) (string, string, string, error) {
	fieldPath := strings.Split(fieldApiName, ".")
	if len(fieldPath) == 1 {
		if targetObjectId == "" {
			return "", "", "", fmt.Errorf("missing target object id")
		}
		return objectApiName, targetObjectId, fieldApiName, nil
	}

	currentObjectName := objectApiName
	currentRecordID := targetObjectId

	for _, lookupField := range fieldPath[:len(fieldPath)-1] {
		if currentObjectName == "" || currentRecordID == "" {
			return "", "", "", fmt.Errorf("missing target for lookup path %q", fieldApiName)
		}

		record, err := s.readApprovalSystemFields(ctx, currentObjectName, currentRecordID, []string{lookupField})
		if err != nil {
			return "", "", "", fmt.Errorf("failed to load %s %s for lookup path %q: %w", currentObjectName, currentRecordID, fieldApiName, err)
		}
		if record == nil {
			return "", "", "", fmt.Errorf("%s %s not found for lookup path %q", currentObjectName, currentRecordID, fieldApiName)
		}

		nextRecordID, _ := record[lookupField].(string)
		if strings.TrimSpace(nextRecordID) == "" {
			return "", "", "", fmt.Errorf("lookup field %q is empty on %s %s", lookupField, currentObjectName, currentRecordID)
		}

		nextObjectName, resolveErr := s.resolveLookupTargetObject(ctx, currentObjectName, lookupField)
		if resolveErr != nil {
			return "", "", "", resolveErr
		}

		currentObjectName = nextObjectName
		currentRecordID = nextRecordID
	}

	return currentObjectName, currentRecordID, fieldPath[len(fieldPath)-1], nil
}

// resolveLookupTargetObject returns the object API name that a lookup field
// references, reading `referenceTo` from the object's metadata. Polymorphic
// fields (len(referenceTo) > 1) are not supported for FieldUpdate targets —
// the caller must specify a concrete path. Requires a schema resolver; tests
// must supply a stub rather than relying on a <Name>Id heuristic, which
// silently mis-resolved names like ParentId/WhoId on unfamiliar objects.
func (s *ApprovalService) resolveLookupTargetObject(ctx context.Context, currentObjectName, lookupField string) (string, error) {
	if s.schema == nil {
		return "", fmt.Errorf("approval service misconfigured: schema resolver is nil (cannot resolve lookup %q on %s)", lookupField, currentObjectName)
	}
	schema := s.schema.GetSchema(ctx, currentObjectName)
	if schema == nil {
		return "", fmt.Errorf("schema for %s not found; cannot resolve lookup %q", currentObjectName, lookupField)
	}
	fm, ok := schema.FieldsMap[lookupField]
	if !ok || fm == nil {
		return "", fmt.Errorf("field %q not found on %s", lookupField, currentObjectName)
	}
	switch {
	case len(fm.ReferenceTo) == 1:
		return fm.ReferenceTo[0], nil
	case len(fm.ReferenceTo) > 1:
		return "", fmt.Errorf("polymorphic lookup %q on %s is not supported in FieldUpdate (referenceTo=%v)", lookupField, currentObjectName, fm.ReferenceTo)
	default:
		return "", fmt.Errorf("field %q on %s has no referenceTo — not a lookup", lookupField, currentObjectName)
	}
}
