package sfcrmappservice

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"log"
	"net/http"
	"strconv"
	"strings"
	"time"
	"unicode"

	"github.com/pingcap-inc/nexuscrm-api-host/runtimegrpc"
	scimdialect "github.com/pingcap-inc/nexuscrm-app-packs/sfcrm/scim"
	authorizationsemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/authorizationv1"
	querysemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/queryv1"
	mutationsemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/recordmutationv1"
	sessionsemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/sessionv1"
)

const (
	scimBasePath                 = "/scim/v2"
	scimContentType              = "application/scim+json"
	scimErrorSchema              = "urn:ietf:params:scim:api:messages:2.0:Error"
	scimUserObject               = "User"
	scimProfile                  = "Profile"
	scimPolicy                   = "IdentitySyncSetting"
	scimTicket                   = "IdentitySyncDeactivationTicket"
	scimUserRoleID               = "UserRoleId"
	scimRuntimeAccessTokenMaxTTL = 24 * time.Hour
	scimAccessTokenRevokeTries   = 3
	scimAccessTokenRetryDelay    = 25 * time.Millisecond
)

var scimUserFields = []string{
	"Id", scimdialect.FieldUsername, scimdialect.FieldEmail,
	scimdialect.FieldFirstName, scimdialect.FieldLastName,
	scimdialect.FieldIsActive, scimdialect.FieldTitle,
	scimdialect.FieldPhone, scimdialect.FieldMobilePhone,
	scimdialect.FieldDepartment,
}

func (s *Service) serveSCIM(writer http.ResponseWriter, request *http.Request) bool {
	if request.URL.Path != scimBasePath && !strings.HasPrefix(request.URL.Path, scimBasePath+"/") {
		return false
	}
	clientID, clientSecret, ok := scimConnectedAppCredential(request.Header.Values("Authorization"))
	if !ok {
		writer.Header().Set("WWW-Authenticate", `Bearer realm="NexusCRM SCIM"`)
		writeSCIMError(writer, http.StatusUnauthorized, "invalid connected-app credential", "")
		return true
	}
	if s.draining.Load() {
		writeSCIMError(writer, http.StatusServiceUnavailable, "SCIM service is draining", "")
		return true
	}
	accessToken, accessTokenDeadline, cleanup, authenticated := s.authenticateSCIM(writer, request, clientID, clientSecret)
	if !authenticated {
		return true
	}
	defer cleanup()
	switch request.URL.Path {
	case scimBasePath + "/ServiceProviderConfig":
		if !scimRequireMethod(writer, request, http.MethodGet) {
			return true
		}
		writeSCIMJSON(writer, http.StatusOK, map[string]any{
			"schemas":        []string{"urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig"},
			"patch":          map[string]any{"supported": true},
			"bulk":           map[string]any{"supported": false, "maxOperations": 0, "maxPayloadSize": 0},
			"filter":         map[string]any{"supported": true, "maxResults": 500},
			"changePassword": map[string]any{"supported": false},
			"sort":           map[string]any{"supported": false},
			"etag":           map[string]any{"supported": false},
			"authenticationSchemes": []map[string]any{{
				"type": "oauthbearertoken", "name": "NexusCRM connected-app credential",
				"description": "Bearer <client_id>.<client_secret>", "primary": true,
			}},
		})
	case scimBasePath + "/ResourceTypes":
		if !scimRequireMethod(writer, request, http.MethodGet) {
			return true
		}
		writeSCIMJSON(writer, http.StatusOK, map[string]any{
			"schemas":      []string{"urn:ietf:params:scim:api:messages:2.0:ListResponse"},
			"totalResults": 1,
			"startIndex":   1,
			"itemsPerPage": 1,
			"Resources": []map[string]any{{
				"id": "User", "name": "User", "endpoint": "/Users",
				"schema": "urn:ietf:params:scim:schemas:core:2.0:User",
				"schemaExtensions": []map[string]any{{
					"schema":   "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User",
					"required": false,
				}},
			}},
		})
	case scimBasePath + "/Schemas":
		if !scimRequireMethod(writer, request, http.MethodGet) {
			return true
		}
		writeSCIMJSON(writer, http.StatusOK, map[string]any{
			"schemas":      []string{"urn:ietf:params:scim:api:messages:2.0:ListResponse"},
			"totalResults": 2,
			"startIndex":   1,
			"itemsPerPage": 2,
			"Resources": []map[string]any{
				{"id": "urn:ietf:params:scim:schemas:core:2.0:User", "name": "User"},
				{"id": "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User", "name": "EnterpriseUser"},
			},
		})
	default:
		if request.URL.Path == scimBasePath+"/Users" || strings.HasPrefix(request.URL.Path, scimBasePath+"/Users/") {
			s.serveSCIMUsers(writer, request, accessToken, accessTokenDeadline)
			return true
		}
		writeSCIMError(writer, http.StatusNotFound, "SCIM resource not found", "")
	}
	return true
}

func scimRequireMethod(writer http.ResponseWriter, request *http.Request, method string) bool {
	if request.Method == method {
		return true
	}
	writer.Header().Set("Allow", method)
	writeSCIMError(writer, http.StatusMethodNotAllowed, "method not allowed", "")
	return false
}

type scimTenantPolicy struct {
	defaultProfileName string
	defaultProfileID   string
	serviceUsername    string
	serviceProfileName string
	serviceProfileID   string
	exempt             map[string]struct{}
	timeZone           string
	locale             string
	emailEncoding      string
	language           string
	deactivationLimit  int
	deactivationWindow time.Duration
	tokenTTL           time.Duration
}

type scimRequestAuthority struct {
	principal sessionsemantic.Principal
	policy    scimTenantPolicy
}

