package service

import (
	"bytes"
	"context"
	"crypto"
	"crypto/rand"
	"crypto/rsa"
	"crypto/sha256"
	"crypto/x509"
	"encoding/base64"
	"encoding/json"
	"encoding/pem"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"strconv"
	"strings"
	"time"
)

const (
	defaultWorksmobileAPIBaseURL    = "https://www.worksapis.com"
	defaultWorksmobileOAuthTokenURL = "https://auth.worksmobile.com/oauth2/v2.0/token"
	defaultWorksmobileOAuthScope    = "directory"
)

type WorksmobileDirectoryClient interface {
	CreateOrgUnit(ctx context.Context, payload WorksmobileOrgUnitPayload) error
	UpsertOrgUnit(ctx context.Context, payload WorksmobileOrgUnitPayload, matchLocalPart string) error
	DeleteOrgUnit(ctx context.Context, orgUnitID string) error
	CreateUser(ctx context.Context, payload WorksmobileUserPayload) error
	UpsertUser(ctx context.Context, payload WorksmobileUserPayload) error
	DeleteUser(ctx context.Context, userID string) error
	SetUserActive(ctx context.Context, userID string, active bool) error
	ListUsers(ctx context.Context) ([]WorksmobileRemoteUser, error)
	ListGroups(ctx context.Context) ([]WorksmobileRemoteGroup, error)
}

type WorksmobileHTTPClient struct {
	BaseURL           string
	DirectoryToken    string
	SCIMToken         string
	HTTPClient        *http.Client
	OAuthConfig       WorksmobileOAuthConfig
	DomainIDs         []int64
	OrgUnitWriteDelay time.Duration
	tokenCache        worksmobileAccessTokenCache
	now               func() time.Time
}

type WorksmobileOAuthConfig struct {
	ClientID       string
	ClientSecret   string
	ServiceAccount string
	PrivateKey     string
	Scope          string
	TokenURL       string
}

type worksmobileAccessTokenCache struct {
	Token     string
	ExpiresAt time.Time
}

func (c WorksmobileOAuthConfig) normalized() WorksmobileOAuthConfig {
	c.ClientID = strings.Trim(strings.TrimSpace(c.ClientID), `"`)
	c.ClientSecret = strings.Trim(strings.TrimSpace(c.ClientSecret), `"`)
	c.ServiceAccount = strings.Trim(strings.TrimSpace(c.ServiceAccount), `"`)
	c.PrivateKey = normalizeWorksmobilePrivateKey(c.PrivateKey)
	c.Scope = strings.TrimSpace(c.Scope)
	if c.Scope == "" {
		c.Scope = defaultWorksmobileOAuthScope
	}
	c.TokenURL = strings.TrimSpace(c.TokenURL)
	if c.TokenURL == "" {
		c.TokenURL = defaultWorksmobileOAuthTokenURL
	}
	return c
}

func (c WorksmobileOAuthConfig) validate() error {
	if strings.TrimSpace(c.ClientID) == "" || strings.TrimSpace(c.ClientSecret) == "" {
		return fmt.Errorf("worksmobile directory token is not configured")
	}
	if strings.TrimSpace(c.ServiceAccount) == "" || strings.TrimSpace(c.PrivateKey) == "" {
		return fmt.Errorf("worksmobile oauth service account is not configured")
	}
	return nil
}

func normalizeWorksmobilePrivateKey(value string) string {
	value = strings.Trim(strings.TrimSpace(value), `"`)
	value = strings.ReplaceAll(value, `\n`, "\n")
	return value
}

func buildWorksmobileJWTAssertion(config WorksmobileOAuthConfig, now time.Time) (string, error) {
	privateKey, err := parseWorksmobilePrivateKey(config.PrivateKey)
	if err != nil {
		return "", err
	}
	header := map[string]string{"alg": "RS256", "typ": "JWT"}
	payload := map[string]any{
		"iss": config.ClientID,
		"sub": config.ServiceAccount,
		"iat": now.Unix(),
		"exp": now.Add(time.Hour).Unix(),
	}
	encodedHeader, err := encodeWorksmobileJWTPart(header)
	if err != nil {
		return "", err
	}
	encodedPayload, err := encodeWorksmobileJWTPart(payload)
	if err != nil {
		return "", err
	}
	signingInput := encodedHeader + "." + encodedPayload
	sum := sha256.Sum256([]byte(signingInput))
	signature, err := rsa.SignPKCS1v15(rand.Reader, privateKey, crypto.SHA256, sum[:])
	if err != nil {
		return "", err
	}
	return signingInput + "." + base64.RawURLEncoding.EncodeToString(signature), nil
}

func encodeWorksmobileJWTPart(value any) (string, error) {
	data, err := json.Marshal(value)
	if err != nil {
		return "", err
	}
	return base64.RawURLEncoding.EncodeToString(data), nil
}

func parseWorksmobilePrivateKey(value string) (*rsa.PrivateKey, error) {
	block, _ := pem.Decode([]byte(normalizeWorksmobilePrivateKey(value)))
	if block == nil {
		return nil, fmt.Errorf("worksmobile private key is not a valid PEM block")
	}
	if key, err := x509.ParsePKCS1PrivateKey(block.Bytes); err == nil {
		return key, nil
	}
	parsed, err := x509.ParsePKCS8PrivateKey(block.Bytes)
	if err != nil {
		return nil, err
	}
	key, ok := parsed.(*rsa.PrivateKey)
	if !ok {
		return nil, fmt.Errorf("worksmobile private key is not RSA")
	}
	return key, nil
}

