package apierr

import (
	"fmt"
	"net/http"
)

// Error represents a structured API error
type Error struct {
	Code    string                 `json:"code"`    // PascalCase Error Code (e.g. "InvalidInput")
	Message string                 `json:"message"` // Human-readable message
	Details map[string]interface{} `json:"details,omitempty"`
	Status  int                    `json:"-"` // HTTP Status (not serialized)
	Cause   error                  `json:"-"` // Internal error cause (logged but not sent)
}

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

// Unwrap returns the internal cause
func (e *Error) Unwrap() error {
	return e.Cause
}

// New creates a new API Error
func New(status int, code string, msg string) *Error {
	return &Error{
		Status:  status,
		Code:    code,
		Message: msg,
	}
}

// WithDetails adds details to the error
func (e *Error) WithDetails(details map[string]interface{}) *Error {
	e.Details = details
	return e
}

// WithCause adds an internal cause to the error
func (e *Error) WithCause(err error) *Error {
	e.Cause = err
	return e
}

// Standard Error Constructors

func Invalid(msg string) *Error {
	return New(http.StatusBadRequest, "InvalidRequest", msg)
}

func NotFound(msg string) *Error {
	return New(http.StatusNotFound, "ResourceNotFound", msg)
}

func Unauthorized(msg string) *Error {
	return New(http.StatusUnauthorized, "Unauthorized", msg)
}

func Forbidden(msg string) *Error {
	return New(http.StatusForbidden, "Forbidden", msg)
}

func Internal(err error) *Error {
	return New(http.StatusInternalServerError, "InternalServerError", "An unexpected error occurred").WithCause(err)
}

func Conflict(msg string) *Error {
	return New(http.StatusConflict, "ResourceConflict", msg)
}

func ValidationFailed(details map[string]interface{}) *Error {
	return New(http.StatusBadRequest, "ValidationFailed", "Validation failed").WithDetails(details)
}

// =============================================================================
// Salesforce REST API v63.0 Compatible Error Responses
// Salesforce returns errors as a JSON array of objects, not a single object.
// =============================================================================

// SFError represents a Salesforce-compatible error (must be wrapped in array)
type SFError struct {
	ErrorCode string   `json:"errorCode"`
	Message   string   `json:"message"`
	Fields    []string `json:"fields,omitempty"`
}

// SalesforceAPIError preserves a specific Salesforce REST error payload and
// HTTP status so higher layers can surface parity-critical failures without
// flattening them into operation-specific fallback codes.
type SalesforceAPIError struct {
	Status int
	Err    SFError
	Cause  error
}

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

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

func NewSalesforce(status int, errorCode, message string, fields ...string) *SalesforceAPIError {
	return &SalesforceAPIError{
		Status: status,
		Err: SFError{
			ErrorCode: errorCode,
			Message:   message,
			Fields:    fields,
		},
	}
}

func (e *SalesforceAPIError) WithCause(err error) *SalesforceAPIError {
	e.Cause = err
	return e
}

// SFErrorResponse returns a Salesforce-compatible error array
// Usage: c.JSON(status, apierr.SFErrorResponse("NOT_FOUND", "Resource not found"))
func SFErrorResponse(errorCode, message string) []SFError {
	return []SFError{{ErrorCode: errorCode, Message: message}}
}

// SFErrorWithFields returns a Salesforce-compatible error array with field names
// Usage: c.JSON(status, apierr.SFErrorWithFields("REQUIRED_FIELD_MISSING", "Required fields", "Name", "Email"))
func SFErrorWithFields(errorCode, message string, fields ...string) []SFError {
	return []SFError{{ErrorCode: errorCode, Message: message, Fields: fields}}
}

// Standard Salesforce Error Codes (for consistency)
const (
	SFCodeUnauthorized             = "UNAUTHORIZED"
	SFCodeForbidden                = "FORBIDDEN"
	SFCodeNotFound                 = "NOT_FOUND"
	SFCodeInvalidRequest           = "INVALID_REQUEST"
	SFCodeInvalidField             = "INVALID_FIELD"
	SFCodeInvalidQuery             = "INVALID_QUERY"
	SFCodeMalformedQuery           = "MALFORMED_QUERY"
	SFCodeJSONParserError          = "JSON_PARSER_ERROR"
	SFCodeRequiredFieldMissing     = "REQUIRED_FIELD_MISSING"
	SFCodeLimitExceeded            = "LIMIT_EXCEEDED"
	SFCodeUnknownException         = "UNKNOWN_EXCEPTION"
	SFCodeMissingQuery             = "MISSING_QUERY"
	SFCodeInvalidLocator           = "INVALID_LOCATOR"
	SFCodeInvalidQueryLocator      = "INVALID_QUERY_LOCATOR"
	SFCodeInvalidSessionID         = "INVALID_SESSION_ID"
	SFCodeMalformedSearch          = "MALFORMED_SEARCH"
	SFCodeMissingOAuthToken        = "Missing_OAuth_Token"
	SFCodeInsertFailed             = "INSERT_FAILED"
	SFCodeUpdateFailed             = "UPDATE_FAILED"
	SFCodeDeleteFailed             = "DELETE_FAILED"
	SFCodeUpsertFailed             = "UPSERT_FAILED"
	SFCodeInvalidCrossReferenceKey = "INVALID_CROSS_REFERENCE_KEY"
	// SFCodeInvalidFieldForInsertUpdate is the SF v63.0 error code emitted when a
	// payload contains a field the caller cannot write due to user-level FLS
	// Edit denial, or a handful of explicit immutable-field rejection paths
	// (e.g. CreatedDate on update). Intrinsically non-writable fields
	// (formula, auto-number, system-managed) are silently dropped by the
	// current write paths rather than surfaced with this code. Verified
	// against real SF sandbox.
	SFCodeInvalidFieldForInsertUpdate = "INVALID_FIELD_FOR_INSERT_UPDATE"
	// SFCodeAllOrNoneOperationRolledBack is the per-record statusCode SF emits
	// for records that would have succeeded in a composite collections write
	// but were rolled back because a sibling record failed under allOrNone=true.
	// Verified against SF sandbox for DELETE/PATCH on /composite/sobjects.
	SFCodeAllOrNoneOperationRolledBack = "ALL_OR_NONE_OPERATION_ROLLED_BACK"
	// SFCodeProcessingHalted is the generic per-record statusCode SF uses when
	// a composite collections record hit a non-specific processing error
	// (not an FLS / not-found / invalid-id case). Used as the fallback in
	// bulkErrorEntry when no more specific code applies.
	SFCodeProcessingHalted = "PROCESSING_HALTED"
	SFCodeQueryFailed      = "QUERY_FAILED"
	SFCodeInternalError    = "INTERNAL_ERROR"
	// SFCodeFieldIntegrityException is the SF v63.0 error code for save failures
	// caused by the current field state, such as a required parent field being
	// null when an autonumber format references it. SF returns this as HTTP 400.
	SFCodeFieldIntegrityException = "FIELD_INTEGRITY_EXCEPTION"
	// Bulk API specific error codes
	SFCodeInvalidJobState   = "INVALID_JOB_STATE"
	SFCodeJobCreationFailed = "JOB_CREATION_FAILED"
	SFCodeJobNotFound       = "JOB_NOT_FOUND"
	SFCodeNoQuery           = "NO_QUERY"
	SFCodeInvalidObject     = "INVALID_OBJECT"
	SFCodeInvalidOperation  = "INVALID_OPERATION"
	SFCodeInsufficientPerms = "INSUFFICIENT_PERMISSIONS"
	SFCodeFileTooLarge      = "FILE_TOO_LARGE"
)
