package errors

import (
	"errors"
	"fmt"
	"net/http"
	"sort"
	"strings"

	appErrors "github.com/nexuscrm/shared/pkg/errors"
	commonsemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/commonv1"
)

// AppError is the base interface for all application errors
type AppError interface {
	error
	HTTPStatus() int
	Code() string
}

// NotFoundError represents a resource that was not found
type NotFoundError struct {
	Resource string
	ID       string
}

func (e *NotFoundError) Error() string {
	if e.ID != "" {
		return fmt.Sprintf("%s with ID '%s' not found", e.Resource, e.ID)
	}
	return fmt.Sprintf("%s not found", e.Resource)
}

func (e *NotFoundError) HTTPStatus() int {
	return http.StatusNotFound
}

func (e *NotFoundError) Code() string {
	return string(appErrors.ErrNotFound)
}

func (e *NotFoundError) NotFoundCondition() bool {
	return true
}

// NewNotFoundError creates a new NotFoundError
func NewNotFoundError(resource, id string) *NotFoundError {
	return &NotFoundError{Resource: resource, ID: id}
}

// ValidationError represents invalid input
type ValidationError struct {
	Field   string
	Message string
	Value   interface{}
}

func (e *ValidationError) Error() string {
	if e.Field != "" {
		return fmt.Sprintf("validation error on field '%s': %s", e.Field, e.Message)
	}
	return fmt.Sprintf("validation error: %s", e.Message)
}

func (e *ValidationError) HTTPStatus() int {
	return http.StatusBadRequest
}

func (e *ValidationError) Code() string {
	return string(appErrors.ErrValidation)
}

func (e *ValidationError) ValidationField() string {
	if e == nil {
		return ""
	}
	return e.Field
}

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

// NewValidationError creates a new ValidationError
func NewValidationError(field, message string) *ValidationError {
	return &ValidationError{Field: field, Message: message}
}

// ValidationRuleError represents a metadata validation-rule failure. The
// REST layer maps it to the validation-rule wire code instead of the
// operation fallback code.
type ValidationRuleError struct {
	RuleName string
	Message  string
	Field    string
}

// ValidationRuleFailure is the shared neutral detail for one triggered rule.
type ValidationRuleFailure = appErrors.ValidationRuleFailure

type validationRuleFailuresError struct {
	Failures []ValidationRuleFailure
}

// clientValidationErrorsError preserves several client-visible validation
// failures in the order the save pipeline encountered them. It deliberately
// carries only neutral application errors; dialect layers remain responsible
// for projecting each leaf onto their wire envelopes.
type clientValidationErrorsError struct {
	Errors []error
}

func (e *ValidationRuleError) Error() string {
	if e.RuleName != "" {
		return fmt.Sprintf("validation rule '%s': %s", e.RuleName, e.Message)
	}
	return fmt.Sprintf("validation rule: %s", e.Message)
}

func (e *ValidationRuleError) HTTPStatus() int {
	return http.StatusBadRequest
}

func (e *ValidationRuleError) Code() string {
	return string(appErrors.ErrValidation)
}

func (e *ValidationRuleError) ValidationRuleMessage() string {
	if e == nil {
		return ""
	}
	return e.Message
}

func (e *ValidationRuleError) ValidationRuleFailures() []ValidationRuleFailure {
	if e == nil {
		return nil
	}
	return []ValidationRuleFailure{{RuleName: e.RuleName, Message: e.Message, Field: e.Field}}
}

func (e *validationRuleFailuresError) Error() string {
	if e == nil || len(e.Failures) == 0 {
		return "validation rule"
	}
	parts := make([]string, 0, len(e.Failures))
	for _, failure := range e.Failures {
		parts = append(parts, (&ValidationRuleError{RuleName: failure.RuleName, Message: failure.Message, Field: failure.Field}).Error())
	}
	return strings.Join(parts, "; ")
}

func (e *validationRuleFailuresError) HTTPStatus() int {
	return http.StatusBadRequest
}