type WorksmobileHTTPError struct {
	StatusCode int
	Body       string
}

func (e WorksmobileHTTPError) Error() string {
	return fmt.Sprintf("worksmobile api failed status=%d body=%s", e.StatusCode, e.Body)
}

func NewWorksmobileHTTPClient(scimToken string) *WorksmobileHTTPClient {
	return &WorksmobileHTTPClient{
		BaseURL:   defaultWorksmobileAPIBaseURL,
		SCIMToken: strings.Trim(strings.TrimSpace(scimToken), `"`),
	}
}

func NewWorksmobileHTTPClientWithTokens(directoryToken string, scimToken string) *WorksmobileHTTPClient {
	return &WorksmobileHTTPClient{
		BaseURL:        defaultWorksmobileAPIBaseURL,
		DirectoryToken: strings.Trim(strings.TrimSpace(directoryToken), `"`),
		SCIMToken:      strings.Trim(strings.TrimSpace(scimToken), `"`),
	}
}

func NewWorksmobileHTTPClientWithAuth(directoryToken string, scimToken string, oauthConfig WorksmobileOAuthConfig) *WorksmobileHTTPClient {
	return &WorksmobileHTTPClient{
		BaseURL:        defaultWorksmobileAPIBaseURL,
		DirectoryToken: strings.Trim(strings.TrimSpace(directoryToken), `"`),
		SCIMToken:      strings.Trim(strings.TrimSpace(scimToken), `"`),
		OAuthConfig:    oauthConfig.normalized(),
		DomainIDs:      WorksmobileDomainIDsFromEnv(),
	}
}

func (c *WorksmobileHTTPClient) CreateOrgUnit(ctx context.Context, payload WorksmobileOrgUnitPayload) error {
	if payload.DisplayOrder < 1 {
		payload.DisplayOrder = 1
	}
	return c.sendDirectoryJSON(ctx, http.MethodPost, "/v1.0/orgunits", payload)
}

func (c *WorksmobileHTTPClient) UpsertOrgUnit(ctx context.Context, payload WorksmobileOrgUnitPayload, matchLocalPart string) error {
	err := c.CreateOrgUnit(ctx, payload)
	if apiErr, ok := err.(WorksmobileHTTPError); ok && apiErr.StatusCode == http.StatusConflict {
		return c.BackfillOrgUnitExternalKeyByLocalPart(ctx, payload, matchLocalPart)
	}
	return err
}

func (c *WorksmobileHTTPClient) BackfillOrgUnitExternalKeyByLocalPart(ctx context.Context, payload WorksmobileOrgUnitPayload, matchLocalPart string) error {
	groups, err := c.ListGroups(ctx)
	if err != nil {
		return err
	}
	normalizedMatchLocalPart := worksmobileMailLocalPart(matchLocalPart)
	var localPartMatch *WorksmobileRemoteGroup
	for _, group := range groups {
		if payload.DomainID > 0 && group.DomainID > 0 && payload.DomainID != group.DomainID {
			continue
		}
		if group.ExternalID == payload.OrgUnitExternalKey {
			if strings.TrimSpace(group.ID) == "" {
				return nil
			}
			if delay := c.orgUnitWriteDelay(); delay > 0 {
				time.Sleep(delay)
			}
			return c.PatchOrgUnit(ctx, group.ID, NewWorksmobileOrgUnitPatchPayload(payload))
		}
		if normalizedMatchLocalPart != "" && worksmobileMailLocalPart(group.MailLocalPart) == normalizedMatchLocalPart {
			matched := group
			if localPartMatch != nil && localPartMatch.ID != matched.ID {
				return fmt.Errorf("worksmobile orgunit local-part match is ambiguous: %s", normalizedMatchLocalPart)
			}
			localPartMatch = &matched
		}
	}
	if localPartMatch != nil {
		if strings.TrimSpace(localPartMatch.ID) == "" {
			return nil
		}
		if delay := c.orgUnitWriteDelay(); delay > 0 {
			time.Sleep(delay)
		}
		return c.PatchOrgUnit(ctx, localPartMatch.ID, NewWorksmobileOrgUnitPatchPayload(payload))
	}
	return fmt.Errorf("worksmobile orgunit external key match not found after create conflict: %s", payload.OrgUnitExternalKey)
}

func (c *WorksmobileHTTPClient) PatchOrgUnit(ctx context.Context, orgUnitID string, payload WorksmobileOrgUnitPatchPayload) error {
	orgUnitID = strings.TrimSpace(orgUnitID)
	if orgUnitID == "" {
		return fmt.Errorf("worksmobile orgunit id is required")
	}
	return c.sendDirectoryJSON(ctx, http.MethodPatch, "/v1.0/orgunits/"+url.PathEscape(orgUnitID), payload)
}

func (c *WorksmobileHTTPClient) ClearOrgUnitExternalKey(ctx context.Context, orgUnitID string, domainID int64) error {
	orgUnitID = strings.TrimSpace(orgUnitID)
	if orgUnitID == "" {
		return fmt.Errorf("worksmobile orgunit id is required")
	}
	payload := map[string]any{
		"domainId":           domainID,
		"orgUnitExternalKey": "",
	}
	return c.sendDirectoryJSON(ctx, http.MethodPatch, "/v1.0/orgunits/"+url.PathEscape(orgUnitID), payload)
}

func (c *WorksmobileHTTPClient) DeleteOrgUnit(ctx context.Context, orgUnitID string) error {
	orgUnitID = strings.TrimSpace(orgUnitID)
	if orgUnitID == "" {
		return fmt.Errorf("worksmobile orgunit id is required")
	}
	if delay := c.orgUnitWriteDelay(); delay > 0 {
		time.Sleep(delay)
	}
	return c.sendDirectoryJSON(ctx, http.MethodDelete, "/v1.0/orgunits/"+url.PathEscape(orgUnitID), nil)
}

