package run

import (
	"errors"
	"fmt"
)

// ValidationError is the structured validation failure shared by the callback
// boundary and the pure reducers. It replaces bare path-prefixed fmt.Errorf so
// transports can project typed, path-aware diagnostics (code/path/field/index)
// instead of collapsing every callback validation failure to a single fixed
// message. Error() preserves the exact prose the validators always produced, so
// callers and tests that match on message substrings keep working.
type ValidationError struct {
	// Code is the stable machine-readable diagnostic code.
	Code string
	// Path is the full normalized path of the offending value.
	Path string
	// Field names the concrete field when the failure names one.
	Field string
	// Index names the concrete array element when the failure names one.
	Index *int
	// Message is the display-safe problem text.
	Message string
	// Err is the optional underlying cause.
	Err error
}

func (e *ValidationError) Error() string {
	if e == nil {
		return ""
	}
	return e.Message
}

func (e *ValidationError) Unwrap() error {
	if e == nil {
		return nil
	}
	return e.Err
}

// validationErrorf builds a structured validation failure while preserving the
// exact message text the previous fmt.Errorf call produced. It uses
// fmt.Errorf internally so the format string may carry %w; the wrapped cause is
// preserved on the ValidationError so errors.Is/As still walk the chain.
func validationErrorf(code, path, field string, index *int, format string, args ...any) error {
	message := fmt.Errorf(format, args...)
	var cause error
	if wrapped, ok := message.(interface{ Unwrap() error }); ok {
		cause = wrapped.Unwrap()
	}
	return &ValidationError{
		Code: code, Path: path, Field: field, Index: index,
		Message: message.Error(), Err: cause,
	}
}

