package dialectsfcrm

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

	"github.com/nexuscrm/dialect-sf-crm/wire"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/contract"
	"github.com/nexuscrm/shared/pkg/dialect"
	sharederrors "github.com/nexuscrm/shared/pkg/errors"
	"github.com/nexuscrm/shared/pkg/models"
)

type compositeCollectionRequest struct {
	AllOrNone bool             `json:"allOrNone"`
	Records   []models.SObject `json:"records"`
}

type objectTypeCandidateResolver interface {
	ObjectTypeCandidatesForIDs(ctx context.Context, ids []string) (map[string][]string, error)
}

func compositeCollectionDeleteGuardForObject(ctx context.Context, req *dialect.Request, metadata contract.Metadata, objectName string) *dialect.Response {
	schema, lookupResp := resolveRecordMutationSchema(ctx, req, metadata, objectName)
	if lookupResp != nil {
		return lookupResp
	}
	return recordMutationDenialResponse(recordMutationDenial(schema, models.RecordMutationSurfaceCollectionDelete, nil), wire.SFCodeInvalidFieldForInsertUpdate)
}

func CompositeCollectionsCreateNeutralHandler(ctx context.Context, req *dialect.Request, deps dialect.Deps) (*dialect.Response, error) {
	records := deps.Contracts.Records
	metadata := deps.Contracts.Metadata
	if records == nil || metadata == nil {
		return internalErrorResponse("records and metadata contracts unavailable"), nil
	}
	body, resp := decodeCompositeCollectionRequest(req)
	if resp != nil {
		return resp, nil
	}
	if resp := validateCompositeCollectionRecordLimit(body.Records); resp != nil {
		return resp, nil
	}
	for _, record := range body.Records {
		denial, lookupResp := recordMutationDenialForObject(ctx, req, metadata, compositeRecordObjectName(record), models.RecordMutationSurfaceCollectionCreate, record)
		if lookupResp != nil {
			return lookupResp, nil
		}
		if resp := recordMutationDenialResponse(denial, wire.SFCodeInvalidFieldForInsertUpdate); resp != nil {
			return resp, nil
		}
	}

	results, err := records.CreateRecords(ctx, contractRecordsFromV63(body.Records), body.AllOrNone, req.User)
	if err != nil {
		return writeErrorResponse(req, err, wire.SFCodeInvalidRequest), nil
	}
	return &dialect.Response{Status: http.StatusOK, Body: recordMutationResultsToV63(results, req.Version)}, nil
}

func CompositeCollectionsUpdateNeutralHandler(ctx context.Context, req *dialect.Request, deps dialect.Deps) (*dialect.Response, error) {
	records := deps.Contracts.Records
	metadata := deps.Contracts.Metadata
	if records == nil || metadata == nil {
		return internalErrorResponse("records and metadata contracts unavailable"), nil
	}
	body, resp := decodeCompositeCollectionRequest(req)
	if resp != nil {
		return resp, nil
	}
	if resp := validateCompositeCollectionRecordLimit(body.Records); resp != nil {
		return resp, nil
	}
	for _, record := range body.Records {
		schema, lookupResp := resolveRecordMutationSchema(ctx, req, metadata, compositeRecordObjectName(record))
		if lookupResp != nil {
			return lookupResp, nil
		}
		denial, err := recordMutationDenialForExistingID(ctx, records, deps.Contracts.Query, schema, models.RecordMutationSurfaceCollectionUpdate, compositeRecordID(record), record, req.User)
		if err != nil {
			return writeErrorResponse(req, err, wire.SFCodeInvalidFieldForInsertUpdate), nil
		}
		if resp := recordMutationDenialResponse(denial, wire.SFCodeInvalidFieldForInsertUpdate); resp != nil {
			return resp, nil
		}
	}

	results, err := records.UpdateRecords(ctx, contractRecordsFromV63(body.Records), body.AllOrNone, req.User)
	if err != nil {
		return writeErrorResponse(req, err, wire.SFCodeInvalidRequest), nil
	}
	return &dialect.Response{Status: http.StatusOK, Body: recordMutationResultsToV63(results, req.Version)}, nil
}