func (c *WorksmobileHTTPClient) CreateUser(ctx context.Context, payload WorksmobileUserPayload) error {
	return c.sendDirectoryJSON(ctx, http.MethodPost, "/v1.0/users", payload)
}

func (c *WorksmobileHTTPClient) UpsertUser(ctx context.Context, payload WorksmobileUserPayload) error {
	err := c.CreateUser(ctx, payload)
	if apiErr, ok := err.(WorksmobileHTTPError); ok && apiErr.StatusCode == http.StatusConflict {
		identifier := strings.TrimSpace(payload.Email)
		if identifier == "" {
			identifier = strings.TrimSpace(payload.UserExternalKey)
		}
		return c.PatchUser(ctx, identifier, NewWorksmobileUserPatchPayload(payload))
	}
	return err
}

func (c *WorksmobileHTTPClient) PatchUser(ctx context.Context, identifier string, payload WorksmobileUserPatchPayload) error {
	identifier = strings.TrimSpace(identifier)
	if identifier == "" {
		return fmt.Errorf("worksmobile user identifier is required")
	}
	return c.sendDirectoryJSON(ctx, http.MethodPatch, "/v1.0/users/"+url.PathEscape(identifier), payload)
}

func (c *WorksmobileHTTPClient) DeleteUser(ctx context.Context, userID string) error {
	userID = strings.TrimSpace(userID)
	if userID == "" {
		return fmt.Errorf("worksmobile user id is required")
	}
	remote, err := c.FindUser(ctx, userID)
	if err != nil {
		return err
	}
	if remote == nil {
		return nil
	}
	if c.directoryAuthConfigured() && remote.Email != "" {
		err := c.sendDirectoryJSON(ctx, http.MethodDelete, "/v1.0/users/"+url.PathEscape(remote.Email), nil)
		if err == nil || strings.TrimSpace(c.SCIMToken) == "" {
			return err
		}
	}
	return c.sendJSON(ctx, http.MethodDelete, "/scim/v2/Users/"+url.PathEscape(remote.ID), nil)
}

func (c *WorksmobileHTTPClient) SetUserActive(ctx context.Context, userID string, active bool) error {
	userID = strings.TrimSpace(userID)
	if userID == "" {
		return fmt.Errorf("worksmobile user id is required")
	}
	if strings.TrimSpace(c.SCIMToken) == "" {
		return fmt.Errorf("worksmobile scim token is not configured")
	}
	remote, err := c.findSCIMUser(ctx, userID)
	if err != nil {
		return err
	}
	if remote == nil {
		return nil
	}
	return c.sendJSON(ctx, http.MethodPatch, "/scim/v2/Users/"+url.PathEscape(remote.ID), map[string]any{
		"schemas": []string{"urn:ietf:params:scim:api:messages:2.0:PatchOp"},
		"Operations": []map[string]any{
			{
				"op":    "replace",
				"path":  "active",
				"value": active,
			},
		},
	})
}

func (c *WorksmobileHTTPClient) FindUser(ctx context.Context, identifier string) (*WorksmobileRemoteUser, error) {
	users, err := c.ListUsers(ctx)
	if err != nil {
		return nil, err
	}
	identifier = strings.TrimSpace(identifier)
	for _, user := range users {
		if strings.EqualFold(user.UserName, identifier) || user.ExternalID == identifier || strings.EqualFold(user.Email, identifier) {
			return &user, nil
		}
	}
	return nil, nil
}

func (c *WorksmobileHTTPClient) findSCIMUser(ctx context.Context, identifier string) (*WorksmobileRemoteUser, error) {
	identifier = strings.TrimSpace(identifier)
	var matched *WorksmobileRemoteUser
	err := c.listSCIM(ctx, "/scim/v2/Users", func(resource map[string]any) {
		if matched != nil {
			return
		}
		user := parseWorksmobileRemoteUser(resource)
		if strings.EqualFold(user.UserName, identifier) || user.ExternalID == identifier || strings.EqualFold(user.Email, identifier) {
			matched = &user
		}
	})
	return matched, err
}

func (c *WorksmobileHTTPClient) ListUsers(ctx context.Context) ([]WorksmobileRemoteUser, error) {
	if c.directoryAuthConfigured() && len(c.DomainIDs) > 0 {
		users, err := c.listDirectoryUsers(ctx, c.DomainIDs)
		if err == nil {
			return users, nil
		}
		if strings.TrimSpace(c.SCIMToken) == "" {
			return nil, err
		}
	}
	var users []WorksmobileRemoteUser
	err := c.listSCIM(ctx, "/scim/v2/Users", func(resource map[string]any) {
		users = append(users, parseWorksmobileRemoteUser(resource))
	})
	return users, err
}

func (c *WorksmobileHTTPClient) ListGroups(ctx context.Context) ([]WorksmobileRemoteGroup, error) {
	if c.directoryAuthConfigured() && len(c.DomainIDs) > 0 {
		groups, err := c.listDirectoryGroups(ctx, c.DomainIDs)
		if err == nil {
			return groups, nil
		}
		if strings.TrimSpace(c.SCIMToken) == "" {
			return nil, err
		}
	}
	var groups []WorksmobileRemoteGroup
	err := c.listSCIM(ctx, "/scim/v2/Groups", func(resource map[string]any) {
		groups = append(groups, parseWorksmobileRemoteGroup(resource))
	})
	return groups, err
}

