package kb

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"log/slog"
	"regexp"
	"strings"
	"sync"
	"time"

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

	"github.com/tidbcloud/AgentTeam/internal/core/identifiers"
	"github.com/tidbcloud/AgentTeam/internal/kbgovernance"
	"github.com/tidbcloud/AgentTeam/internal/store/automation"
	"github.com/tidbcloud/AgentTeam/internal/store/content"
	"github.com/tidbcloud/AgentTeam/internal/store/kb"
	"github.com/tidbcloud/AgentTeam/internal/store/providertokens"
	activitystore "github.com/tidbcloud/AgentTeam/internal/store/resource/activity"
	"github.com/tidbcloud/AgentTeam/internal/store/resource/memory"
	reposstore "github.com/tidbcloud/AgentTeam/internal/store/resource/repos"
	"gorm.io/gorm"
)

const (
	defaultKBPage    = 1
	defaultKBPerPage = 50
	maxKBPerPage     = 100
)

type KBMutation struct {
	Name                                string
	RepoNameOverride                    string
	Description                         string
	StorageScope                        string
	GroupID                             *string
	TagIDs                              []string
	TagIDsSet                           bool
	DefaultWorkspaceMemberRole          *string
	DefaultRepositoryAccessProfile      *string
	AllowKnowledgeQAWithoutSourceAccess *bool
	IsActive                            *bool
	SetupMode                           string
}

type KBSavedViewMutation struct {
	Name        string
	Description string
	Filter      string
	Visibility  string
}

type KBSourceMutation struct {
	SourceType            string
	Name                  string
	Description           string
	RepoMode              string
	DefaultRef            string
	IndexingMode          string
	SyncStatus            string
	IndexStatus           string
	Config                string
	Repo                  KBSourceRepositoryCreateInput
	Mirror                KBSourceMirrorMutation
	SourceRepositoryActor repos.ResourceRepositoryActor
}

type KBSourceMirrorMutation struct {
	UpstreamType       string
	RemoteURL          string
	CredentialRef      string
	AccessToken        string
	DefaultRef         string
	DefaultRefSet      bool
	ExpectedDefaultRef *string
	IntervalSeconds    *int
	Enabled            *bool
}

type KBPermissionGrantMutation struct {
	GrantID                 string
	SubjectType             string
	SubjectID               string
	Role                    string
	RoleSet                 bool
	RepositoryAccessProfile *string
	RepositoryOverrides     []KBPermissionRepositoryOverrideMutation
}

type KBPermissionRepositoryOverrideMutation struct {
	SourceID                string
	RepoRole                string
	RepositoryAccessProfile *string
}

type KBPermissionRepositoryAccess struct {
	SourceID                         string
	SourceName                       string
	RepoRole                         string
	RepoFullName                     string
	InheritedRepositoryAccessProfile string
	OverrideRepositoryAccessProfile  string
	EffectiveRepositoryAccessProfile string
	RepositoryAccessStatus           string
	RepositoryAccessLastError        string
}

type AgentKnowledgeBaseGrantResult struct {
	WorkspaceID     string
	AgentID         string
	KnowledgeBaseID string
	AccessLevel     string
	CreatedBy       string
	UpdatedBy       string
	CreatedAt       time.Time
	UpdatedAt       time.Time
}

type AgentKnowledgeBaseBulkAccessMutation struct {
	KnowledgeBaseIDs []string
	KnowledgeBases   []AgentKnowledgeBaseAccessMutation
}

type AgentKnowledgeBaseAccessMutation struct {
	KnowledgeBaseID string
	AccessLevel     string
}

type AgentKnowledgeBaseBulkAccessResult struct {
	AgentID                 string
	KnowledgeBaseIDs        []string
	GrantedKnowledgeBaseIDs []string
	RevokedKnowledgeBaseIDs []string
}

type KBPermissionGrant struct {
	ID                        string
	WorkspaceID               string
	KnowledgeBaseID           string
	SubjectType               string
	SubjectID                 string
	Role                      string
	RepositoryAccessProfile   string
	RepositoryAccessStatus    string
	RepositoryAccessLastError string
	CreatedBy                 string
	UpdatedBy                 string
	CreatedAt                 time.Time
	UpdatedAt                 time.Time
	SourceRepositoryAccess    []KBPermissionRepositoryAccess
}

type KBPermissionsSummary struct {
	MyRole    string
	CanManage bool
	Owner     *KBPermissionGrant
	Self      *KBPermissionGrant
	Members   []KBPermissionGrant
	Agents    []KBPermissionGrant
}

// KBRetrievalSource is the repo-backed source projection used by retrieval. It
// is not persisted separately; the KB repo derives from knowledge_bases and
// linked source repos derive from knowledge_base_sources.
type KBRetrievalSource struct {
	ID                        string
	WorkspaceID               string
	KnowledgeBaseID           string
	SourceKind                string
	RepoRole                  string
	RepoID                    string
	ResourceType              string
	ResourceID                string
	IndexStatus               string
	Revision                  string
	ContentID                 string
	ContentVersionID          string
	Native                    bool
	SearchProjectionTruncated bool
	FreshnessStatus           string
	PermissionPolicyJSON      string
	LastValidatedAt           *time.Time
	CreatedAt                 time.Time
	UpdatedAt                 time.Time
}