type scimHTTPError struct {
	status   int
	detail   string
	scimType string
}

func (e *scimHTTPError) Error() string { return e.detail }

func (s *Service) serveSCIMUsers(writer http.ResponseWriter, request *http.Request, bearer string, accessTokenDeadline time.Time) {
	if s.runtimeQuery == nil || s.mutation == nil || s.sessions == nil || s.sessions.read == nil {
		writeSCIMError(writer, http.StatusServiceUnavailable, "SCIM Runtime composition is unavailable", "")
		return
	}
	ctx, cancel := context.WithTimeout(request.Context(), s.requestTimeout)
	defer cancel()
	authority, err := s.scimRequestAuthority(ctx, bearer, accessTokenDeadline)
	if err != nil {
		s.writeSCIMFailure(writer, err)
		return
	}
	userID := strings.TrimPrefix(request.URL.Path, scimBasePath+"/Users")
	if userID != "" {
		if !strings.HasPrefix(userID, "/") || strings.Contains(strings.TrimPrefix(userID, "/"), "/") {
			writeSCIMError(writer, http.StatusNotFound, "SCIM User resource not found", "")
			return
		}
		userID = strings.TrimPrefix(userID, "/")
		if strings.TrimSpace(userID) == "" {
			writeSCIMError(writer, http.StatusNotFound, "SCIM User resource not found", "")
			return
		}
	}
	switch request.Method {
	case http.MethodGet:
		if userID == "" {
			s.scimListUsers(ctx, writer, request, bearer)
			return
		}
		record, err := s.scimReadUser(ctx, bearer, userID)
		if err != nil {
			s.writeSCIMFailure(writer, err)
			return
		}
		writeSCIMJSON(writer, http.StatusOK, scimUserResource(record))
	case http.MethodPost:
		if userID != "" {
			writeSCIMError(writer, http.StatusMethodNotAllowed, "POST is only supported on /Users", "")
			return
		}
		s.scimCreateUser(ctx, writer, request, bearer, authority)
	case http.MethodPut:
		if userID == "" {
			writeSCIMError(writer, http.StatusMethodNotAllowed, "PUT requires a User id", "")
			return
		}
		s.scimReplaceUser(ctx, writer, request, bearer, authority, userID)
	case http.MethodPatch:
		if userID == "" {
			writeSCIMError(writer, http.StatusMethodNotAllowed, "PATCH requires a User id", "")
			return
		}
		s.scimPatchUser(ctx, writer, request, bearer, authority, userID)
	case http.MethodDelete:
		if userID == "" {
			writeSCIMError(writer, http.StatusMethodNotAllowed, "DELETE requires a User id", "")
			return
		}
		s.scimDeactivateUser(ctx, writer, bearer, authority, userID)
	default:
		writer.Header().Set("Allow", "GET, POST, PUT, PATCH, DELETE")
		writeSCIMError(writer, http.StatusMethodNotAllowed, "method not allowed", "")
	}
}

func (s *Service) scimRequestAuthority(ctx context.Context, bearer string, accessTokenDeadline time.Time) (scimRequestAuthority, error) {
	readRequest := sessionsemantic.ReadRequest{}
	response, issued, err := s.sessions.read(ctx,
		runtimegrpc.SessionPresentedCredentials{AccessToken: bearer},
		readRequest,
	)
	if err != nil {
		return scimRequestAuthority{}, err
	}
	if response.ReplacementAccessTokenIssued || issued.AccessToken != "" {
		if issued.AccessToken != "" {
			_ = s.scimRevokeAccessToken(issued.AccessToken)
		}
		return scimRequestAuthority{}, &scimHTTPError{status: http.StatusServiceUnavailable, detail: "Runtime attempted to rotate a non-renewable SCIM access token"}
	}
	if err := sessionsemantic.ValidateReadResponse(readRequest, response); err != nil {
		return scimRequestAuthority{}, &scimHTTPError{status: http.StatusServiceUnavailable, detail: "Runtime returned an invalid SCIM principal projection"}
	}
	policy, err := s.scimLoadPolicy(ctx, bearer)
	if err != nil {
		return scimRequestAuthority{}, err
	}
	maxTokenTTL := policy.tokenTTL
	if maxTokenTTL > scimRuntimeAccessTokenMaxTTL {
		maxTokenTTL = scimRuntimeAccessTokenMaxTTL
	}
	expiresAt := time.Unix(response.Session.ExpiresAtUnixSecond, 0)
	if maxTokenTTL <= 0 || accessTokenDeadline.IsZero() || !expiresAt.After(time.Now()) ||
		expiresAt.After(accessTokenDeadline) || expiresAt.After(time.Now().Add(maxTokenTTL)) {
		return scimRequestAuthority{}, &scimHTTPError{status: http.StatusServiceUnavailable, detail: "Runtime returned a SCIM access token outside the configured lifetime bound"}
	}
	principal := response.Session.Principal
	if !strings.EqualFold(strings.TrimSpace(principal.Email), policy.serviceUsername) || principal.ProfileID != policy.serviceProfileID {
		return scimRequestAuthority{}, &scimHTTPError{status: http.StatusForbidden, detail: "connected app is not bound to the configured SCIM service principal"}
	}
	return scimRequestAuthority{principal: principal, policy: policy}, nil
}