func (c *WorksmobileHTTPClient) listSCIM(ctx context.Context, path string, consume func(map[string]any)) error {
	startIndex := 1
	count := 100
	for {
		var response struct {
			TotalResults int              `json:"totalResults"`
			ItemsPerPage int              `json:"itemsPerPage"`
			Resources    []map[string]any `json:"Resources"`
		}
		if err := c.getJSON(ctx, fmt.Sprintf("%s?startIndex=%d&count=%d", path, startIndex, count), &response); err != nil {
			return err
		}
		for _, resource := range response.Resources {
			consume(resource)
		}
		if len(response.Resources) == 0 || startIndex+len(response.Resources) > response.TotalResults {
			return nil
		}
		startIndex += len(response.Resources)
	}
}

func (c *WorksmobileHTTPClient) getJSON(ctx context.Context, path string, target any) error {
	token := strings.TrimSpace(c.SCIMToken)
	if token == "" {
		token = strings.TrimSpace(c.DirectoryToken)
	}
	if token == "" {
		return fmt.Errorf("worksmobile read token is not configured")
	}
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, strings.TrimRight(c.baseURL(), "/")+path, nil)
	if err != nil {
		return err
	}
	req.Header.Set("Authorization", "Bearer "+token)
	req.Header.Set("Accept", "application/json")

	resp, err := c.httpClient().Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	if resp.StatusCode >= 300 {
		data, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))
		return WorksmobileHTTPError{StatusCode: resp.StatusCode, Body: string(data)}
	}
	return json.NewDecoder(resp.Body).Decode(target)
}

func (c *WorksmobileHTTPClient) getDirectoryJSON(ctx context.Context, path string, target any) error {
	token, err := c.directoryAccessToken(ctx)
	if err != nil {
		return err
	}
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, strings.TrimRight(c.baseURL(), "/")+path, nil)
	if err != nil {
		return err
	}
	req.Header.Set("Authorization", "Bearer "+token)
	req.Header.Set("Accept", "application/json")

	resp, err := c.httpClient().Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	if resp.StatusCode >= 300 {
		data, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))
		return WorksmobileHTTPError{StatusCode: resp.StatusCode, Body: string(data)}
	}
	return json.NewDecoder(resp.Body).Decode(target)
}

func (c *WorksmobileHTTPClient) listDirectoryUsers(ctx context.Context, domainIDs []int64) ([]WorksmobileRemoteUser, error) {
	users := make([]WorksmobileRemoteUser, 0)
	for _, domainID := range uniqueWorksmobileDomainIDs(domainIDs) {
		cursor := ""
		for {
			path := fmt.Sprintf("/v1.0/users?domainId=%d&count=100", domainID)
			if cursor != "" {
				path += "&cursor=" + url.QueryEscape(cursor)
			}
			var response struct {
				Users            []map[string]any `json:"users"`
				ResponseMetaData struct {
					NextCursor string `json:"nextCursor"`
				} `json:"responseMetaData"`
			}
			if err := c.getDirectoryJSON(ctx, path, &response); err != nil {
				return nil, err
			}
			for _, raw := range response.Users {
				user := parseWorksmobileDirectoryUser(raw)
				user.DomainID = domainID
				user.DomainName = WorksmobileDomainLabelForID(domainID)
				users = append(users, user)
			}
			cursor = strings.TrimSpace(response.ResponseMetaData.NextCursor)
			if cursor == "" {
				break
			}
		}
	}
	return users, nil
}

func (c *WorksmobileHTTPClient) listDirectoryGroups(ctx context.Context, domainIDs []int64) ([]WorksmobileRemoteGroup, error) {
	groups := make([]WorksmobileRemoteGroup, 0)
	for _, domainID := range uniqueWorksmobileDomainIDs(domainIDs) {
		cursor := ""
		for {
			path := fmt.Sprintf("/v1.0/orgunits?domainId=%d&count=100", domainID)
			if cursor != "" {
				path += "&cursor=" + url.QueryEscape(cursor)
			}
			var response struct {
				OrgUnits         []map[string]any `json:"orgUnits"`
				ResponseMetaData struct {
					NextCursor string `json:"nextCursor"`
				} `json:"responseMetaData"`
			}
			if err := c.getDirectoryJSON(ctx, path, &response); err != nil {
				return nil, err
			}
			for _, raw := range response.OrgUnits {
				group := parseWorksmobileDirectoryGroup(raw)
				group.DomainID = domainID
				group.DomainName = WorksmobileDomainLabelForID(domainID)
				groups = append(groups, group)
			}
			cursor = strings.TrimSpace(response.ResponseMetaData.NextCursor)
			if cursor == "" {
				break
			}
		}
	}
	return groups, nil
}

func uniqueWorksmobileDomainIDs(domainIDs []int64) []int64 {
	result := make([]int64, 0, len(domainIDs))
	seen := map[int64]bool{}
	for _, id := range domainIDs {
		if id <= 0 || seen[id] {
			continue
		}
		seen[id] = true
		result = append(result, id)
	}
	return result
}

func (c *WorksmobileHTTPClient) sendJSON(ctx context.Context, method string, path string, payload any) error {
	token := strings.TrimSpace(c.SCIMToken)
	if token == "" {
		return fmt.Errorf("worksmobile scim token is not configured")
	}
	return c.sendJSONWithToken(ctx, method, path, payload, token)
}

func (c *WorksmobileHTTPClient) sendDirectoryJSON(ctx context.Context, method string, path string, payload any) error {
	token, err := c.directoryAccessToken(ctx)
	if err != nil {
		return err
	}
	return c.sendJSONWithToken(ctx, method, path, payload, token)
}

