package automation

import (
	"context"
	"fmt"
	"strings"
	"time"

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

// approvalRuntimeRecord is the existing engine's private record value shape.
// The Runtime Approval adapter uses this alias so no product wire vocabulary
// is introduced into a new L0 boundary file.
type approvalRuntimeRecord = models.SObject

// Submit submits a record for approval.
// 1. Resolves the target and rejects an existing authorized Pending instance
// 2. Finds a matching active ProcessDefinition for the object
// 3. Evaluates Entry Criteria
// 4. Reads ProcessNode steps and starts the first metadata-matching step
// 5. Creates ProcessInstance + ProcessInstanceNode + ProcessInstanceWorkitem
func (s *ApprovalService) Submit(ctx context.Context, req SubmitRequest, user *models.UserSession) (models.SObject, error) {
	// Resolve eligibility and check the lock gate under the ORIGINAL user context,
	// before any SystemContext escalation. CheckRecordAccessAll uses canBypassApprovalLock
	// which must not see a trusted context — otherwise the lock is silently bypassed.
	eligibility, err := s.resolveSubmitEligibilityForExecution(ctx, req, user)
	if err != nil {
		return nil, err
	}
	return s.SubmitResolved(ctx, req, eligibility, user)
}

func (s *ApprovalService) checkSubmitPendingConflict(ctx context.Context, req SubmitRequest, user *models.UserSession) error {
	pending, err := s.hasPendingApprovalInstance(ctx, req, user, false)
	if err != nil {
		return err
	}
	if !pending {
		return nil
	}
	return wrapSubmitTargetError(ErrRecordAlreadyPending, strings.TrimSpace(req.RecordID))
}

// SubmitResolved executes one submit against eligibility resolved earlier in
// the same caller-owned transaction. Runtime uses this seam so durable
// idempotency acceptance and the selected Catalog definition share one
// snapshot; ordinary callers retain Submit's resolve-and-execute behavior.
func (s *ApprovalService) SubmitResolved(
	ctx context.Context,
	req SubmitRequest,
	eligibility *SubmitEligibility,
	user *models.UserSession,
) (models.SObject, error) {
	if eligibility == nil || strings.TrimSpace(eligibility.TargetRecordID) == "" ||
		strings.TrimSpace(eligibility.ObjectAPIName) == "" ||
		strings.TrimSpace(eligibility.ProcessDefinitionID) == "" ||
		eligibility.ProcessDefinition == nil || eligibility.TargetRecord == nil {
		return nil, apperrors.NewInternalError("resolved approval submit eligibility is incomplete", nil)
	}
	objectName := eligibility.ObjectAPIName
	processDef := eligibility.ProcessDefinition
	processDefId := eligibility.ProcessDefinitionID
	targetRecordID := eligibility.TargetRecordID
	submitErr := func(err error) error {
		return wrapSubmitTargetError(err, targetRecordID)
	}

	// LockRule gate: before any approval state is mutated, verify that the
	// target record is not in a read-only locked state. We check IsLocked only —
	// not PermEdit — so that a user who has read access but not edit access is
	// not incorrectly denied submission. Submission authorization is handled
	// separately by canUserSubmitForProcess below.
	//
	// We must evaluate the LockRule against the raw (un-FLS-filtered) record,
	// not eligibility.TargetRecord which is a user-visible projection. A LockRule
	// formula that references a field the submitter cannot read would otherwise
	// receive an incomplete record, causing the formula engine to return an error
	// and CheckRecordAccessAll to fall back to IsLocked=false — silently allowing
	// submission of a locked record. The Records-owned exact System read bypasses
	// FLS but selects only the direct fields the LockRule references. A rule that
	// cannot be statically capped retains the established broad read and
	// fail-closed posture. Failing to establish either basis fails closed.
	if s.permissions != nil {
		var schema *models.ObjectMetadata
		if s.schema != nil {
			schema = s.schema.GetSchema(ctx, objectName)
		}
		lockFields, boundedLockRule := approvalLockRuleReadFields(schema)
		var rawRecord models.SObject
		var rawErr error
		if !boundedLockRule {
			rawRecord, rawErr = s.persistence.GetRaw(ctx, objectName, targetRecordID)
		} else {
			rawRecord, rawErr = s.readApprovalSystemFields(ctx, objectName, targetRecordID, lockFields)
		}
		if rawErr != nil {
			return nil, submitErr(apperrors.NewInternalError("failed to load raw record for lock check", rawErr))
		}
		if s.permissions.IsRecordLockedForGate(ctx, schema, rawRecord, user) {
			return nil, submitErr(apperrors.NewPermissionError("submit", "record is locked and cannot be submitted for approval"))
		}
	}

	// Escalate to SystemContext only after the user-context lock gate passes.
	// All subsequent writes to ProcessInstance / ProcessInstanceNode / Workitem
	// tables are internal approval-engine operations and must bypass normal CRUD/FLS.
	ctx = constants.WithSecurityContext(ctx, constants.SystemContext, "ApprovalService: managing approval system tables")

	// Step 4: Enforce submitter authorization once the process is selected.
	canSubmit, err := s.canUserSubmitForProcess(ctx, processDef, objectName, targetRecordID, user)
	if err != nil {
		s.logger.Error("Failed to evaluate allowed submitters for "+processDefId, err)
		return nil, submitErr(apperrors.NewInternalError("failed to evaluate approval submitter permissions", err))
	}
	if !canSubmit {
		return nil, submitErr(ErrSubmitPermissionDenied)
	}

	// Step 5: Find the initial step from the materialized ProcessNode topology.
	// Pending admission ran before eligibility; the transaction owner repeats
	// the pending check after locking the target row as the concurrency fence.
	// Product-owned definitions covered by the startup proof are query-only;
	// definitions outside that proof retain the bounded compatibility repair.
	// Initial routing uses the same step criteria / ifCriteriaNotMet semantics as
	// downstream advancement.
	nodeRecords, err := s.ensureProcessNodesForDefinition(ctx, processDef, user)
	if err != nil {
		return nil, submitErr(err)
	}
	var firstNode models.SObject
	var firstNodeId, firstNodeName string
	var actorIDs []string
	initialStatus := ""
	firstNode, actorIDs, initialStatus, err = s.selectInitialApprovalStep(ctx, processDef, nodeRecords, objectName, targetRecordID, user.ID, user)
	if err != nil {
		return nil, submitErr(err)
	}
	if firstNode != nil {
		firstNodeId, _ = firstNode[constants.FieldID].(string)
		firstNodeName, _ = firstNode["Name"].(string)
	}

	// Step 6: Create ProcessInstance
	now := time.Now().UTC().Format(time.RFC3339)
	instanceRecord := models.SObject{
		"ProcessDefinitionId": processDefId,
		"TargetObjectId":      targetRecordID,
		"Status":              initialStatus,
		"SubmittedById":       user.ID,
		"CreatedById":         user.ID,
		"CreatedDate":         now,
		"LastModifiedDate":    now,
	}
	if initialStatus != constants.ApprovalStatusPending {
		instanceRecord["CompletedDate"] = now
	}

	// Ensure an AfterSave chain id BEFORE the runtime transaction: the
	// FieldUpdate writes inside it are what emit this submit's echo events,
	// and they inherit whatever lineage is on ctx at emit time. Pre-minting
	// (via the single mint path, LineageFromContext) lets the submit-lineage
	// dedup row below share the exact chain id the echoes will carry — for
	// interactive and REST-invoked submits alike, whose ctx has no chain yet.
	// Record-triggered dispatch already restored a chain id; it is inherited
	// untouched.
	chainID, _, freshChain := events.LineageFromContext(ctx)
	if freshChain {
		ctx = events.WithLineage(ctx, chainID, 0)
	}

	// Steps 6-8 + Submit-event FieldUpdate actions are wrapped in a transaction
	// so that a failed linked-record update does not leave behind a half-created
	// ProcessInstance / ProcessNode / Workitem set. Notification actions
	// (legacy Alert and native Flow) remain best-effort and run after the
	// transaction commits.
	runtimeResult, txErr := s.createSubmitRuntimeRows(ctx, approvalSubmitRuntimeRequest{
		processDefinitionID: processDefId,
		objectName:          objectName,
		targetRecordID:      targetRecordID,
		comments:            req.Comments,
		firstNodeID:         firstNodeId,
		firstNodeName:       firstNodeName,
		actorIDs:            actorIDs,
		initialStatus:       initialStatus,
		instanceRecord:      instanceRecord,
		now:                 now,
		chainID:             chainID,
		skipEntryCriteria:   req.SkipEntryCriteria,
		runtimeEligibility:  eligibility,
	}, user)
	if txErr != nil {
		return nil, submitErr(txErr)
	}
	// Opportunistic lineage retention: chains are minutes-lived, so any row
	// older than a week is dead weight. Best-effort on the base connection
	// (never the submit tx), bounded batch, failures only logged.
	if pruneErr := s.submitLineage.PruneOlderThan(ctx, time.Now().UTC().Add(-7*24*time.Hour), 200); pruneErr != nil {
		s.logger.Warn("approval submit lineage prune failed: " + pruneErr.Error())
	}
	instanceId := runtimeResult.instanceID
	workItemIDs := runtimeResult.workItemIDs
	submitNotifications := runtimeResult.submitNotifications
	finalNotifications := runtimeResult.finalNotifications

	// Post-commit: run notification actions (legacy Alert and native Flow)
	// best-effort.
	var actionFailures []ProcessActionFailure
	for _, batch := range []pendingNotifications{submitNotifications, finalNotifications} {
		if failures := s.executeProcessActionNotifications(ctx, batch, user); len(failures) > 0 {
			actionFailures = append(actionFailures, failures...)
		}
	}

	responseObj := models.SObject{
		"ProcessInstanceId": instanceId,
		"TargetObjectId":    targetRecordID,
		"InstanceStatus":    initialStatus,
		"ActorIds":          actorIDs,
		"NewWorkitemIds":    workItemIDs,
	}
	if len(workItemIDs) > 0 {
		responseObj[constants.FieldID] = workItemIDs[0]
	}
	if len(actorIDs) > 0 {
		responseObj["ActorId"] = actorIDs[0]
	}
	if len(actionFailures) > 0 {
		responseObj["ActionFailures"] = actionFailures
	}

	return responseObj, nil
}

func (s *ApprovalService) requireNoPendingApproval(ctx context.Context, objectName, targetRecordID string, user *models.UserSession) error {
	pendingFilter := fmt.Sprintf("TargetObjectId == '%s' && Status == '%s'", escapeApprovalFilterValue(targetRecordID), constants.ApprovalStatusPending)
	pendingResult, err := s.query.QueryWithFilter(ctx, constants.TableProcessInstance, pendingFilter, user, "", "", 200)
	if err != nil {
		return apperrors.NewInternalError("failed to check for an existing pending approval", err)
	}
	if pendingResult == nil {
		return apperrors.NewInternalError("pending approval query returned no result", nil)
	}
	processDef, pendingDefID, err := s.pendingInstanceProcessDefinitionForObject(ctx, pendingResult, objectName, user)
	if err != nil {
		return err
	}
	if processDef == nil {
		return nil
	}
	canSubmit, err := s.canUserSubmitForProcess(ctx, processDef, objectName, targetRecordID, user)
	if err != nil {
		s.logger.Error("Failed to evaluate allowed submitters for "+pendingDefID, err)
		return apperrors.NewInternalError("failed to evaluate approval submitter permissions", err)
	}
	if !canSubmit {
		return ErrSubmitPermissionDenied
	}
	return ErrRecordAlreadyPending
}