// KBServiceOptions configures optional retrieval integrations used by the KB
// service. Stores remain the MeshX control-plane source of truth; integrations
// only read concrete repo/wiki primitives.
type KBServiceOptions struct {
	WikiReader                        KBWikiReader
	NativeWikiReader                  KBWikiReader
	ContentAuthority                  KBSourceAuthorityReader
	WikiStore                         wiki.AGSRepositoryStore
	RepositoryStore                   *reposstore.Store
	UADashboardRepository             UADashboardRepository
	NativeUADashboardRepository       UADashboardRepository
	UADashboardLimits                 UADashboardImportLimits
	SourceMirrorSyncer                KBSourceMirrorRepositorySyncer
	SourceMirrorPreflighter           KBSourceMirrorPreflighter
	SourceRepositoryInitializer       KBSourceRepositoryInitializer
	AgentOwnershipSource              AgentOwnershipSource
	AllowPrivateSourceMirrorHosts     bool
	AutomationSecretStore             *automation.Store
	AutomationCredentialsKey          []byte
	PersonalAccessTokenStore          *providertokens.Store
	PersonalAccessTokenCredentialsKey []byte
	PrincipalWorkspaceAccessVerifier  PrincipalWorkspaceAccessVerifier
	WorkspaceGovernanceAuthorizer     WorkspaceGovernanceAuthorizer
	WikiGenerator                     KBWikiGenerator
	WikiGenerationApprovalGateway     kbgovernance.WikiGenerationApprovalGateway
	WikiGenerationTerminalGateway     kbgovernance.WikiGenerationTerminalGateway
	PublicationApprovalGateway        kbgovernance.PublicationApprovalGateway
	ActivityStore                     *activitystore.Store
	ActivityProjector                 activity.ResourceActivityProjectorRefresher
	ActivityRefreshAfter              time.Duration
	AssetAccessOutbox                 *memory.Store
	SyncTokenIssuer                   repos.RepositorySyncTokenIssuer
	CitationTokenKey                  []byte
	Logger                            *slog.Logger
}

type PrincipalWorkspaceAccessVerifier interface {
	HasWorkspaceAccess(ctx context.Context, workspaceID, principalID string) (bool, error)
}

// AgentWorkspaceAccessVerifier checks an explicit agent identity. It is
// intentionally separate from human membership verification so callers never
// infer principal kind from an identifier prefix.
type AgentWorkspaceAccessVerifier interface {
	HasAgentWorkspaceAccess(ctx context.Context, workspaceID, agentID string) (bool, error)
}

type KBSourceAuthorityReader interface {
	ListAuthoritiesForBindings(context.Context, string, string, []string) (map[string]content.Authority, error)
}

// WorkspaceGovernanceAuthorizer owns privileged workspace-level decisions.
// KB roles remain necessary for KB mutations, but never substitute for the
// workspace owner/admin authority required by governance-only setup actions.
type WorkspaceGovernanceAuthorizer interface {
	CanAdministerWorkspace(ctx context.Context, workspaceID, principalID string) (bool, error)
}

type AgentAccessibleKnowledgeBase struct {
	ID              string
	WorkspaceID     string
	Name            string
	Description     string
	AccessLevel     string
	PermissionSetAt time.Time
	UpdatedAt       time.Time
}

type AgentKnowledgeBaseAccessBatchOptions struct {
	AgentIDs []string
}

type AgentKnowledgeBaseAccessBatch struct {
	Agents           map[string][]AgentAccessibleKnowledgeBaseForSync
	SyncToken        string
	SyncTokenExpires time.Time
}

type AgentAccessibleKnowledgeBaseForSync struct {
	ID              string
	WorkspaceID     string
	Name            string
	Description     string
	AccessLevel     string
	PermissionSetAt time.Time
	UpdatedAt       time.Time
	Sources         []AgentAccessibleKBSourceForSync
}

type AgentAccessibleKBSourceForSync struct {
	ID           string
	Name         string
	Description  string
	SourceType   string
	DefaultRef   string
	Repositories []AgentAccessibleKBRepositoryForSync
}

type AgentAccessibleKBRepositoryForSync struct {
	RepoRole      string
	Repository    *reposstore.ResourceRepository
	Branch        string
	AccessProfile string
	AccessStatus  string
	Writable      bool
}

type KnowledgeBaseCollectionPage struct {
	Items       []kb.KnowledgeBase
	GroupCounts []kb.KnowledgeBaseGroupCount
	Total       int64
	Page        int
	PerPage     int
	HasMore     bool
}

type KBService struct {
	store                             *kb.Store
	wikiReader                        KBWikiReader
	nativeWikiReader                  KBWikiReader
	contentAuthority                  KBSourceAuthorityReader
	wikiStore                         wiki.AGSRepositoryStore
	repositoryStore                   *reposstore.Store
	uaDashboardRepository             UADashboardRepository
	nativeUADashboardRepository       UADashboardRepository
	uaDashboardLimits                 UADashboardImportLimits
	sourceMirrorSyncer                KBSourceMirrorRepositorySyncer
	sourceMirrorPreflighter           KBSourceMirrorPreflighter
	sourceRepositoryInitializer       KBSourceRepositoryInitializer
	agentOwnershipSource              AgentOwnershipSource
	allowPrivateSourceMirrorHosts     bool
	automationSecretStore             *automation.Store
	automationCredentialsKey          []byte
	personalAccessTokenStore          *providertokens.Store
	personalAccessTokenCredentialsKey []byte
	principalWorkspaceAccessVerifier  PrincipalWorkspaceAccessVerifier
	workspaceGovernanceAuthorizer     WorkspaceGovernanceAuthorizer
	wikiGenerator                     KBWikiGenerator
	wikiApprovalMu                    sync.RWMutex
	wikiApprovalGateway               kbgovernance.WikiGenerationApprovalGateway
	wikiTerminalMu                    sync.RWMutex
	wikiTerminalGateway               kbgovernance.WikiGenerationTerminalGateway
	publicationApprovalMu             sync.RWMutex
	publicationApprovalGateway        kbgovernance.PublicationApprovalGateway
	activityStore                     *activitystore.Store
	activityProjector                 activity.ResourceActivityProjectorRefresher
	activityRefreshAfter              time.Duration
	assetAccessOutbox                 *memory.Store
	syncTokenIssuer                   repos.RepositorySyncTokenIssuer
	citationTokenKey                  []byte
	logger                            *slog.Logger
	now                               func() time.Time
}

func NewKBService(store *kb.Store, now func() time.Time) *KBService {
	return NewKBServiceWithOptions(store, now, KBServiceOptions{})
}