func (s *Service) scimLoadPolicy(ctx context.Context, bearer string) (scimTenantPolicy, error) {
	records, _, err := s.scimQuery(ctx, bearer, scimPolicy, []string{
		"Name", "DefaultProfileName", "ServiceProfileName", "ServiceUsername", "ExemptPrincipals",
		"TimeZoneSidKey", "LocaleSidKey", "EmailEncodingKey", "LanguageLocaleKey",
		"BreakerFailureThreshold", "BreakerCooldownSeconds", "TokenTtlHours",
	}, stringComparison("Name", querysemantic.ComparisonEqual, "default"), 2, 0)
	if err != nil {
		return scimTenantPolicy{}, err
	}
	if len(records) != 1 {
		return scimTenantPolicy{}, &scimHTTPError{status: http.StatusServiceUnavailable, detail: "IdentitySyncSetting default policy is missing or ambiguous"}
	}
	record := records[0]
	policy := scimTenantPolicy{
		defaultProfileName: scimString(record, "DefaultProfileName"),
		serviceProfileName: scimString(record, "ServiceProfileName"),
		serviceUsername:    strings.ToLower(scimString(record, "ServiceUsername")),
		exempt:             make(map[string]struct{}),
		timeZone:           scimString(record, "TimeZoneSidKey"), locale: scimString(record, "LocaleSidKey"),
		emailEncoding: scimString(record, "EmailEncodingKey"), language: scimString(record, "LanguageLocaleKey"),
	}
	threshold, thresholdOK := scimInteger(record, "BreakerFailureThreshold")
	cooldownSeconds, cooldownOK := scimInteger(record, "BreakerCooldownSeconds")
	tokenTTLHours, tokenTTLOK := scimInteger(record, "TokenTtlHours")
	if !thresholdOK || threshold <= 0 || threshold > 100 || !cooldownOK || cooldownSeconds <= 0 || cooldownSeconds > 86400 ||
		!tokenTTLOK || tokenTTLHours <= 0 || tokenTTLHours > 87600 {
		return scimTenantPolicy{}, &scimHTTPError{status: http.StatusServiceUnavailable, detail: "IdentitySyncSetting safety bounds are invalid"}
	}
	policy.deactivationLimit = int(threshold)
	policy.deactivationWindow = time.Duration(cooldownSeconds) * time.Second
	policy.tokenTTL = time.Duration(tokenTTLHours) * time.Hour
	for _, value := range strings.FieldsFunc(scimString(record, "ExemptPrincipals"), func(r rune) bool {
		return r == ',' || r == ';' || r == '\n' || r == '\r'
	}) {
		if normalized := strings.ToLower(strings.TrimSpace(value)); normalized != "" {
			policy.exempt[normalized] = struct{}{}
		}
	}
	if policy.defaultProfileName == "" || policy.serviceProfileName == "" || policy.serviceUsername == "" {
		return scimTenantPolicy{}, &scimHTTPError{status: http.StatusServiceUnavailable, detail: "IdentitySyncSetting default policy is incomplete"}
	}
	policy.defaultProfileID, err = s.scimProfileID(ctx, bearer, policy.defaultProfileName)
	if err != nil {
		return scimTenantPolicy{}, err
	}
	policy.serviceProfileID, err = s.scimProfileID(ctx, bearer, policy.serviceProfileName)
	if err != nil {
		return scimTenantPolicy{}, err
	}
	if policy.defaultProfileID == policy.serviceProfileID {
		return scimTenantPolicy{}, &scimHTTPError{status: http.StatusServiceUnavailable, detail: "SCIM onboarding and service profiles must be distinct"}
	}
	return policy, nil
}

func (s *Service) scimProfileID(ctx context.Context, bearer, name string) (string, error) {
	records, _, err := s.scimQuery(ctx, bearer, scimProfile, []string{"Id", "Name"}, stringComparison("Name", querysemantic.ComparisonEqual, name), 2, 0)
	if err != nil {
		return "", err
	}
	if len(records) != 1 || scimString(records[0], "Id") == "" {
		return "", &scimHTTPError{status: http.StatusServiceUnavailable, detail: "configured SCIM profile is missing or ambiguous"}
	}
	return scimString(records[0], "Id"), nil
}

func (s *Service) scimQuery(ctx context.Context, bearer, object string, fields []string, filter querysemantic.Predicate, limit uint32, offset uint64) ([]map[string]any, uint64, error) {
	request := querysemantic.Request{ObjectID: object, Selections: fieldSelections(fields), Filter: filter, Limit: limit, Offset: offset}
	if err := querysemantic.ValidateRequest(ctx, request); err != nil {
		return nil, 0, err
	}
	response, err := s.runtimeQuery(ctx, bearer, request)
	if err != nil {
		return nil, 0, err
	}
	if err := querysemantic.ValidateResponse(response); err != nil {
		return nil, 0, err
	}
	result := make([]map[string]any, 0, len(response.Records))
	for _, record := range response.Records {
		mapped, err := queryRecordFromSemantic(record)
		if err != nil {
			return nil, 0, err
		}
		result = append(result, mapped.Fields)
	}
	return result, response.TotalSize, nil
}

func (s *Service) scimReadUser(ctx context.Context, bearer, id string) (map[string]any, error) {
	records, _, err := s.scimQuery(ctx, bearer, scimUserObject, scimUserFields, stringComparison("Id", querysemantic.ComparisonEqual, id), 2, 0)
	if err != nil {
		return nil, err
	}
	if len(records) != 1 {
		return nil, &scimHTTPError{status: http.StatusNotFound, detail: "User not found"}
	}
	return records[0], nil
}