func (e *validationRuleFailuresError) Code() string {
	return string(appErrors.ErrValidation)
}

func (e *validationRuleFailuresError) ValidationRuleMessage() string {
	if e == nil || len(e.Failures) == 0 {
		return ""
	}
	return e.Failures[0].Message
}

func (e *validationRuleFailuresError) ValidationRuleFailures() []ValidationRuleFailure {
	if e == nil {
		return nil
	}
	return append([]ValidationRuleFailure(nil), e.Failures...)
}

func (e *validationRuleFailuresError) Unwrap() []error {
	if e == nil {
		return nil
	}
	out := make([]error, 0, len(e.Failures))
	for _, failure := range e.Failures {
		out = append(out, &ValidationRuleError{RuleName: failure.RuleName, Message: failure.Message, Field: failure.Field})
	}
	return out
}

func (e *clientValidationErrorsError) Error() string {
	if e == nil || len(e.Errors) == 0 {
		return "validation error"
	}
	parts := make([]string, 0, len(e.Errors))
	for _, err := range e.Errors {
		if err != nil {
			parts = append(parts, err.Error())
		}
	}
	if len(parts) == 0 {
		return "validation error"
	}
	return strings.Join(parts, "; ")
}

func (e *clientValidationErrorsError) HTTPStatus() int {
	return http.StatusBadRequest
}

func (e *clientValidationErrorsError) Code() string {
	return string(appErrors.ErrValidation)
}

func (e *clientValidationErrorsError) ClientValidationErrors() []error {
	if e == nil {
		return nil
	}
	return append([]error(nil), e.Errors...)
}

func (e *clientValidationErrorsError) ValidationRuleFailures() []ValidationRuleFailure {
	if e == nil {
		return nil
	}
	failures := make([]ValidationRuleFailure, 0)
	for _, err := range e.Errors {
		nested, ok := AsValidationRuleFailures(err)
		if !ok {
			// A mixed client-validation aggregate is not itself a rule-only
			// error. Returning a subset here lets errors.As consumers silently
			// discard its non-rule siblings.
			return nil
		}
		failures = append(failures, nested...)
	}
	return failures
}

func (e *clientValidationErrorsError) Unwrap() []error {
	if e == nil {
		return nil
	}
	return append([]error(nil), e.Errors...)
}

// NewValidationRuleError creates a new validation-rule failure.
func NewValidationRuleError(ruleName, message string) *ValidationRuleError {
	return &ValidationRuleError{RuleName: ruleName, Message: message}
}

// NewValidationRuleFailures returns one typed validation error containing all
// triggered rules in evaluation order. A single failure retains the legacy
// ValidationRuleError shape for compatibility.
func NewValidationRuleFailures(failures []ValidationRuleFailure) error {
	if len(failures) == 0 {
		return nil
	}
	if len(failures) == 1 {
		failure := failures[0]
		return &ValidationRuleError{RuleName: failure.RuleName, Message: failure.Message, Field: failure.Field}
	}
	return &validationRuleFailuresError{Failures: append([]ValidationRuleFailure(nil), failures...)}
}

// AsValidationRuleFailures returns every metadata-authored validation-rule
// failure carried by err, including through wrappers.
func AsValidationRuleFailures(err error) ([]ValidationRuleFailure, bool) {
	return appErrors.ValidationRuleFailures(err)
}

// NewClientValidationErrors combines client-visible validation errors without
// losing their type, field, rule identity, or encounter order. Nil entries are
// ignored and a single entry retains its original concrete type.
func NewClientValidationErrors(errs ...error) error {
	compacted := make([]error, 0, len(errs))
	for _, err := range errs {
		if err == nil {
			continue
		}
		leaves, ok := AsClientValidationErrors(err)
		if !ok {
			return err
		}
		compacted = append(compacted, leaves...)
	}
	switch len(compacted) {
	case 0:
		return nil
	case 1:
		return compacted[0]
	default:
		return &clientValidationErrorsError{Errors: compacted}
	}
}

