package sfcrmappservice

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"strings"

	crmwire "github.com/pingcap-inc/nexuscrm-app-packs/sfcrm/wire"
	semantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/recordmutationv1"
)

const (
	crmCodeAllOrNoneRolledBack    = crmwire.SFCodeAllOrNoneOperationRolledBack
	crmCodeDeleteFailed           = crmwire.SFCodeDeleteFailed
	crmCodeFieldIntegrity         = crmwire.SFCodeFieldIntegrityException
	crmCodeInvalidCrossReference  = crmwire.SFCodeInvalidCrossReferenceKey
	crmCodeInvalidField           = crmwire.SFCodeInvalidField
	crmCodeInvalidFieldForWrite   = crmwire.SFCodeInvalidFieldForInsertUpdate
	crmCodeInvalidID              = crmwire.SFCodeInvalidID
	crmCodeInvalidOperation       = crmwire.SFCodeInvalidOperation
	crmCodeInsertFailed           = crmwire.SFCodeInsertFailed
	crmCodeMalformedID            = crmwire.SFCodeMalformedID
	crmCodeNotFound               = crmwire.SFCodeNotFound
	crmCodeProcessingHalted       = crmwire.SFCodeProcessingHalted
	crmCodeRequiredFieldMissing   = crmwire.SFCodeRequiredFieldMissing
	crmCodeUpdateFailed           = crmwire.SFCodeUpdateFailed
	crmCodeCustomValidation       = crmwire.SFCodeFieldCustomValidationException
	crmCodeInsufficientPermission = crmwire.SFCodeInsufficientPerms
)

const crmAllOrNoneRolledBackMessage = "Record rolled back because not all records were valid and the request was using AllOrNone header"

const crmMutationIdempotencyHeader = "Idempotency-Key"

type mutationRoute struct {
	apiVersion string
	useCase    semantic.MutationUseCase
	kind       semantic.OperationKind
	objectID   string
	recordID   string
}

type compositeMutationRequest struct {
	AllOrNone bool             `json:"allOrNone"`
	Records   []map[string]any `json:"records"`
}

type mutationLocalResponse struct {
	status int
	body   any
}

func (s *Service) serveRecordMutation(writer http.ResponseWriter, request *http.Request) bool {
	route, allowed, recognized := matchMutationRoute(request.URL.Path, request.Method)
	if !recognized {
		return false
	}
	if route.kind == "" {
		writer.Header().Set("Allow", strings.Join(allowed, ", "))
		writeCRMError(writer, http.StatusMethodNotAllowed, crmwire.SFCodeInvalidRequest, "Method not allowed")
		return true
	}

	s.requests.Add(1)
	if s.draining.Load() {
		s.drainRejected.Add(1)
		s.failures.Add(1)
		writeCRMError(writer, http.StatusServiceUnavailable, crmwire.SFCodeUnknownException, "Service is draining")
		return true
	}
	s.inFlight.Add(1)
	defer s.inFlight.Add(-1)
	ctx, cancel := context.WithTimeout(request.Context(), s.requestTimeout)
	defer cancel()

	bearer, ok := endUserBearer(request.Header.Values("Authorization"))
	if !ok {
		s.localRejected.Add(1)
		s.failures.Add(1)
		writeCRMError(writer, http.StatusUnauthorized, crmwire.SFCodeInvalidSessionID, "Session expired or invalid")
		return true
	}
	operations, mode, localResponse, err := s.decodeMutationRequest(writer, request, route)
	if err != nil {
		s.localRejected.Add(1)
		s.failures.Add(1)
		writeCRMError(writer, http.StatusBadRequest, mutationJSONErrorCode(route), mutationJSONErrorMessage(route, err))
		return true
	}
	if localResponse != nil {
		if localResponse.status >= http.StatusBadRequest {
			s.localRejected.Add(1)
			s.failures.Add(1)
		} else {
			s.successes.Add(1)
		}
		writeJSON(writer, localResponse.status, localResponse.body)
		return true
	}
	commandID, err := mutationCommandID(request.Header.Values(crmMutationIdempotencyHeader))
	if err != nil {
		s.localRejected.Add(1)
		s.failures.Add(1)
		writeCRMError(writer, http.StatusBadRequest, crmwire.SFCodeInvalidRequest, err.Error())
		return true
	}
	response, err := s.mutation.Mutate(ctx, bearer, commandID, route.useCase, mode, operations)
	if err != nil {
		s.failures.Add(1)
		writeRuntimeFailure(writer, err)
		return true
	}
	if mutationObjectNotFound(response) {
		s.failures.Add(1)
		writeCRMError(writer, http.StatusNotFound, crmCodeNotFound, "Object not found")
		return true
	}
	if denial := commandPreflightPolicyDenial(response); denial != nil {
		s.failures.Add(1)
		writeMutationPolicyDenial(writer, route, *denial)
		return true
	}
	if route.useCase == semantic.MutationUseCaseSingle {
		if !response.Results[0].Success {
			s.failures.Add(1)
			writeSingleMutationFailures(writer, route, response.Results[0].Errors)
			return true
		}
		s.successes.Add(1)
		if route.kind == semantic.OperationCreate {
			writeJSON(writer, http.StatusCreated, map[string]any{
				"id": response.Results[0].RecordID, "success": true, "errors": []any{},
			})
		} else {
			writer.WriteHeader(http.StatusNoContent)
		}
		return true
	}

	s.successes.Add(1)
	writeJSON(writer, http.StatusOK, compositeMutationProjection(route.apiVersion, operations, response.Results))
	return true
}