func (c *WorksmobileHTTPClient) directoryAccessToken(ctx context.Context) (string, error) {
	if token := strings.TrimSpace(c.DirectoryToken); token != "" {
		return token, nil
	}
	now := c.currentTime()
	if c.tokenCache.Token != "" && now.Before(c.tokenCache.ExpiresAt) {
		return c.tokenCache.Token, nil
	}
	token, expiresAt, err := c.requestDirectoryAccessToken(ctx, now)
	if err != nil {
		return "", err
	}
	c.tokenCache = worksmobileAccessTokenCache{Token: token, ExpiresAt: expiresAt}
	return token, nil
}

func (c *WorksmobileHTTPClient) directoryAuthConfigured() bool {
	if strings.TrimSpace(c.DirectoryToken) != "" {
		return true
	}
	return c.OAuthConfig.normalized().validate() == nil
}

func (c *WorksmobileHTTPClient) requestDirectoryAccessToken(ctx context.Context, now time.Time) (string, time.Time, error) {
	config := c.OAuthConfig.normalized()
	if err := config.validate(); err != nil {
		return "", time.Time{}, err
	}
	assertion, err := buildWorksmobileJWTAssertion(config, now)
	if err != nil {
		return "", time.Time{}, err
	}
	form := url.Values{}
	form.Set("grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer")
	form.Set("assertion", assertion)
	form.Set("client_id", config.ClientID)
	form.Set("client_secret", config.ClientSecret)
	form.Set("scope", config.Scope)

	req, err := http.NewRequestWithContext(ctx, http.MethodPost, config.TokenURL, strings.NewReader(form.Encode()))
	if err != nil {
		return "", time.Time{}, err
	}
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	req.Header.Set("Accept", "application/json")

	resp, err := c.httpClient().Do(req)
	if err != nil {
		return "", time.Time{}, err
	}
	defer resp.Body.Close()
	if resp.StatusCode >= 300 {
		data, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))
		return "", time.Time{}, WorksmobileHTTPError{StatusCode: resp.StatusCode, Body: string(data)}
	}
	var tokenResponse struct {
		AccessToken string `json:"access_token"`
		ExpiresIn   any    `json:"expires_in"`
		TokenType   string `json:"token_type"`
	}
	if err := json.NewDecoder(resp.Body).Decode(&tokenResponse); err != nil {
		return "", time.Time{}, err
	}
	if strings.TrimSpace(tokenResponse.AccessToken) == "" {
		return "", time.Time{}, fmt.Errorf("worksmobile token response is missing access_token")
	}
	expiresIn := worksmobileTokenExpiresIn(tokenResponse.ExpiresIn)
	if expiresIn <= 0 {
		expiresIn = 3600
	}
	return strings.TrimSpace(tokenResponse.AccessToken), now.Add(time.Duration(expiresIn-60) * time.Second), nil
}

func worksmobileTokenExpiresIn(raw any) int64 {
	switch value := raw.(type) {
	case float64:
		return int64(value)
	case int64:
		return value
	case string:
		parsed, _ := strconv.ParseInt(strings.TrimSpace(value), 10, 64)
		return parsed
	default:
		return 0
	}
}

func (c *WorksmobileHTTPClient) sendJSONWithToken(ctx context.Context, method string, path string, payload any, token string) error {
	var body io.Reader
	if payload != nil {
		data, err := json.Marshal(payload)
		if err != nil {
			return err
		}
		body = bytes.NewReader(data)
	}

	req, err := http.NewRequestWithContext(ctx, method, strings.TrimRight(c.baseURL(), "/")+path, body)
	if err != nil {
		return err
	}
	req.Header.Set("Authorization", "Bearer "+strings.TrimSpace(token))
	if payload != nil {
		req.Header.Set("Content-Type", "application/json")
	}

	resp, err := c.httpClient().Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	if resp.StatusCode >= 200 && resp.StatusCode < 300 {
		return nil
	}
	data, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))
	return WorksmobileHTTPError{StatusCode: resp.StatusCode, Body: string(data)}
}

const worksmobileSCIMUserExtensionSchema = "urn:ietf:params:scim:schemas:extension:works:2.0:User"

type WorksmobileSCIMUserPayload struct {
	Schemas           []string                     `json:"schemas"`
	UserName          string                       `json:"userName"`
	ExternalID        string                       `json:"externalId"`
	DisplayName       string                       `json:"displayName"`
	Name              WorksmobileSCIMName          `json:"name"`
	Emails            []WorksmobileSCIMEmail       `json:"emails"`
	PhoneNumbers      []WorksmobileSCIMPhoneNumber `json:"phoneNumbers,omitempty"`
	Password          string                       `json:"password,omitempty"`
	Active            bool                         `json:"active"`
	PreferredLanguage string                       `json:"preferredLanguage,omitempty"`
	WorksExtension    map[string]any               `json:"urn:ietf:params:scim:schemas:extension:works:2.0:User,omitempty"`
}

type WorksmobileSCIMName struct {
	FamilyName string `json:"familyName"`
}

type WorksmobileSCIMEmail struct {
	Value   string `json:"value"`
	Primary bool   `json:"primary"`
	Type    string `json:"type,omitempty"`
}

type WorksmobileSCIMPhoneNumber struct {
	Value   string `json:"value"`
	Primary bool   `json:"primary"`
	Type    string `json:"type,omitempty"`
}