func NewKBServiceWithOptions(store *kb.Store, now func() time.Time, options KBServiceOptions) *KBService {
	if now == nil {
		now = func() time.Time { return time.Now().UTC() }
	}
	logger := options.Logger
	if logger == nil {
		logger = slog.Default()
	}
	citationTokenKey := options.CitationTokenKey
	if len(citationTokenKey) == 0 {
		citationTokenKey = options.AutomationCredentialsKey
	}
	if len(citationTokenKey) == 0 {
		citationTokenKey = options.PersonalAccessTokenCredentialsKey
	}
	return &KBService{
		store:                             store,
		wikiReader:                        options.WikiReader,
		nativeWikiReader:                  options.NativeWikiReader,
		contentAuthority:                  options.ContentAuthority,
		wikiStore:                         options.WikiStore,
		repositoryStore:                   options.RepositoryStore,
		uaDashboardRepository:             options.UADashboardRepository,
		nativeUADashboardRepository:       options.NativeUADashboardRepository,
		uaDashboardLimits:                 normalizedUADashboardLimits(options.UADashboardLimits),
		sourceMirrorSyncer:                options.SourceMirrorSyncer,
		sourceMirrorPreflighter:           options.SourceMirrorPreflighter,
		sourceRepositoryInitializer:       options.SourceRepositoryInitializer,
		agentOwnershipSource:              options.AgentOwnershipSource,
		allowPrivateSourceMirrorHosts:     options.AllowPrivateSourceMirrorHosts,
		automationSecretStore:             options.AutomationSecretStore,
		automationCredentialsKey:          options.AutomationCredentialsKey,
		personalAccessTokenStore:          options.PersonalAccessTokenStore,
		personalAccessTokenCredentialsKey: options.PersonalAccessTokenCredentialsKey,
		principalWorkspaceAccessVerifier:  options.PrincipalWorkspaceAccessVerifier,
		workspaceGovernanceAuthorizer:     options.WorkspaceGovernanceAuthorizer,
		wikiGenerator:                     options.WikiGenerator,
		wikiApprovalGateway:               options.WikiGenerationApprovalGateway,
		wikiTerminalGateway:               options.WikiGenerationTerminalGateway,
		publicationApprovalGateway:        options.PublicationApprovalGateway,
		activityStore:                     options.ActivityStore,
		activityProjector:                 options.ActivityProjector,
		activityRefreshAfter:              activity.NormalizeResourceActivityRefreshAfter(options.ActivityRefreshAfter),
		assetAccessOutbox:                 options.AssetAccessOutbox,
		syncTokenIssuer:                   options.SyncTokenIssuer,
		citationTokenKey:                  normalizeKBCitationTokenKey(citationTokenKey),
		logger:                            logger,
		now:                               now,
	}
}

func (s *KBService) ListKnowledgeBases(ctx context.Context, workspaceID, subject, view, query string, tagIDs []string, page, perPage int) (KnowledgeBaseCollectionPage, error) {
	if err := s.validateScope(workspaceID, subject); err != nil {
		return KnowledgeBaseCollectionPage{}, err
	}
	page, perPage = normalizeKBPagination(page, perPage)
	view = normalizeKBCollectionView(view)
	if view == "" {
		return KnowledgeBaseCollectionPage{}, invalidArgument("unknown knowledge base collection")
	}
	tagIDs = uniqueTaxonomyIDs(tagIDs)
	if len(tagIDs) > maxKnowledgeBaseTagsPerBase {
		return KnowledgeBaseCollectionPage{}, invalidArgument("too many knowledge base tag filters")
	}
	canAdministerWorkspace := s.canAdministerWorkspaceForRead(ctx, workspaceID, subject)
	result, err := s.store.ListKnowledgeBaseCollection(ctx, kb.KnowledgeBaseCollectionOptions{
		WorkspaceID:                 workspaceID,
		PrincipalID:                 subject,
		BypassPrincipalAccessFilter: canAdministerWorkspace,
		BypassDraftVisibilityFilter: canAdministerWorkspace,
		View:                        view,
		Query:                       query,
		TagIDs:                      tagIDs,
		IncludeInactive:             false,
		Page:                        page,
		PerPage:                     perPage,
	})
	if err != nil {
		return KnowledgeBaseCollectionPage{}, err
	}
	_, visibleSources, err := s.visibleKnowledgeBaseActivitySources(ctx, workspaceID, result.Items)
	if err != nil {
		return KnowledgeBaseCollectionPage{}, err
	}
	activity.RefreshVisibleResourceActivity(ctx, s.activityProjector, s.repositoryStore, workspaceID, reposstore.ResourceTypeKBSource, visibleSources, s.activityRefreshAfter, nil)
	if err := s.decorateKnowledgeBaseAccessListWithWorkspaceAuthority(ctx, result.Items, subject, canAdministerWorkspace); err != nil {
		return KnowledgeBaseCollectionPage{}, err
	}
	if err := s.store.DecorateKnowledgeBaseTaxonomy(ctx, workspaceID, result.Items); err != nil {
		return KnowledgeBaseCollectionPage{}, err
	}
	draftIDs := make([]string, 0, len(result.Items))
	for _, base := range result.Items {
		if strings.TrimSpace(base.PublicationState) == KBPublicationDraft {
			draftIDs = append(draftIDs, base.ID)
		}
	}
	setupSessions, err := s.store.ListKnowledgeBaseSetupSessions(ctx, workspaceID, draftIDs)
	if err != nil {
		return KnowledgeBaseCollectionPage{}, err
	}
	setupByKB := make(map[string]kb.KnowledgeBaseSetupSummary, len(setupSessions))
	for _, session := range setupSessions {
		setupByKB[session.KnowledgeBaseID] = kb.KnowledgeBaseSetupSummary{
			Mode: session.Mode, ActiveStep: session.ActiveStep, Version: session.Version,
		}
	}
	for index := range result.Items {
		if setup, ok := setupByKB[result.Items[index].ID]; ok {
			result.Items[index].SetupSummary = &setup
		}
	}
	return KnowledgeBaseCollectionPage{Items: result.Items, GroupCounts: result.GroupCounts, Total: result.Total, Page: result.Page, PerPage: result.PerPage, HasMore: result.HasMore}, nil
}