func CompositeCollectionsDeleteNeutralHandler(ctx context.Context, req *dialect.Request, deps dialect.Deps) (*dialect.Response, error) {
	records := deps.Contracts.Records
	metadata := deps.Contracts.Metadata
	if records == nil || metadata == nil {
		return internalErrorResponse("records and metadata contracts unavailable"), nil
	}
	idsParam := queryValue(req, "ids")
	allOrNone := queryValue(req, "allOrNone") == "true"
	if idsParam == "" {
		return &dialect.Response{Status: http.StatusBadRequest, Body: wire.SFErrorResponse(wire.SFCodeInvalidRequest, "Missing ids parameter")}, nil
	}
	ids := strings.Split(idsParam, ",")
	if len(ids) > 200 {
		return &dialect.Response{Status: http.StatusBadRequest, Body: wire.SFErrorResponse(wire.SFCodeLimitExceeded, "Maximum 200 IDs allowed")}, nil
	}

	if resolver, ok := records.(objectTypeCandidateResolver); ok {
		candidatesByID, err := resolver.ObjectTypeCandidatesForIDs(ctx, ids)
		if err != nil {
			return writeErrorResponse(req, err, wire.SFCodeInvalidRequest), nil
		}
		for _, id := range ids {
			for _, objectName := range candidatesByID[id] {
				if resp := compositeCollectionDeleteGuardForObject(ctx, req, metadata, objectName); resp != nil {
					return resp, nil
				}
			}
		}
	} else {
		for _, id := range ids {
			objectName, err := records.ObjectTypeForID(ctx, id)
			if err != nil {
				// Only infrastructure/probe failures hard-stop; missing/business
				// failures must fall through to DeleteRecords so the per-record
				// collection contract stays byte-identical.
				if sharederrors.IsInternal(err) {
					return writeErrorResponse(req, err, wire.SFCodeInvalidRequest), nil
				}
				continue
			}
			if resp := compositeCollectionDeleteGuardForObject(ctx, req, metadata, objectName); resp != nil {
				return resp, nil
			}
		}
	}
	results, err := records.DeleteRecords(ctx, ids, allOrNone, req.User)
	if err != nil {
		return writeErrorResponse(req, err, wire.SFCodeInvalidRequest), nil
	}
	return &dialect.Response{Status: http.StatusOK, Body: recordMutationResultsToV63(results, req.Version)}, nil
}

func CompositeCollectionsRetrieveNeutralHandler(ctx context.Context, req *dialect.Request, deps dialect.Deps) (*dialect.Response, error) {
	records := deps.Contracts.Records
	if records == nil {
		return internalErrorResponse("records contract unavailable"), nil
	}
	idsParam := queryValue(req, "ids")
	fieldsParam := queryValue(req, "fields")
	if idsParam == "" {
		return &dialect.Response{Status: http.StatusBadRequest, Body: wire.SFErrorResponse(wire.SFCodeInvalidRequest, "Missing ids parameter")}, nil
	}
	ids := strings.Split(idsParam, ",")
	if len(ids) > 200 {
		return &dialect.Response{Status: http.StatusBadRequest, Body: wire.SFErrorResponse(wire.SFCodeLimitExceeded, "Maximum 200 IDs allowed")}, nil
	}
	var fields []string
	if fieldsParam != "" {
		fields = strings.Split(fieldsParam, ",")
	}

	results, err := records.RetrieveRecords(ctx, "", ids, fields, req.User)
	if err != nil {
		return writeErrorResponse(req, err, wire.SFCodeInvalidRequest), nil
	}
	wireResults := recordsToV63SObjects(results)
	for i := range wireResults {
		stampCompositeRetrieveRecordURL(wireResults[i], ids[i], req.Version)
	}
	return &dialect.Response{Status: http.StatusOK, Body: wireResults}, nil
}