type WorksmobileUserPatchPayload struct {
	DomainID        int64                         `json:"domainId"`
	Email           string                        `json:"email,omitempty"`
	UserExternalKey string                        `json:"userExternalKey,omitempty"`
	UserName        WorksmobileUserName           `json:"userName,omitempty"`
	CellPhone       string                        `json:"cellPhone,omitempty"`
	EmployeeNumber  string                        `json:"employeeNumber,omitempty"`
	AliasEmails     []string                      `json:"aliasEmails,omitempty"`
	Locale          string                        `json:"locale,omitempty"`
	Task            string                        `json:"task,omitempty"`
	Organizations   []WorksmobileUserOrganization `json:"organizations,omitempty"`
}

type WorksmobileOrgUnitPatchPayload struct {
	DomainID           int64  `json:"domainId"`
	Email              string `json:"email,omitempty"`
	OrgUnitName        string `json:"orgUnitName,omitempty"`
	OrgUnitExternalKey string `json:"orgUnitExternalKey,omitempty"`
	ParentOrgUnitID    string `json:"parentOrgUnitId,omitempty"`
	DisplayOrder       int    `json:"displayOrder,omitempty"`
}

type WorksmobileRemoteUser struct {
	ID                         string `json:"id"`
	ExternalID                 string `json:"externalId"`
	UserName                   string `json:"userName"`
	Email                      string `json:"email"`
	DisplayName                string `json:"displayName"`
	LevelID                    string `json:"levelId"`
	LevelName                  string `json:"levelName"`
	Task                       string `json:"task"`
	DomainID                   int64  `json:"domainId"`
	DomainName                 string `json:"domainName"`
	PrimaryOrgUnitID           string `json:"primaryOrgUnitId"`
	PrimaryOrgUnitName         string `json:"primaryOrgUnitName"`
	PrimaryOrgUnitPositionID   string `json:"primaryOrgUnitPositionId"`
	PrimaryOrgUnitPositionName string `json:"primaryOrgUnitPositionName"`
	PrimaryOrgUnitIsManager    *bool  `json:"primaryOrgUnitIsManager,omitempty"`
	Active                     bool   `json:"active"`
}

type WorksmobileRemoteGroup struct {
	ID            string `json:"id"`
	ExternalID    string `json:"externalId"`
	DisplayName   string `json:"displayName"`
	Email         string `json:"email,omitempty"`
	MailLocalPart string `json:"mailLocalPart,omitempty"`
	DomainID      int64  `json:"domainId"`
	DomainName    string `json:"domainName"`
	ParentID      string `json:"parentId"`
	ParentName    string `json:"parentName"`
}

func NewWorksmobileSCIMUserPayload(payload WorksmobileUserPayload) WorksmobileSCIMUserPayload {
	name := strings.TrimSpace(payload.UserName.LastName)
	if name == "" {
		name = strings.TrimSpace(payload.Email)
	}
	result := WorksmobileSCIMUserPayload{
		Schemas:           []string{"urn:ietf:params:scim:schemas:core:2.0:User", worksmobileSCIMUserExtensionSchema},
		UserName:          strings.TrimSpace(payload.Email),
		ExternalID:        strings.TrimSpace(payload.UserExternalKey),
		DisplayName:       name,
		Name:              WorksmobileSCIMName{FamilyName: name},
		Emails:            []WorksmobileSCIMEmail{{Value: strings.TrimSpace(payload.Email), Primary: true, Type: "other"}},
		Password:          payload.PasswordConfig.Password,
		Active:            true,
		PreferredLanguage: worksmobileSCIMPreferredLanguage(payload.Locale),
		WorksExtension: map[string]any{
			"employeeNumber": payload.EmployeeNumber,
			"task":           payload.Task,
		},
	}
	if strings.TrimSpace(payload.CellPhone) != "" {
		result.PhoneNumbers = []WorksmobileSCIMPhoneNumber{{Value: strings.TrimSpace(payload.CellPhone), Primary: true, Type: "mobile"}}
	}
	return result
}

func NewWorksmobileUserPatchPayload(payload WorksmobileUserPayload) WorksmobileUserPatchPayload {
	return WorksmobileUserPatchPayload{
		DomainID:        payload.DomainID,
		Email:           strings.TrimSpace(payload.Email),
		UserExternalKey: strings.TrimSpace(payload.UserExternalKey),
		UserName:        payload.UserName,
		CellPhone:       strings.TrimSpace(payload.CellPhone),
		EmployeeNumber:  strings.TrimSpace(payload.EmployeeNumber),
		AliasEmails:     payload.AliasEmails,
		Locale:          strings.TrimSpace(payload.Locale),
		Task:            strings.TrimSpace(payload.Task),
		Organizations:   payload.Organizations,
	}
}

func NewWorksmobileOrgUnitPatchPayload(payload WorksmobileOrgUnitPayload) WorksmobileOrgUnitPatchPayload {
	return WorksmobileOrgUnitPatchPayload{
		DomainID:           payload.DomainID,
		Email:              strings.TrimSpace(payload.Email),
		OrgUnitName:        strings.TrimSpace(payload.OrgUnitName),
		OrgUnitExternalKey: strings.TrimSpace(payload.OrgUnitExternalKey),
		ParentOrgUnitID:    strings.TrimSpace(payload.ParentOrgUnitID),
		DisplayOrder:       payload.DisplayOrder,
	}
}

func worksmobileSCIMPreferredLanguage(locale string) string {
	locale = strings.TrimSpace(locale)
	if locale == "" {
		return ""
	}
	return strings.ReplaceAll(locale, "_", "-")
}

