package serviceaccount

import (
	"crypto/sha256"
	"encoding/json"
	"fmt"
	"slices"
	"strings"
	"time"

	"github.com/tidbcloud/AgentTeam/internal/core/identifiers"
	serviceaccountstore "github.com/tidbcloud/AgentTeam/internal/store/serviceaccount"
	"github.com/tidbcloud/AgentTeam/internal/store/storeerr"
)

const (
	serviceAccountScopeType        = "service_account"
	serviceAccountActorType        = "service"
	platformOperatorActorType      = "platform_operator"
	serviceAccountDeliveryContract = "service_account_message"
)

type preparedWorkspaceMaterialization struct {
	WorkspaceMaterialization
	Identity         IdentityRefs
	Name             string
	ManagerMemberIDs []string
	Receipt          materializationReceipt
	ReceiptDigest    string
	Now              time.Time
}

type preparedPlatformMaterialization struct {
	PlatformInstanceMaterialization
	Identity      IdentityRefs
	Receipt       materializationReceipt
	ReceiptDigest string
	Now           time.Time
}

type localMaterialization struct {
	identity                      IdentityRefs
	workspaceID                   string
	principalID                   string
	managementKind                string
	platformServiceAccountID      string
	name                          string
	description                   string
	avatarAssetID                 string
	avatarURL                     string
	appliedPlatformProfileVersion int64
	createdByMemberID             string
	createdByOperatorUserID       string
	creatorActorType              string
	creatorActorID                string
	grantMemberID                 string
	managerMemberIDs              []string
	authorizationPath             string
	correlationID                 string
	receipt                       materializationReceipt
	receiptDigest                 string
	reconcileIntent               materializationReconcileIntent
	now                           time.Time
}

func prepareWorkspaceMaterialization(input WorkspaceMaterialization) (preparedWorkspaceMaterialization, error) {
	callerIdentity := normalizeIdentityRefs(input.Identity)
	input.Identity = callerIdentity
	input.WorkspaceID = trimmed(input.WorkspaceID)
	input.CreatorMemberID = trimmed(input.CreatorMemberID)
	input.Name = trimmed(input.Name)
	input.Description = strings.TrimSpace(input.Description)
	input.AvatarAssetID = trimmed(input.AvatarAssetID)
	input.AvatarURL = trimmed(input.AvatarURL)
	input.AuthorizationPath = trimmed(input.AuthorizationPath)
	if input.AuthorizationPath == "" {
		input.AuthorizationPath = serviceaccountstore.AuthorizationPathDirectCreate
	}
	input.CorrelationID = trimmed(input.CorrelationID)
	if input.Identity.AccountID == "" || input.WorkspaceID == "" || input.CreatorMemberID == "" || input.Name == "" {
		return preparedWorkspaceMaterialization{}, storeerr.BadRequest("accountId, workspaceId, creatorMemberId, and name are required")
	}
	if len(input.Identity.AccountID) > 64 {
		return preparedWorkspaceMaterialization{}, storeerr.BadRequest("accountId must be at most 64 bytes")
	}
	name, err := serviceaccountstore.NormalizeName(input.Name)
	if err != nil {
		return preparedWorkspaceMaterialization{}, err
	}
	managers := initialWorkspaceManagerIDs(input.CreatorMemberID, input.ManagerMemberIDs)
	input.ManagerMemberIDs = managers
	identity, err := completeIdentityRefs(input.Identity, false)
	if err != nil {
		return preparedWorkspaceMaterialization{}, err
	}
	receipt := newWorkspaceMaterializationReceipt(input, callerIdentity, name, managers)
	receiptDigest, err := digestMaterializationReceipt(receipt)
	if err != nil {
		return preparedWorkspaceMaterialization{}, err
	}
	return preparedWorkspaceMaterialization{
		WorkspaceMaterialization: input,
		Identity:                 identity,
		Name:                     name,
		ManagerMemberIDs:         managers,
		Receipt:                  receipt,
		ReceiptDigest:            receiptDigest,
		Now:                      normalizedNow(input.Now),
	}, nil
}

// initialWorkspaceManagerIDs makes the creator's initial manager assignment an
// invariant of the materialization boundary rather than a convention callers
// must remember. The creator stays first while the remaining manager set is
// normalized into a stable order for receipt hashing and replay comparison.
func initialWorkspaceManagerIDs(creatorMemberID string, managerMemberIDs []string) []string {
	managers := sortedUnique(managerMemberIDs)
	result := make([]string, 0, len(managers)+1)
	result = append(result, creatorMemberID)
	for _, managerMemberID := range managers {
		if managerMemberID != creatorMemberID {
			result = append(result, managerMemberID)
		}
	}
	return result
}