// Validation error codes projected as the public diagnostic code. The
// callback/step transport projects these typed codes through the workflow step
// get/list/complete/fail/wait surface instead of collapsing every callback
// validation failure to a single fixed step_output_invalid message.
const (
	// Output contract diagnostics.
	validationCodeOutputUndeclaredField  = "callback.output.undeclared_field"
	validationCodeOutputMissingRequired  = "callback.output.missing_required_field"
	validationCodeOutputDefaultInvalid   = "callback.output.default_invalid"
	validationCodeOutputTypeInvalid      = "callback.output.type_invalid"
	validationCodeOutputEnumInvalid      = "callback.output.enum_invalid"
	validationCodeOutputNumberInvalid    = "callback.output.number_invalid"
	validationCodeOutputBooleanInvalid   = "callback.output.boolean_invalid"
	validationCodeOutputDurationInvalid  = "callback.output.duration_invalid"
	validationCodeOutputDatetimeInvalid  = "callback.output.datetime_invalid"
	validationCodeOutputScopeInvalid     = "callback.output.scope_invalid"
	validationCodeOutputObjectInvalid    = "callback.output.object_invalid"
	validationCodeOutputArrayInvalid     = "callback.output.array_invalid"
	validationCodeOutputUnsupportedType  = "callback.output.unsupported_type"
	validationCodeOutputResponderInvalid = "callback.output.responder_invalid"
	validationCodeOutputSelectOneInvalid = "callback.output.select_one_invalid"
	validationCodeOutputJSONValueInvalid = "callback.output.json_value_invalid"

	// WorkItem shape/budget/key/index/field/responder/DAG diagnostics.
	validationCodeWorkItemsNotArray             = "callback.work_items.not_array"
	validationCodeWorkItemsTooMany              = "callback.work_items.too_many"
	validationCodeWorkItemsItemNotObject        = "callback.work_items.item_not_object"
	validationCodeWorkItemsMissingField         = "callback.work_items.missing_field"
	validationCodeWorkItemsUndeclaredField      = "callback.work_items.undeclared_field"
	validationCodeWorkItemsKeyTooLong           = "callback.work_items.key_too_long"
	validationCodeWorkItemsKeyInvalid           = "callback.work_items.key_invalid"
	validationCodeWorkItemsKeyDuplicate         = "callback.work_items.key_duplicate"
	validationCodeWorkItemsDependencyExceeded   = "callback.work_items.dependency_edges_exceeded"
	validationCodeWorkItemsRelatedScopeExceeded = "callback.work_items.related_scope_references_exceeded"
	validationCodeWorkItemsJSONValuesExceeded   = "callback.work_items.json_values_exceeded"
	validationCodeWorkItemsJSONBytesExceeded    = "callback.work_items.json_bytes_exceeded"
	validationCodeWorkItemsResponderInvalid     = "callback.work_items.responder_invalid"
	validationCodeWorkItemsResponderNotAllowed  = "callback.work_items.responder_not_allowed"
	validationCodeWorkItemsScopeInvalid         = "callback.work_items.scope_invalid"
	validationCodeWorkItemsRelatedScopeInvalid  = "callback.work_items.related_scope_invalid"
	validationCodeWorkItemsInstructionInvalid   = "callback.work_items.instruction_invalid"
	validationCodeWorkItemsSelfDependency       = "callback.work_items.self_dependency"
	validationCodeWorkItemsUnknownDependency    = "callback.work_items.unknown_dependency"
	validationCodeWorkItemsDependencyCycle      = "callback.work_items.dependency_cycle"
	validationCodeWorkItemsAllowedResponders    = "callback.work_items.allowed_responders_invalid"

	// Dependency-reference syntax/static contract diagnostics. These flow
	// through ValidateWorkItemInstructionDependencyContract and the instruction
	// reference validator, and the public step surface projects them typed.
	validationCodeInstructionReference            = workItemInstructionReferenceFailureCode
	validationCodeInstructionPath                 = "callback.instruction.path_invalid"
	validationCodeInstructionSegment              = "callback.instruction.segment_invalid"
	validationCodeInstructionForbidden            = "callback.instruction.segment_forbidden"
	validationCodeInstructionTooMany              = "callback.instruction.too_many_references"
	validationCodeInstructionMaterializedExceeded = "callback.instruction.materialized_exceeded"
	validationCodeInstructionMinimumExceeded      = "callback.instruction.minimum_materialized_exceeded"
	validationCodeInstructionUTF8                 = "callback.instruction.utf8_invalid"
	validationCodeInstructionContractAmbiguous    = "callback.instruction.contract_ambiguous"
	validationCodeInstructionContractUndeclared   = "callback.instruction.contract_undeclared"
	validationCodeInstructionContractType         = "callback.instruction.contract_type_invalid"
	validationCodeInstructionContractObjectLeaf   = "callback.instruction.contract_object_leaf"
	validationCodeInstructionScalarExceeded       = "callback.instruction.scalar_exceeded"

	// Dependency envelope/value resolution diagnostics (runtime, not callback).
	validationCodeDependencyMissing      = "callback.dependency.missing"
	validationCodeDependencyNotSucceeded = "callback.dependency.not_succeeded"
	validationCodeDependencyOutput       = "callback.dependency.output_invalid"
	validationCodeDependencyTraversal    = "callback.dependency.traversal_invalid"
)

// intPtr returns a pointer to index for diagnostics that name a concrete array
// element.
func intPtr(index int) *int {
	return &index
}

// ValidationErrorDiagnostic extracts the structured diagnostic carried by an
// error chain, if any. Transports use it to project typed diagnostics.
func ValidationErrorDiagnostic(err error) (ValidationError, bool) {
	var validation *ValidationError
	if errors.As(err, &validation) && validation != nil {
		return *validation, true
	}
	return ValidationError{}, false
}

// NewTestValidationErrorForTest builds a structured validation error with the
// given code/path/field/index/message. It is exported for tests that project
// typed callback diagnostics without exercising a full validator.
func NewTestValidationErrorForTest(code, path, field string, index int, message string) *ValidationError {
	var indexPtr *int
	if index >= 0 {
		value := index
		indexPtr = &value
	}
	return &ValidationError{
		Code: code, Path: path, Field: field, Index: indexPtr, Message: message,
	}
}

// Test validation codes used by tests that project typed diagnostics without a
// full validator. They mirror the stable production codes.
const (
	TestValidationCodeOutputUndeclaredField = validationCodeOutputUndeclaredField
	TestValidationCodeWorkItemsKeyInvalid   = validationCodeWorkItemsKeyInvalid
)