func (s *KBService) CreateKnowledgeBase(ctx context.Context, workspaceID, subject string, input KBMutation) (kb.KnowledgeBase, error) {
	if err := s.validateScope(workspaceID, subject); err != nil {
		return kb.KnowledgeBase{}, err
	}
	name := strings.TrimSpace(input.Name)
	if name == "" {
		return kb.KnowledgeBase{}, invalidArgument("knowledge base name is required")
	}
	now := s.now()
	workspace := strings.TrimSpace(workspaceID)
	kbSlug, _, err := repos.ResolveRepositoryNameStem(name, input.RepoNameOverride, "kb", ".kb", "knowledge base")
	if err != nil {
		return kb.KnowledgeBase{}, err
	}
	storage, err := repos.ResolveRepositoryStorage(workspace, subject, input.StorageScope)
	if err != nil {
		return kb.KnowledgeBase{}, err
	}
	defaultWorkspaceMemberRole, err := normalizeKBDefaultWorkspaceMemberRole(input.DefaultWorkspaceMemberRole)
	if err != nil {
		return kb.KnowledgeBase{}, err
	}
	defaultRepositoryAccessProfile, err := normalizeKBDefaultRepositoryAccessProfile(input.DefaultRepositoryAccessProfile)
	if err != nil {
		return kb.KnowledgeBase{}, err
	}
	groupID, tagIDs, err := s.resolveKnowledgeBaseClassification(ctx, workspace, subject, input.GroupID, input.TagIDs)
	if err != nil {
		return kb.KnowledgeBase{}, err
	}
	publicationState := "official"
	workingRevision := ""
	publishedRevision := newKBID("kbr")
	publishedAt := &now
	if strings.TrimSpace(input.SetupMode) == "create_draft" {
		publicationState = "draft"
		workingRevision = newKBID("kbr")
		publishedRevision = ""
		publishedAt = nil
	}
	base := kb.KnowledgeBase{ID: newKBID("kb"), WorkspaceID: workspace, GroupID: groupID, Slug: kbSlug, Name: name, Description: strings.TrimSpace(input.Description), StorageScope: storage.Scope, StorageOwnerLogin: storage.OwnerLogin, DefaultWorkspaceMemberRole: defaultWorkspaceMemberRole, DefaultRepositoryAccessProfile: defaultRepositoryAccessProfile, PublicationState: publicationState, WorkingRevision: workingRevision, PublishedRevision: publishedRevision, PublishedAt: publishedAt, IsActive: true, RetrievalPolicy: "{}", SourceCollectionState: "{}", CreatedBy: strings.TrimSpace(subject), UpdatedBy: strings.TrimSpace(subject), CreatedAt: now, UpdatedAt: now}
	if input.AllowKnowledgeQAWithoutSourceAccess != nil {
		base.AllowKnowledgeQAWithoutSourceAccess = *input.AllowKnowledgeQAWithoutSourceAccess
	}
	if input.IsActive != nil {
		base.IsActive = *input.IsActive
	}
	var created kb.KnowledgeBase
	err = s.store.Transaction(ctx, func(tx *kb.Store) error {
		var err error
		created, err = tx.CreateKnowledgeBase(ctx, base)
		if err != nil {
			return err
		}
		if publicationState == "draft" {
			_, err = tx.CreateKnowledgeBaseSetupSession(ctx, kb.KnowledgeBaseSetupSession{
				ID: newKBID("kbs"), WorkspaceID: created.WorkspaceID, KnowledgeBaseID: created.ID,
				Mode: "create_draft", ActiveStep: "basic", WorkingRevision: workingRevision,
				Version: 1, CreatedBy: strings.TrimSpace(subject), UpdatedBy: strings.TrimSpace(subject),
				CreatedAt: now, UpdatedAt: now,
			})
			if err != nil {
				return err
			}
		}
		if err = tx.ReplaceKnowledgeBaseTags(ctx, created.WorkspaceID, created.ID, subject, tagIDs, now); err != nil {
			return err
		}
		_, err = tx.UpsertKnowledgeBaseGrant(ctx, kb.KnowledgeBaseGrant{ID: newKBID("kbg"), WorkspaceID: created.WorkspaceID, KnowledgeBaseID: created.ID, SubjectType: "user", SubjectID: strings.TrimSpace(subject), Role: "owner", CreatedBy: strings.TrimSpace(subject), UpdatedBy: strings.TrimSpace(subject), CreatedAt: now, UpdatedAt: now})
		if err != nil {
			return err
		}
		audit := s.auditEvent(created.WorkspaceID, created.ID, subject, "kb.create", "knowledge_base", created.ID, "Created knowledge base", "")
		if _, err = tx.CreateAuditEvent(ctx, audit); err != nil {
			return err
		}
		return s.writeKnowledgeBaseAuditActivityWithStore(ctx, s.knowledgeBaseActivityStore(tx), audit)
	})
	if err == nil {
		s.decorateKnowledgeBaseAccess(ctx, &created, subject)
		decorated := []kb.KnowledgeBase{created}
		if decorateErr := s.store.DecorateKnowledgeBaseTaxonomy(ctx, workspace, decorated); decorateErr != nil {
			return kb.KnowledgeBase{}, decorateErr
		}
		created = decorated[0]
	}
	return created, err
}

