package automation

import (
	"errors"
	"fmt"
	"strings"

	"github.com/go-sql-driver/mysql"
	apperrors "github.com/nexuscrm/kernel/pkg/errors"
	"github.com/nexuscrm/kernel/pkg/ports"
	"github.com/nexuscrm/shared/pkg/contract"
	sharedErrors "github.com/nexuscrm/shared/pkg/errors"
	"github.com/nexuscrm/shared/pkg/models"
	contractmodels "github.com/pingcap-inc/nexuscrm-contract/pkg/models"
)

// classifyFlowExecutionCause converts runtime-native failures into the stable,
// message-free cause taxonomy carried across the connector boundary. The
// caller-facing FlowExecutionResult remains the presentation authority.
func classifyFlowExecutionCause(err error, elementType, elementName string) *contractmodels.FlowExecutionCause {
	if err == nil {
		return nil
	}
	cause := &contractmodels.FlowExecutionCause{
		Code:        "FLOW_ERROR",
		Category:    contractmodels.FlowExecutionCauseCategoryUnknown,
		ElementType: strings.TrimSpace(elementType),
		ElementName: strings.TrimSpace(elementName),
		Retriable:   sharedErrors.IsRetriable(err),
	}

	// The records owner marks only a conflict on the upsert's exact unique
	// identity as replay-safe. Ordinary duplicate keys remain terminal.
	if ports.IsUniqueFieldUpsertRace(err) {
		cause.Code = "DB_UNIQUE_FIELD_UPSERT_RACE"
		cause.Category = contractmodels.FlowExecutionCauseCategoryConcurrency
		cause.Retriable = true
		return cause
	}

	var mysqlErr *mysql.MySQLError
	if errors.As(err, &mysqlErr) {
		switch mysqlErr.Number {
		case 1205:
			cause.Code = "DB_LOCK_WAIT_TIMEOUT"
			cause.Category = contractmodels.FlowExecutionCauseCategoryConcurrency
			cause.Retriable = true
		case 1213:
			cause.Code = "DB_DEADLOCK"
			cause.Category = contractmodels.FlowExecutionCauseCategoryConcurrency
			cause.Retriable = true
		case 9007:
			cause.Code = "DB_WRITE_CONFLICT"
			cause.Category = contractmodels.FlowExecutionCauseCategoryConcurrency
			cause.Retriable = true
		default:
			cause.Code = fmt.Sprintf("DB_ERROR_%d", mysqlErr.Number)
			cause.Category = contractmodels.FlowExecutionCauseCategoryPersistence
		}
		return cause
	}

	var lockContention *apperrors.LockContentionError
	if errors.As(err, &lockContention) {
		cause.Code = "DB_LOCK_CONTENTION"
		cause.Category = contractmodels.FlowExecutionCauseCategoryConcurrency
		cause.Retriable = true
		return cause
	}
	if validationErr, surfaced := flowClientValidationForPresentation(err); surfaced {
		leaves, ok := apperrors.AsClientValidationErrors(validationErr)
		if !ok || len(leaves) == 0 {
			return cause
		}
		firstLeaf := leaves[0]
		if _, ok := firstLeaf.(sharedErrors.ValidationRuleFailuresConditioner); ok {
			cause.Code = "VALIDATION_RULE"
			cause.Category = contractmodels.FlowExecutionCauseCategoryValidation
			return cause
		}
		if _, ok := firstLeaf.(sharedErrors.FieldWriteDeniedConditioner); ok {
			cause.Code = contract.FlowExecutionErrorCodeFieldWriteDenied
			cause.Category = contractmodels.FlowExecutionCauseCategoryAuthorization
			return cause
		}
		if _, ok := firstLeaf.(sharedErrors.ValidationFieldConditioner); ok {
			cause.Code = "VALIDATION"
			cause.Category = contractmodels.FlowExecutionCauseCategoryValidation
			return cause
		}
	}
	if sharedErrors.IsPermissionDenied(err) || sharedErrors.IsUnauthenticated(err) {
		cause.Code = "AUTHORIZATION_DENIED"
		cause.Category = contractmodels.FlowExecutionCauseCategoryAuthorization
		return cause
	}
	if sharedErrors.IsNotFound(err) {
		cause.Code = "NOT_FOUND"
		cause.Category = contractmodels.FlowExecutionCauseCategoryNotFound
		return cause
	}
	if _, ok := flowCustomErrorFaultForPresentation(err); ok {
		cause.Code = "FLOW_CUSTOM_ERROR"
		cause.Category = contractmodels.FlowExecutionCauseCategoryUnknown
		return cause
	}
	var retriableFault *FlowRetriableFault
	if errors.As(err, &retriableFault) {
		cause.Code = "FLOW_RETRIABLE"
		cause.Retriable = true
		return cause
	}
	var statusFault *FlowStatusFault
	if errors.As(err, &statusFault) {
		cause.Code = "FLOW_STATUS_ERROR"
		if strings.EqualFold(strings.TrimSpace(statusFault.StatusCode), statusCodeInvalidFlow) {
			cause.Code = statusCodeInvalidFlow
			cause.Category = contractmodels.FlowExecutionCauseCategoryConfiguration
		}
		return cause
	}
	if sharedErrors.IsInternal(err) {
		cause.Code = "INTERNAL_ERROR"
		cause.Category = contractmodels.FlowExecutionCauseCategoryInternal
	}
	return cause
}