func parseWorksmobileRemoteUser(resource map[string]any) WorksmobileRemoteUser {
	user := WorksmobileRemoteUser{
		ID:          stringFromMap(resource, "id"),
		ExternalID:  stringFromMap(resource, "externalId"),
		UserName:    stringFromMap(resource, "userName"),
		DisplayName: stringFromMap(resource, "displayName"),
		Active:      boolFromMap(resource, "active"),
	}
	if emails, ok := resource["emails"].([]any); ok {
		for _, raw := range emails {
			email, ok := raw.(map[string]any)
			if !ok {
				continue
			}
			if user.Email == "" || boolFromMap(email, "primary") {
				user.Email = stringFromMap(email, "value")
			}
		}
	}
	if user.Email == "" && strings.Contains(user.UserName, "@") {
		user.Email = user.UserName
	}
	user.PrimaryOrgUnitID, user.PrimaryOrgUnitName = parseWorksmobilePrimaryOrgUnit(resource)
	return user
}

func parseWorksmobileRemoteGroup(resource map[string]any) WorksmobileRemoteGroup {
	email := firstStringFromMap(resource, "email", "mail", "groupEmail", "mailingList", "orgUnitEmail", "loginId", "userName")
	group := WorksmobileRemoteGroup{
		ID:            stringFromMap(resource, "id"),
		ExternalID:    stringFromMap(resource, "externalId"),
		DisplayName:   stringFromMap(resource, "displayName"),
		Email:         email,
		MailLocalPart: worksmobileMailLocalPart(email),
	}
	group.ParentID, group.ParentName = parseWorksmobileParentOrgUnit(resource)
	return group
}

func parseWorksmobileDirectoryUser(resource map[string]any) WorksmobileRemoteUser {
	email := firstStringFromMap(resource, "email", "loginId", "userName")
	user := WorksmobileRemoteUser{
		ID:          firstStringFromMap(resource, "userId", "id"),
		ExternalID:  firstStringFromMap(resource, "userExternalKey", "externalKey", "externalId"),
		UserName:    email,
		Email:       email,
		DisplayName: parseWorksmobileDirectoryUserName(resource),
		LevelID:     parseWorksmobileUserLevelID(resource),
		LevelName:   parseWorksmobileUserLevelName(resource),
		Task:        firstStringFromMap(resource, "task", "job", "jobDescription"),
		Active:      true,
	}
	if active, ok := resource["active"].(bool); ok {
		user.Active = active
	}
	primaryOrgUnit := parseWorksmobilePrimaryOrgUnitDetail(resource)
	user.PrimaryOrgUnitID = primaryOrgUnit.ID
	user.PrimaryOrgUnitName = primaryOrgUnit.Name
	user.PrimaryOrgUnitPositionID = primaryOrgUnit.PositionID
	user.PrimaryOrgUnitPositionName = primaryOrgUnit.PositionName
	user.PrimaryOrgUnitIsManager = primaryOrgUnit.IsManager
	return user
}

func parseWorksmobileDirectoryGroup(resource map[string]any) WorksmobileRemoteGroup {
	email := firstStringFromMap(resource, "email", "mail", "groupEmail", "mailingList", "orgUnitEmail", "loginId", "userName")
	return WorksmobileRemoteGroup{
		ID:            firstStringFromMap(resource, "orgUnitId", "id"),
		ExternalID:    firstStringFromMap(resource, "orgUnitExternalKey", "externalKey", "externalId"),
		DisplayName:   firstStringFromMap(resource, "orgUnitName", "displayName", "name"),
		Email:         email,
		MailLocalPart: worksmobileMailLocalPart(email),
		ParentID:      firstStringFromMap(resource, "parentOrgUnitId", "parentId"),
		ParentName:    firstStringFromMap(resource, "parentOrgUnitName", "parentName"),
	}
}

func worksmobileMailLocalPart(value string) string {
	normalized := strings.ToLower(strings.TrimSpace(value))
	if normalized == "" {
		return ""
	}
	if at := strings.Index(normalized, "@"); at >= 0 {
		normalized = normalized[:at]
	}
	return strings.TrimSpace(normalized)
}

func parseWorksmobileDirectoryUserName(resource map[string]any) string {
	if value := firstStringFromMap(resource, "displayName", "name"); value != "" {
		return value
	}
	if name, ok := resource["userName"].(map[string]any); ok {
		if value := firstStringFromMap(name, "fullName", "displayName", "name"); value != "" {
			return value
		}
		if value := joinWorksmobileNameParts(firstStringFromMap(name, "lastName", "familyName"), firstStringFromMap(name, "firstName", "givenName")); value != "" {
			return value
		}
	}
	if name, ok := resource["name"].(map[string]any); ok {
		if value := firstStringFromMap(name, "fullName", "displayName", "name"); value != "" {
			return value
		}
		if value := joinWorksmobileNameParts(firstStringFromMap(name, "lastName", "familyName"), firstStringFromMap(name, "firstName", "givenName")); value != "" {
			return value
		}
	}
	return ""
}

func joinWorksmobileNameParts(lastName, firstName string) string {
	lastName = strings.TrimSpace(lastName)
	firstName = strings.TrimSpace(firstName)
	if lastName == "" {
		return firstName
	}
	if firstName == "" {
		return lastName
	}
	return lastName + firstName
}

func parseWorksmobileUserLevelID(resource map[string]any) string {
	if value := firstStringFromMap(resource, "levelId"); value != "" {
		return value
	}
	if level, ok := resource["level"].(map[string]any); ok {
		return firstStringFromMap(level, "levelId", "id", "value")
	}
	return ""
}