func (s *KBService) GetKnowledgeBase(ctx context.Context, workspaceID, subject, id string) (kb.KnowledgeBase, error) {
	if err := s.validateScope(workspaceID, subject); err != nil {
		return kb.KnowledgeBase{}, err
	}
	base, err := s.store.GetKnowledgeBase(ctx, workspaceID, id)
	if err != nil {
		return kb.KnowledgeBase{}, mapStoreNotFound(err)
	}
	canAdministerWorkspace := s.canAdministerWorkspaceForRead(ctx, workspaceID, subject)
	if !s.knowledgeBaseVisibleToSubjectWithWorkspaceAuthority(ctx, base, subject, canAdministerWorkspace) {
		return kb.KnowledgeBase{}, forbidden("knowledge base is not accessible")
	}
	if err := s.store.DecorateKnowledgeBasePin(ctx, workspaceID, subject, &base); err != nil {
		return kb.KnowledgeBase{}, err
	}
	s.decorateKnowledgeBaseAccessWithWorkspaceAuthority(ctx, &base, subject, canAdministerWorkspace)
	decorated := []kb.KnowledgeBase{base}
	if err := s.store.DecorateKnowledgeBaseTaxonomy(ctx, workspaceID, decorated); err != nil {
		return kb.KnowledgeBase{}, err
	}
	base = decorated[0]
	base.CanEditOfficial = base.PublicationState == KBPublicationOfficial && base.IsActive && strings.TrimSpace(base.PublishedRevision) != "" && (base.Role == "owner" || canAdministerWorkspace)
	base.CanConvertToDraft = base.PublicationState == KBPublicationOfficial && base.IsActive && base.CanManage && canAdministerWorkspace
	return base, nil
}

func (s *KBService) UpdateKnowledgeBase(ctx context.Context, workspaceID, subject, id string, input KBMutation) (kb.KnowledgeBase, error) {
	base, err := s.GetKnowledgeBase(ctx, workspaceID, subject, id)
	if err != nil {
		return kb.KnowledgeBase{}, err
	}
	if !s.canManageKnowledgeBase(ctx, base, subject) {
		return kb.KnowledgeBase{}, forbidden("knowledge base requires manager access")
	}
	if base.PublicationState == KBPublicationOfficial && knowledgeBasePublicationMetadataChanges(base, input) {
		return kb.KnowledgeBase{}, codedConflict("kb-official-edit-required", "Official metadata must be changed through the revision-fenced Official editor", nil)
	}
	if strings.TrimSpace(input.Name) != "" {
		base.Name = strings.TrimSpace(input.Name)
	}
	base.Description = strings.TrimSpace(input.Description)
	if input.GroupID != nil {
		groupID, _, err := s.resolveKnowledgeBaseClassification(ctx, workspaceID, subject, input.GroupID, nil)
		if err != nil {
			return kb.KnowledgeBase{}, err
		}
		base.GroupID = groupID
	}
	var tagIDs []string
	if input.TagIDsSet {
		_, tagIDs, err = s.resolveKnowledgeBaseClassification(ctx, workspaceID, subject, nil, input.TagIDs)
		if err != nil {
			return kb.KnowledgeBase{}, err
		}
	}
	if input.IsActive != nil {
		base.IsActive = *input.IsActive
	}
	if input.DefaultWorkspaceMemberRole != nil {
		role, err := normalizeKBDefaultWorkspaceMemberRole(input.DefaultWorkspaceMemberRole)
		if err != nil {
			return kb.KnowledgeBase{}, err
		}
		base.DefaultWorkspaceMemberRole = role
	}
	if input.DefaultRepositoryAccessProfile != nil {
		profile, err := normalizeKBDefaultRepositoryAccessProfile(input.DefaultRepositoryAccessProfile)
		if err != nil {
			return kb.KnowledgeBase{}, err
		}
		base.DefaultRepositoryAccessProfile = profile
	}
	if input.AllowKnowledgeQAWithoutSourceAccess != nil {
		base.AllowKnowledgeQAWithoutSourceAccess = *input.AllowKnowledgeQAWithoutSourceAccess
	}
	base.UpdatedBy = strings.TrimSpace(subject)
	base.UpdatedAt = s.now()
	var updated kb.KnowledgeBase
	err = s.store.Transaction(ctx, func(tx *kb.Store) error {
		var err error
		updated, err = tx.UpdateKnowledgeBase(ctx, base)
		if err != nil {
			return err
		}
		if input.TagIDsSet {
			if err = tx.ReplaceKnowledgeBaseTags(ctx, updated.WorkspaceID, updated.ID, subject, tagIDs, base.UpdatedAt); err != nil {
				return err
			}
		}
		audit := s.auditEvent(updated.WorkspaceID, updated.ID, subject, "kb.update", "knowledge_base", updated.ID, "Updated knowledge base", "")
		if _, err = tx.CreateAuditEvent(ctx, audit); err != nil {
			return err
		}
		return s.writeKnowledgeBaseAuditActivityWithStore(ctx, s.knowledgeBaseActivityStore(tx), audit)
	})
	if err == nil {
		s.decorateKnowledgeBaseAccess(ctx, &updated, subject)
		decorated := []kb.KnowledgeBase{updated}
		if decorateErr := s.store.DecorateKnowledgeBaseTaxonomy(ctx, workspaceID, decorated); decorateErr != nil {
			return kb.KnowledgeBase{}, decorateErr
		}
		updated = decorated[0]
	}
	return updated, err
}

func (s *KBService) ArchiveKnowledgeBase(ctx context.Context, workspaceID, subject, id string) (kb.KnowledgeBase, error) {
	inactive := false
	base, err := s.GetKnowledgeBase(ctx, workspaceID, subject, id)
	if err != nil {
		return kb.KnowledgeBase{}, err
	}
	return s.UpdateKnowledgeBase(ctx, workspaceID, subject, id, KBMutation{
		Name:        base.Name,
		Description: base.Description,
		IsActive:    &inactive,
	})
}

