package repos

import (
	"context"
	"crypto/rand"
	"encoding/hex"
	"fmt"
	"regexp"
	"strings"
	"time"

	"github.com/tidbcloud/AgentTeam/internal/agsbridge"
	"github.com/tidbcloud/AgentTeam/internal/identity/trusted"
	reposstore "github.com/tidbcloud/AgentTeam/internal/store/resource/repos"
)

var repositoryNameStemCleanupPattern = regexp.MustCompile(`[^a-z0-9]+`)

const (
	RepositoryStorageScopePersonal  = "personal"
	RepositoryStorageScopeWorkspace = "workspace"

	RepositoryNameConflictCode        = "repo_name_conflict"
	RepositoryAlreadyExistsCode       = "ags_repo_already_exists"
	RepositoryReservedSuffixCode      = "slug_reserved_suffix"
	RepositoryNameOverrideInvalidCode = "repo_name_override_invalid"

	RepositoryNameFieldName             = "name"
	RepositoryNameFieldRepoNameOverride = "repoNameOverride"
)

// RepositoryStorage is the lifecycle service's resolved AGS storage owner for
// a resource repository. It is not a repository persistence model, workspace
// membership fact, or authorization DTO.
type RepositoryStorage struct {
	Scope      string
	OwnerLogin string
	OwnerKind  string
}

// RepositoryOwners is the lifecycle service's presentation of the personal
// and workspace AGS owners available to one request. It is not a AgentTeam
// membership model or a durable repository binding.
type RepositoryOwners struct {
	PersonalOwnerLogin  string `json:"personalOwnerLogin"`
	WorkspaceOwnerLogin string `json:"workspaceOwnerLogin"`
}

// RepositoryNameConflictDetails is the lifecycle service's structured error
// detail for a resource repository name collision. It is not a repository
// store model or an AGS repository response.
type RepositoryNameConflictDetails struct {
	ConflictOwner     string `json:"conflictOwner,omitempty"`
	ConflictRepo      string `json:"conflictRepo,omitempty"`
	ResourceType      string `json:"resourceType,omitempty"`
	ResourceID        string `json:"resourceId,omitempty"`
	RepoRole          string `json:"repoRole,omitempty"`
	Field             string `json:"field,omitempty"`
	SuggestedRepoName string `json:"suggestedRepoName,omitempty"`
	RepoName          string `json:"repoName,omitempty"`
	RepoFullName      string `json:"repoFullName,omitempty"`
}

// RepositoryNameAvailabilityRequest is the lifecycle service input used to
// resolve and check one candidate resource repository name. It is not a public
// API request DTO and carries no authoritative membership facts.
type RepositoryNameAvailabilityRequest struct {
	WorkspaceID       string
	Subject           string
	ResourceType      string
	Name              string
	RepoNameOverride  string
	StorageScope      string
	StorageOwnerLogin string
}

// RepositoryNameAvailability is the lifecycle service result for one resolved
// repository name. It is not a persisted repository binding or a general AGS
// repository DTO.
type RepositoryNameAvailability struct {
	RepoName          string                         `json:"repoName"`
	RepoFullName      string                         `json:"repoFullName"`
	StorageOwnerLogin string                         `json:"storageOwnerLogin"`
	Available         bool                           `json:"available"`
	Conflict          *RepositoryNameConflictDetails `json:"conflict,omitempty"`
	SuggestedRepoName string                         `json:"suggestedRepoName,omitempty"`
}

type agsRepositoryResponse struct {
	ID            int64  `json:"id"`
	Name          string `json:"name"`
	FullName      string `json:"full_name"`
	DefaultBranch string `json:"default_branch"`
	Archived      bool   `json:"archived"`
}

func canonicalWorkspaceRepositoryOwnerKey(workspaceID string) string {
	return strings.TrimSpace(workspaceID)
}