func parseWorksmobileUserLevelName(resource map[string]any) string {
	if value := firstStringFromMap(resource, "levelName"); value != "" {
		return value
	}
	if level, ok := resource["level"].(map[string]any); ok {
		return firstStringFromMap(level, "levelName", "displayName", "name")
	}
	return ""
}

type worksmobileOrgUnitDetail struct {
	ID           string
	Name         string
	PositionID   string
	PositionName string
	IsManager    *bool
}

func (d worksmobileOrgUnitDetail) empty() bool {
	return d.ID == "" && d.Name == "" && d.PositionID == "" && d.PositionName == "" && d.IsManager == nil
}

func parseWorksmobilePrimaryOrgUnit(resource map[string]any) (string, string) {
	detail := parseWorksmobilePrimaryOrgUnitDetail(resource)
	return detail.ID, detail.Name
}

func parseWorksmobilePrimaryOrgUnitDetail(resource map[string]any) worksmobileOrgUnitDetail {
	if detail := parseWorksmobileOrgUnitDetailList(resource["organizations"], true); !detail.empty() {
		return detail
	}
	if detail := parseWorksmobileOrgUnitDetailList(resource["orgUnits"], true); !detail.empty() {
		return detail
	}
	for key, raw := range resource {
		if !strings.Contains(strings.ToLower(key), "works") {
			continue
		}
		if values, ok := raw.(map[string]any); ok {
			if detail := parseWorksmobileOrgUnitDetailList(values["organizations"], true); !detail.empty() {
				return detail
			}
			if detail := parseWorksmobileOrgUnitDetailList(values["orgUnits"], true); !detail.empty() {
				return detail
			}
		}
	}
	return worksmobileOrgUnitDetail{}
}

func parseWorksmobileParentOrgUnit(resource map[string]any) (string, string) {
	id := firstStringFromMap(resource, "parentOrgUnitId", "parentId")
	name := firstStringFromMap(resource, "parentOrgUnitName", "parentName")
	if id != "" || name != "" {
		return id, name
	}
	for _, key := range []string{"parent", "parentOrgUnit"} {
		if values, ok := resource[key].(map[string]any); ok {
			id = firstStringFromMap(values, "id", "orgUnitId", "value")
			name = firstStringFromMap(values, "displayName", "orgUnitName", "name")
			if id != "" || name != "" {
				return id, name
			}
		}
	}
	for key, raw := range resource {
		if !strings.Contains(strings.ToLower(key), "works") {
			continue
		}
		if values, ok := raw.(map[string]any); ok {
			if id, name := parseWorksmobileParentOrgUnit(values); id != "" || name != "" {
				return id, name
			}
		}
	}
	return "", ""
}

func parseWorksmobileOrgUnitList(raw any, preferPrimary bool) (string, string) {
	detail := parseWorksmobileOrgUnitDetailList(raw, preferPrimary)
	return detail.ID, detail.Name
}

func parseWorksmobileOrgUnitDetailList(raw any, preferPrimary bool) worksmobileOrgUnitDetail {
	values, ok := raw.([]any)
	if !ok {
		return worksmobileOrgUnitDetail{}
	}
	var fallback worksmobileOrgUnitDetail
	for _, item := range values {
		orgUnit, ok := item.(map[string]any)
		if !ok {
			continue
		}
		detail := worksmobileOrgUnitDetail{
			ID:           firstStringFromMap(orgUnit, "orgUnitId", "id", "value"),
			Name:         firstStringFromMap(orgUnit, "orgUnitName", "displayName", "name"),
			PositionID:   firstStringFromMap(orgUnit, "positionId"),
			PositionName: firstStringFromMap(orgUnit, "positionName"),
			IsManager:    boolPointerFromMap(orgUnit, "isManager", "manager"),
		}
		if detail.empty() {
			if nested := parseWorksmobileOrgUnitDetailList(orgUnit["orgUnits"], preferPrimary); !nested.empty() {
				detail = nested
			}
		}
		if fallback.empty() {
			fallback = detail
		}
		if !preferPrimary || boolFromMap(orgUnit, "primary") {
			return detail
		}
	}
	return fallback
}

func stringFromMap(values map[string]any, key string) string {
	value, _ := values[key].(string)
	return strings.TrimSpace(value)
}

func firstStringFromMap(values map[string]any, keys ...string) string {
	for _, key := range keys {
		if value := stringFromMap(values, key); value != "" {
			return value
		}
	}
	return ""
}

func boolFromMap(values map[string]any, key string) bool {
	value, _ := values[key].(bool)
	return value
}

func boolPointerFromMap(values map[string]any, keys ...string) *bool {
	for _, key := range keys {
		if value, ok := values[key].(bool); ok {
			return &value
		}
	}
	return nil
}

func (c *WorksmobileHTTPClient) baseURL() string {
	if strings.TrimSpace(c.BaseURL) == "" {
		return defaultWorksmobileAPIBaseURL
	}
	return c.BaseURL
}

func (c *WorksmobileHTTPClient) httpClient() *http.Client {
	if c.HTTPClient != nil {
		return c.HTTPClient
	}
	return &http.Client{Timeout: 15 * time.Second}
}

func (c *WorksmobileHTTPClient) currentTime() time.Time {
	if c.now != nil {
		return c.now()
	}
	return time.Now()
}

func (c *WorksmobileHTTPClient) orgUnitWriteDelay() time.Duration {
	if c.OrgUnitWriteDelay < 0 {
		return 0
	}
	if c.OrgUnitWriteDelay > 0 {
		return c.OrgUnitWriteDelay
	}
	return 1100 * time.Millisecond
}