// AsClientValidationErrors returns the ordered atomic client-validation
// failures carried by err. Validation-rule aggregates are expanded to one
// typed leaf per rule so neutral mutation projections can emit N errors.
func AsClientValidationErrors(err error) ([]error, bool) {
	if err == nil || IsInternal(err) {
		return nil, false
	}
	return clientValidationErrorLeaves(err)
}

func clientValidationErrorLeaves(err error) ([]error, bool) {
	if err == nil {
		return nil, false
	}

	// A multi-error is valid only when every sibling is client-visible. Inspect
	// the complete tree before honoring any classifier that one child exposes;
	// errors.As would otherwise select that child and hide an unclassified peer.
	if multi, ok := err.(interface{ Unwrap() []error }); ok {
		return clientValidationErrorChildren(multi.Unwrap())
	}

	if aggregate, ok := err.(interface{ ClientValidationErrors() []error }); ok {
		return clientValidationErrorChildren(aggregate.ClientValidationErrors())
	}

	if ruleErr, ok := err.(*ValidationRuleError); ok {
		return []error{ruleErr}, true
	}
	if ruleFailures, ok := err.(appErrors.ValidationRuleFailuresConditioner); ok {
		failures := ruleFailures.ValidationRuleFailures()
		if len(failures) == 0 {
			return nil, false
		}
		leaves := make([]error, 0, len(failures))
		for _, failure := range failures {
			leaves = append(leaves, &ValidationRuleError{
				RuleName: failure.RuleName,
				Message:  failure.Message,
				Field:    failure.Field,
			})
		}
		return leaves, true
	}

	if _, ok := err.(appErrors.FieldWriteDeniedConditioner); ok {
		return []error{err}, true
	}
	if _, ok := err.(appErrors.InvalidFieldForWriteConditioner); ok {
		return []error{err}, true
	}
	if _, ok := err.(appErrors.ValidationFieldConditioner); ok {
		return []error{err}, true
	}

	if single, ok := err.(interface{ Unwrap() error }); ok {
		return clientValidationErrorLeaves(single.Unwrap())
	}
	return nil, false
}

func clientValidationErrorChildren(children []error) ([]error, bool) {
	if len(children) == 0 {
		return nil, false
	}
	leaves := make([]error, 0, len(children))
	for _, child := range children {
		childLeaves, ok := clientValidationErrorLeaves(child)
		if !ok {
			return nil, false
		}
		leaves = append(leaves, childLeaves...)
	}
	if len(leaves) == 0 {
		return nil, false
	}
	return leaves, true
}

// FLSDeniedError represents a write-path rejection caused by one or more
// fields the caller may not create/update — in practice, **user-level FLS
// Edit denial** and a handful of **explicit immutable-field rejections**
// (e.g. CreatedDate on update). Intrinsically non-writable fields
// (formula, auto-number, system-managed) are **silently dropped** by the
// current write paths and are NOT surfaced as FLSDeniedError — callers
// should not rely on this type to detect intrinsic-capability failures.
//
// Public write APIs return this with a field-specific client error and a
// fields array; the handler layer unwraps this type via errors.As and emits
// the active wire shape.
type FLSDeniedError struct {
	Fields  []string
	Message string
}

func (e *FLSDeniedError) Error() string { return e.Message }

func (e *FLSDeniedError) HTTPStatus() int { return http.StatusBadRequest }

func (e *FLSDeniedError) Code() string {
	return string(appErrors.ErrValidation)
}

func (e *FLSDeniedError) FieldWriteDeniedFields() []string {
	if e == nil {
		return nil
	}
	return append([]string(nil), e.Fields...)
}

func (e *FLSDeniedError) FieldWriteDeniedMessage() string {
	if e == nil {
		return ""
	}
	return e.Message
}

// NewFLSDeniedError creates a new FLSDeniedError. The message SHOULD remain
// stable so clients that parse it continue to work.
func NewFLSDeniedError(fields []string, message string) *FLSDeniedError {
	return &FLSDeniedError{Fields: fields, Message: message}
}