func mutationObjectNotFound(response semantic.Response) bool {
	for _, result := range response.Results {
		for _, operationError := range result.Errors {
			if operationError.Kind == semantic.ErrorObjectNotFound {
				return true
			}
		}
	}
	return false
}

func (s *Service) decodeMutationRequest(writer http.ResponseWriter, request *http.Request, route mutationRoute) ([]semantic.Operation, semantic.BatchMode, *mutationLocalResponse, error) {
	mode := semantic.BatchIndependent
	if route.useCase == semantic.MutationUseCaseSingle {
		operation := semantic.Operation{Kind: route.kind, ObjectID: route.objectID, RecordID: route.recordID}
		if route.kind != semantic.OperationDelete {
			var body map[string]any
			if err := decodeMutationJSON(writer, request, s.maxRequestBytes, &body); err != nil {
				return nil, mode, nil, err
			}
			fields, err := mutationFields(body, false)
			if err != nil {
				return nil, mode, nil, err
			}
			operation.Fields = fields
		}
		return []semantic.Operation{operation}, mode, nil, nil
	}

	if route.kind == semantic.OperationDelete {
		ids := request.URL.Query().Get("ids")
		if ids == "" {
			return nil, mode, &mutationLocalResponse{status: http.StatusBadRequest, body: crmwire.SFErrorResponse(crmwire.SFCodeInvalidRequest, "Missing ids parameter")}, nil
		}
		parts := strings.Split(ids, ",")
		if len(parts) > semantic.MaxOperations {
			return nil, mode, &mutationLocalResponse{status: http.StatusBadRequest, body: crmwire.SFErrorResponse(crmwire.SFCodeLimitExceeded, "Maximum 200 IDs allowed")}, nil
		}
		if request.URL.Query().Get("allOrNone") == "true" {
			mode = semantic.BatchAtomic
		}
		operations := make([]semantic.Operation, 0, len(parts))
		for _, id := range parts {
			operations = append(operations, semantic.Operation{Kind: semantic.OperationDelete, RecordID: id})
		}
		return operations, mode, nil, nil
	}

	var body compositeMutationRequest
	if err := decodeMutationJSON(writer, request, s.maxRequestBytes, &body); err != nil {
		return nil, mode, nil, err
	}
	if len(body.Records) > semantic.MaxOperations {
		return nil, mode, &mutationLocalResponse{status: http.StatusBadRequest, body: crmwire.SFErrorResponse(crmwire.SFCodeLimitExceeded, "Maximum 200 records allowed")}, nil
	}
	if len(body.Records) == 0 {
		return nil, mode, &mutationLocalResponse{status: http.StatusOK, body: []any{}}, nil
	}
	if body.AllOrNone {
		mode = semantic.BatchAtomic
	}
	operations := make([]semantic.Operation, 0, len(body.Records))
	for _, record := range body.Records {
		operation := semantic.Operation{Kind: route.kind, ObjectID: compositeObjectName(record)}
		omitID := false
		if route.kind == semantic.OperationUpdate {
			id, err := compositeRecordID(record)
			if err != nil {
				return nil, mode, nil, err
			}
			operation.RecordID, omitID = id, true
		}
		fields, err := mutationFields(record, omitID)
		if err != nil {
			return nil, mode, nil, err
		}
		operation.Fields = fields
		operations = append(operations, operation)
	}
	return operations, mode, nil, nil
}