func CompositeCollectionsUpsertNeutralHandler(ctx context.Context, req *dialect.Request, deps dialect.Deps) (*dialect.Response, error) {
	records := deps.Contracts.Records
	metadata := deps.Contracts.Metadata
	if records == nil || metadata == nil {
		return internalErrorResponse("records and metadata contracts unavailable"), nil
	}
	objectName := strings.TrimSpace(req.Params["sObjectName"])
	externalIDField := strings.TrimSpace(req.Params["externalIdFieldName"])
	if objectName == "" || externalIDField == "" {
		return &dialect.Response{Status: http.StatusBadRequest, Body: wire.SFErrorResponse(wire.SFCodeInvalidRequest, "sObjectName and externalIdFieldName are required")}, nil
	}
	routeSchema, lookupResp := resolveRecordMutationSchema(ctx, req, metadata, objectName)
	if lookupResp != nil {
		return lookupResp, nil
	}
	if resp := recordMutationDenialResponse(recordMutationDenial(routeSchema, models.RecordMutationSurfaceCollectionUpsert, nil), wire.SFCodeInvalidFieldForInsertUpdate); resp != nil {
		return resp, nil
	}

	body, resp := decodeCompositeCollectionRequest(req)
	if resp != nil {
		return resp, nil
	}
	if resp := validateCompositeCollectionRecordLimit(body.Records); resp != nil {
		return resp, nil
	}
	for _, record := range body.Records {
		recordObjectName := compositeRecordObjectName(record)
		if recordObjectName == "" {
			recordObjectName = objectName
		}
		denial, lookupResp := recordMutationDenialForObject(ctx, req, metadata, recordObjectName, models.RecordMutationSurfaceCollectionUpsert, record)
		if lookupResp != nil {
			return lookupResp, nil
		}
		if resp := recordMutationDenialResponse(denial, wire.SFCodeInvalidFieldForInsertUpdate); resp != nil {
			return resp, nil
		}
	}
	for _, record := range body.Records {
		recordObjectName := compositeRecordObjectName(record)
		if recordObjectName == "" {
			recordObjectName = objectName
		}
		schema, lookupResp := resolveRecordMutationSchema(ctx, req, metadata, recordObjectName)
		if lookupResp != nil {
			return lookupResp, nil
		}
		fieldValue := strings.TrimSpace(fmt.Sprint(record[externalIDField]))
		denial, err := recordMutationDenialForExistingExternalID(ctx, records, deps.Contracts.Query, schema, models.RecordMutationSurfaceCollectionUpsert, externalIDField, fieldValue, record, req.User)
		if err != nil {
			return writeErrorResponse(req, err, wire.SFCodeInvalidFieldForInsertUpdate), nil
		}
		if resp := recordMutationDenialResponse(denial, wire.SFCodeInvalidFieldForInsertUpdate); resp != nil {
			return resp, nil
		}
	}

	results, err := records.UpsertRecords(ctx, objectName, externalIDField, contractRecordsFromV63(body.Records), body.AllOrNone, req.User)
	if err != nil {
		return writeErrorResponse(req, err, wire.SFCodeInvalidRequest), nil
	}
	return &dialect.Response{Status: http.StatusOK, Body: recordMutationResultsToV63(results, req.Version)}, nil
}

func decodeCompositeCollectionRequest(req *dialect.Request) (compositeCollectionRequest, *dialect.Response) {
	var body compositeCollectionRequest
	if err := json.NewDecoder(bytes.NewReader(req.Body)).Decode(&body); err != nil {
		return body, &dialect.Response{Status: http.StatusBadRequest, Body: wire.SFErrorResponse(wire.SFCodeInvalidRequest, "Invalid request body")}
	}
	return body, nil
}

func validateCompositeCollectionRecordLimit(records []models.SObject) *dialect.Response {
	if len(records) <= 200 {
		return nil
	}
	return &dialect.Response{Status: http.StatusBadRequest, Body: wire.SFErrorResponse(wire.SFCodeLimitExceeded, "Maximum 200 records allowed")}
}

func compositeRecordObjectName(record models.SObject) string {
	if record == nil {
		return ""
	}
	if raw, ok := record["attributes"]; ok {
		if attrs, ok := raw.(map[string]interface{}); ok {
			if objectName, ok := attrs["type"].(string); ok {
				return strings.TrimSpace(objectName)
			}
		}
	}
	if objectName, ok := record["attributes.type"].(string); ok {
		return strings.TrimSpace(objectName)
	}
	return ""
}

func compositeRecordID(record models.SObject) string {
	if record == nil {
		return ""
	}
	return firstCompositeNonEmptyString(record[constants.FieldID], record["id"])
}

func firstCompositeNonEmptyString(values ...interface{}) string {
	for _, value := range values {
		if value == nil {
			continue
		}
		resolved := strings.TrimSpace(fmt.Sprint(value))
		if resolved != "" && resolved != "<nil>" {
			return resolved
		}
	}
	return ""
}

func contractRecordsFromV63(records []models.SObject) []contract.Record {
	if records == nil {
		return nil
	}
	out := make([]contract.Record, 0, len(records))
	for _, record := range records {
		out = append(out, contractRecordFromV63(record))
	}
	return out
}

func contractRecordFromV63(record models.SObject) contract.Record {
	if record == nil {
		return nil
	}
	out := make(contract.Record, len(record))
	for key, value := range record {
		if models.IsRecordMetadataKey(key) {
			continue
		}
		out[key] = value
	}
	if objectType := models.RecordObjectType(record); objectType != "" {
		out[models.ObjectTypeMetadataKey] = objectType
	}
	return out
}