func (s *Service) scimListUsers(ctx context.Context, writer http.ResponseWriter, request *http.Request, bearer string) {
	startIndex, err := scimPositiveQueryInt(request, "startIndex", 1, 1, int(querysemantic.MaxOffset)+1)
	if err != nil {
		s.writeSCIMFailure(writer, err)
		return
	}
	count, err := scimPositiveQueryInt(request, "count", 100, 0, 500)
	if err != nil {
		s.writeSCIMFailure(writer, err)
		return
	}
	filter := querysemantic.Predicate{}
	if rawFilter := request.URL.Query().Get("filter"); strings.TrimSpace(rawFilter) != "" {
		field, value, present, parseErr := scimdialect.ParseUserLookupFilter(rawFilter)
		if parseErr != nil {
			s.writeSCIMFailure(writer, parseErr)
			return
		}
		if present {
			filter = stringComparison(field, querysemantic.ComparisonEqual, value)
		}
	}
	runtimeLimit := count
	if runtimeLimit == 0 {
		runtimeLimit = 1 // Runtime limit zero means its default, not SCIM count=0.
	}
	records, total, err := s.scimQuery(ctx, bearer, scimUserObject, scimUserFields, filter, uint32(runtimeLimit), uint64(startIndex-1))
	if err != nil {
		s.writeSCIMFailure(writer, err)
		return
	}
	if count == 0 {
		records = nil
	}
	resources := make([]map[string]any, 0, len(records))
	for _, record := range records {
		resources = append(resources, scimUserResource(record))
	}
	writeSCIMJSON(writer, http.StatusOK, map[string]any{
		"schemas": []string{scimdialect.SchemaListResp}, "totalResults": total,
		"startIndex": startIndex, "itemsPerPage": len(resources), "Resources": resources,
	})
}

func scimPositiveQueryInt(request *http.Request, name string, fallback, minimum, maximum int) (int, error) {
	values, present := request.URL.Query()[name]
	if !present {
		return fallback, nil
	}
	if len(values) != 1 {
		return 0, &scimHTTPError{status: http.StatusBadRequest, detail: "duplicate " + name + " parameter", scimType: "invalidValue"}
	}
	value, err := strconv.Atoi(values[0])
	if err != nil || value < minimum || value > maximum {
		return 0, &scimHTTPError{status: http.StatusBadRequest, detail: name + " is outside the supported range", scimType: "invalidValue"}
	}
	return value, nil
}

func (s *Service) scimCreateUser(ctx context.Context, writer http.ResponseWriter, request *http.Request, bearer string, authority scimRequestAuthority) {
	body, err := s.scimRequestBody(writer, request)
	if err != nil {
		s.writeSCIMFailure(writer, err)
		return
	}
	parsed, err := scimdialect.ParseUser(body)
	if err != nil {
		s.writeSCIMFailure(writer, err)
		return
	}
	mapped, err := parsed.MapUserIgnoringManager(false)
	if err != nil {
		s.writeSCIMFailure(writer, err)
		return
	}
	username := scimString(mapped.Fields, scimdialect.FieldUsername)
	email := scimString(mapped.Fields, scimdialect.FieldEmail)
	existing, err := s.scimFindUserByIdentity(ctx, bearer, username, email)
	if err != nil {
		s.writeSCIMFailure(writer, err)
		return
	}
	if existing != nil {
		id := scimString(existing, "Id")
		if err := s.scimApplyUpdate(ctx, bearer, authority, id, existing, mapped); err != nil {
			s.writeSCIMFailure(writer, err)
			return
		}
		record, err := s.scimReadUser(ctx, bearer, id)
		if err != nil {
			s.writeSCIMFailure(writer, err)
			return
		}
		writeSCIMJSON(writer, http.StatusOK, scimUserResource(record))
		return
	}
	fields := make(map[string]any, len(mapped.Fields)+8)
	for key, value := range mapped.Fields {
		fields[key] = value
	}
	// Manager is intentionally outside the Helix -> NexusCRM contract. The
	// shared dialect supports it for other SCIM consumers, and PUT replacement
	// represents an omitted manager as an explicit clear; discard that write so
	// Helix can neither set nor erase CRM-owned manager data.
	delete(fields, scimdialect.FieldManagerID)
	fields[scimdialect.FieldProfileID] = authority.policy.defaultProfileID
	fields[scimUserRoleID] = nil
	fields[scimdialect.FieldAlias] = scimAlias(username)
	fields[scimdialect.FieldIsActive] = true
	if mapped.Active != nil {
		fields[scimdialect.FieldIsActive] = *mapped.Active
	}
	for field, value := range map[string]string{
		scimdialect.FieldTimeZoneSidKey:   authority.policy.timeZone,
		scimdialect.FieldLocaleSidKey:     authority.policy.locale,
		scimdialect.FieldEmailEncodingKey: authority.policy.emailEncoding,
		scimdialect.FieldLanguageLocale:   authority.policy.language,
	} {
		if value != "" {
			fields[field] = value
		}
	}
	id, err := s.scimMutate(ctx, bearer, mutationsemantic.OperationCreate, "", fields)
	if err != nil {
		s.writeSCIMFailure(writer, err)
		return
	}
	record, err := s.scimReadUser(ctx, bearer, id)
	if err != nil {
		s.writeSCIMFailure(writer, err)
		return
	}
	writer.Header().Set("Location", scimBasePath+"/Users/"+id)
	writeSCIMJSON(writer, http.StatusCreated, scimUserResource(record))
}

func (s *Service) scimReplaceUser(ctx context.Context, writer http.ResponseWriter, request *http.Request, bearer string, authority scimRequestAuthority, id string) {
	body, err := s.scimRequestBody(writer, request)
	if err != nil {
		s.writeSCIMFailure(writer, err)
		return
	}
	parsed, err := scimdialect.ParseUser(body)
	if err != nil {
		s.writeSCIMFailure(writer, err)
		return
	}
	mapped, err := parsed.MapUserIgnoringManager(true)
	if err != nil {
		s.writeSCIMFailure(writer, err)
		return
	}
	s.scimWriteUserUpdate(ctx, writer, bearer, authority, id, mapped)
}