func decodeMutationJSON(writer http.ResponseWriter, request *http.Request, maximum int64, target any) error {
	request.Body = http.MaxBytesReader(writer, request.Body, maximum)
	raw, err := io.ReadAll(request.Body)
	if err != nil {
		return err
	}
	decoder := json.NewDecoder(bytes.NewReader(raw))
	decoder.UseNumber()
	// The existing CRM record and composite handlers decode the first JSON value
	// and intentionally ignore trailing input. Preserve that product-owned wire
	// grammar while still reading the complete body above so the AppService's
	// semantic request envelope cannot be bypassed with an oversized suffix.
	return decoder.Decode(target)
}

func matchMutationRoute(path, method string) (mutationRoute, []string, bool) {
	parts := strings.Split(strings.Trim(path, "/"), "/")
	if len(parts) < 3 || parts[0] != "services" || parts[1] != "data" || !supportedAPIVersion(parts[2]) {
		return mutationRoute{}, nil, false
	}
	apiVersion := parts[2]
	parts = parts[3:]
	if len(parts) == 2 && parts[0] == "composite" && parts[1] == "sobjects" {
		allowed := []string{http.MethodPost, http.MethodPatch, http.MethodDelete}
		kind := map[string]semantic.OperationKind{
			http.MethodPost: semantic.OperationCreate, http.MethodPatch: semantic.OperationUpdate, http.MethodDelete: semantic.OperationDelete,
		}[method]
		return mutationRoute{apiVersion: apiVersion, useCase: semantic.MutationUseCaseCollection, kind: kind}, allowed, true
	}
	if len(parts) == 2 && parts[0] == "sobjects" && parts[1] != "" {
		kind := semantic.OperationKind("")
		if method == http.MethodPost {
			kind = semantic.OperationCreate
		}
		return mutationRoute{apiVersion: apiVersion, useCase: semantic.MutationUseCaseSingle, kind: kind, objectID: parts[1]}, []string{http.MethodPost}, true
	}
	if len(parts) == 3 && parts[0] == "sobjects" && parts[1] != "" && parts[2] != "" {
		kind := map[string]semantic.OperationKind{http.MethodPatch: semantic.OperationUpdate, http.MethodDelete: semantic.OperationDelete}[method]
		return mutationRoute{apiVersion: apiVersion, useCase: semantic.MutationUseCaseSingle, kind: kind, objectID: parts[1], recordID: parts[2]}, []string{http.MethodPatch, http.MethodDelete}, true
	}
	return mutationRoute{}, nil, false
}

func supportedAPIVersion(value string) bool {
	switch value {
	case "v59.0", "v60.0", "v61.0", "v62.0", "v63.0":
		return true
	default:
		return false
	}
}

func mutationJSONErrorCode(route mutationRoute) string {
	if route.useCase == semantic.MutationUseCaseSingle {
		return crmwire.SFCodeJSONParserError
	}
	return crmwire.SFCodeInvalidRequest
}

func mutationJSONErrorMessage(route mutationRoute, err error) string {
	if route.useCase != semantic.MutationUseCaseSingle {
		return "Invalid request body"
	}
	if route.kind == semantic.OperationCreate {
		return "Invalid JSON"
	}
	return err.Error()
}