// ResolveRepositoryStorage selects the lifecycle-owned AGS storage identity
// from authenticated subject and workspace context. It does not verify AgentTeam
// membership; callers must supply already-authorized context.
func ResolveRepositoryStorage(workspaceID, subject, scope string) (RepositoryStorage, error) {
	resolvedScope := strings.TrimSpace(scope)
	if resolvedScope == "" {
		resolvedScope = RepositoryStorageScopePersonal
	}
	switch resolvedScope {
	case RepositoryStorageScopePersonal:
		owner := strings.TrimSpace(subject)
		if owner == "" {
			return RepositoryStorage{}, unauthorized("Missing authenticated identity")
		}
		return RepositoryStorage{Scope: RepositoryStorageScopePersonal, OwnerLogin: owner, OwnerKind: RepositoryStorageScopePersonal}, nil
	case RepositoryStorageScopeWorkspace:
		owner := strings.TrimSpace(workspaceID)
		if owner == "" {
			return RepositoryStorage{}, invalidArgument("workspace id is required")
		}
		return RepositoryStorage{Scope: RepositoryStorageScopeWorkspace, OwnerLogin: owner, OwnerKind: RepositoryStorageScopeWorkspace}, nil
	default:
		return RepositoryStorage{}, invalidArgument("storage scope must be personal or workspace")
	}
}

// ResolveRepositoryOwners returns the personal and workspace AGS owner names
// derived from already-authorized request context.
func ResolveRepositoryOwners(workspaceID, subject string) (RepositoryOwners, error) {
	personal, err := ResolveRepositoryStorage(workspaceID, subject, RepositoryStorageScopePersonal)
	if err != nil {
		return RepositoryOwners{}, err
	}
	workspace, err := ResolveRepositoryStorage(workspaceID, subject, RepositoryStorageScopeWorkspace)
	if err != nil {
		return RepositoryOwners{}, err
	}
	return RepositoryOwners{PersonalOwnerLogin: personal.OwnerLogin, WorkspaceOwnerLogin: workspace.OwnerLogin}, nil
}

// RepositoryFullName joins a non-empty owner and repository name.
func RepositoryFullName(owner, name string) string {
	owner = strings.TrimSpace(owner)
	name = strings.TrimSpace(name)
	if owner == "" || name == "" {
		return ""
	}
	return owner + "/" + name
}

// CanonicalResourceRepositories filters resource repository bindings down to
// ensured canonical repositories that can back product activity.
func CanonicalResourceRepositories(repos []reposstore.ResourceRepository) []reposstore.ResourceRepository {
	out := make([]reposstore.ResourceRepository, 0, len(repos))
	for _, repo := range repos {
		if strings.TrimSpace(repo.RepoRole) != reposstore.RepoRolePrimary {
			continue
		}
		if strings.TrimSpace(repo.RepoFullName) == "" {
			continue
		}
		out = append(out, repo)
	}
	return out
}

func kbDerivedRepositoryName(sourceSlug string) string {
	return strings.TrimSpace(sourceSlug) + ".kb"
}

func memoryRepositoryName(slug string) string {
	return strings.TrimSpace(slug) + ".mem"
}

// RepositoryNameStem converts a display value to the lifecycle
// service's lowercase, hyphenated repository stem.
func RepositoryNameStem(value string) string {
	stem := strings.ToLower(strings.TrimSpace(value))
	stem = repositoryNameStemCleanupPattern.ReplaceAllString(stem, "-")
	return strings.Trim(stem, "-")
}