func (s *KBService) PinKnowledgeBase(ctx context.Context, workspaceID, subject, id string) (kb.KnowledgeBasePin, error) {
	if _, err := s.GetKnowledgeBase(ctx, workspaceID, subject, id); err != nil {
		return kb.KnowledgeBasePin{}, err
	}
	now := s.now()
	return s.store.UpsertKnowledgeBasePin(ctx, kb.KnowledgeBasePin{
		WorkspaceID:     strings.TrimSpace(workspaceID),
		PrincipalID:     strings.TrimSpace(subject),
		KnowledgeBaseID: strings.TrimSpace(id),
		CreatedAt:       now,
		UpdatedAt:       now,
	})
}

func (s *KBService) UnpinKnowledgeBase(ctx context.Context, workspaceID, subject, id string) error {
	if _, err := s.GetKnowledgeBase(ctx, workspaceID, subject, id); err != nil {
		return err
	}
	err := s.store.DeleteKnowledgeBasePin(ctx, workspaceID, strings.TrimSpace(subject), id)
	if err != nil {
		return mapStoreNotFound(err)
	}
	return nil
}

func (s *KBService) listRetrievalSourcesForBase(ctx context.Context, base kb.KnowledgeBase, page, perPage int) ([]KBRetrievalSource, error) {
	if page <= 0 || perPage <= 0 || perPage > maxKBRetrievalContextSourcesPerKB+1 {
		return nil, invalidArgument("knowledge retrieval source page is out of bounds")
	}
	sources, err := s.store.ListSources(ctx, base.WorkspaceID, base.ID, page, perPage)
	if err != nil {
		return nil, err
	}
	return s.projectRetrievalSources(ctx, base.WorkspaceID, sources)
}

func (s *KBService) listRetrievalSourcesForBaseSelection(ctx context.Context, base kb.KnowledgeBase, sourceIDs []string) ([]KBRetrievalSource, error) {
	selected := normalizeKBIDSelection(sourceIDs)
	if len(selected) == 0 {
		return s.listRetrievalSourcesForBase(ctx, base, 1, maxKBRetrievalContextSourcesPerKB+1)
	}
	if len(selected) > maxKBRetrievalContextSourcesPerKB {
		return nil, invalidArgument("knowledge retrieval selects too many sources")
	}
	rows, err := s.store.ListSourcesByIDs(ctx, base.WorkspaceID, selected)
	if err != nil {
		return nil, err
	}
	owned := rows[:0]
	for _, row := range rows {
		if row.KnowledgeBaseID == base.ID {
			owned = append(owned, row)
		}
	}
	return s.projectRetrievalSources(ctx, base.WorkspaceID, owned)
}

func (s *KBService) projectRetrievalSources(ctx context.Context, workspaceID string, sources []kb.Source) ([]KBRetrievalSource, error) {
	if len(sources) == 0 {
		return []KBRetrievalSource{}, nil
	}
	if s.repositoryStore == nil {
		return []KBRetrievalSource{}, nil
	}
	sourceIDs := make([]string, 0, len(sources))
	for _, source := range sources {
		sourceIDs = append(sourceIDs, source.ID)
	}
	revisions, err := s.store.ListCurrentSourceRevisions(ctx, workspaceID, sourceIDs)
	if err != nil {
		return nil, err
	}
	revisionBySource := make(map[string]kb.SourceRevision, len(revisions))
	revisionIDs := make([]string, 0, len(revisions))
	for _, revision := range revisions {
		if _, exists := revisionBySource[revision.SourceID]; exists {
			continue
		}
		revisionBySource[revision.SourceID] = revision
		revisionIDs = append(revisionIDs, revision.ID)
	}
	mappingByRevision := make(map[string]kb.SourceContentMapping)
	if s.contentAuthority != nil {
		mappings, err := s.store.ListSourceContentMappingsForRevisions(ctx, workspaceID, revisionIDs)
		if err != nil {
			return nil, err
		}
		mappingByRevision = make(map[string]kb.SourceContentMapping, len(mappings))
		for _, mapping := range mappings {
			mappingByRevision[mapping.SourceRevisionID] = mapping
		}
	}
	repositories, err := s.repositoryStore.ListForResourceBatch(ctx, workspaceID, reposstore.ResourceTypeKBSource, sourceIDs)
	if err != nil {
		return nil, err
	}
	repositoriesBySource := make(map[string]map[string]reposstore.ResourceRepository, len(sourceIDs))
	for _, repository := range repositories {
		if repositoriesBySource[repository.ResourceID] == nil {
			repositoriesBySource[repository.ResourceID] = make(map[string]reposstore.ResourceRepository)
		}
		repositoriesBySource[repository.ResourceID][repository.RepoRole] = repository
	}
	mirrors, err := s.store.ListSourceMirrorsBySourceIDs(ctx, workspaceID, sourceIDs)
	if err != nil {
		return nil, err
	}
	mirrorBySource := make(map[string]kb.SourceMirror, len(mirrors))
	for _, mirror := range mirrors {
		mirrorBySource[mirror.SourceID] = mirror
	}
	authorities := map[string]content.Authority{}
	if s.contentAuthority != nil {
		authorities, err = s.contentAuthority.ListAuthoritiesForBindings(ctx, workspaceID, kbSourceContentDomain, sourceIDs)
		if err != nil {
			return nil, fmt.Errorf("resolve KB source content authorities: %w", err)
		}
	}

	out := make([]KBRetrievalSource, 0, len(sources))
	for _, source := range sources {
		revision := revisionBySource[source.ID]
		authority, found := authorities[source.ID]
		state := content.AuthorityStateLegacyActive
		if found {
			state = strings.TrimSpace(authority.State)
		}
		var projected KBRetrievalSource
		var ok bool
		switch state {
		case content.AuthorityStateLegacyActive:
			projected, ok = legacyKBRetrievalSource(source, revision, mirrorBySource[source.ID], repositoriesBySource[source.ID])
		case content.AuthorityStateNativeActive:
			if s.nativeWikiReader == nil {
				return nil, fmt.Errorf("native KB source content is unavailable")
			}
			projected, err = nativeKBRetrievalSource(source, revision, mappingByRevision[revision.ID], repositoriesBySource[source.ID])
			if err != nil {
				return nil, err
			}
			ok = true
		default:
			return nil, fmt.Errorf("unknown KB source content authority state %q", state)
		}
		if ok {
			out = append(out, projected)
		}
	}
	return out, nil
}