// commandPreflightPolicyDenial identifies the only policy-denial result shape
// that the existing CRM routes render as one top-level HTTP error: one
// Catalog-policy cause, every sibling halted, and no committed position.
//
// Runtime also re-evaluates policy under the mutation lock. That later check
// can legitimately produce a per-position policy denial after an independent
// sibling has committed, or an atomic rollback marker around one causal
// denial. Those are durable command results, not request-preflight failures;
// projecting either shape as a top-level error would discard position-stable
// outcomes the caller must receive.
func commandPreflightPolicyDenial(response semantic.Response) *semantic.OperationError {
	var denial *semantic.OperationError
	for _, result := range response.Results {
		if result.Success || len(result.Errors) != 1 {
			return nil
		}
		operationError := result.Errors[0]
		switch operationError.Kind {
		case semantic.ErrorPolicyDenied:
			if denial != nil {
				return nil
			}
			copy := operationError
			denial = &copy
		case semantic.ErrorProcessingHalted:
			// The sole policy denial below remains the command-level cause.
		default:
			return nil
		}
	}
	return denial
}

func writeMutationPolicyDenial(writer http.ResponseWriter, route mutationRoute, operationError semantic.OperationError) {
	code := mutationFallbackCode(route)
	writeCRMErrorFields(writer, http.StatusBadRequest, code, operationError.AuthoredMessage, operationError.Fields)
}

func mutationFallbackCode(route mutationRoute) string {
	if route.useCase == semantic.MutationUseCaseCollection {
		return crmCodeInvalidFieldForWrite
	}
	switch route.kind {
	case semantic.OperationCreate:
		return crmCodeInsertFailed
	case semantic.OperationUpdate:
		return crmCodeUpdateFailed
	case semantic.OperationDelete:
		return crmCodeDeleteFailed
	default:
		return crmwire.SFCodeInvalidRequest
	}
}

func writeSingleMutationFailures(writer http.ResponseWriter, route mutationRoute, operationErrors []semantic.OperationError) {
	status := http.StatusBadRequest
	body := make([]crmwire.SFError, 0, len(operationErrors))
	for _, operationError := range operationErrors {
		code, message := crmMutationErrorProjection(route.apiVersion, route.objectID, route.kind, operationError)
		switch operationError.Kind {
		case semantic.ErrorMissingOrDenied, semantic.ErrorObjectNotFound:
			status = http.StatusNotFound
		case semantic.ErrorAccessDenied:
			if len(operationError.Fields) == 0 {
				if status != http.StatusNotFound {
					status = http.StatusForbidden
				}
				code = crmwire.SFCodeForbidden
			}
		case semantic.ErrorInternal:
			writeCRMError(writer, http.StatusInternalServerError, crmwire.SFCodeInternalError, crmwire.SFGenericInternalErrorMessage)
			return
		case semantic.ErrorProcessingHalted:
			code = mutationFallbackCode(route)
		}
		fields := append([]string(nil), operationError.Fields...)
		body = append(body, crmwire.SFError{ErrorCode: code, Message: message, Fields: fields})
	}
	writeJSON(writer, status, body)
}

