package kb

import (
	"context"
	"errors"
	"fmt"
	"strings"
	"time"

	"github.com/tidbcloud/AgentTeam/internal/service/resource/activity"
	reposservice "github.com/tidbcloud/AgentTeam/internal/service/resource/repos"

	"github.com/tidbcloud/AgentTeam/internal/store/kb"
	reposstore "github.com/tidbcloud/AgentTeam/internal/store/resource/repos"
	"gorm.io/gorm"
)

func (s *KBService) ListSources(ctx context.Context, workspaceID, subject, kbID string, page, perPage int) ([]kb.Source, error) {
	base, err := s.GetKnowledgeBase(ctx, workspaceID, subject, kbID)
	if err != nil {
		return nil, err
	}
	allSources, err := s.store.ListSourcesForKnowledgeBases(ctx, base.WorkspaceID, []string{base.ID})
	if err != nil {
		return nil, err
	}
	visibleAssets := make([]activity.ResourceActivityVisibleAsset, 0, len(allSources))
	for _, source := range allSources {
		visibleAssets = append(visibleAssets, activity.ResourceActivityVisibleAsset{ID: source.ID, Name: firstNonEmptyString(source.Name, base.Name)})
	}
	activity.RefreshVisibleResourceActivity(ctx, s.activityProjector, s.repositoryStore, base.WorkspaceID, reposstore.ResourceTypeKBSource, visibleAssets, s.activityRefreshAfter, nil)
	page, perPage = normalizeKBPagination(page, perPage)
	sources, err := s.store.ListSources(ctx, base.WorkspaceID, base.ID, page, perPage)
	if err != nil {
		return nil, err
	}
	sources = s.withSourceMirrors(ctx, sources)
	sources, err = s.withSourceImportProgress(ctx, sources)
	if err != nil {
		return nil, err
	}
	return s.withSourceRepositoryAccess(ctx, base, subject, sources)
}

// ListRetrievableSources returns only Sources whose names the current requester
// is allowed to see in a Knowledge Q&A scope selector. Unlike ListSources,
// which is the KB management surface, this method applies the same
// repository-access or manager-enabled Q&A-only boundary as the hosted Q&A root
// catalog. Filtering happens before pagination so callers cannot infer hidden
// Source names or counts from sparse pages.
func (s *KBService) ListRetrievableSources(ctx context.Context, workspaceID, subject, kbID string, page, perPage int) ([]kb.Source, error) {
	if err := s.validateScope(workspaceID, subject); err != nil {
		return nil, err
	}
	if s.repositoryStore == nil {
		return nil, errors.New("resource repository store is not configured")
	}
	workspaceID = strings.TrimSpace(workspaceID)
	subject = strings.TrimSpace(subject)
	kbID = strings.TrimSpace(kbID)
	base, err := s.store.GetKnowledgeBase(ctx, workspaceID, kbID)
	if err != nil {
		return nil, mapStoreNotFound(err)
	}
	if !base.IsActive || base.DeletedAt != nil {
		return nil, notFound("knowledge base resource not found")
	}
	canAdministerWorkspace := s.canAdministerWorkspaceForRead(ctx, workspaceID, subject)
	if strings.TrimSpace(base.PublicationState) == KBPublicationDraft &&
		!s.knowledgeBaseVisibleToSubjectWithWorkspaceAuthority(ctx, base, subject, canAdministerWorkspace) {
		return nil, forbidden("knowledge base is not accessible")
	}

	grant, grantErr := s.store.GetKnowledgeBaseGrant(ctx, workspaceID, base.ID, "user", subject)
	accessWithoutSourceGrant := false
	visibleSourceIDs := make(map[string]struct{})
	switch {
	case grantErr == nil:
		if strings.TrimSpace(grant.Role) == "" || strings.TrimSpace(grant.Role) == "none" {
			return nil, forbidden("knowledge base is not accessible")
		}
		accessWithoutSourceGrant = base.AllowKnowledgeQAWithoutSourceAccess
		if !accessWithoutSourceGrant && !knowledgeQASourceProfileCanBrowse(grant.RepositoryAccessProfile) {
			return nil, forbidden("knowledge base is not accessible")
		}
		if accessWithoutSourceGrant {
			break
		}
		rows, err := s.repositoryStore.ListAccessGrantsForIntent(ctx, workspaceID, reposstore.AccessIntentTypeKBGrant, grant.ID)
		if err != nil {
			return nil, err
		}
		for _, row := range rows {
			if row.SubjectType != "user" || strings.TrimSpace(row.SubjectID) != subject ||
				row.ResourceType != reposstore.ResourceTypeKBSource ||
				!row.Materialized || strings.TrimSpace(row.Status) != reposstore.AccessStatusCurrent {
				continue
			}
			visibleSourceIDs[strings.TrimSpace(row.ResourceID)] = struct{}{}
		}
	case errors.Is(grantErr, gorm.ErrRecordNotFound):
		if normalizedKBDefaultWorkspaceMemberRole(base.DefaultWorkspaceMemberRole) == "" {
			return nil, forbidden("knowledge base is not accessible")
		}
		accessWithoutSourceGrant = base.AllowKnowledgeQAWithoutSourceAccess ||
			knowledgeQASourceProfileCanBrowse(base.DefaultRepositoryAccessProfile)
		if !accessWithoutSourceGrant {
			return nil, forbidden("knowledge base is not accessible")
		}
	default:
		return nil, grantErr
	}

	allSources, err := s.store.ListSourcesForKnowledgeBases(ctx, workspaceID, []string{base.ID})
	if err != nil {
		return nil, err
	}
	visible := make([]kb.Source, 0, len(allSources))
	for _, source := range allSources {
		if !accessWithoutSourceGrant {
			if _, ok := visibleSourceIDs[source.ID]; !ok {
				continue
			}
		}
		visible = append(visible, source)
	}

	page, perPage = normalizeKBPagination(page, perPage)
	start := (page - 1) * perPage
	if start >= len(visible) {
		return []kb.Source{}, nil
	}
	end := start + perPage
	if end > len(visible) {
		end = len(visible)
	}
	sources := append([]kb.Source(nil), visible[start:end]...)
	visibleAssets := make([]activity.ResourceActivityVisibleAsset, 0, len(sources))
	for _, source := range sources {
		visibleAssets = append(visibleAssets, activity.ResourceActivityVisibleAsset{ID: source.ID, Name: firstNonEmptyString(source.Name, base.Name)})
	}
	activity.RefreshVisibleResourceActivity(ctx, s.activityProjector, s.repositoryStore, base.WorkspaceID, reposstore.ResourceTypeKBSource, visibleAssets, s.activityRefreshAfter, nil)
	sources = s.withSourceMirrors(ctx, sources)
	return s.withSourceImportProgress(ctx, sources)
}