// InvalidFieldForWriteError represents a write input that names a field the
// operation cannot legally author. Unlike FLSDeniedError, this is not a caller
// permission failure; unlike ValidationError, it is not a value-integrity
// failure.
type InvalidFieldForWriteError struct {
	Fields  []string
	Message string
	legacy  *ValidationError
}

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

func (e *InvalidFieldForWriteError) HTTPStatus() int { return http.StatusBadRequest }

func (e *InvalidFieldForWriteError) Code() string {
	return string(appErrors.ErrValidation)
}

// Unwrap preserves errors.As compatibility with the historical concrete
// ValidationError while service boundaries use the more precise neutral
// InvalidFieldForWriteConditioner above.
func (e *InvalidFieldForWriteError) Unwrap() error {
	if e == nil {
		return nil
	}
	return e.legacy
}

func (e *InvalidFieldForWriteError) InvalidFieldForWriteFields() []string {
	if e == nil {
		return nil
	}
	return append([]string(nil), e.Fields...)
}

func (e *InvalidFieldForWriteError) InvalidFieldForWriteMessage() string {
	if e == nil {
		return ""
	}
	return e.Error()
}

// ValidationField and ValidationMessage preserve the historical neutral
// validation interface for in-process audit and Flow consumers that classify
// already-flattened leaves by direct interface assertion. Invalid-write-aware
// service boundaries test InvalidFieldForWriteConditioner first.
func (e *InvalidFieldForWriteError) ValidationField() string {
	if e == nil || len(e.Fields) == 0 {
		return ""
	}
	return e.Fields[0]
}

func (e *InvalidFieldForWriteError) ValidationMessage() string {
	if e == nil {
		return ""
	}
	return e.Message
}

func NewInvalidFieldForWriteError(fields []string, message string) *InvalidFieldForWriteError {
	fields = append([]string(nil), fields...)
	legacyField := ""
	if len(fields) > 0 {
		legacyField = fields[0]
	}
	return &InvalidFieldForWriteError{
		Fields:  fields,
		Message: message,
		legacy:  NewValidationError(legacyField, message),
	}
}

// NewFLSDeniedErrorForFields builds the stable FLS response for one or more
// denied fields. Public write APIs list ALL denied fields in a single error
// per record, comma-separated in the message and enumerated in the fields
// array. Callers should accumulate every denied field in one pass and invoke
// this helper once, rather than failing on the first denial.
//
// The caller's slice is NOT mutated — we sort a copy so the `fields`
// array and message are deterministic across runs. Callers typically
// populate `fields` by ranging a Go map, which has non-deterministic
// iteration order; without the sort, wire parity would vary across invocations,
// making tests flaky and client-side caching inconsistent.
func NewFLSDeniedErrorForFields(fields []string) *FLSDeniedError {
	if len(fields) == 0 {
		return nil
	}
	// Copy to avoid mutating caller's slice, then sort for determinism.
	sorted := append([]string(nil), fields...)
	sort.Strings(sorted)
	return &FLSDeniedError{
		Fields: sorted,
		Message: fmt.Sprintf(
			"Unable to create/update fields: %s. Please check the security settings of this field and verify that it is read/write for your profile or permission set.",
			strings.Join(sorted, ", "),
		),
	}
}

// PermissionError represents insufficient permissions
type PermissionError struct {
	Action   string
	Resource string
	UserID   string
}

func (e *PermissionError) Error() string {
	return fmt.Sprintf("permission denied: cannot %s %s", e.Action, e.Resource)
}

func (e *PermissionError) HTTPStatus() int {
	return http.StatusForbidden
}

func (e *PermissionError) Code() string {
	return string(appErrors.ErrPermissionDenied)
}

func (e *PermissionError) PermissionDeniedCondition() bool { return true }

// NewPermissionError creates a new PermissionError
func NewPermissionError(action, resource string) *PermissionError {
	return &PermissionError{Action: action, Resource: resource}
}

