// Package wire owns reusable Salesforce-compatible HTTP wire grammar for the
// CRM dialect and independently deployable CRM AppServices. It intentionally
// uses only the Go standard library. Neutral Runtime outcomes are translated
// into these product-specific envelopes at the L1 boundary.
package wire

import (
	"fmt"
	"net/http"
	"regexp"
	"sort"
	"strings"
)

// standardActionIDPattern bounds a standard action's identifier to one URL path segment.
//
// The identifier is metadata-authored and is interpolated into the endpoint this package spells,
// so without a shape rule the segment after the API version — the surface class — depends on what
// a pack or overlay author wrote. An identifier of `../../composite/batch` yields a path every
// client normalises to `/services/data/<version>/composite/batch` before sending: a request issued
// against one API surface arriving at another.
//
// Escaping cannot substitute for the shape rule. `url.PathEscape` leaves `.` untouched, and a
// percent-encoded separator or dot segment is decoded back into net/http's URL.Path before any
// router sees it (`%2F..` arrives as `/..`), so the escaped form is not the form that travels.
//
// The shape is Salesforce's DeveloperName, optionally qualified by an object: `Case.Escalate` and
// `Lead.Convert` are canonical and frozen in this package's wire goldens. Because every dot must be
// followed by a letter, an identifier matching this can never *be* a dot segment, which is what
// makes the class decidable.
var standardActionIDPattern = regexp.MustCompile(`^[A-Za-z][A-Za-z0-9_]*(\.[A-Za-z][A-Za-z0-9_]*)*$`)

// IsAddressableStandardActionID reports whether a standard action identifier can be both published
// as an endpoint and accepted from a request path.
//
// It is exported because the same question is asked in three places — the endpoint this package
// spells, the discovery projections that publish it, and the route parsers that accept an incoming
// name — and answering it three times is how the advertised set and the executable set drift apart.
// A name a projection would refuse to publish must not be executable, and vice versa.
//
// The identifier is not trimmed. Padding is a different identifier, not a spelling of this one.
func IsAddressableStandardActionID(actionID string) bool {
	return standardActionIDPattern.MatchString(actionID)
}

// SFError represents a Salesforce-compatible error. HTTP handlers serialize a
// response as an array of these values.
type SFError struct {
	ErrorCode string   `json:"errorCode"`
	Message   string   `json:"message"`
	Fields    []string `json:"fields,omitempty"`
}

// SFGenericInternalErrorMessage is the canonical client-safe 500 message.
const SFGenericInternalErrorMessage = "An internal server error occurred. Contact your system administrator."

// SFErrorResponse returns a Salesforce-compatible error array.
func SFErrorResponse(errorCode, message string) []SFError {
	return []SFError{{ErrorCode: errorCode, Message: message}}
}

// SFGenericInternalErrorResponse returns the canonical generic 500 envelope.
func SFGenericInternalErrorResponse() []SFError {
	return SFErrorResponse(SFCodeInternalError, SFGenericInternalErrorMessage)
}

// SFErrorWithFields returns an error array with the supplied field names.
func SFErrorWithFields(errorCode, message string, fields ...string) []SFError {
	return []SFError{{ErrorCode: errorCode, Message: message, Fields: fields}}
}

// Salesforce-compatible error codes emitted by CRM HTTP routes.
const (
	SFCodeUnauthorized                   = "UNAUTHORIZED"
	SFCodeConflict                       = "CONFLICT"
	SFCodeInvalidRequest                 = "INVALID_REQUEST"
	SFCodeLimitExceeded                  = "LIMIT_EXCEEDED"
	SFCodeInvalidFieldForInsertUpdate    = "INVALID_FIELD_FOR_INSERT_UPDATE"
	SFCodeFieldCustomValidationException = "FIELD_CUSTOM_VALIDATION_EXCEPTION"
	SFCodeForbidden                      = "FORBIDDEN"
	SFCodeUnableToLockRow                = "UNABLE_TO_LOCK_ROW"
	SFCodeInternalError                  = "INTERNAL_ERROR"
	SFCodeServerError                    = "SERVER_ERROR"
	SFCodeUnknownException               = "UNKNOWN_EXCEPTION"
	SFCodeInvalidSessionID               = "INVALID_SESSION_ID"
	SFCodeInvalidQuery                   = "INVALID_QUERY"
	SFCodeQueryFailed                    = "QUERY_FAILED"
	SFCodeMalformedQuery                 = "MALFORMED_QUERY"
	SFCodeMalformedSearch                = "MALFORMED_SEARCH"
	SFCodeInvalidObject                  = "INVALID_OBJECT"
	SFCodeMissingQuery                   = "MISSING_QUERY"
	SFCodeInvalidQueryLocator            = "INVALID_QUERY_LOCATOR"
	SFCodeExceededIDLimit                = "EXCEEDED_ID_LIMIT"
	SFCodeJobCreationFailed              = "JOB_CREATION_FAILED"
	SFCodeNoQuery                        = "NO_QUERY"
	SFCodeInvalidJobState                = "INVALID_JOB_STATE"
	SFCodeUpsertFailed                   = "UPSERT_FAILED"
	SFCodeFileTooLarge                   = "FILE_TOO_LARGE"
	SFCodeJSONParserError                = "JSON_PARSER_ERROR"
	SFCodeRequiredFieldMissing           = "REQUIRED_FIELD_MISSING"
	SFCodeInsertFailed                   = "INSERT_FAILED"
	SFCodeUpdateFailed                   = "UPDATE_FAILED"
	SFCodeDeleteFailed                   = "DELETE_FAILED"
	SFCodeAllOrNoneOperationRolledBack   = "ALL_OR_NONE_OPERATION_ROLLED_BACK"
	SFCodeProcessingHalted               = "PROCESSING_HALTED"
	SFCodeInvalidCrossReferenceKey       = "INVALID_CROSS_REFERENCE_KEY"
	SFCodeFieldIntegrityException        = "FIELD_INTEGRITY_EXCEPTION"
	SFCodeInvalidOperation               = "INVALID_OPERATION"
	SFCodeInvalidField                   = "INVALID_FIELD"
	SFCodeInvalidID                      = "INVALID_ID"
	SFCodeMalformedID                    = "MALFORMED_ID"
	SFCodeNotFound                       = "NOT_FOUND"
	SFCodeInsufficientPerms              = "INSUFFICIENT_PERMISSIONS"
)