func legacyKBRetrievalSource(source kb.Source, revision kb.SourceRevision, mirror kb.SourceMirror, repositories map[string]reposstore.ResourceRepository) (KBRetrievalSource, bool) {
	sourceType := strings.TrimSpace(source.SourceType)
	if sourceType == "" {
		return KBRetrievalSource{}, false
	}
	indexStatus := strings.TrimSpace(source.IndexStatus)
	if indexStatus == "" {
		indexStatus = strings.TrimSpace(source.Status)
	}
	freshnessStatus := firstNonEmptyString(source.FreshnessStatus, "unknown")
	if strings.TrimSpace(revision.FreshnessStatus) != "" {
		freshnessStatus = strings.TrimSpace(revision.FreshnessStatus)
	}
	// Codebase mirrors are useful knowledge before an optional external insight
	// pipeline has published generated wiki pages. Prefer a completed generated
	// revision when one exists; otherwise expose the mirrored repository itself
	// through the same read-only retrieval surface. This keeps a successfully
	// synced source from appearing ready while all Knowledge tools target an
	// empty insights repository.
	hasInsightsRevision := strings.TrimSpace(revision.PublishedWikiRevision) != "" || strings.TrimSpace(revision.InsightsCommitSHA) != ""
	if !hasInsightsRevision && (sourceType == "codebase" || sourceType == "file_repository") {
		// LastMirroredCommit is only advanced after a successful mirror. Keep
		// serving that immutable snapshot while a later sync is queued/running or
		// has failed instead of falling back to an empty insights repository.
		if strings.TrimSpace(mirror.LastMirroredCommit) != "" && mirror.LastSuccessAt != nil {
			mirrorRepo, repoOK := repositories[reposstore.RepoRoleMirror]
			if repoOK && strings.TrimSpace(mirrorRepo.RepoFullName) != "" {
				mirrorFreshness := "stale"
				if strings.TrimSpace(mirror.SyncStatus) == kb.SourceMirrorSyncStatusSuccess {
					mirrorFreshness = "current"
				}
				return KBRetrievalSource{
					ID:                   source.ID,
					WorkspaceID:          source.WorkspaceID,
					KnowledgeBaseID:      source.KnowledgeBaseID,
					SourceKind:           "source_repo",
					RepoRole:             reposstore.RepoRoleMirror,
					RepoID:               strings.TrimSpace(mirrorRepo.AGSRepoID),
					ResourceType:         sourceType,
					ResourceID:           strings.TrimSpace(mirrorRepo.RepoFullName),
					IndexStatus:          firstNonEmptyString(indexStatus, "ready"),
					Revision:             strings.TrimSpace(mirror.LastMirroredCommit),
					FreshnessStatus:      mirrorFreshness,
					PermissionPolicyJSON: "{}",
					LastValidatedAt:      mirror.LastSuccessAt,
					CreatedAt:            source.CreatedAt,
					UpdatedAt:            mirror.UpdatedAt,
				}, true
			}
		}
	}
	insights, exists := repositories[reposstore.RepoRoleInsights]
	if exists && strings.TrimSpace(insights.RepoFullName) != "" {
		return KBRetrievalSource{
			ID:                   source.ID,
			WorkspaceID:          source.WorkspaceID,
			KnowledgeBaseID:      source.KnowledgeBaseID,
			SourceKind:           "insights_repo",
			RepoRole:             firstNonEmptyString(insights.RepoRole, reposstore.RepoRoleInsights),
			RepoID:               strings.TrimSpace(insights.AGSRepoID),
			ResourceType:         sourceType,
			ResourceID:           strings.TrimSpace(insights.RepoFullName),
			IndexStatus:          firstNonEmptyString(indexStatus, "ready"),
			Revision:             firstNonEmptyString(revision.PublishedWikiRevision, revision.InsightsCommitSHA),
			FreshnessStatus:      freshnessStatus,
			PermissionPolicyJSON: "{}",
			LastValidatedAt:      source.LastIndexAt,
			CreatedAt:            source.CreatedAt,
			UpdatedAt:            source.UpdatedAt,
		}, true
	}
	return KBRetrievalSource{}, false
}

func nativeKBRetrievalSource(source kb.Source, revision kb.SourceRevision, mapping kb.SourceContentMapping, repositories map[string]reposstore.ResourceRepository) (KBRetrievalSource, error) {
	if strings.TrimSpace(source.SourceType) == "" || revision.ID == "" || revision.SourceContentVerifiedAt == nil ||
		revision.SourceContentID == "" || revision.SourceContentVersionID == "" || mapping.ID == "" || mapping.VerifiedAt == nil ||
		mapping.WorkspaceID != source.WorkspaceID || mapping.KnowledgeBaseID != source.KnowledgeBaseID || mapping.SourceID != source.ID ||
		mapping.SourceRevisionID != revision.ID || mapping.ContentID != revision.SourceContentID || mapping.ContentVersionID != revision.SourceContentVersionID ||
		!sourceContentMappingHasSearchProjection(mapping) {
		return KBRetrievalSource{}, fmt.Errorf("native KB source %q has no complete exact ContentVersion mapping", source.ID)
	}
	repository, ok := repositories[mapping.RepositoryRole]
	if !ok || strings.TrimSpace(repository.RepoFullName) == "" || strings.TrimSpace(repository.AGSRepoID) != strings.TrimSpace(mapping.RepositoryID) ||
		!kbRepositoryFileRole(mapping.RepositoryRole) {
		return KBRetrievalSource{}, fmt.Errorf("native KB source %q repository mapping is unavailable", source.ID)
	}
	indexStatus := firstNonEmptyString(strings.TrimSpace(source.IndexStatus), strings.TrimSpace(source.Status), "ready")
	freshnessStatus := firstNonEmptyString(strings.TrimSpace(revision.FreshnessStatus), strings.TrimSpace(source.FreshnessStatus), "unknown")
	return KBRetrievalSource{
		ID: source.ID, WorkspaceID: source.WorkspaceID, KnowledgeBaseID: source.KnowledgeBaseID,
		SourceKind: "native_source", RepoRole: mapping.RepositoryRole, RepoID: mapping.RepositoryID,
		ResourceType: source.SourceType, ResourceID: repository.RepoFullName, IndexStatus: indexStatus,
		Revision: mapping.ContentVersionID, ContentID: mapping.ContentID, ContentVersionID: mapping.ContentVersionID, Native: true,
		SearchProjectionTruncated: mapping.SearchProjectionTruncated,
		FreshnessStatus:           freshnessStatus, PermissionPolicyJSON: "{}", LastValidatedAt: mapping.VerifiedAt,
		CreatedAt: source.CreatedAt, UpdatedAt: mapping.UpdatedAt,
	}, nil
}