func (s *Service) scimPatchUser(ctx context.Context, writer http.ResponseWriter, request *http.Request, bearer string, authority scimRequestAuthority, id string) {
	body, err := s.scimRequestBody(writer, request)
	if err != nil {
		s.writeSCIMFailure(writer, err)
		return
	}
	mapped, err := scimdialect.ApplyPatchIgnoringManager(body)
	if err != nil {
		s.writeSCIMFailure(writer, err)
		return
	}
	s.scimWriteUserUpdate(ctx, writer, bearer, authority, id, mapped)
}

func (s *Service) scimWriteUserUpdate(ctx context.Context, writer http.ResponseWriter, bearer string, authority scimRequestAuthority, id string, mapped *scimdialect.FieldMap) {
	current, err := s.scimReadUser(ctx, bearer, id)
	if err != nil {
		s.writeSCIMFailure(writer, err)
		return
	}
	if err := s.scimApplyUpdate(ctx, bearer, authority, id, current, mapped); err != nil {
		s.writeSCIMFailure(writer, err)
		return
	}
	record, err := s.scimReadUser(ctx, bearer, id)
	if err != nil {
		s.writeSCIMFailure(writer, err)
		return
	}
	writeSCIMJSON(writer, http.StatusOK, scimUserResource(record))
}

func (s *Service) scimApplyUpdate(ctx context.Context, bearer string, authority scimRequestAuthority, id string, current map[string]any, mapped *scimdialect.FieldMap) error {
	if mapped == nil {
		return &scimHTTPError{status: http.StatusBadRequest, detail: "empty SCIM mutation", scimType: "invalidValue"}
	}
	if err := s.scimValidateUpdateIdentity(ctx, bearer, id, mapped); err != nil {
		return err
	}
	fields := make(map[string]any, len(mapped.Fields)+1)
	for key, value := range mapped.Fields {
		fields[key] = value
	}
	if mapped.Active != nil {
		if !*mapped.Active {
			if err := scimGuardDeactivation(authority, id, current); err != nil {
				return err
			}
			if active, _ := scimBool(current, scimdialect.FieldIsActive); active {
				if err := s.scimClaimDeactivation(ctx, bearer, authority.policy, time.Now().UTC()); err != nil {
					return err
				}
			} else {
				delete(fields, scimdialect.FieldIsActive)
			}
		}
		if *mapped.Active {
			fields[scimdialect.FieldIsActive] = true
		} else if _, alreadyInactive := fields[scimdialect.FieldIsActive]; !alreadyInactive {
			// The active deactivation branch above installs the write after a
			// durable slot is claimed. An already-inactive User stays a no-op.
			if active, _ := scimBool(current, scimdialect.FieldIsActive); active {
				fields[scimdialect.FieldIsActive] = false
			}
		}
	}
	delete(fields, scimdialect.FieldProfileID)
	delete(fields, scimUserRoleID)
	delete(fields, scimdialect.FieldManagerID)
	if scimIsExempt(authority.policy, current) {
		for _, field := range []string{scimdialect.FieldUsername, scimdialect.FieldEmail} {
			value, changing := fields[field]
			if changing && !strings.EqualFold(strings.TrimSpace(fmt.Sprint(value)), scimString(current, field)) {
				return &scimHTTPError{status: http.StatusForbidden, detail: "identity of an exemption-listed User cannot be changed by SCIM", scimType: "mutability"}
			}
		}
	}
	if len(fields) == 0 {
		return nil
	}
	_, err := s.scimMutate(ctx, bearer, mutationsemantic.OperationUpdate, id, fields)
	return err
}

func (s *Service) scimValidateUpdateIdentity(ctx context.Context, bearer, targetID string, mapped *scimdialect.FieldMap) error {
	username, usernamePresent := mapped.Fields[scimdialect.FieldUsername]
	email, emailPresent := mapped.Fields[scimdialect.FieldEmail]
	if !usernamePresent && !emailPresent {
		return nil
	}
	usernameValue, emailValue := "", ""
	if usernamePresent {
		var ok bool
		usernameValue, ok = username.(string)
		if !ok || strings.TrimSpace(usernameValue) == "" {
			return &scimHTTPError{status: http.StatusBadRequest, detail: "SCIM userName update must be a non-empty string", scimType: "invalidValue"}
		}
	}
	if emailPresent {
		var ok bool
		emailValue, ok = email.(string)
		if !ok || strings.TrimSpace(emailValue) == "" {
			return &scimHTTPError{status: http.StatusBadRequest, detail: "SCIM email update must be a non-empty string", scimType: "invalidValue"}
		}
	}
	matched, err := s.scimFindUserByIdentity(ctx, bearer, usernameValue, emailValue)
	if err != nil {
		return err
	}
	if matched != nil && scimString(matched, "Id") != targetID {
		return &scimHTTPError{status: http.StatusConflict, detail: "SCIM identity belongs to a different User", scimType: "uniqueness"}
	}
	return nil
}

func (s *Service) scimDeactivateUser(ctx context.Context, writer http.ResponseWriter, bearer string, authority scimRequestAuthority, id string) {
	current, err := s.scimReadUser(ctx, bearer, id)
	if err != nil {
		s.writeSCIMFailure(writer, err)
		return
	}
	if err := scimGuardDeactivation(authority, id, current); err != nil {
		s.writeSCIMFailure(writer, err)
		return
	}
	if active, ok := scimBool(current, scimdialect.FieldIsActive); ok && !active {
		writer.WriteHeader(http.StatusNoContent)
		return
	}
	if err := s.scimClaimDeactivation(ctx, bearer, authority.policy, time.Now().UTC()); err != nil {
		s.writeSCIMFailure(writer, err)
		return
	}
	if _, err := s.scimMutate(ctx, bearer, mutationsemantic.OperationUpdate, id, map[string]any{scimdialect.FieldIsActive: false}); err != nil {
		s.writeSCIMFailure(writer, err)
		return
	}
	writer.WriteHeader(http.StatusNoContent)
}