func stampCompositeRetrieveRecordURL(record models.SObject, fallbackID, apiVersion string) {
	if record == nil {
		return
	}
	attrs, _ := record["attributes"].(map[string]interface{})
	if attrs == nil {
		return
	}
	objectName, _ := attrs["type"].(string)
	if objectName == "" {
		return
	}
	id, _ := record[constants.FieldID].(string)
	if id == "" {
		id = fallbackID
	}
	if id == "" {
		return
	}
	attrs["url"] = fmt.Sprintf("%s/%s/%s", versionedSObjectsBasePath(apiVersion), objectName, id)
}

func recordMutationResultsToV63(results []contract.RecordMutationResult, apiVersion string) []models.SObject {
	out := make([]models.SObject, 0, len(results))
	for _, result := range results {
		out = append(out, recordMutationResultToV63(result, apiVersion))
	}
	return out
}

func recordMutationResultToV63(result contract.RecordMutationResult, apiVersion string) models.SObject {
	entry := models.SObject{
		"id":      result.ID,
		"success": result.Success,
		"errors":  recordMutationErrorsToV63(result.Errors, apiVersion),
	}
	if result.Created != nil {
		entry["created"] = *result.Created
	}
	return entry
}

func recordMutationErrorsToV63(errors []contract.RecordMutationError, apiVersion string) []interface{} {
	out := make([]interface{}, 0, len(errors))
	for _, err := range errors {
		fields := make([]interface{}, 0, len(err.Fields))
		for _, field := range err.Fields {
			fields = append(fields, field)
		}
		message := err.Message
		if err.CreateFromSourceGuard != nil {
			message = wire.CreateFromSourceGuardMessageV63(err.CreateFromSourceGuard, apiVersion)
		} else if message == "" {
			// The L0 engine is app-blind: for kinds whose user-facing text is a
			// fixed Salesforce string (the allOrNone rollback marker) it emits an
			// empty Message and this dialect renders the SF English from Kind.
			message = recordMutationErrorMessageV63(err.Kind)
		}
		out = append(out, map[string]interface{}{
			"message":    message,
			"statusCode": recordMutationErrorStatusCodeV63(err.Kind),
			"fields":     fields,
		})
	}
	return out
}

func recordMutationErrorStatusCodeV63(kind contract.RecordMutationErrorKind) string {
	switch kind {
	case contract.RecordMutationErrorAllOrNoneRolledBack:
		return wire.SFCodeAllOrNoneOperationRolledBack
	case contract.RecordMutationErrorInternal:
		return wire.SFCodeInternalError
	case contract.RecordMutationErrorInvalidFieldForUpdate:
		return wire.SFCodeInvalidFieldForInsertUpdate
	case contract.RecordMutationErrorCustomValidation:
		return wire.SFCodeFieldCustomValidationException
	case contract.RecordMutationErrorInvalidCrossReference:
		return wire.SFCodeInvalidCrossReferenceKey
	case contract.RecordMutationErrorFieldIntegrity:
		return wire.SFCodeFieldIntegrityException
	case contract.RecordMutationErrorInvalidOperation:
		return wire.SFCodeInvalidOperation
	case contract.RecordMutationErrorInvalidField:
		return wire.SFCodeInvalidField
	case contract.RecordMutationErrorNotFound:
		return wire.SFCodeNotFound
	case contract.RecordMutationErrorMalformedID:
		return "MALFORMED_ID"
	case contract.RecordMutationErrorInvalidID:
		return "INVALID_ID"
	default:
		return wire.SFCodeProcessingHalted
	}
}

// sfAllOrNoneRolledBackMessageV63 is the fixed Salesforce v63 English rendered
// for a record that would have committed but was rolled back because the request
// used the allOrNone header and a sibling failed. The engine is app-blind and
// emits an empty Message for this kind; this dialect owns the SF spelling.
const sfAllOrNoneRolledBackMessageV63 = "Record rolled back because not all records were valid and the request was using AllOrNone header"

// recordMutationErrorMessageV63 renders the Salesforce v63 English for kinds
// whose user-facing text is a fixed SF string the engine no longer carries.
// It is only consulted when the engine emitted an empty Message (see
// recordMutationErrorsToV63); kinds that always carry a caller-specific message
// return "" here and keep the engine's own text.
func recordMutationErrorMessageV63(kind contract.RecordMutationErrorKind) string {
	switch kind {
	case contract.RecordMutationErrorAllOrNoneRolledBack:
		return sfAllOrNoneRolledBackMessageV63
	default:
		return ""
	}
}