func crmMutationErrorProjection(apiVersion, objectID string, operationKind semantic.OperationKind, operationError semantic.OperationError) (string, string) {
	switch operationError.Kind {
	case semantic.ErrorCustomValidation:
		// Kind owns the Salesforce error code even when the authorized
		// metadata-authored message is empty. Falling through would silently
		// turn a validation failure into PROCESSING_HALTED.
		return crmCodeCustomValidation, operationError.AuthoredMessage
	case semantic.ErrorPolicyDenied:
		return crmCodeInvalidFieldForWrite, operationError.AuthoredMessage
	}
	switch operationError.Kind {
	case semantic.ErrorAtomicRolledBack:
		return crmCodeAllOrNoneRolledBack, crmAllOrNoneRolledBackMessage
	case semantic.ErrorInvalidFieldForWrite:
		return crmCodeInvalidFieldForWrite, "Invalid field for insert or update"
	case semantic.ErrorInvalidReference:
		return crmCodeInvalidCrossReference, "Invalid cross reference"
	case semantic.ErrorFieldIntegrity:
		return crmCodeFieldIntegrity, "Field integrity exception"
	case semantic.ErrorInvalidOperation:
		if guard := operationError.CreateFromSourceGuard; guard != nil {
			// Filtered before the message is rendered. The pinned sfcrm/wire v0.1.1 spells an
			// endpoint for whatever identifier it is handed, so an unaddressable one would put a
			// URL in this error that a client normalises onto an unrelated API surface — telling a
			// caller to "invoke this instead" and pointing somewhere else. The newer owner filters
			// internally; until the pin moves, filtering here is what makes the two agree.
			return crmCodeInvalidOperation, crmwire.CreateFromSourceGuardMessage(
				apiVersion, guard.ObjectID, addressableStandardActionIDs(guard.ActionIDs),
			)
		}
		return crmCodeInvalidOperation, "Invalid operation"
	case semantic.ErrorInvalidField:
		if objectID != "" && len(operationError.Fields) > 0 {
			return crmCodeInvalidField, fmt.Sprintf("field '%s' does not exist on object '%s'", operationError.Fields[0], objectID)
		}
		return crmCodeInvalidField, "Invalid field"
	case semantic.ErrorMissingOrDenied:
		return crmCodeNotFound, "Record not found"
	case semantic.ErrorObjectNotFound:
		return crmCodeNotFound, "Object not found"
	case semantic.ErrorMalformedIdentifier:
		return crmCodeMalformedID, "Malformed record identifier"
	case semantic.ErrorInvalidIdentifier:
		return crmCodeInvalidID, "Invalid record identifier"
	case semantic.ErrorAccessDenied:
		if len(operationError.Fields) > 0 {
			return crmCodeInvalidFieldForWrite, 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(operationError.Fields, ", "),
			)
		}
		if action, ok := mutationPermissionAction(operationKind); ok && strings.TrimSpace(objectID) != "" {
			return crmCodeInsufficientPermission, fmt.Sprintf(
				"permission denied: cannot %s %s", action, objectID,
			)
		}
		return crmCodeInsufficientPermission, "Insufficient access"
	case semantic.ErrorRequiredFieldMissing:
		return crmCodeRequiredFieldMissing, fmt.Sprintf("required field(s) missing: %s", strings.Join(operationError.Fields, ", "))
	case semantic.ErrorInternal:
		return crmwire.SFCodeInternalError, crmwire.SFGenericInternalErrorMessage
	default:
		return crmCodeProcessingHalted, "Processing halted"
	}
}

func mutationPermissionAction(kind semantic.OperationKind) (string, bool) {
	switch kind {
	case semantic.OperationCreate:
		return "Create", true
	case semantic.OperationUpdate:
		return "Edit", true
	case semantic.OperationDelete:
		return "Delete", true
	default:
		return "", false
	}
}

func compositeMutationProjection(apiVersion string, operations []semantic.Operation, results []semantic.OperationResult) []map[string]any {
	out := make([]map[string]any, 0, len(results))
	for _, result := range results {
		objectID := ""
		if int(result.Index) < len(operations) {
			objectID = operations[result.Index].ObjectID
		}
		errors := make([]map[string]any, 0, len(result.Errors))
		for _, operationError := range result.Errors {
			code, message := crmMutationErrorProjection(apiVersion, objectID, result.Kind, operationError)
			fields := make([]string, len(operationError.Fields))
			copy(fields, operationError.Fields)
			errors = append(errors, map[string]any{
				"message": message, "statusCode": code, "fields": fields,
			})
		}
		entry := map[string]any{"id": result.RecordID, "success": result.Success, "errors": errors}
		if result.Created != nil {
			entry["created"] = *result.Created
		}
		out = append(out, entry)
	}
	return out
}

func writeCRMErrorFields(writer http.ResponseWriter, status int, code, message string, fields []string) {
	writeJSON(writer, status, crmwire.SFErrorWithFields(code, message, fields...))
}