// UnauthorizedError represents authentication failures
type UnauthorizedError struct {
	Reason string
}

func (e *UnauthorizedError) Error() string {
	if e.Reason != "" {
		return fmt.Sprintf("unauthorized: %s", e.Reason)
	}
	return "unauthorized"
}

func (e *UnauthorizedError) HTTPStatus() int {
	return http.StatusUnauthorized
}

func (e *UnauthorizedError) Code() string {
	return string(appErrors.ErrUnauthorized)
}

func (e *UnauthorizedError) UnauthenticatedCondition() bool { return true }

// NewUnauthorizedError creates a new UnauthorizedError
func NewUnauthorizedError(reason string) *UnauthorizedError {
	return &UnauthorizedError{Reason: reason}
}

// ConflictError represents a conflict with existing data.
//
// Detail is an optional free-form message. When set it is rendered verbatim by
// Error(), overriding the fixed "<Resource> already exists" template. This lets
// a conflict carry an actionable remediation to the client (the contract at
// metadata_authoring_contract.go maps a Conflict to Kind=Conflict and passes
// err.Error() through as the wire Message), e.g. an orphaned-physical-table
// create block or an inbound-FK-blocked delete naming the referencing
// constraint (#3484). The {Resource, Field, Value} fields keep the legacy
// "already exists" behavior for every existing caller that leaves Detail empty.
type ConflictError struct {
	Resource string
	Field    string
	Value    string
	Detail   string
	Reason   commonsemantic.ConflictReason
}

func (e *ConflictError) Error() string {
	if e.Detail != "" {
		return e.Detail
	}
	if e.Field != "" && e.Value != "" {
		return fmt.Sprintf("%s already exists with %s='%s'", e.Resource, e.Field, e.Value)
	}
	return fmt.Sprintf("%s already exists", e.Resource)
}

func (e *ConflictError) HTTPStatus() int {
	return http.StatusConflict
}

func (e *ConflictError) Code() string {
	return string(appErrors.ErrConflict)
}

func (e *ConflictError) ConflictCondition() bool { return true }

func (e *ConflictError) ConflictReason() commonsemantic.ConflictReason { return e.Reason }

// NewConflictErrorWithReason carries a finite public recovery condition.
func NewConflictErrorWithReason(resource string, reason commonsemantic.ConflictReason, message string) *ConflictError {
	return &ConflictError{Resource: resource, Reason: reason, Detail: message}
}

// NewConflictError creates a new ConflictError
func NewConflictError(resource, field, value string) *ConflictError {
	return &ConflictError{Resource: resource, Field: field, Value: value}
}

// NewConflictErrorWithMessage creates a ConflictError whose Error() renders the
// given message verbatim, carrying an actionable remediation to the client while
// still mapping to HTTP 409 / Kind=Conflict. Resource is retained for logging.
func NewConflictErrorWithMessage(resource, message string) *ConflictError {
	return &ConflictError{Resource: resource, Detail: message}
}

// ForbiddenError represents a generic 403 Forbidden error
type ForbiddenError struct {
	Message string
}

func (e *ForbiddenError) Error() string {
	return e.Message
}

func (e *ForbiddenError) HTTPStatus() int {
	return http.StatusForbidden
}

func (e *ForbiddenError) Code() string {
	return string(appErrors.ErrForbidden)
}

func (e *ForbiddenError) PermissionDeniedCondition() bool { return true }

// NewForbiddenError creates a new ForbiddenError
func NewForbiddenError(message string) *ForbiddenError {
	return &ForbiddenError{Message: message}
}

// ColumnNotFoundError represents an unknown column in a SQL query (MySQL 1054)
type ColumnNotFoundError struct {
	ColumnName string
}

func (e *ColumnNotFoundError) Error() string {
	return fmt.Sprintf("unknown column: %s", e.ColumnName)
}

func (e *ColumnNotFoundError) HTTPStatus() int {
	return http.StatusBadRequest
}

func (e *ColumnNotFoundError) Code() string {
	return string(appErrors.ErrInvalidQuery)
}