func (s *Service) scimClaimDeactivation(ctx context.Context, bearer string, policy scimTenantPolicy, now time.Time) error {
	if policy.deactivationLimit <= 0 || policy.deactivationWindow <= 0 {
		return &scimHTTPError{status: http.StatusServiceUnavailable, detail: "SCIM deactivation bound is unavailable"}
	}
	bucketSeconds := int64(policy.deactivationWindow / time.Second)
	if bucketSeconds <= 0 {
		return &scimHTTPError{status: http.StatusServiceUnavailable, detail: "SCIM deactivation window is invalid"}
	}
	bucket := now.Unix() / bucketSeconds
	for slot := 0; slot < policy.deactivationLimit; slot++ {
		claimed, err := s.scimTryClaimDeactivationSlot(ctx, bearer, bucket, int64(slot))
		if err != nil {
			return err
		}
		if claimed {
			return nil
		}
	}
	return &scimHTTPError{status: http.StatusTooManyRequests, detail: "SCIM deactivation volume bound is full"}
}

func (s *Service) scimTryClaimDeactivationSlot(ctx context.Context, bearer string, bucket, slot int64) (bool, error) {
	name := strconv.FormatInt(bucket, 10) + ":" + strconv.FormatInt(slot, 10)
	fields, err := mutationFields(map[string]any{
		"Name": name, "Bucket": json.Number(strconv.FormatInt(bucket, 10)), "Slot": json.Number(strconv.FormatInt(slot, 10)),
	}, false)
	if err != nil {
		return false, err
	}
	response, err := s.mutation.Mutate(ctx, bearer, "", mutationsemantic.MutationUseCaseSingle, mutationsemantic.BatchIndependent, []mutationsemantic.Operation{{
		Kind: mutationsemantic.OperationCreate, ObjectID: scimTicket, Fields: fields,
	}})
	if err != nil {
		return false, err
	}
	if len(response.Results) != 1 {
		return false, &scimHTTPError{status: http.StatusServiceUnavailable, detail: "SCIM deactivation ticket returned an invalid result"}
	}
	result := response.Results[0]
	if result.Success {
		return true, nil
	}
	for _, operationError := range result.Errors {
		if operationError.Kind == mutationsemantic.ErrorFieldIntegrity {
			return false, nil // unique Name already claimed by this or another replica
		}
	}
	return false, &scimHTTPError{status: http.StatusServiceUnavailable, detail: "SCIM deactivation ticket was rejected"}
}

func scimGuardDeactivation(authority scimRequestAuthority, id string, current map[string]any) error {
	if id == authority.principal.PrincipalID {
		return &scimHTTPError{status: http.StatusForbidden, detail: "SCIM service principal cannot deactivate itself", scimType: "mutability"}
	}
	if len(authority.policy.exempt) == 0 {
		return &scimHTTPError{status: http.StatusServiceUnavailable, detail: "SCIM exempt-principal policy is empty"}
	}
	if scimString(current, scimdialect.FieldUsername) == "" || scimString(current, scimdialect.FieldEmail) == "" {
		return &scimHTTPError{status: http.StatusServiceUnavailable, detail: "User identity is not readable enough to verify the exemption policy"}
	}
	if _, readable := scimBool(current, scimdialect.FieldIsActive); !readable {
		return &scimHTTPError{status: http.StatusServiceUnavailable, detail: "User activation state is not readable"}
	}
	if scimIsExempt(authority.policy, current) {
		return &scimHTTPError{status: http.StatusForbidden, detail: "exemption-listed User cannot be deactivated by SCIM", scimType: "mutability"}
	}
	return nil
}

func scimIsExempt(policy scimTenantPolicy, record map[string]any) bool {
	for _, field := range []string{scimdialect.FieldUsername, scimdialect.FieldEmail} {
		if _, ok := policy.exempt[strings.ToLower(scimString(record, field))]; ok {
			return true
		}
	}
	return false
}

func (s *Service) scimFindUserByIdentity(ctx context.Context, bearer, username, email string) (map[string]any, error) {
	var matched map[string]any
	matchedID := ""
	for _, candidate := range []struct{ field, value string }{
		{scimdialect.FieldUsername, username}, {scimdialect.FieldEmail, email},
	} {
		if candidate.value == "" {
			continue
		}
		records, _, err := s.scimQuery(ctx, bearer, scimUserObject, scimUserFields, stringComparison(candidate.field, querysemantic.ComparisonEqual, candidate.value), 2, 0)
		if err != nil {
			return nil, err
		}
		if len(records) > 1 {
			return nil, &scimHTTPError{status: http.StatusConflict, detail: "SCIM identity is ambiguous", scimType: "uniqueness"}
		}
		if len(records) == 1 {
			id := scimString(records[0], "Id")
			if id == "" {
				return nil, &scimHTTPError{status: http.StatusServiceUnavailable, detail: "SCIM identity lookup returned an unreadable User id"}
			}
			if matchedID != "" && matchedID != id {
				return nil, &scimHTTPError{status: http.StatusConflict, detail: "SCIM userName and email resolve to different Users", scimType: "uniqueness"}
			}
			matched, matchedID = records[0], id
		}
	}
	return matched, nil
}