// ResolveRepositoryNameStem selects and validates a resource repository stem,
// reporting which caller-visible field caused a conflict.
func ResolveRepositoryNameStem(name, override, fallback, reservedSuffix, resource string) (string, string, error) {
	override = strings.TrimSpace(override)
	if override != "" {
		if hasReservedRepositorySuffix(override, reservedSuffix) {
			suggested := RepositoryNameStem(strings.TrimSuffix(strings.TrimSpace(override), strings.TrimSpace(reservedSuffix)))
			if suggested == "" {
				suggested = RepositoryNameStem(name)
			}
			return "", RepositoryNameFieldRepoNameOverride, codedConflict(RepositoryReservedSuffixCode, resource+" repository name override must not end with "+reservedSuffix, RepositoryNameConflictDetails{
				Field:             RepositoryNameFieldRepoNameOverride,
				SuggestedRepoName: suggested,
			})
		}
		stem := RepositoryNameStem(override)
		if stem == "" {
			suggested := RepositoryNameStem(name)
			return "", RepositoryNameFieldRepoNameOverride, codedInvalidArgumentDetails(RepositoryNameOverrideInvalidCode, resource+" repository name override is invalid", RepositoryNameConflictDetails{
				Field:             RepositoryNameFieldRepoNameOverride,
				SuggestedRepoName: suggested,
			})
		}
		return stem, RepositoryNameFieldRepoNameOverride, nil
	}
	stem := RepositoryNameStem(name)
	if stem == "" {
		stem = RepositoryNameStem(fallback)
	}
	if stem == "" {
		return "", RepositoryNameFieldName, codedInvalidArgumentDetails(RepositoryNameOverrideInvalidCode, resource+" name is invalid", RepositoryNameConflictDetails{Field: RepositoryNameFieldName})
	}
	return stem, RepositoryNameFieldName, nil
}

func hasReservedRepositorySuffix(value, suffix string) bool {
	suffix = strings.ToLower(strings.TrimSpace(suffix))
	if suffix == "" {
		return false
	}
	return strings.HasSuffix(strings.ToLower(strings.TrimSpace(value)), suffix)
}

// EnsureRepositoryFullNameAvailable rejects a repository name reserved by a
// different active resource binding.
func EnsureRepositoryFullNameAvailable(ctx context.Context, store *reposstore.Store, repoFullName, resourceType, resourceID, repoRole string) error {
	return EnsureRepositoryFullNameAvailableForField(ctx, store, repoFullName, resourceType, resourceID, repoRole, RepositoryNameFieldName, "")
}

// EnsureRepositoryFullNameAvailableForField performs the bounded binding
// lookup and preserves caller-field and suggestion details in conflicts.
func EnsureRepositoryFullNameAvailableForField(ctx context.Context, store *reposstore.Store, repoFullName, resourceType, resourceID, repoRole, field, suggestedRepoName string) error {
	if store == nil {
		return invalidArgument("resource repository store is not configured")
	}
	repoFullName = strings.TrimSpace(repoFullName)
	if repoFullName == "" {
		return invalidArgument("repository full name is required")
	}
	existing, err := store.ListByRepoFullName(ctx, repoFullName)
	if err != nil {
		return err
	}
	for _, repo := range existing {
		if !resourceRepositoryBlocksNameReservation(repo) {
			continue
		}
		if strings.TrimSpace(repo.ResourceType) == strings.TrimSpace(resourceType) &&
			strings.TrimSpace(repo.ResourceID) == strings.TrimSpace(resourceID) &&
			strings.TrimSpace(repo.RepoRole) == strings.TrimSpace(repoRole) {
			continue
		}
		owner, name, _ := strings.Cut(firstNonEmptyString(repo.RepoFullName, repoFullName), "/")
		details := RepositoryNameConflictDetails{
			ConflictOwner:     strings.TrimSpace(owner),
			ConflictRepo:      strings.TrimSpace(name),
			ResourceType:      repo.ResourceType,
			ResourceID:        repo.ResourceID,
			RepoRole:          repo.RepoRole,
			Field:             strings.TrimSpace(field),
			SuggestedRepoName: strings.TrimSpace(suggestedRepoName),
			RepoName:          strings.TrimSpace(name),
			RepoFullName:      strings.TrimSpace(repoFullName),
		}
		return codedConflict(RepositoryNameConflictCode, fmt.Sprintf("repository %s is already used", repoFullName), details)
	}
	return nil
}