func knowledgeQASourceProfileCanBrowse(profile string) bool {
	switch kb.NormalizeKnowledgeBaseRepositoryAccessProfile(profile) {
	case kb.KnowledgeBaseRepositoryAccessProfileRead,
		kb.KnowledgeBaseRepositoryAccessProfileWrite,
		kb.KnowledgeBaseRepositoryAccessProfileManage:
		return true
	default:
		return false
	}
}

func (s *KBService) CreateSource(ctx context.Context, workspaceID, subject, kbID string, input KBSourceMutation) (kb.Source, error) {
	base, err := s.GetKnowledgeBase(ctx, workspaceID, subject, kbID)
	if err != nil {
		return kb.Source{}, err
	}
	if !roleCanEdit(s.knowledgeBaseRole(ctx, base, subject)) {
		return kb.Source{}, forbidden("knowledge base requires editor access")
	}
	name := strings.TrimSpace(input.Name)
	if name == "" {
		return kb.Source{}, invalidArgument("source name is required")
	}
	sourceType := strings.TrimSpace(input.SourceType)
	switch sourceType {
	case "file_repository", "codebase", "external_connector", "database_schema":
	default:
		return kb.Source{}, invalidArgument("source_type must be file_repository, codebase, external_connector, or database_schema")
	}
	sourceID := newKBID("src")
	defaultRef := strings.TrimSpace(input.DefaultRef)
	if defaultRef == "" && strings.TrimSpace(input.RepoMode) != "mirror" {
		defaultRef = "main"
	}
	now := s.now()
	status := KBSourceStatusReady
	indexingMode := firstNonEmptyString(input.IndexingMode, defaultKBSourceIndexingMode(sourceType))
	syncStatus := firstNonEmptyString(input.SyncStatus, status)
	indexStatus := firstNonEmptyString(input.IndexStatus, status)
	source := kb.Source{ID: sourceID, WorkspaceID: strings.TrimSpace(workspaceID), KnowledgeBaseID: strings.TrimSpace(kbID), SourceType: sourceType, Status: status, Name: name, Description: strings.TrimSpace(input.Description), DefaultRef: defaultRef, IndexingMode: indexingMode, SyncStatus: syncStatus, IndexStatus: indexStatus, AnalysisStatus: "pending", FreshnessStatus: "unknown", Config: strings.TrimSpace(input.Config), CreatedBy: strings.TrimSpace(subject), UpdatedBy: strings.TrimSpace(subject), CreatedAt: now, UpdatedAt: now}
	source.Status = KBSourceStatusQueued
	source.SyncStatus = KBSourceStatusQueued
	source.IndexStatus = KBSourceStatusQueued
	var mirror *kb.SourceMirror
	if strings.TrimSpace(input.RepoMode) == "mirror" {
		prepared, err := s.prepareSourceMirrorMutation(ctx, source, input.Mirror, subject, now)
		if err != nil {
			return kb.Source{}, err
		}
		mirror = &prepared
		source.SyncStatus = mirror.SyncStatus
	}
	hasMirrorAccessToken := mirror != nil && strings.TrimSpace(input.Mirror.AccessToken) != ""
	// Preflight: verify the remote URL and access token can reach the
	// upstream repository before provisioning any AGS repos or configuring
	// token storage. This catches token permission errors and unreachable
	// remotes early, preventing orphan AGS repos from partial provisioning
	// failures.
	if mirror != nil && s.sourceMirrorPreflighter != nil {
		if err := s.sourceMirrorPreflighter.PreflightSourceMirror(ctx, SourceMirrorPreflightRequest{
			WorkspaceID:       base.WorkspaceID,
			KnowledgeBaseID:   base.ID,
			SourceID:          source.ID,
			RemoteURL:         mirror.RemoteURL,
			DefaultRef:        mirror.DefaultRef,
			UpstreamType:      mirror.UpstreamType,
			CredentialRef:     mirror.CredentialRef,
			AccessToken:       input.Mirror.AccessToken,
			AllowPrivateHosts: s.allowPrivateSourceMirrorHosts,
		}); err != nil {
			return kb.Source{}, err
		}
	}
	if hasMirrorAccessToken {
		if err := s.ensureMirrorAccessTokenStorageConfigured(); err != nil {
			return kb.Source{}, err
		}
	}
	repositories, err := s.provisionSourceRepositories(ctx, base, sourceID, name, input, subject, now)
	if err != nil {
		return kb.Source{}, err
	}
	if hasMirrorAccessToken {
		mirrorRepo := findMirrorResourceRepository(repositories)
		if mirrorRepo == nil {
			err := fmt.Errorf("mirror repository was not provisioned")
			_ = reposservice.RecordResourceRepositoryLifecycleFailure(ctx, s.repositoryStore, repositories, reposstore.ResourceRepositoryLifecycleOperationCreateBind, subject, now, err)
			if cleanupErr := reposservice.DeleteManagedResourceRepositories(ctx, s.repositoryStore, s.resourceRepositoryDeleter(), repositories, subject, now); cleanupErr != nil {
				return kb.Source{}, fmt.Errorf("%w (cleanup provisioned repositories: %v)", err, cleanupErr)
			}
			return kb.Source{}, err
		}
		credentialRef, err := s.storeMirrorAccessToken(ctx, base.WorkspaceID, mirrorRepo.RepoOwner, mirrorRepo.RepoName, input.Mirror.AccessToken, subject)
		if err != nil {
			_ = reposservice.RecordResourceRepositoryLifecycleFailure(ctx, s.repositoryStore, repositories, reposstore.ResourceRepositoryLifecycleOperationCreateBind, subject, now, err)
			if cleanupErr := reposservice.DeleteManagedResourceRepositories(ctx, s.repositoryStore, s.resourceRepositoryDeleter(), repositories, subject, now); cleanupErr != nil {
				return kb.Source{}, fmt.Errorf("store mirror access token failed: %w (cleanup provisioned repositories: %v)", err, cleanupErr)
			}
			return kb.Source{}, err
		}
		mirror.CredentialRef = credentialRef
	}
	var created kb.Source
	err = s.store.Transaction(ctx, func(tx *kb.Store) error {
		var err error
		created, err = tx.CreateSource(ctx, source)
		if err != nil {
			return err
		}
		if mirror != nil {
			mirror.SourceID = created.ID
			if _, err := tx.CreateSourceMirror(ctx, *mirror); err != nil {
				return err
			}
		}
		audit := s.auditEvent(source.WorkspaceID, source.KnowledgeBaseID, subject, "kb.source.create", "source", source.ID, "Created knowledge base source", "")
		if _, err = tx.CreateAuditEvent(ctx, audit); err != nil {
			return err
		}
		return s.writeKnowledgeBaseAuditActivityWithStore(ctx, s.knowledgeBaseActivityStore(tx), audit)
	})
	if err != nil {
		_ = reposservice.RecordResourceRepositoryLifecycleFailure(ctx, s.repositoryStore, repositories, reposstore.ResourceRepositoryLifecycleOperationCreateBind, subject, now, err)
		if cleanupErr := reposservice.DeleteManagedResourceRepositories(ctx, s.repositoryStore, s.resourceRepositoryDeleter(), repositories, subject, now); cleanupErr != nil {
			return kb.Source{}, fmt.Errorf("create source failed: %w (cleanup provisioned repositories: %v)", err, cleanupErr)
		}
		return kb.Source{}, err
	}
	for _, repo := range repositories {
		repoUpsert, err := s.repositoryStore.Upsert(ctx, repo)
		if err != nil {
			_ = reposservice.RecordResourceRepositoryLifecycleFailure(ctx, s.repositoryStore, repositories, reposstore.ResourceRepositoryLifecycleOperationCreateBind, subject, now, err)
			// The source row exists in the DB but resource binding failed.
			// Mark the source as failed so the user can see and delete it,
			// and clean up the AGS repos that were provisioned but never
			// bound. This prevents orphan AGS repos with no visible source.
			_ = s.markSourceProvisioningFailed(ctx, created, subject, err)
			_ = reposservice.DeleteManagedResourceRepositories(ctx, s.repositoryStore, s.resourceRepositoryDeleter(), repositories, subject, now)
			return kb.Source{}, err
		}
		if repoUpsert.Created {
			if err := activity.WriteResourceRepositoryLifecycleActivity(ctx, s.activityStore, repoUpsert.Repository, activity.ResourceRepositoryActivityCreated, subject, now); err != nil {
				_ = reposservice.RecordResourceRepositoryLifecycleFailure(ctx, s.repositoryStore, repositories, reposstore.ResourceRepositoryLifecycleOperationCreateBind, subject, now, err)
				_ = s.markSourceProvisioningFailed(ctx, created, subject, err)
				_ = reposservice.DeleteManagedResourceRepositories(ctx, s.repositoryStore, s.resourceRepositoryDeleter(), repositories, subject, now)
				return kb.Source{}, err
			}
		}
	}
	created.Status = status
	created.SyncStatus = syncStatus
	created.IndexStatus = indexStatus
	created.UpdatedBy = strings.TrimSpace(subject)
	created.UpdatedAt = now
	updated, err := s.store.UpdateSource(ctx, created)
	if err != nil {
		_ = reposservice.RecordResourceRepositoryLifecycleFailure(ctx, s.repositoryStore, repositories, reposstore.ResourceRepositoryLifecycleOperationCreateBind, subject, now, err)
		return kb.Source{}, err
	}
	created = updated
	if mirror == nil {
		resolver, ok := s.wikiStore.(reposservice.RepositorySnapshotResolver)
		if !ok {
			err := invalidArgument("source repository snapshot resolver is not configured")
			_ = s.markSourceProvisioningFailed(ctx, created, subject, err)
			return kb.Source{}, err
		}
		primary := findSourceRepositoryForRole(repositories, reposstore.RepoRolePrimary)
		if primary == nil {
			err := invalidArgument("canonical source repository was not provisioned")
			_ = s.markSourceProvisioningFailed(ctx, created, subject, err)
			return kb.Source{}, err
		}
		commitSHA, resolveErr := resolver.ResolveRepositorySnapshot(ctx, reposservice.RepositoryIdentity{
			Owner: primary.RepoOwner, Name: primary.RepoName, FullName: primary.RepoFullName, ID: primary.AGSRepoID,
			DefaultBranch: firstNonEmptyString(primary.DefaultRef, created.DefaultRef),
		})
		if resolveErr != nil {
			_ = s.markSourceProvisioningFailed(ctx, created, subject, resolveErr)
			return kb.Source{}, resolveErr
		}
		if _, _, advanceErr := s.store.AdvanceSourceRevision(ctx, kb.AdvanceSourceRevisionParams{Revision: kb.SourceRevision{
			ID: newKBID("rev"), WorkspaceID: created.WorkspaceID, KnowledgeBaseID: created.KnowledgeBaseID,
			SourceID: created.ID, SourceCommitSHA: commitSHA, FreshnessStatus: "current", CreatedAt: now, UpdatedAt: now,
		}}); advanceErr != nil {
			_ = s.markSourceProvisioningFailed(ctx, created, subject, advanceErr)
			return kb.Source{}, advanceErr
		}
	}
	if err := s.reconcileRepositoryAccessAfterSourceCreate(ctx, base, created, repositories, subject); err != nil {
		return kb.Source{}, err
	}
	if err == nil && mirror != nil {
		loaded, loadErr := s.store.GetSourceMirror(ctx, created.WorkspaceID, created.KnowledgeBaseID, created.ID)
		if loadErr == nil {
			s.decorateSourceMirrorCredentialSummaries(ctx, []*kb.SourceMirror{&loaded})
			created.Mirror = &loaded
		}
	}
	decorated, err := s.withSourceRepositoryAccess(ctx, base, subject, []kb.Source{created})
	if err != nil {
		return kb.Source{}, err
	}
	return decorated[0], nil
}