// NewColumnNotFoundError creates a new ColumnNotFoundError
func NewColumnNotFoundError(columnName string) *ColumnNotFoundError {
	return &ColumnNotFoundError{ColumnName: columnName}
}

// IsColumnNotFound checks if an error is a ColumnNotFoundError
func IsColumnNotFound(err error) bool {
	var colErr *ColumnNotFoundError
	return errors.As(err, &colErr)
}

// LockContentionError represents exhausted retryable lock/contention attempts,
// such as TiDB row-lock waits/deadlocks or durable parent-lane admission
// contention. The caller may retry the operation.
type LockContentionError struct {
	Cause error
}

func (e *LockContentionError) Error() string {
	return "could not acquire row lock after retries; the record is being modified by another transaction"
}

func (e *LockContentionError) HTTPStatus() int {
	return http.StatusConflict
}

func (e *LockContentionError) Code() string {
	return string(appErrors.ErrConflict)
}

func (e *LockContentionError) RetriableCondition() bool { return true }

func (e *LockContentionError) Unwrap() error {
	return e.Cause
}

// NewLockContentionError creates a new LockContentionError wrapping the
// underlying typed contention cause for operator logging/classification.
func NewLockContentionError(cause error) *LockContentionError {
	return &LockContentionError{Cause: cause}
}

// InternalError represents unexpected server errors
type InternalError struct {
	Message string
	Cause   error
}

func (e *InternalError) Error() string {
	if e.Cause != nil {
		return fmt.Sprintf("internal error: %s (caused by: %v)", e.Message, e.Cause)
	}
	return fmt.Sprintf("internal error: %s", e.Message)
}

func (e *InternalError) HTTPStatus() int {
	return http.StatusInternalServerError
}

func (e *InternalError) Code() string {
	return string(appErrors.ErrInternalServerError)
}

func (e *InternalError) Unwrap() error {
	return e.Cause
}

// InternalCondition marks this as an internal/infrastructure failure for the
// neutral shared classifier (appErrors.InternalConditioner). An InternalError
// is always internal.
func (e *InternalError) InternalCondition() bool {
	return true
}

// NewInternalError creates a new InternalError
func NewInternalError(message string, cause error) *InternalError {
	return &InternalError{Message: message, Cause: cause}
}

// WireConditionKind and DomainWireError live in shared so neutral contract
// consumers and dialect codecs can inspect them without importing backend. Keep
// aliases here while existing engine code imports backend/pkg/errors.
type WireConditionKind = appErrors.WireConditionKind

const (
	WireInvalidReference = appErrors.WireInvalidReference
	WireFieldIntegrity   = appErrors.WireFieldIntegrity
	WireInvalidOperation = appErrors.WireInvalidOperation
	WireInvalidField     = appErrors.WireInvalidField
	WireNotFound         = appErrors.WireNotFound
)

type DomainWireError = appErrors.DomainWireError
type CreateFromSourceGuardDetails = appErrors.CreateFromSourceGuardDetails

func NewInvalidReferenceError(message string, fields ...string) *DomainWireError {
	return appErrors.NewInvalidReferenceError(message, fields...)
}

func NewFieldIntegrityError(message string, fields ...string) *DomainWireError {
	return appErrors.NewFieldIntegrityError(message, fields...)
}

func NewInvalidOperationError(message string) *DomainWireError {
	return appErrors.NewInvalidOperationError(message)
}

func NewCreateFromSourceGuardError(objectName string, actionNames []string) *DomainWireError {
	return appErrors.NewCreateFromSourceGuardError(objectName, actionNames)
}

func NewInvalidFieldError(message string, fields ...string) *DomainWireError {
	return appErrors.NewInvalidFieldError(message, fields...)
}

func NewNotFoundWireError(message string) *DomainWireError {
	return appErrors.NewNotFoundWireError(message)
}

// Helper functions for error checking

// IsNotFound checks if an error is a NotFoundError
func IsNotFound(err error) bool {
	var notFound *NotFoundError
	return errors.As(err, &notFound)
}