// RepositoryAlreadyExistsConflict constructs the lifecycle error returned when
// AGS reports that the resolved repository already exists.
func RepositoryAlreadyExistsConflict(repoFullName, resourceType, resourceID, repoRole, field, suggestedRepoName string) error {
	repoFullName = strings.TrimSpace(repoFullName)
	owner, name, _ := strings.Cut(repoFullName, "/")
	suggestedRepoName = nextRepositoryNameSuggestionForExistingConflict(name, suggestedRepoName)
	return codedConflict(RepositoryAlreadyExistsCode, fmt.Sprintf("repository %s already exists", repoFullName), RepositoryNameConflictDetails{
		ConflictOwner:     strings.TrimSpace(owner),
		ConflictRepo:      strings.TrimSpace(name),
		ResourceType:      strings.TrimSpace(resourceType),
		ResourceID:        strings.TrimSpace(resourceID),
		RepoRole:          strings.TrimSpace(repoRole),
		Field:             strings.TrimSpace(field),
		SuggestedRepoName: strings.TrimSpace(suggestedRepoName),
		RepoName:          strings.TrimSpace(name),
		RepoFullName:      repoFullName,
	})
}

func nextRepositoryNameSuggestionForExistingConflict(repoName, suggestedRepoName string) string {
	suggested := RepositoryNameStem(suggestedRepoName)
	currentStem := strings.TrimSpace(repoName)
	if dot := strings.LastIndex(currentStem, "."); dot > 0 {
		currentStem = currentStem[:dot]
	}
	currentStem = RepositoryNameStem(currentStem)
	if suggested == "" {
		suggested = currentStem
	}
	if suggested == currentStem {
		return suggested + "-2"
	}
	return suggested
}

// CheckRepositoryNameAvailability resolves one candidate and checks existing
// resource bindings without creating an AGS repository or persistence row.
func CheckRepositoryNameAvailability(ctx context.Context, store *reposstore.Store, request RepositoryNameAvailabilityRequest) (RepositoryNameAvailability, error) {
	if store == nil {
		return RepositoryNameAvailability{}, invalidArgument("resource repository store is not configured")
	}
	resourceType := strings.TrimSpace(request.ResourceType)
	storage, err := resolveRepositoryNameAvailabilityStorage(request)
	if err != nil {
		return RepositoryNameAvailability{}, err
	}
	stem, field, err := resolveAvailabilityStem(request, resourceType)
	if err != nil {
		return RepositoryNameAvailability{}, err
	}
	suffixes, err := availabilityResourceSuffixes(resourceType)
	if err != nil {
		return RepositoryNameAvailability{}, err
	}
	repoName := stem + suffixes[0]
	repoFullName := RepositoryFullName(storage.OwnerLogin, repoName)
	suggested, err := FirstAvailableRepositoryNameStem(ctx, store, storage.OwnerLogin, stem, nil, suffixes...)
	if err != nil {
		return RepositoryNameAvailability{}, err
	}
	availability := RepositoryNameAvailability{
		RepoName:          repoName,
		RepoFullName:      repoFullName,
		StorageOwnerLogin: storage.OwnerLogin,
		Available:         suggested == stem,
	}
	if suggested != stem {
		availability.SuggestedRepoName = suggested
		details, err := repositoryNameAvailabilityConflictDetails(ctx, store, storage.OwnerLogin, stem, suffixes, field, suggested)
		if err != nil {
			return RepositoryNameAvailability{}, err
		}
		availability.Conflict = &details
	}
	return availability, nil
}

func repositoryNameAvailabilityConflictDetails(ctx context.Context, store *reposstore.Store, owner, stem string, suffixes []string, field, suggestedRepoName string) (RepositoryNameConflictDetails, error) {
	for _, suffix := range suffixes {
		repoName := stem + strings.TrimSpace(suffix)
		repoFullName := RepositoryFullName(owner, repoName)
		rows, err := store.ListByRepoFullName(ctx, repoFullName)
		if err != nil {
			return RepositoryNameConflictDetails{}, err
		}
		conflict, ok := firstNameReservationConflict(rows)
		if !ok {
			continue
		}
		conflictOwner, conflictRepo, _ := strings.Cut(firstNonEmptyString(conflict.RepoFullName, repoFullName), "/")
		return RepositoryNameConflictDetails{
			ConflictOwner:     strings.TrimSpace(conflictOwner),
			ConflictRepo:      strings.TrimSpace(conflictRepo),
			ResourceType:      conflict.ResourceType,
			ResourceID:        conflict.ResourceID,
			RepoRole:          conflict.RepoRole,
			Field:             strings.TrimSpace(field),
			SuggestedRepoName: strings.TrimSpace(suggestedRepoName),
			RepoName:          repoName,
			RepoFullName:      repoFullName,
		}, nil
	}
	return RepositoryNameConflictDetails{
		Field:             strings.TrimSpace(field),
		SuggestedRepoName: strings.TrimSpace(suggestedRepoName),
		RepoName:          stem + strings.TrimSpace(suffixes[0]),
		RepoFullName:      RepositoryFullName(owner, stem+strings.TrimSpace(suffixes[0])),
	}, nil
}