func (s *KBService) UpdateSource(ctx context.Context, workspaceID, subject, kbID, sourceID string, input KBSourceMutation) (kb.Source, error) {
	base, err := s.GetKnowledgeBase(ctx, workspaceID, subject, kbID)
	if err != nil {
		return kb.Source{}, err
	}
	if !roleCanEdit(s.knowledgeBaseRole(ctx, base, subject)) {
		return kb.Source{}, forbidden("knowledge base requires editor access")
	}
	source, err := s.store.GetSource(ctx, base.WorkspaceID, base.ID, sourceID)
	if err != nil {
		return kb.Source{}, mapStoreNotFound(err)
	}
	writeRoute, err := s.beginKBSourceWrite(ctx, source.WorkspaceID, source.ID)
	if err != nil {
		return kb.Source{}, err
	}
	if writeRoute.native {
		return kb.Source{}, nativeKBSourceLegacyOperationConflict()
	}
	name := strings.TrimSpace(input.Name)
	if name == "" {
		return kb.Source{}, invalidArgument("source name is required")
	}
	source.Name = name
	source.Description = strings.TrimSpace(input.Description)
	if defaultRef := strings.TrimSpace(input.DefaultRef); defaultRef != "" {
		source.DefaultRef = defaultRef
	}
	if indexingMode := strings.TrimSpace(input.IndexingMode); indexingMode != "" {
		source.IndexingMode = indexingMode
	}
	source.UpdatedBy = strings.TrimSpace(subject)
	source.UpdatedAt = s.now()
	if err := s.ensureKBSourceWriteCurrent(ctx, source.WorkspaceID, source.ID, writeRoute); err != nil {
		return kb.Source{}, err
	}
	updated, err := s.store.UpdateSource(ctx, source)
	if err != nil {
		return kb.Source{}, err
	}
	decorated, err := s.withSourceRepositoryAccess(ctx, base, subject, []kb.Source{updated})
	if err != nil {
		return kb.Source{}, err
	}
	return decorated[0], nil
}