func classifyFailedFlowExecutionResult(result *models.FlowExecutionResult, origin flowExecutionResultErrorOrigin) *contractmodels.FlowExecutionCause {
	if result == nil || result.IsSuccess || len(result.Errors) == 0 {
		return nil
	}
	code := strings.TrimSpace(result.Errors[0].StatusCode)
	cause := &contractmodels.FlowExecutionCause{Code: code, Category: contractmodels.FlowExecutionCauseCategoryUnknown, Retriable: result.IsRetriable}
	if cause.Code == "" {
		cause.Code = "FLOW_ERROR"
	}
	switch cause.Code {
	case statusCodeInvalidFlow:
		cause.Category = contractmodels.FlowExecutionCauseCategoryConfiguration
	case "NOT_FOUND":
		cause.Category = contractmodels.FlowExecutionCauseCategoryNotFound
	case contract.FlowExecutionErrorCodeValidation:
		if origin == flowExecutionResultErrorOriginNeutralEngine {
			cause.Category = contractmodels.FlowExecutionCauseCategoryValidation
		}
	case contract.FlowExecutionErrorCodeFieldWriteDenied:
		if origin == flowExecutionResultErrorOriginNeutralEngine {
			cause.Category = contractmodels.FlowExecutionCauseCategoryAuthorization
		}
	}
	return cause
}

// classifyFailedFlowExecutionResultAtElement preserves a failed child flow's
// result-level taxonomy while locating it at the parent element that invoked
// it. Legacy FlowExecutionResult deliberately has no typed Cause field, so an
// actionCall must capture this before it wraps the result as a plain error.
func classifyFailedFlowExecutionResultAtElement(result *models.FlowExecutionResult, origin flowExecutionResultErrorOrigin, elementType, elementName string) *contractmodels.FlowExecutionCause {
	cause := classifyFailedFlowExecutionResult(result, origin)
	if cause == nil {
		return nil
	}
	cause.ElementType = strings.TrimSpace(elementType)
	cause.ElementName = strings.TrimSpace(elementName)
	return cause
}

func selectTerminalFlowExecutionCause(collector *flowAttemptCauseCollector, err error, elementType, elementName string) *contractmodels.FlowExecutionCause {
	if internalErrorWrapsFlowCustomErrorFault(err) {
		return classifyFlowExecutionCause(err, elementType, elementName)
	}
	if collector != nil && collector.cause != nil {
		// A terminal child failure is already classified before legacy parent
		// wrapping. A pending fault cause is retained only when its designated
		// CustomError presentation is the actual terminal element. A recovered
		// fault followed by an independent failure must classify that new error.
		_, hasCustomFault := flowCustomErrorFaultForPresentation(err)
		if collector.terminal || (hasCustomFault && strings.EqualFold(strings.TrimSpace(collector.faultTarget), strings.TrimSpace(elementName))) {
			copy := *collector.cause
			return &copy
		}
	}
	return classifyFlowExecutionCause(err, elementType, elementName)
}

func isRetriableFlowExecutionFailure(err error, cause *contractmodels.FlowExecutionCause) bool {
	return sharedErrors.IsRetriable(err) || (cause != nil && cause.Retriable)
}