func (s *Service) scimMutate(ctx context.Context, bearer string, kind mutationsemantic.OperationKind, id string, fields map[string]any) (string, error) {
	values, err := mutationFields(fields, false)
	if err != nil {
		return "", err
	}
	response, err := s.mutation.Mutate(ctx, bearer, "", mutationsemantic.MutationUseCaseSingle, mutationsemantic.BatchIndependent, []mutationsemantic.Operation{{
		Kind: kind, ObjectID: scimUserObject, RecordID: id, Fields: values,
	}})
	if err != nil {
		return "", err
	}
	if len(response.Results) != 1 || !response.Results[0].Success {
		status, scimType := http.StatusBadRequest, "invalidValue"
		if len(response.Results) == 1 {
			for _, operationError := range response.Results[0].Errors {
				if operationError.Kind == mutationsemantic.ErrorAccessDenied || operationError.Kind == mutationsemantic.ErrorPolicyDenied {
					status, scimType = http.StatusForbidden, "mutability"
					break
				}
				if operationError.Kind == mutationsemantic.ErrorMissingOrDenied {
					status = http.StatusNotFound
				}
			}
		}
		return "", &scimHTTPError{status: status, detail: "NexusCRM rejected the SCIM User mutation", scimType: scimType}
	}
	return response.Results[0].RecordID, nil
}

func (s *Service) scimRequestBody(writer http.ResponseWriter, request *http.Request) ([]byte, error) {
	request.Body = http.MaxBytesReader(writer, request.Body, s.maxRequestBytes)
	body, err := io.ReadAll(request.Body)
	if err != nil {
		return nil, &scimHTTPError{status: http.StatusBadRequest, detail: "invalid or oversized SCIM request body", scimType: "invalidSyntax"}
	}
	if len(body) == 0 {
		return nil, &scimHTTPError{status: http.StatusBadRequest, detail: "SCIM request body is required", scimType: "invalidSyntax"}
	}
	return body, nil
}

func scimAlias(username string) string {
	local := username
	if index := strings.IndexByte(local, '@'); index >= 0 {
		local = local[:index]
	}
	var builder strings.Builder
	for _, r := range local {
		if unicode.IsLetter(r) || unicode.IsDigit(r) {
			builder.WriteRune(r)
		}
		if builder.Len() >= 8 {
			break
		}
	}
	if builder.Len() == 0 {
		return "scimuser"
	}
	return builder.String()
}

func scimString(record map[string]any, field string) string {
	value, _ := record[field].(string)
	return strings.TrimSpace(value)
}

func scimBool(record map[string]any, field string) (bool, bool) {
	value, ok := record[field].(bool)
	return value, ok
}

func scimInteger(record map[string]any, field string) (int64, bool) {
	value, ok := record[field]
	if !ok {
		return 0, false
	}
	switch typed := value.(type) {
	case int64:
		return typed, true
	case uint64:
		if typed > uint64(^uint64(0)>>1) {
			return 0, false
		}
		return int64(typed), true
	case json.Number:
		parsed, err := typed.Int64()
		return parsed, err == nil
	default:
		return 0, false
	}
}

func scimUserResource(record map[string]any) map[string]any {
	id := scimString(record, "Id")
	resource := map[string]any{
		"schemas": []string{scimdialect.SchemaUser}, "id": id,
		"userName": scimString(record, scimdialect.FieldUsername),
		"active":   false,
		"name": map[string]any{
			"givenName":  scimString(record, scimdialect.FieldFirstName),
			"familyName": scimString(record, scimdialect.FieldLastName),
		},
		"emails": []map[string]any{{"value": scimString(record, scimdialect.FieldEmail), "primary": true}},
		"meta":   map[string]any{"resourceType": "User", "location": scimBasePath + "/Users/" + id},
	}
	if active, ok := scimBool(record, scimdialect.FieldIsActive); ok {
		resource["active"] = active
	}
	if title := scimString(record, scimdialect.FieldTitle); title != "" {
		resource["title"] = title
	}
	phones := make([]map[string]any, 0, 2)
	if phone := scimString(record, scimdialect.FieldPhone); phone != "" {
		phones = append(phones, map[string]any{"type": "work", "value": phone})
	}
	if phone := scimString(record, scimdialect.FieldMobilePhone); phone != "" {
		phones = append(phones, map[string]any{"type": "mobile", "value": phone})
	}
	if len(phones) > 0 {
		resource["phoneNumbers"] = phones
	}
	if department := scimString(record, scimdialect.FieldDepartment); department != "" {
		resource["schemas"] = []string{scimdialect.SchemaUser, scimdialect.SchemaEnterprise}
		resource[scimdialect.SchemaEnterprise] = map[string]any{"department": department}
	}
	return resource
}

func (s *Service) writeSCIMFailure(writer http.ResponseWriter, err error) {
	var dialectErr *scimdialect.Error
	if errors.As(err, &dialectErr) {
		detail := dialectErr.Detail
		if dialectErr.Status >= 500 {
			detail = "SCIM service temporarily unavailable"
		}
		writeSCIMError(writer, dialectErr.Status, detail, dialectErr.ScimType)
		return
	}
	var httpErr *scimHTTPError
	if errors.As(err, &httpErr) {
		detail := httpErr.detail
		if httpErr.status >= 500 {
			detail = "SCIM service temporarily unavailable"
		}
		writeSCIMError(writer, httpErr.status, detail, httpErr.scimType)
		return
	}
	var runtimeErr *querysemantic.Failure
	if errors.As(err, &runtimeErr) {
		switch runtimeErr.Category {
		case querysemantic.FailureUnauthenticated:
			writeSCIMError(writer, http.StatusUnauthorized, "SCIM Runtime credential is no longer valid", "")
		case querysemantic.FailureForbidden:
			writeSCIMError(writer, http.StatusForbidden, "SCIM service principal lacks required access", "")
		case querysemantic.FailureMissing:
			writeSCIMError(writer, http.StatusNotFound, "SCIM resource not found", "")
		case querysemantic.FailureInvalidRequest:
			writeSCIMError(writer, http.StatusBadRequest, "invalid SCIM Runtime request", "invalidValue")
		default:
			writeSCIMError(writer, http.StatusServiceUnavailable, "SCIM service temporarily unavailable", "")
		}
		return
	}
	writeSCIMError(writer, http.StatusServiceUnavailable, "SCIM service temporarily unavailable", "")
}