func (s *KBService) DeleteSource(ctx context.Context, workspaceID, subject, kbID, sourceID string) error {
	base, err := s.GetKnowledgeBase(ctx, workspaceID, subject, kbID)
	if err != nil {
		return err
	}
	if !s.canManageKnowledgeBase(ctx, base, subject) {
		return forbidden("knowledge base requires manage access")
	}
	source, err := s.store.GetSource(ctx, base.WorkspaceID, base.ID, sourceID)
	if err != nil {
		return mapStoreNotFound(err)
	}
	writeRoute, err := s.beginKBSourceWrite(ctx, source.WorkspaceID, source.ID)
	if err != nil {
		return err
	}
	if writeRoute.native {
		return nativeKBSourceLegacyOperationConflict()
	}
	if s.repositoryStore == nil {
		return invalidArgument("resource repository store is not configured")
	}
	repos, err := s.repositoryStore.ListForResource(ctx, source.WorkspaceID, reposstore.ResourceTypeKBSource, source.ID)
	if err != nil {
		return err
	}
	native, err := s.kbSourceNativeActive(ctx, source.WorkspaceID, source.ID)
	if err != nil {
		return err
	}
	now := s.now()
	if err := s.ensureKBSourceWriteCurrent(ctx, source.WorkspaceID, source.ID, writeRoute); err != nil {
		return err
	}
	if !native {
		if err := reposservice.DeleteManagedResourceRepositories(ctx, s.repositoryStore, s.resourceRepositoryDeleter(), repos, subject, now); err != nil {
			return err
		}
	}
	err = s.store.Transaction(ctx, func(tx *kb.Store) error {
		if _, err := tx.CancelSourceJobs(ctx, source.WorkspaceID, source.KnowledgeBaseID, source.ID, KBJobTypeSyncSourceMirror, now); err != nil {
			return err
		}
		if err := tx.DeleteSourceMirror(ctx, source.WorkspaceID, source.KnowledgeBaseID, source.ID); err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
			return err
		}
		if err := tx.DeleteKnowledgeBaseSourceRepositoryAccessOverridesForSource(ctx, source.WorkspaceID, source.KnowledgeBaseID, source.ID); err != nil {
			return err
		}
		if err := tx.DeleteSource(ctx, source.WorkspaceID, source.KnowledgeBaseID, source.ID); err != nil {
			return mapStoreNotFound(err)
		}
		repoTx := s.repositoryStore.WithDB(tx.DB())
		if err := deleteAutomationRepositoryBindingsForManagedRepositories(ctx, repoTx, source.WorkspaceID, repos); err != nil {
			return err
		}
		deletedRepos, err := repoTx.DeleteForResource(ctx, source.WorkspaceID, reposstore.ResourceTypeKBSource, source.ID)
		if err != nil {
			return err
		}
		for _, repo := range deletedRepos {
			if err := activity.WriteResourceRepositoryLifecycleActivity(ctx, s.knowledgeBaseActivityStore(tx), repo, activity.ResourceRepositoryActivityDeleted, strings.TrimSpace(subject), now); err != nil {
				return err
			}
		}
		audit := s.auditEvent(source.WorkspaceID, source.KnowledgeBaseID, subject, "kb.source.delete", "source", source.ID, "Deleted knowledge base source", "")
		if _, err := tx.CreateAuditEvent(ctx, audit); err != nil {
			return err
		}
		return s.writeKnowledgeBaseAuditActivityWithStore(ctx, s.knowledgeBaseActivityStore(tx), audit)
	})
	if err != nil {
		_ = reposservice.RecordResourceRepositoryLifecycleFailure(ctx, s.repositoryStore, repos, reposstore.ResourceRepositoryLifecycleOperationDeleteCleanup, subject, now, err)
	}
	return err
}

