package errors

import (
	"fmt"
	"sort"
	"strings"
)

// WireConditionKind is a neutral, dialect-agnostic classification of an
// engine-detected error condition. API dialect boundaries map each kind to a
// dialect-specific wire error code + transport status. Engines name the
// condition; dialects own their vocabulary.
type WireConditionKind string

const (
	// WireInvalidReference: a referenced record/key does not resolve to a live
	// row (cross-reference key failure).
	WireInvalidReference WireConditionKind = "InvalidReference"
	// WireFieldIntegrity: the save cannot proceed given the current field state
	// (e.g. a required parent field is null when an autonumber format references
	// it).
	WireFieldIntegrity WireConditionKind = "FieldIntegrity"
	// WireInvalidOperation: the requested operation is not permitted in this
	// context (e.g. a direct create that would bypass an owning action).
	WireInvalidOperation WireConditionKind = "InvalidOperation"
	// WireInvalidField: a field in the request is invalid or malformed.
	WireInvalidField WireConditionKind = "InvalidField"
	// WireNotFound: the addressed resource does not exist.
	WireNotFound WireConditionKind = "NotFound"
	// WireMalformedRequest: the request itself could not be read or decoded at
	// the transport boundary (e.g. an unreadable/truncated/aborted request body),
	// before any field or record was interpreted. Distinct from WireInvalidField,
	// which names a bad field within an otherwise-readable request.
	WireMalformedRequest WireConditionKind = "MalformedRequest"
	// WireRequestTooLarge: the request body exceeds the transport-layer size cap,
	// so the platform refuses to buffer it. A transport-level limit, independent of
	// any per-resource semantic limit (e.g. a max-records rule) a handler may apply.
	WireRequestTooLarge WireConditionKind = "RequestTooLarge"
)

// DomainWireError is a neutral typed error carrying a semantic condition kind,
// a client-facing message, optional field names, and an optional internal
// cause. Engines return this instead of a finished dialect payload; the API
// boundary translates the kind into the concrete wire dialect.
//
// It intentionally does NOT expose HTTPStatus() or Code(); transport status and
// dialect error codes belong to dialect mappers above the neutral contract.
type DomainWireError struct {
	Kind                  WireConditionKind
	Message               string
	Fields                []string
	Cause                 error
	CreateFromSourceGuard *CreateFromSourceGuardDetails
}

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

func (e *DomainWireError) Unwrap() error {
	if e == nil {
		return nil
	}
	return e.Cause
}

// WithCause attaches an internal cause, which dialect boundaries may log but
// must not serialize into the wire body, and returns the receiver for chaining.
func (e *DomainWireError) WithCause(err error) *DomainWireError {
	e.Cause = err
	return e
}

// NewInvalidReferenceError builds a DomainWireError for a cross-reference key
// failure. Field names, when supplied, are carried in order.
func NewInvalidReferenceError(message string, fields ...string) *DomainWireError {
	return &DomainWireError{Kind: WireInvalidReference, Message: message, Fields: fields}
}

// NewFieldIntegrityError builds a DomainWireError for a field-integrity save
// failure.
func NewFieldIntegrityError(message string, fields ...string) *DomainWireError {
	return &DomainWireError{Kind: WireFieldIntegrity, Message: message, Fields: fields}
}

// NewInvalidOperationError builds a DomainWireError for a disallowed operation.
func NewInvalidOperationError(message string) *DomainWireError {
	return &DomainWireError{Kind: WireInvalidOperation, Message: message}
}

// CreateFromSourceGuardDetails carries the neutral facts for a direct-create
// rejection owned by one or more standard actions. Engines report action names;
// API dialect boundaries decide how those actions are addressed on the wire.
type CreateFromSourceGuardDetails struct {
	ObjectName  string
	ActionNames []string
}

// Clone returns a detached copy safe to move across neutral contract results.
func (d *CreateFromSourceGuardDetails) Clone() *CreateFromSourceGuardDetails {
	if d == nil {
		return nil
	}
	return &CreateFromSourceGuardDetails{
		ObjectName:  d.ObjectName,
		ActionNames: append([]string(nil), d.ActionNames...),
	}
}

// NewCreateFromSourceGuardError builds the neutral invalid-operation error for
// a direct sObject create that must go through its owning standard action.
func NewCreateFromSourceGuardError(objectName string, actionNames []string) *DomainWireError {
	details := &CreateFromSourceGuardDetails{
		ObjectName:  objectName,
		ActionNames: dedupAndSortStrings(actionNames),
	}
	return &DomainWireError{
		Kind:                  WireInvalidOperation,
		Message:               FormatCreateFromSourceGuardMessage(objectName, details.ActionNames, "the owning standard action", "one of the owning standard actions"),
		CreateFromSourceGuard: details,
	}
}

// FormatCreateFromSourceGuardMessage renders the stable client-facing message
// for a create-from-source guard rejection. actionReferences are already in the
// caller's presentation form: neutral callers pass action names; wire adapters
// may pass route addresses.
func FormatCreateFromSourceGuardMessage(objectName string, actionReferences []string, singularTarget, pluralTarget string) string {
	actionReferences = dedupAndSortStrings(actionReferences)
	if len(actionReferences) == 1 {
		return fmt.Sprintf(
			"Direct create of %s with a configured source-FK populated is not permitted. "+
				"Invoke %s instead: %s",
			objectName, singularTarget, actionReferences[0],
		)
	}
	return fmt.Sprintf(
		"Direct create of %s with a configured source-FK populated is not permitted. "+
			"Invoke %s instead: %s",
		objectName, pluralTarget, strings.Join(actionReferences, ", "),
	)
}

// NewInvalidFieldError builds a DomainWireError for an invalid/malformed field.
func NewInvalidFieldError(message string, fields ...string) *DomainWireError {
	return &DomainWireError{Kind: WireInvalidField, Message: message, Fields: fields}
}

// NewNotFoundWireError builds a DomainWireError for a missing resource. Missing
// resource errors have no field-specific payload in the neutral contract.
func NewNotFoundWireError(message string) *DomainWireError {
	return &DomainWireError{Kind: WireNotFound, Message: message}
}

// NewMalformedRequestError builds a DomainWireError for a request that could not
// be read or decoded at the transport boundary (e.g. a body-read failure). Attach
// the underlying transport cause with WithCause; per that method's contract the
// cause is for server-side logging only and is never serialized to the wire.
func NewMalformedRequestError(message string) *DomainWireError {
	return &DomainWireError{Kind: WireMalformedRequest, Message: message}
}

// NewRequestTooLargeError builds a DomainWireError for a request body that exceeds
// the transport-layer size cap (refused before buffering completes).
func NewRequestTooLargeError(message string) *DomainWireError {
	return &DomainWireError{Kind: WireRequestTooLarge, Message: message}
}

func dedupAndSortStrings(in []string) []string {
	if len(in) == 0 {
		return nil
	}
	seen := make(map[string]struct{}, len(in))
	out := make([]string, 0, len(in))
	for _, s := range in {
		if _, ok := seen[s]; ok {
			continue
		}
		seen[s] = struct{}{}
		out = append(out, s)
	}
	sort.Strings(out)
	return out
}