func preparePlatformMaterialization(input PlatformInstanceMaterialization) (preparedPlatformMaterialization, error) {
	callerIdentity := normalizeIdentityRefs(input.Identity)
	input.Identity = callerIdentity
	input.WorkspaceID = trimmed(input.WorkspaceID)
	input.PlatformServiceAccountID = trimmed(input.PlatformServiceAccountID)
	input.OperatorUserID = trimmed(input.OperatorUserID)
	input.AvatarURL = trimmed(input.AvatarURL)
	input.CorrelationID = trimmed(input.CorrelationID)
	if input.Identity.AccountID == "" || input.WorkspaceID == "" || input.PlatformServiceAccountID == "" {
		return preparedPlatformMaterialization{}, storeerr.BadRequest("local accountId, workspaceId, and platformServiceAccountId are required")
	}
	if len(input.Identity.AccountID) > 64 || len(input.PlatformServiceAccountID) > 64 {
		return preparedPlatformMaterialization{}, storeerr.BadRequest("service account ids must be at most 64 bytes")
	}
	if input.OpenedProvisioningGeneration <= 0 {
		input.OpenedProvisioningGeneration = 1
	}
	identity, err := completeIdentityRefs(input.Identity, true)
	if err != nil {
		return preparedPlatformMaterialization{}, err
	}
	receipt := newPlatformMaterializationReceipt(input, callerIdentity)
	receiptDigest, err := digestMaterializationReceipt(receipt)
	if err != nil {
		return preparedPlatformMaterialization{}, err
	}
	return preparedPlatformMaterialization{
		PlatformInstanceMaterialization: input,
		Identity:                        identity,
		Receipt:                         receipt,
		ReceiptDigest:                   receiptDigest,
		Now:                             normalizedNow(input.Now),
	}, nil
}

func normalizeIdentityRefs(refs IdentityRefs) IdentityRefs {
	refs.AccountID = trimmed(refs.AccountID)
	refs.ServiceInstallationID = trimmed(refs.ServiceInstallationID)
	refs.ServiceMemberID = trimmed(refs.ServiceMemberID)
	refs.AuthorMemberID = trimmed(refs.AuthorMemberID)
	refs.MessageScopeID = trimmed(refs.MessageScopeID)
	refs.NameClaimID = trimmed(refs.NameClaimID)
	refs.PlatformInstanceID = trimmed(refs.PlatformInstanceID)
	return refs
}

func completeIdentityRefs(refs IdentityRefs, platform bool) (IdentityRefs, error) {
	refs = normalizeIdentityRefs(refs)
	if err := validateIdentityRefLengths(refs); err != nil {
		return IdentityRefs{}, err
	}
	var err error
	if refs.ServiceInstallationID = trimmed(refs.ServiceInstallationID); refs.ServiceInstallationID == "" {
		refs.ServiceInstallationID, err = identifiers.NewEntityID("svcinst")
		if err != nil {
			return IdentityRefs{}, fmt.Errorf("generate service installation id: %w", err)
		}
	}
	if refs.ServiceMemberID = trimmed(refs.ServiceMemberID); refs.ServiceMemberID == "" {
		refs.ServiceMemberID, err = identifiers.NewEntityID("svcmem")
		if err != nil {
			return IdentityRefs{}, fmt.Errorf("generate service member id: %w", err)
		}
	}
	if refs.AuthorMemberID = trimmed(refs.AuthorMemberID); refs.AuthorMemberID == "" {
		refs.AuthorMemberID, err = identifiers.NewMemberID()
		if err != nil {
			return IdentityRefs{}, fmt.Errorf("generate author member id: %w", err)
		}
	}
	if refs.MessageScopeID = trimmed(refs.MessageScopeID); refs.MessageScopeID == "" {
		refs.MessageScopeID, err = identifiers.NewEntityID("sasc")
		if err != nil {
			return IdentityRefs{}, fmt.Errorf("generate service account scope id: %w", err)
		}
	}
	if !platform {
		if refs.NameClaimID = trimmed(refs.NameClaimID); refs.NameClaimID == "" {
			refs.NameClaimID, err = identifiers.NewEntityID("sanc")
			if err != nil {
				return IdentityRefs{}, fmt.Errorf("generate service account name claim id: %w", err)
			}
		}
	} else if refs.PlatformInstanceID = trimmed(refs.PlatformInstanceID); refs.PlatformInstanceID == "" {
		refs.PlatformInstanceID, err = identifiers.NewEntityID("sapwi")
		if err != nil {
			return IdentityRefs{}, fmt.Errorf("generate platform workspace instance id: %w", err)
		}
	}
	return refs, nil
}

func validateIdentityRefLengths(refs IdentityRefs) error {
	for _, field := range []struct {
		name  string
		value string
		max   int
	}{
		{name: "accountId", value: refs.AccountID, max: 64},
		{name: "serviceInstallationId", value: refs.ServiceInstallationID, max: 255},
		{name: "serviceMemberId", value: refs.ServiceMemberID, max: 255},
		{name: "authorMemberId", value: refs.AuthorMemberID, max: 255},
		{name: "messageScopeId", value: refs.MessageScopeID, max: 255},
		{name: "nameClaimId", value: refs.NameClaimID, max: 64},
		{name: "platformInstanceId", value: refs.PlatformInstanceID, max: 64},
	} {
		if len(field.value) > field.max {
			return storeerr.BadRequest(fmt.Sprintf("%s must be at most %d bytes", field.name, field.max))
		}
	}
	return nil
}

func sortedUnique(values []string) []string {
	seen := make(map[string]struct{}, len(values))
	out := make([]string, 0, len(values))
	for _, value := range values {
		value = trimmed(value)
		if value == "" {
			continue
		}
		if _, ok := seen[value]; ok {
			continue
		}
		seen[value] = struct{}{}
		out = append(out, value)
	}
	slices.Sort(out)
	return out
}

func firstNonEmpty(values ...string) string {
	for _, value := range values {
		if value = trimmed(value); value != "" {
			return value
		}
	}
	return ""
}

func serviceAccountBindingKey(accountID string) string {
	sum := sha256.Sum256([]byte("service-account:" + trimmed(accountID)))
	return fmt.Sprintf("%x", sum)
}

func jsonValue(value any) ([]byte, error) {
	encoded, err := json.Marshal(value)
	if err != nil {
		return nil, fmt.Errorf("marshal service account metadata: %w", err)
	}
	return encoded, nil
}