func (s *KBService) ensureSourceRepositories(ctx context.Context, base kb.KnowledgeBase, sourceID, sourceName string, input KBSourceMutation, subject string, unavailableAutoNameStems map[string]struct{}) ([]reposstore.ResourceRepository, error) {
	if s.wikiStore == nil {
		return nil, fmt.Errorf("KB repository provisioner is not configured")
	}
	if s.repositoryStore == nil {
		return nil, fmt.Errorf("resource repository store is not configured")
	}
	now := s.now()
	storageOwner, storageScope, slug, field, mirrorMode, err := plannedKBSourceRepositoryStem(base, sourceName, input)
	if err != nil {
		return nil, err
	}
	for _, repoName := range []string{slug, kbDerivedRepositoryName(slug)} {
		if err := s.cleanupDeletedKBSourceRepositoryMappingsForFullName(ctx, repositoryFullName(storageOwner, repoName)); err != nil {
			return nil, err
		}
	}
	autoName := strings.TrimSpace(input.Repo.RepoNameOverride) == ""
	if autoName {
		slug, err = s.firstAvailableSourceRepositoryStem(ctx, storageOwner, slug, unavailableAutoNameStems)
		if err != nil {
			return nil, err
		}
	}
	type repoRequest struct {
		role        string
		ownerKind   string
		owner       string
		name        string
		kind        string
		description string
		visibility  string
		userVisible bool
		features    reposservice.AGSRepositoryFeatures
		autoInit    bool
	}
	var requests []repoRequest
	if mirrorMode {
		requests = []repoRequest{
			{role: reposstore.RepoRoleMirror, ownerKind: storageScope, owner: storageOwner, name: slug, kind: "mirror", description: "AgentTeam mirror repository for " + sourceName, visibility: "private", userVisible: true, features: reposservice.AGSRepositoryFeatures{Issues: true, Wiki: true}, autoInit: false},
			{role: reposstore.RepoRoleInsights, ownerKind: storageScope, owner: storageOwner, name: kbDerivedRepositoryName(slug), kind: "kb_insights", description: "AgentTeam KB insights repository for " + sourceName, visibility: "private", userVisible: false, features: reposservice.AGSRepositoryFeatures{Wiki: true}, autoInit: true},
		}
	} else {
		requests = []repoRequest{
			{role: reposstore.RepoRolePrimary, ownerKind: storageScope, owner: storageOwner, name: slug, kind: "kb_source", description: "AgentTeam KB source repository for " + sourceName, visibility: "private", userVisible: true, features: reposservice.AGSRepositoryFeatures{Issues: true, Wiki: true}, autoInit: true},
			{role: reposstore.RepoRoleInsights, ownerKind: storageScope, owner: storageOwner, name: kbDerivedRepositoryName(slug), kind: "kb_insights", description: "AgentTeam KB insights repository for " + sourceName, visibility: "private", userVisible: false, features: reposservice.AGSRepositoryFeatures{Wiki: true}, autoInit: true},
		}
	}
	sourceType := strings.TrimSpace(input.SourceType)
	repos := make([]reposstore.ResourceRepository, 0, len(requests))
	suggestedRepoName := slug
	if !autoName {
		suggestionExclusions := map[string]struct{}{slug: {}}
		suggestedRepoName, err = s.firstAvailableSourceRepositoryStem(ctx, storageOwner, slug, suggestionExclusions)
		if err != nil {
			return nil, err
		}
	}
	for _, request := range requests {
		if err := reposservice.EnsureRepositoryFullNameAvailableForField(ctx, s.repositoryStore, repositoryFullName(request.owner, request.name), reposstore.ResourceTypeKBSource, sourceID, request.role, field, suggestedRepoName); err != nil {
			return nil, err
		}
		resolved, err := s.wikiStore.EnsureRepository(ctx, reposservice.AGSRepositoryEnsureRequest{
			WorkspaceID:       base.WorkspaceID,
			ResourceType:      reposstore.ResourceTypeKBSource,
			ResourceID:        sourceID,
			RepoRole:          request.role,
			Owner:             request.owner,
			Name:              request.name,
			Description:       request.description,
			Kind:              request.kind,
			SourceType:        sourceType,
			Visibility:        request.visibility,
			UserVisible:       request.userVisible,
			AutoInit:          request.autoInit,
			RejectExisting:    true,
			NameConflictField: field,
			SuggestedRepoName: suggestedRepoName,
			DefaultBranch:     firstNonEmptyString(input.DefaultRef, input.Mirror.DefaultRef, "main"),
			Features:          request.features,
			Actor:             input.SourceRepositoryActor,
		})
		if err != nil {
			// If EnsureRepository returned a non-empty identity, the AGS repo
			// was created but a post-create step (e.g. grant or response
			// decode) failed. Append it to repos so the caller's cleanup path
			// can delete the orphan.
			if resolved.FullName != "" {
				repos = append(repos, reposstore.ResourceRepository{
					ID:           newKBID("rr"),
					WorkspaceID:  base.WorkspaceID,
					ResourceType: reposstore.ResourceTypeKBSource,
					ResourceID:   sourceID,
					RepoRole:     request.role,
					OwnerKind:    request.ownerKind,
					RepoOwner:    resolved.Owner,
					RepoName:     resolved.Name,
					RepoFullName: resolved.FullName,
					AGSRepoID:    resolved.ID,
					DefaultRef:   firstNonEmptyString(resolved.DefaultBranch, input.DefaultRef, input.Mirror.DefaultRef, "main"),
					Visibility:   request.visibility,
					UserVisible:  request.userVisible,
					CreatedBy:    strings.TrimSpace(subject),
					UpdatedBy:    strings.TrimSpace(subject),
					CreatedAt:    now,
					UpdatedAt:    now,
				})
			}
			return repos, err
		}
		repos = append(repos, reposstore.ResourceRepository{
			ID:           newKBID("rr"),
			WorkspaceID:  base.WorkspaceID,
			ResourceType: reposstore.ResourceTypeKBSource,
			ResourceID:   sourceID,
			RepoRole:     request.role,
			OwnerKind:    request.ownerKind,
			RepoOwner:    resolved.Owner,
			RepoName:     resolved.Name,
			RepoFullName: resolved.FullName,
			AGSRepoID:    resolved.ID,
			DefaultRef:   firstNonEmptyString(resolved.DefaultBranch, input.DefaultRef, input.Mirror.DefaultRef, "main"),
			Visibility:   request.visibility,
			UserVisible:  request.userVisible,
			CreatedBy:    strings.TrimSpace(subject),
			UpdatedBy:    strings.TrimSpace(subject),
			CreatedAt:    now,
			UpdatedAt:    now,
		})
	}
	return repos, nil
}