// IsValidation checks if an error is a ValidationError
func IsValidation(err error) bool {
	var validation *ValidationError
	return errors.As(err, &validation)
}

// AsFLSDenied unwraps err into *FLSDeniedError. Callers use this instead of
// importing stdlib errors for errors.As, keeping the write-path abstraction
// local to this package.
func AsFLSDenied(err error) (*FLSDeniedError, bool) {
	var flsErr *FLSDeniedError
	if errors.As(err, &flsErr) {
		return flsErr, true
	}
	return nil, false
}

// AsConflict unwraps err into *ConflictError. Mirrors AsFLSDenied so callers
// that need the conflicting FIELD (not the rendered message, which embeds the
// conflicting value) can read it structurally.
func AsConflict(err error) (*ConflictError, bool) {
	var conflictErr *ConflictError
	if errors.As(err, &conflictErr) {
		return conflictErr, true
	}
	return nil, false
}

// AsClientValidationError returns a user-facing write validation error only
// when the chain is not already classified as InternalError. Source layers use
// this before adding generic hook/context wrappers so validation failures from
// the records lifecycle do not become opaque 500s, while rollback/IO failures
// that already carry InternalError keep REST's internal-first safety contract.
func AsClientValidationError(err error) (error, bool) {
	leaves, ok := AsClientValidationErrors(err)
	if !ok {
		return nil, false
	}
	if len(leaves) == 1 {
		return leaves[0], true
	}
	return &clientValidationErrorsError{Errors: leaves}, true
}

// IsPermission checks if an error is a PermissionError
func IsPermission(err error) bool {
	var permission *PermissionError
	return errors.As(err, &permission)
}

// IsConflict checks if an error is a ConflictError
func IsConflict(err error) bool {
	var conflict *ConflictError
	return errors.As(err, &conflict)
}

// IsInternal checks if an error is an InternalError (or wraps one).
//
// Callers at system boundaries (REST handlers, bulk orchestrators) MUST
// check this FIRST before classifying an error as a 4xx business failure.
// The transaction manager wraps rollback failures as InternalError(cause=businessErr);
// without this guard, errors.As would unwrap through the InternalError into
// the business cause and misreport a genuine infra failure as a user-facing
// 4xx. Infra problems must surface as 500 so operators see the real signal.
//
// Also used by bulk/allOrNone flows to distinguish true infrastructure
// failures (tx begin/commit/rollback, IO) from business rollbacks, so
// that per-record "rolled back" responses don't mask a dead database.
func IsInternal(err error) bool {
	return appErrors.IsInternal(err)
}

// GetHTTPStatus returns the HTTP status code for an error
// Returns 500 if the error doesn't implement AppError
func GetHTTPStatus(err error) int {
	var appErr AppError
	if errors.As(err, &appErr) {
		return appErr.HTTPStatus()
	}
	return http.StatusInternalServerError
}

// GetErrorCode returns the error code for an error
// Returns "UNKNOWN_ERROR" if the error doesn't implement AppError
func GetErrorCode(err error) string {
	var appErr AppError
	if errors.As(err, &appErr) {
		return appErr.Code()
	}
	// Use explicit SERVICE_UNAVAILABLE or similar for unknown system types,
	// or stick to INTERNAL_SERVER_ERROR.
	// Let's use a generic 'INTERNAL_SERVER_ERROR' as default in SSoT spirit.
	return string(appErrors.ErrInternalServerError)
}

// ErrorResponse represents a standardized error response
type ErrorResponse struct {
	Code      string `json:"code"`
	Message   string `json:"message"`
	Details   any    `json:"details,omitempty"`
	RequestID string `json:"request_id,omitempty"`
}

// ToResponseWithID converts an error to an ErrorResponse with a request ID
func ToResponseWithID(err error, requestID string) ErrorResponse {
	return ErrorResponse{
		Code:      GetErrorCode(err),
		Message:   err.Error(),
		RequestID: requestID,
	}
}