func resolveRepositoryNameAvailabilityStorage(request RepositoryNameAvailabilityRequest) (RepositoryStorage, error) {
	if owner := strings.TrimSpace(request.StorageOwnerLogin); owner != "" {
		scope := strings.TrimSpace(request.StorageScope)
		if scope == "" {
			scope = RepositoryStorageScopeWorkspace
		}
		return RepositoryStorage{Scope: scope, OwnerLogin: owner, OwnerKind: scope}, nil
	}
	return ResolveRepositoryStorage(strings.TrimSpace(request.WorkspaceID), strings.TrimSpace(request.Subject), strings.TrimSpace(request.StorageScope))
}

func resolveAvailabilityStem(request RepositoryNameAvailabilityRequest, resourceType string) (string, string, error) {
	switch resourceType {
	case reposstore.ResourceTypeMemory:
		return ResolveRepositoryNameStem(request.Name, request.RepoNameOverride, "memory", ".mem", "memory")
	case "kb":
		return ResolveRepositoryNameStem(request.Name, request.RepoNameOverride, "kb", ".kb", "knowledge base")
	case "skill":
		return ResolveRepositoryNameStem(request.Name, request.RepoNameOverride, "skill", ".sk", "skill")
	case reposstore.ResourceTypeKBSource:
		return ResolveRepositoryNameStem(request.Name, request.RepoNameOverride, "source", ".kb", "source")
	default:
		return "", "", invalidArgument("resourceType must be memory, kb, skill, or kb_source")
	}
}

func availabilityResourceSuffixes(resourceType string) ([]string, error) {
	switch resourceType {
	case reposstore.ResourceTypeMemory:
		return []string{".mem"}, nil
	case "kb":
		return []string{".kb"}, nil
	case "skill":
		return []string{".sk"}, nil
	case reposstore.ResourceTypeKBSource:
		return []string{"", ".kb"}, nil
	default:
		return nil, invalidArgument("resourceType must be memory, kb, skill, or kb_source")
	}
}

// FirstAvailableRepositoryNameStem finds a bounded unused stem across supplied
// suffix variants and existing resource bindings.
func FirstAvailableRepositoryNameStem(ctx context.Context, store *reposstore.Store, owner, stem string, excludedStems map[string]struct{}, suffixes ...string) (string, error) {
	owner = strings.TrimSpace(owner)
	stem = RepositoryNameStem(stem)
	if stem == "" {
		stem = "repo"
	}
	excluded := make(map[string]struct{}, len(excludedStems))
	for candidate := range excludedStems {
		candidate = RepositoryNameStem(candidate)
		if candidate != "" {
			excluded[candidate] = struct{}{}
		}
	}
	for index := 0; index < 1000; index++ {
		candidate := stem
		if index > 0 {
			candidate = fmt.Sprintf("%s-%d", stem, index)
		}
		if _, skip := excluded[candidate]; skip {
			continue
		}
		available := true
		for _, suffix := range suffixes {
			rows, err := store.ListByRepoFullName(ctx, RepositoryFullName(owner, candidate+strings.TrimSpace(suffix)))
			if err != nil {
				return "", err
			}
			if _, ok := firstNameReservationConflict(rows); ok {
				available = false
				break
			}
		}
		if available {
			return candidate, nil
		}
	}
	return "", invalidArgument("repository name suggestion could not be generated")
}