func (s *KBService) firstAvailableSourceRepositoryStem(ctx context.Context, owner, stem string, unavailableStems map[string]struct{}) (string, error) {
	if unavailableStems == nil {
		unavailableStems = map[string]struct{}{}
	}
	for attempt := 0; attempt < maxKBSourceRepositoryAutoNameAttempts; attempt++ {
		candidate, err := reposservice.FirstAvailableRepositoryNameStem(ctx, s.repositoryStore, owner, stem, unavailableStems, "", ".kb")
		if err != nil {
			return "", err
		}
		primaryExists, err := s.wikiStore.RepositoryExists(ctx, reposservice.RepositoryIdentity{Owner: owner, Name: candidate})
		if err != nil {
			return "", err
		}
		insightsExists, err := s.wikiStore.RepositoryExists(ctx, reposservice.RepositoryIdentity{Owner: owner, Name: kbDerivedRepositoryName(candidate)})
		if err != nil {
			return "", err
		}
		if !primaryExists && !insightsExists {
			return candidate, nil
		}
		unavailableStems[candidate] = struct{}{}
	}
	return "", invalidArgument("source repository name allocation exhausted")
}

const maxKBSourceRepositoryAutoNameAttempts = 1000

func (s *KBService) provisionSourceRepositories(ctx context.Context, base kb.KnowledgeBase, sourceID, sourceName string, input KBSourceMutation, subject string, now time.Time) ([]reposstore.ResourceRepository, error) {
	autoName := strings.TrimSpace(input.Repo.RepoNameOverride) == ""
	unavailableAutoNameStems := map[string]struct{}{}
	for attempt := 0; attempt < maxKBSourceRepositoryAutoNameAttempts; attempt++ {
		repositories, err := s.ensureSourceRepositories(ctx, base, sourceID, sourceName, input, subject, unavailableAutoNameStems)
		if err == nil {
			return repositories, nil
		}
		_ = reposservice.RecordResourceRepositoryLifecycleFailure(ctx, s.repositoryStore, repositories, reposstore.ResourceRepositoryLifecycleOperationCreateBind, subject, now, err)
		// Repository creation spans MeshX and AGS. Clean up a primary repo if
		// provisioning its paired insights repo failed before either retrying
		// another automatically selected stem or returning the original error.
		if cleanupErr := reposservice.DeleteManagedResourceRepositories(ctx, s.repositoryStore, s.resourceRepositoryDeleter(), repositories, subject, now); cleanupErr != nil {
			return nil, fmt.Errorf("%w (cleanup provisioned repositories: %v)", err, cleanupErr)
		}
		if !autoName || attempt+1 >= maxKBSourceRepositoryAutoNameAttempts {
			return nil, err
		}
		details, ok := reposservice.Details(err).(reposservice.RepositoryNameConflictDetails)
		if !ok || !errors.Is(err, reposservice.ErrConflict) {
			return nil, err
		}
		conflictedStem := strings.TrimSuffix(firstNonEmptyString(details.RepoName, details.ConflictRepo), ".kb")
		conflictedStem = reposservice.RepositoryNameStem(conflictedStem)
		if conflictedStem == "" {
			return nil, err
		}
		if _, repeated := unavailableAutoNameStems[conflictedStem]; repeated {
			return nil, err
		}
		unavailableAutoNameStems[conflictedStem] = struct{}{}
	}
	return nil, invalidArgument("source repository name allocation exhausted")
}