// CreateFromSourceGuardMessage renders structured Runtime guard facts into the
// Salesforce-compatible, API-versioned action-endpoint message. Input order and
// duplicates cannot change the result.
func CreateFromSourceGuardMessage(apiVersion, objectID string, actionIDs []string) string {
	endpoints := make([]string, 0, len(actionIDs))
	for _, actionID := range actionIDs {
		endpoint := StandardActionEndpoint(apiVersion, actionID)
		if endpoint == "" {
			// An identifier with no addressable endpoint is left out rather than listed as an
			// empty entry. Naming a URL that a client would normalise onto a different API
			// surface would be worse than naming one action fewer, and a pack whose actions are
			// all unaddressable is an authoring fault this message cannot repair.
			continue
		}
		endpoints = append(endpoints, endpoint)
	}
	endpoints = dedupAndSort(endpoints)
	target := "one of the owning standard action endpoints"
	if len(endpoints) == 1 {
		target = "the owning standard action endpoint"
	}
	return fmt.Sprintf(
		"Direct create of %s with a configured source-FK populated is not permitted. Invoke %s instead: %s",
		objectID, target, strings.Join(endpoints, ", "),
	)
}

// StandardActionEndpoint returns the Salesforce-compatible endpoint for one
// standard action in the requested API version, or "" when the identifier cannot be spelled as one
// path segment.
//
// An identifier that fails standardActionIDPattern has no endpoint to return: the serving route is
// POST /actions/standard/:name, a single segment, so such an identifier could never be invoked
// through it. Returning "" rather than a path is what keeps a metadata-authored value from moving a
// published URL into another surface class — see the pattern's own comment for why escaping does not
// achieve this. Callers must treat "" as "not addressable" rather than pass it on.
//
// The identifier is deliberately not trimmed. Trimming would canonicalise " convertLead " into an
// endpoint for `convertLead` while the caller still reports the raw identifier, so one action would
// be advertised under another's URL.
func StandardActionEndpoint(apiVersion, actionID string) string {
	if !IsAddressableStandardActionID(actionID) {
		return ""
	}
	apiVersion = strings.TrimSpace(apiVersion)
	if apiVersion == "" {
		apiVersion = "v63.0"
	}
	return fmt.Sprintf("/services/data/%s/actions/standard/%s", apiVersion, actionID)
}

func dedupAndSort(values []string) []string {
	if len(values) == 0 {
		return nil
	}
	seen := make(map[string]struct{}, len(values))
	result := make([]string, 0, len(values))
	for _, value := range values {
		if _, exists := seen[value]; exists {
			continue
		}
		seen[value] = struct{}{}
		result = append(result, value)
	}
	sort.Strings(result)
	return result
}

// ValidationErrorEnvelope renders a field-scoped validation error to its
// Salesforce-compatible status and body.
func ValidationErrorEnvelope(field, message, fallbackCode string) (int, any) {
	fields := splitValidationFields(field)
	if len(fields) > 0 {
		return http.StatusBadRequest, SFErrorWithFields(fallbackCode, message, fields...)
	}
	return http.StatusBadRequest, SFErrorResponse(fallbackCode, message)
}

func splitValidationFields(raw string) []string {
	if strings.TrimSpace(raw) == "" {
		return nil
	}
	parts := strings.Split(raw, ",")
	fields := make([]string, 0, len(parts))
	for _, part := range parts {
		field := strings.TrimSpace(part)
		if field != "" {
			fields = append(fields, field)
		}
	}
	if len(fields) == 0 {
		return nil
	}
	return fields
}