func firstNameReservationConflict(rows []reposstore.ResourceRepository) (reposstore.ResourceRepository, bool) {
	for _, row := range rows {
		if resourceRepositoryBlocksNameReservation(row) {
			return row, true
		}
	}
	return reposstore.ResourceRepository{}, false
}

func resourceRepositoryBlocksNameReservation(repo reposstore.ResourceRepository) bool {
	return strings.TrimSpace(repo.ResourceType) != reposstore.ResourceTypeAutomationRepository
}

func firstNonEmptyString(values ...string) string {
	for _, value := range values {
		if strings.TrimSpace(value) != "" {
			return strings.TrimSpace(value)
		}
	}
	return ""
}

// DeleteManagedResourceRepositories deletes explicitly supplied managed AGS
// repositories and records lifecycle state. It does not enumerate unrelated
// repositories or perform best-effort failure suppression.
func DeleteManagedResourceRepositories(ctx context.Context, store *reposstore.Store, deleter RepositoryDeleter, repos []reposstore.ResourceRepository, actor string, now time.Time) error {
	if len(repos) == 0 {
		return nil
	}
	if store == nil {
		return invalidArgument("resource repository store is not configured")
	}
	if deleter == nil {
		err := invalidArgument("resource repository deleter is not configured")
		_ = RecordResourceRepositoryLifecycleFailure(ctx, store, repos, reposstore.ResourceRepositoryLifecycleOperationDelete, actor, now, err)
		return err
	}
	var firstErr error
	for _, repo := range repos {
		event, err := store.CreateLifecycleEvent(ctx, newResourceRepositoryLifecycleEvent(repo, reposstore.ResourceRepositoryLifecycleOperationDelete, reposstore.ResourceRepositoryLifecycleStatusPending, "", actor, now))
		if err != nil {
			if firstErr == nil {
				firstErr = err
			}
			continue
		}
		if err := deleter.DeleteRepository(ctx, IdentityFromStore(repo)); err != nil {
			_ = store.UpdateLifecycleEventStatus(ctx, event.ID, reposstore.ResourceRepositoryLifecycleStatusFailed, err.Error(), actor, now)
			if firstErr == nil {
				firstErr = fmt.Errorf("delete managed repository %s: %w", RepositoryFullName(repo.RepoOwner, repo.RepoName), err)
			}
			continue
		}
		if err := store.UpdateLifecycleEventStatus(ctx, event.ID, reposstore.ResourceRepositoryLifecycleStatusSucceeded, "", actor, now); err != nil {
			if firstErr == nil {
				firstErr = err
			}
		}
	}
	return firstErr
}

// RecordResourceRepositoryLifecycleFailure persists compensation state for the
// explicit repository bindings affected by a failed lifecycle operation.
func RecordResourceRepositoryLifecycleFailure(ctx context.Context, store *reposstore.Store, repos []reposstore.ResourceRepository, operation, actor string, now time.Time, cause error) error {
	if store == nil || len(repos) == 0 {
		return nil
	}
	var firstErr error
	message := ""
	if cause != nil {
		message = cause.Error()
	}
	for _, repo := range repos {
		if _, err := store.CreateLifecycleEvent(ctx, newResourceRepositoryLifecycleEvent(repo, operation, reposstore.ResourceRepositoryLifecycleStatusFailed, message, actor, now)); err != nil && firstErr == nil {
			firstErr = err
		}
	}
	return firstErr
}

func newResourceRepositoryLifecycleEvent(repo reposstore.ResourceRepository, operation, status, lastError, actor string, now time.Time) reposstore.ResourceRepositoryLifecycleEvent {
	if now.IsZero() {
		now = time.Now().UTC()
	}
	return reposstore.ResourceRepositoryLifecycleEvent{
		ID:           newResourceRepositoryLifecycleEventID(),
		WorkspaceID:  repo.WorkspaceID,
		ResourceType: repo.ResourceType,
		ResourceID:   repo.ResourceID,
		RepoRole:     repo.RepoRole,
		RepoOwner:    repo.RepoOwner,
		RepoName:     repo.RepoName,
		RepoFullName: repo.RepoFullName,
		AGSRepoID:    repo.AGSRepoID,
		Operation:    operation,
		Status:       status,
		LastError:    lastError,
		CreatedBy:    strings.TrimSpace(actor),
		UpdatedBy:    strings.TrimSpace(actor),
		CreatedAt:    now,
		UpdatedAt:    now,
	}
}