func plannedKBSourceRepositoryStem(base kb.KnowledgeBase, sourceName string, input KBSourceMutation) (storageOwner string, storageScope string, slug string, field string, mirrorMode bool, err error) {
	rawName := firstNonEmptyString(input.Repo.Name, sourceName)
	slug, field, err = reposservice.ResolveRepositoryNameStem(rawName, input.Repo.RepoNameOverride, "source", ".kb", "source")
	if err != nil {
		return "", "", "", "", false, err
	}
	storageOwner = strings.TrimSpace(base.StorageOwnerLogin)
	storageScope = strings.TrimSpace(base.StorageScope)
	if storageOwner == "" || storageScope == "" {
		return "", "", "", "", false, invalidArgument("knowledge base storage owner is not configured")
	}
	mirrorMode = strings.TrimSpace(input.RepoMode) == "mirror"
	return storageOwner, storageScope, slug, field, mirrorMode, nil
}

func deleteAutomationRepositoryBindingsForManagedRepositories(ctx context.Context, store *reposstore.Store, workspaceID string, repos []reposstore.ResourceRepository) error {
	for _, repo := range repos {
		repoFullName := firstNonEmptyString(repo.RepoFullName, repositoryFullName(repo.RepoOwner, repo.RepoName))
		if repoFullName == "" {
			continue
		}
		resourceID := reposstore.AutomationRepositoryResourceID(repoFullName)
		if _, err := store.DeleteForResource(ctx, workspaceID, reposstore.ResourceTypeAutomationRepository, resourceID); err != nil {
			return err
		}
	}
	return nil
}

func (s *KBService) cleanupDeletedKBSourceRepositoryMappingsForFullName(ctx context.Context, repoFullName string) error {
	if s == nil || s.store == nil {
		return invalidArgument("kb store is not configured")
	}
	if s.repositoryStore == nil {
		return invalidArgument("resource repository store is not configured")
	}
	repoFullName = strings.TrimSpace(repoFullName)
	if repoFullName == "" {
		return nil
	}
	rows, err := s.repositoryStore.ListByRepoFullName(ctx, repoFullName)
	if err != nil {
		return err
	}
	checked := make(map[string]struct{}, len(rows))
	for _, row := range rows {
		if strings.TrimSpace(row.ResourceType) != reposstore.ResourceTypeKBSource {
			continue
		}
		workspaceID := strings.TrimSpace(row.WorkspaceID)
		sourceID := strings.TrimSpace(row.ResourceID)
		if workspaceID == "" || sourceID == "" {
			continue
		}
		key := workspaceID + "\x00" + sourceID
		if _, ok := checked[key]; ok {
			continue
		}
		checked[key] = struct{}{}
		if _, err := s.store.GetSourceByID(ctx, workspaceID, sourceID); err == nil {
			continue
		} else if !errors.Is(err, gorm.ErrRecordNotFound) {
			return err
		}
		if _, err := s.repositoryStore.DeleteForResource(ctx, workspaceID, reposstore.ResourceTypeKBSource, sourceID); err != nil {
			return err
		}
	}
	return nil
}

func (s *KBService) ensureSourceCreatorRepositoryAccessIntent(ctx context.Context, base kb.KnowledgeBase, subject string) (kb.KnowledgeBaseGrant, string, bool, error) {
	subject = strings.TrimSpace(subject)
	previousProfile := kb.KnowledgeBaseRepositoryAccessProfileNone
	if subject == "" || s == nil || s.store == nil {
		return kb.KnowledgeBaseGrant{}, previousProfile, false, nil
	}
	grant, err := s.store.GetKnowledgeBaseGrant(ctx, base.WorkspaceID, base.ID, "user", subject)
	if err != nil {
		if !errors.Is(err, gorm.ErrRecordNotFound) {
			return kb.KnowledgeBaseGrant{}, previousProfile, false, err
		}
		if strings.TrimSpace(base.CreatedBy) != subject {
			return kb.KnowledgeBaseGrant{}, previousProfile, false, nil
		}
		now := s.now()
		created, err := s.store.UpsertKnowledgeBaseGrant(ctx, kb.KnowledgeBaseGrant{
			ID:                      newKBID("kbg"),
			WorkspaceID:             base.WorkspaceID,
			KnowledgeBaseID:         base.ID,
			SubjectType:             "user",
			SubjectID:               subject,
			Role:                    "owner",
			RepositoryAccessProfile: kb.KnowledgeBaseRepositoryAccessProfileWrite,
			CreatedBy:               subject,
			UpdatedBy:               subject,
			CreatedAt:               now,
			UpdatedAt:               now,
		})
		return created, previousProfile, true, err
	}
	previousProfile = kb.NormalizeKnowledgeBaseRepositoryAccessProfile(grant.RepositoryAccessProfile)
	role := strings.TrimSpace(grant.Role)
	if strings.TrimSpace(base.CreatedBy) == subject {
		role = "owner"
	}
	if !roleCanEdit(role) || repositoryAccessPermissionRank(repositoryPermissionForAccessProfile(previousProfile)) >= repositoryAccessPermissionRank(reposstore.AccessPermissionWrite) {
		return grant, previousProfile, false, nil
	}
	now := s.now()
	updated, err := s.store.UpsertKnowledgeBaseGrant(ctx, kb.KnowledgeBaseGrant{
		ID:                      grant.ID,
		WorkspaceID:             grant.WorkspaceID,
		KnowledgeBaseID:         grant.KnowledgeBaseID,
		SubjectType:             grant.SubjectType,
		SubjectID:               grant.SubjectID,
		Role:                    role,
		RepositoryAccessProfile: kb.KnowledgeBaseRepositoryAccessProfileWrite,
		CreatedBy:               firstNonEmptyString(grant.CreatedBy, subject),
		UpdatedBy:               subject,
		CreatedAt:               grant.CreatedAt,
		UpdatedAt:               now,
	})
	return updated, previousProfile, true, err
}