func decodeStringMap(raw string) map[string]any {
	if strings.TrimSpace(raw) == "" {
		return nil
	}
	var out map[string]any
	if err := json.Unmarshal([]byte(raw), &out); err != nil {
		return nil
	}
	return out
}

func firstMetadataString(values map[string]any, keys ...string) string {
	for _, key := range keys {
		value, ok := values[key]
		if !ok {
			continue
		}
		switch typed := value.(type) {
		case string:
			if trimmed := strings.TrimSpace(typed); trimmed != "" {
				return trimmed
			}
		}
	}
	return ""
}

func normalizeRepositoryFullName(raw string) (resourceID string, owner string, repo string, ok bool) {
	candidate := strings.TrimSpace(raw)
	candidate = strings.TrimPrefix(candidate, "repo:")
	parts := strings.Split(candidate, "/")
	if len(parts) != 2 {
		return "", "", "", false
	}
	owner = strings.TrimSpace(parts[0])
	repo = strings.TrimSpace(parts[1])
	if owner == "" || repo == "" {
		return "", "", "", false
	}
	return owner + "/" + repo, owner, repo, true
}

func (s *KBService) auditEvent(workspaceID, kbID, actor, action, resourceType, resourceID, summary, details string) kb.AuditEvent {
	now := s.now()
	return kb.AuditEvent{ID: newKBID("aud"), WorkspaceID: strings.TrimSpace(workspaceID), KnowledgeBaseID: strings.TrimSpace(kbID), ActorID: strings.TrimSpace(actor), Action: action, ResourceType: resourceType, ResourceID: strings.TrimSpace(resourceID), Summary: summary, Details: details, CreatedAt: now}
}

func normalizeKBPagination(page, perPage int) (int, int) {
	if page <= 0 {
		page = defaultKBPage
	}
	if perPage <= 0 {
		perPage = defaultKBPerPage
	}
	if perPage > maxKBPerPage {
		perPage = maxKBPerPage
	}
	return page, perPage
}

func normalizeKBCollectionView(view string) string {
	switch strings.TrimSpace(view) {
	case "", "all":
		return "all"
	case "owned":
		return "owned"
	case "my_knowledge_bases":
		return "my_knowledge_bases"
	case "my_drafts":
		return "my_drafts"
	case "pinned":
		return "pinned"
	case KBPublicationOfficial:
		return KBPublicationOfficial
	case KBPublicationDraft:
		return KBPublicationDraft
	default:
		return ""
	}
}

func mapStoreNotFound(err error) error {
	if err == nil {
		return nil
	}
	if errors.Is(err, gorm.ErrRecordNotFound) {
		return notFound("knowledge base resource not found")
	}
	return err
}

func newKBID(prefix string) string {
	id, err := identifiers.NewEntityID(prefix)
	if err != nil {
		return fmt.Sprintf("%s_%d", prefix, time.Now().UnixNano())
	}
	return id
}

func newKBSavedViewID() (string, error) {
	return identifiers.NewEntityID("ksv")
}

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

func kbSlugForName(name string) string {
	slug := strings.ToLower(strings.TrimSpace(name))
	slug = kbSlugCleanupPattern.ReplaceAllString(slug, "-")
	slug = strings.Trim(slug, "-")
	if slug == "" {
		return "kb"
	}
	return slug
}

func normalizeKBSavedViewInput(input KBSavedViewMutation) KBSavedViewMutation {
	input.Name = strings.TrimSpace(input.Name)
	input.Description = strings.TrimSpace(input.Description)
	input.Filter = strings.TrimSpace(input.Filter)
	if input.Filter == "" {
		input.Filter = `{"collection":"all"}`
	}
	input.Visibility = strings.TrimSpace(input.Visibility)
	if input.Visibility == "" {
		input.Visibility = "private"
	}
	return input
}

func validateKBSavedView(input KBSavedViewMutation) error {
	if input.Visibility != "private" && input.Visibility != "workspace" {
		return invalidArgument("KB saved view visibility is invalid")
	}
	collection, _, _, err := kbSavedViewCollection(input.Filter)
	if err != nil {
		return err
	}
	normalized := normalizeKBCollectionView(collection)
	if normalized == "" {
		return invalidArgument("unknown KB saved view collection")
	}
	return nil
}

func kbSavedViewCollection(filterJSON string) (string, string, []string, error) {
	var parsed struct {
		Collection string   `json:"collection"`
		Query      string   `json:"query"`
		TagIDs     []string `json:"tagIds"`
	}
	if err := json.Unmarshal([]byte(filterJSON), &parsed); err != nil {
		return "", "", nil, invalidArgument("KB saved view filter is invalid JSON")
	}
	return parsed.Collection, parsed.Query, uniqueTaxonomyIDs(parsed.TagIDs), nil
}