// ResourceRepositoryFailureRecord builds the persistence-owner record used to
// retain a failed lifecycle attempt for later compensation.
func ResourceRepositoryFailureRecord(workspaceID, resourceType, resourceID, repoRole string, storage RepositoryStorage, repo RepositoryIdentity, actor string, now time.Time) reposstore.ResourceRepository {
	identity := normalizeRepositoryIdentity(repo)
	return reposstore.ResourceRepository{
		WorkspaceID:  strings.TrimSpace(workspaceID),
		ResourceType: strings.TrimSpace(resourceType),
		ResourceID:   strings.TrimSpace(resourceID),
		RepoRole:     strings.TrimSpace(repoRole),
		OwnerKind:    storage.OwnerKind,
		RepoOwner:    identity.Owner,
		RepoName:     identity.Name,
		RepoFullName: identity.FullName,
		AGSRepoID:    identity.ID,
		DefaultRef:   identity.DefaultBranch,
		CreatedBy:    strings.TrimSpace(actor),
		UpdatedBy:    strings.TrimSpace(actor),
		CreatedAt:    now,
		UpdatedAt:    now,
	}
}

func normalizeRepositoryIdentity(repo RepositoryIdentity) RepositoryIdentity {
	identity := RepositoryIdentity{
		Owner:         strings.TrimSpace(repo.Owner),
		Name:          strings.TrimSpace(repo.Name),
		FullName:      strings.TrimSpace(repo.FullName),
		ID:            strings.TrimSpace(repo.ID),
		DefaultBranch: strings.TrimSpace(repo.DefaultBranch),
		Archived:      repo.Archived,
	}
	if identity.FullName == "" && identity.Owner != "" && identity.Name != "" {
		identity.FullName = identity.Owner + "/" + identity.Name
	}
	if (identity.Owner == "" || identity.Name == "") && identity.FullName != "" {
		owner, name, ok := strings.Cut(identity.FullName, "/")
		if ok {
			if identity.Owner == "" {
				identity.Owner = strings.TrimSpace(owner)
			}
			if identity.Name == "" {
				identity.Name = strings.TrimSpace(name)
			}
		}
	}
	return identity
}

func newResourceRepositoryLifecycleEventID() string {
	var b [8]byte
	if _, err := rand.Read(b[:]); err != nil {
		return fmt.Sprintf("rrle_%d", time.Now().UnixNano())
	}
	return "rrle_" + hex.EncodeToString(b[:])
}

func validateSubject(subject string) error {
	if strings.TrimSpace(subject) == "" {
		return unauthorized("Missing authenticated identity")
	}
	return nil
}

// ResourceRepositoryActorFromAgentTeam assembles the AGS-side actor identity used
// when provisioning AGS-backed resources on behalf of an authenticated
// AgentTeam user.
func ResourceRepositoryActorFromAgentTeam(identity trusted.Identity) ResourceRepositoryActor {
	agsActor := agsbridge.FromAgentTeam(identity)
	return ResourceRepositoryActor{
		Provider: agsActor.Provider,
		Subject:  agsActor.Subject,
		Login:    agsActor.Login,
		Name:     agsActor.Name,
		Email:    agsActor.Email,
		Groups:   agsActor.Groups,
	}
}

// RepositoryCommitAuthorFromActor selects Git author presentation fields from
// an already-derived actor, using non-authoritative fallbacks when absent.
func RepositoryCommitAuthorFromActor(actor ResourceRepositoryActor, fallbackName, fallbackEmail string) (string, string) {
	name := firstNonEmptyString(actor.Name, actor.Login, actor.Subject, fallbackName)
	email := firstNonEmptyString(actor.Email, fallbackEmail)
	return name, email
}