func (s *Service) authenticateSCIM(writer http.ResponseWriter, request *http.Request, clientID, clientSecret string) (string, time.Time, func(), bool) {
	if s.sessions == nil || s.sessions.exchangeAuthorization == nil || s.sessions.revoke == nil {
		writeSCIMError(writer, http.StatusServiceUnavailable, "SCIM Runtime composition is unavailable", "")
		return "", time.Time{}, func() {}, false
	}
	ctx, cancel := context.WithTimeout(request.Context(), s.requestTimeout)
	defer cancel()
	semanticRequest := authorizationsemantic.ExchangeRequest{
		GrantType: authorizationsemantic.GrantTypeClientCredentials,
		ClientID:  clientID,
		Issuer:    requestPublicOrigin(request),
	}
	response, issued, err := s.sessions.exchangeAuthorization(ctx,
		runtimegrpc.AuthorizationExchangeCredentials{ClientSecret: clientSecret},
		semanticRequest,
	)
	if err != nil {
		writeSCIMRuntimeAuthFailure(writer, err)
		return "", time.Time{}, func() {}, false
	}
	if response.ProtocolError != nil ||
		authorizationsemantic.ValidateExchangeResponseForRequest(semanticRequest, response) != nil ||
		response.Token == nil || issued.AccessToken == "" || issued.RefreshToken != "" ||
		response.Token.ExpiresInSecond <= 0 || response.Token.ExpiresInSecond > int64(scimRuntimeAccessTokenMaxTTL/time.Second) {
		if issued.AccessToken != "" {
			_ = s.scimRevokeAccessToken(issued.AccessToken)
		}
		writer.Header().Set("WWW-Authenticate", `Bearer realm="NexusCRM SCIM", error="invalid_token"`)
		writeSCIMError(writer, http.StatusUnauthorized, "invalid connected-app credential", "")
		return "", time.Time{}, func() {}, false
	}
	accessTokenDeadline := time.Now().Add(time.Duration(response.Token.ExpiresInSecond) * time.Second)
	cleanup := func() {
		if !s.scimRevokeAccessToken(issued.AccessToken) {
			log.Printf("sfcrm SCIM access-token cleanup failed: category=RUNTIME_REVOKE")
		}
	}
	return issued.AccessToken, accessTokenDeadline, cleanup, true
}

func (s *Service) scimRevokeAccessToken(accessToken string) bool {
	if s == nil || s.sessions == nil || s.sessions.revoke == nil || accessToken == "" {
		return false
	}
	revokeCtx, revokeCancel := context.WithTimeout(context.Background(), s.requestTimeout)
	defer revokeCancel()
	for attempt := 0; attempt < scimAccessTokenRevokeTries; attempt++ {
		err := s.sessions.revoke(revokeCtx,
			runtimegrpc.SessionPresentedCredentials{AccessToken: accessToken},
			sessionsemantic.RevokeRequest{},
		)
		if err == nil {
			return true
		}
		if attempt+1 < scimAccessTokenRevokeTries {
			select {
			case <-time.After(scimAccessTokenRetryDelay):
			case <-revokeCtx.Done():
				return false
			}
		}
	}
	return false
}

func writeSCIMRuntimeAuthFailure(writer http.ResponseWriter, err error) {
	var authFailure *authorizationsemantic.Failure
	if errors.As(err, &authFailure) {
		switch authFailure.Category {
		case authorizationsemantic.FailureUnauthenticated, authorizationsemantic.FailureForbidden:
			writer.Header().Set("WWW-Authenticate", `Bearer realm="NexusCRM SCIM", error="invalid_token"`)
			writeSCIMError(writer, http.StatusUnauthorized, "invalid connected-app credential", "")
			return
		case authorizationsemantic.FailureResourceExhausted, authorizationsemantic.FailureUnavailable:
			writeSCIMError(writer, http.StatusServiceUnavailable, "authorization service temporarily unavailable", "")
			return
		}
	}
	writeSCIMError(writer, http.StatusServiceUnavailable, "authorization service temporarily unavailable", "")
}

func scimConnectedAppCredential(values []string) (clientID, clientSecret string, ok bool) {
	if len(values) != 1 || !strings.HasPrefix(values[0], "Bearer ") {
		return "", "", false
	}
	token := strings.TrimSpace(strings.TrimPrefix(values[0], "Bearer "))
	clientID, clientSecret, found := strings.Cut(token, ".")
	if !found || clientID == "" || clientSecret == "" || strings.TrimSpace(clientID) != clientID || strings.TrimSpace(clientSecret) != clientSecret {
		return "", "", false
	}
	return clientID, clientSecret, true
}

func writeSCIMError(writer http.ResponseWriter, status int, detail, scimType string) {
	body := map[string]any{
		"schemas": []string{scimErrorSchema},
		"status":  strconv.Itoa(status),
		"detail":  detail,
	}
	if scimType != "" {
		body["scimType"] = scimType
	}
	writeSCIMJSON(writer, status, body)
}

func writeSCIMJSON(writer http.ResponseWriter, status int, body any) {
	writer.Header().Set("Content-Type", scimContentType)
	writer.Header().Set("Cache-Control", "no-store")
	writer.WriteHeader(status)
	_ = json.NewEncoder(writer).Encode(body)
}