func (s *KBService) markSourceProvisioningFailed(ctx context.Context, source kb.Source, subject string, cause error) error {
	source.Status = KBSourceStatusFailed
	source.SyncStatus = KBSourceStatusFailed
	source.IndexStatus = KBSourceStatusFailed
	source.LastError = strings.TrimSpace(cause.Error())
	source.UpdatedBy = strings.TrimSpace(subject)
	source.UpdatedAt = s.now()
	_, err := s.store.UpdateSource(ctx, source)
	return err
}

func (s *KBService) withSourceMirrors(ctx context.Context, sources []kb.Source) []kb.Source {
	if len(sources) == 0 || s == nil || s.store == nil {
		return sources
	}
	mirrors := make([]*kb.SourceMirror, 0, len(sources))
	for i := range sources {
		mirror, err := s.store.GetSourceMirror(ctx, sources[i].WorkspaceID, sources[i].KnowledgeBaseID, sources[i].ID)
		if err == nil {
			sources[i].Mirror = &mirror
			mirrors = append(mirrors, sources[i].Mirror)
		}
	}
	s.decorateSourceMirrorCredentialSummaries(ctx, mirrors)
	return sources
}

func (s *KBService) prepareSourceMirrorMutation(ctx context.Context, source kb.Source, input KBSourceMirrorMutation, subject string, now time.Time) (kb.SourceMirror, error) {
	if strings.TrimSpace(input.CredentialRef) != "" && strings.TrimSpace(input.AccessToken) != "" {
		return kb.SourceMirror{}, invalidArgument("source mirror credentialRef and accessToken cannot both be set")
	}
	upstreamType, err := kb.ValidateSourceMirrorUpstreamType(input.UpstreamType)
	if err != nil {
		return kb.SourceMirror{}, invalidArgument(err.Error())
	}
	remoteURL, err := ValidateSourceMirrorRemoteForUpstream(ctx, upstreamType, input.RemoteURL, s.sourceMirrorRemoteValidationOptions())
	if err != nil {
		return kb.SourceMirror{}, err
	}
	enabled := true
	if input.Enabled != nil {
		enabled = *input.Enabled
	}
	defaultRef := strings.TrimSpace(input.DefaultRef)
	if defaultRef == "" && !input.DefaultRefSet {
		defaultRef = strings.TrimSpace(source.DefaultRef)
	}
	defaultRefExplicit := defaultRef != "" && (input.DefaultRefSet || strings.TrimSpace(input.DefaultRef) != "")
	status := kb.SourceMirrorSyncStatusDisabled
	if enabled {
		status = kb.SourceMirrorSyncStatusIdle
	}
	mirror := kb.SourceMirror{
		SourceID:           source.ID,
		WorkspaceID:        source.WorkspaceID,
		KnowledgeBaseID:    source.KnowledgeBaseID,
		UpstreamType:       upstreamType,
		RemoteURL:          remoteURL,
		CredentialRef:      strings.TrimSpace(input.CredentialRef),
		DefaultRef:         defaultRef,
		DefaultRefExplicit: &defaultRefExplicit,
		Enabled:            enabled,
		SyncStatus:         status,
		CreatedBy:          strings.TrimSpace(subject),
		UpdatedBy:          strings.TrimSpace(subject),
		CreatedAt:          now,
		UpdatedAt:          now,
	}
	if input.IntervalSeconds != nil {
		mirror.IntervalSeconds = *input.IntervalSeconds
	}
	if strings.TrimSpace(mirror.CredentialRef) != "" {
		if err := s.validatePersonalAccessTokenForMirrorMutation(ctx, source, mirror, subject, true); err != nil {
			return kb.SourceMirror{}, err
		}
	}
	mirror.NextSyncAt = nextSourceMirrorSyncAt(mirror, now)
	return mirror, nil
}

func repositoryStateForSource(repository reposservice.RepositoryIdentity, fallback string) string {
	if repository.Archived {
		return "archived"
	}
	return firstNonEmptyString(fallback, "active")
}

func sourceTypeRequiresSourceRepository(sourceType string) bool {
	switch strings.TrimSpace(sourceType) {
	case "file_repository", "codebase":
		return true
	default:
		return false
	}
}

func sourceTypeUsesMaterialization(sourceType string) bool {
	switch strings.TrimSpace(sourceType) {
	case "external_connector", "database_schema":
		return true
	default:
		return false
	}
}

func defaultKBSourceIndexingMode(sourceType string) string {
	switch strings.TrimSpace(sourceType) {
	case "external_connector", "database_schema":
		return "kb_repo_materialized"
	default:
		return "repo_wiki_and_files"
	}
}

func findMirrorResourceRepository(repos []reposstore.ResourceRepository) *reposstore.ResourceRepository {
	for i := range repos {
		if repos[i].RepoRole == reposstore.RepoRoleMirror {
			return &repos[i]
		}
	}
	return nil
}

func findSourceRepositoryForRole(repos []reposstore.ResourceRepository, role string) *reposstore.ResourceRepository {
	for i := range repos {
		if repos[i].RepoRole == role {
			return &repos[i]
		}
	}
	return nil
}

func sourceRepositoryMappingsExceptSource(repos []reposstore.ResourceRepository, sourceID string) []reposstore.ResourceRepository {
	sourceID = strings.TrimSpace(sourceID)
	out := make([]reposstore.ResourceRepository, 0, len(repos))
	for _, repo := range repos {
		if sourceID != "" && strings.TrimSpace(repo.ResourceID) == sourceID {
			continue
		}
		out = append(out, repo)
	}
	return out
}
