package kb

import (
	"context"
	"strings"
	"time"

	"github.com/tidbcloud/AgentTeam/internal/service/resource/access"
	"github.com/tidbcloud/AgentTeam/internal/service/resource/repos"
	"github.com/tidbcloud/AgentTeam/internal/store/kb"
	"github.com/tidbcloud/AgentTeam/internal/store/resource/memory"
	reposstore "github.com/tidbcloud/AgentTeam/internal/store/resource/repos"
)

// BatchListAgentAccessibleKnowledgeBases returns every KB source repository the
// requested local agents are currently allowed to clone. The returned map always
// contains an entry for every requested, owned agent ID; an empty slice is an
// authoritative "no KB access" answer for Desktop sync.
func (s *KBService) BatchListAgentAccessibleKnowledgeBases(ctx context.Context, workspaceID, subject string, options AgentKnowledgeBaseAccessBatchOptions) (AgentKnowledgeBaseAccessBatch, error) {
	if s == nil || s.store == nil {
		return AgentKnowledgeBaseAccessBatch{}, invalidArgument("knowledge base service is not configured")
	}
	if err := s.validateScope(workspaceID, subject); err != nil {
		return AgentKnowledgeBaseAccessBatch{}, err
	}
	workspaceID = strings.TrimSpace(workspaceID)
	subject = strings.TrimSpace(subject)
	agentIDs := access.DeduplicateAgentIDs(options.AgentIDs)
	out := AgentKnowledgeBaseAccessBatch{Agents: make(map[string][]AgentAccessibleKnowledgeBaseForSync, len(agentIDs))}
	for _, agentID := range agentIDs {
		out.Agents[agentID] = []AgentAccessibleKnowledgeBaseForSync{}
	}
	if len(agentIDs) == 0 {
		return out, nil
	}
	if err := s.requireOwnedAgents(ctx, workspaceID, subject, agentIDs); err != nil {
		return AgentKnowledgeBaseAccessBatch{}, err
	}

	canAdministerWorkspace := s.canAdministerWorkspaceForRead(ctx, workspaceID, subject)
	grantsByAgent := make(map[string][]kb.KnowledgeBaseGrant, len(agentIDs))
	basesByID := make(map[string]kb.KnowledgeBase)
	kbIDs := make([]string, 0)
	grantIDs := make([]string, 0)
	for _, agentID := range agentIDs {
		rows, err := s.store.ListAgentAccessibleKnowledgeBases(ctx, workspaceID, agentID, false, 1, 10000)
		if err != nil {
			return AgentKnowledgeBaseAccessBatch{}, err
		}
		for _, row := range rows {
			baseID := strings.TrimSpace(row.KnowledgeBase.ID)
			if baseID == "" || strings.TrimSpace(row.Grant.ID) == "" {
				continue
			}
			if strings.TrimSpace(row.KnowledgeBase.PublicationState) == KBPublicationDraft &&
				!s.knowledgeBaseVisibleToSubjectWithWorkspaceAuthority(ctx, row.KnowledgeBase, subject, canAdministerWorkspace) {
				continue
			}
			if _, seen := basesByID[baseID]; !seen {
				basesByID[baseID] = row.KnowledgeBase
				kbIDs = append(kbIDs, baseID)
			}
			grantsByAgent[agentID] = append(grantsByAgent[agentID], row.Grant)
			grantIDs = append(grantIDs, row.Grant.ID)
		}
	}

	sources, err := s.store.ListSourcesForKnowledgeBases(ctx, workspaceID, kbIDs)
	if err != nil {
		return AgentKnowledgeBaseAccessBatch{}, err
	}
	sourcesByKB := make(map[string][]kb.Source, len(kbIDs))
	sourceIDs := make([]string, 0, len(sources))
	for _, source := range sources {
		sourceID := strings.TrimSpace(source.ID)
		if sourceID == "" {
			continue
		}
		sourcesByKB[source.KnowledgeBaseID] = append(sourcesByKB[source.KnowledgeBaseID], source)
		sourceIDs = append(sourceIDs, sourceID)
	}
	reposBySource, err := s.accessibleKBReposBySource(ctx, workspaceID, sourceIDs)
	if err != nil {
		return AgentKnowledgeBaseAccessBatch{}, err
	}
	nativeSources, err := s.kbSourceNativeBindings(ctx, workspaceID, sourceIDs)
	if err != nil {
		return AgentKnowledgeBaseAccessBatch{}, err
	}
	grantRowsByIntent, err := s.kbRepositoryGrantRowsByIntent(ctx, workspaceID, grantIDs)
	if err != nil {
		return AgentKnowledgeBaseAccessBatch{}, err
	}

	for _, agentID := range agentIDs {
		items := make([]AgentAccessibleKnowledgeBaseForSync, 0, len(grantsByAgent[agentID]))
		for _, grant := range grantsByAgent[agentID] {
			base, ok := basesByID[grant.KnowledgeBaseID]
			if !ok {
				continue
			}
			syncKB := AgentAccessibleKnowledgeBaseForSync{
				ID:              base.ID,
				WorkspaceID:     base.WorkspaceID,
				Name:            base.Name,
				Description:     base.Description,
				AccessLevel:     agentKBAccessLevelFromGrant(grant),
				PermissionSetAt: grant.UpdatedAt,
				UpdatedAt:       base.UpdatedAt,
				Sources:         []AgentAccessibleKBSourceForSync{},
			}
			for _, source := range sourcesByKB[base.ID] {
				if _, native := nativeSources[source.ID]; native {
					// Native KB content is fetched through the exact-version product
					// boundary. Do not disclose an obsolete AGS clone target or mint an
					// AGS repository token after the source-scoped one-way cutover.
					continue
				}
				syncSource := AgentAccessibleKBSourceForSync{
					ID:           source.ID,
					Name:         source.Name,
					Description:  source.Description,
					SourceType:   source.SourceType,
					DefaultRef:   source.DefaultRef,
					Repositories: s.agentAccessibleKBRepositoriesForSource(source, reposBySource[source.ID], grantRowsByIntent[grant.ID]),
				}
				if len(syncSource.Repositories) == 0 {
					continue
				}
				syncKB.Sources = append(syncKB.Sources, syncSource)
			}
			if len(syncKB.Sources) == 0 {
				continue
			}
			items = append(items, syncKB)
		}
		out.Agents[agentID] = items
	}

	lease, err := s.createKBSyncToken(ctx, workspaceID, subject, out.Agents)
	if err != nil {
		return AgentKnowledgeBaseAccessBatch{}, err
	}
	out.SyncToken = lease.Token
	out.SyncTokenExpires = lease.ExpiresAt
	return out, nil
}

func (s *KBService) requireOwnedAgents(ctx context.Context, workspaceID, subject string, agentIDs []string) error {
	if len(agentIDs) == 0 {
		return nil
	}
	agents, err := s.listOwnedAgents(ctx, workspaceID, subject)
	if err != nil {
		return err
	}
	owned := make(map[string]struct{}, len(agents))
	for _, agent := range agents {
		owned[strings.TrimSpace(agent.ID)] = struct{}{}
	}
	for _, id := range agentIDs {
		if _, ok := owned[strings.TrimSpace(id)]; !ok {
			return forbidden("agent must be owned by the current user")
		}
	}
	return nil
}

func (s *KBService) accessibleKBReposBySource(ctx context.Context, workspaceID string, sourceIDs []string) (map[string][]reposstore.ResourceRepository, error) {
	out := make(map[string][]reposstore.ResourceRepository)
	if s == nil || s.repositoryStore == nil {
		return out, nil
	}
	repos, err := s.repositoryStore.ListForResourceBatch(ctx, workspaceID, reposstore.ResourceTypeKBSource, sourceIDs)
	if err != nil {
		return nil, err
	}
	for _, repo := range eligibleAgentRepositoryAccessRepos(repos) {
		out[repo.ResourceID] = append(out[repo.ResourceID], repo)
	}
	return out, nil
}

func (s *KBService) kbRepositoryGrantRowsByIntent(ctx context.Context, workspaceID string, grantIDs []string) (map[string][]reposstore.ResourceRepositoryAccessGrant, error) {
	out := make(map[string][]reposstore.ResourceRepositoryAccessGrant)
	if s == nil || s.repositoryStore == nil {
		return out, nil
	}
	rows, err := s.repositoryStore.ListAccessGrantsByIntentIDs(ctx, workspaceID, reposstore.AccessIntentTypeKBGrant, grantIDs)
	if err != nil {
		return nil, err
	}
	for _, row := range rows {
		if row.SubjectType != "agent" || !row.Materialized || strings.TrimSpace(row.Status) != reposstore.AccessStatusCurrent {
			continue
		}
		out[row.IntentID] = append(out[row.IntentID], row)
	}
	return out, nil
}

func (s *KBService) agentAccessibleKBRepositoriesForSource(source kb.Source, repos []reposstore.ResourceRepository, grants []reposstore.ResourceRepositoryAccessGrant) []AgentAccessibleKBRepositoryForSync {
	if len(repos) == 0 || len(grants) == 0 {
		return nil
	}
	grantsByRole := make(map[string]reposstore.ResourceRepositoryAccessGrant, len(grants))
	for _, grant := range grants {
		if strings.TrimSpace(grant.ResourceType) != reposstore.ResourceTypeKBSource || strings.TrimSpace(grant.ResourceID) != strings.TrimSpace(source.ID) {
			continue
		}
		grantsByRole[strings.TrimSpace(grant.RepoRole)] = grant
	}
	out := make([]AgentAccessibleKBRepositoryForSync, 0, len(repos))
	for i := range repos {
		repo := repos[i]
		grant, ok := grantsByRole[strings.TrimSpace(repo.RepoRole)]
		if !ok {
			continue
		}
		repoCopy := repo
		profile := kb.NormalizeKnowledgeBaseRepositoryAccessProfile(firstNonEmptyString(grant.IntentProfile, grant.Permission))
		if profile == kb.KnowledgeBaseRepositoryAccessProfileNone && grant.Permission != "" {
			profile = accessProfileForRepositoryPermission(grant.Permission)
		}
		out = append(out, AgentAccessibleKBRepositoryForSync{
			RepoRole:      repo.RepoRole,
			Repository:    &repoCopy,
			Branch:        firstNonEmptyString(repo.DefaultRef, source.DefaultRef, "main"),
			AccessProfile: profile,
			AccessStatus:  grant.Status,
			Writable:      repositoryAccessPermissionRank(grant.Permission) >= repositoryAccessPermissionRank(reposstore.AccessPermissionWrite),
		})
	}
	return out
}

func accessProfileForRepositoryPermission(permission string) string {
	switch strings.TrimSpace(permission) {
	case reposstore.AccessPermissionAdmin:
		return kb.KnowledgeBaseRepositoryAccessProfileManage
	case reposstore.AccessPermissionWrite:
		return kb.KnowledgeBaseRepositoryAccessProfileWrite
	case reposstore.AccessPermissionRead:
		return kb.KnowledgeBaseRepositoryAccessProfileRead
	default:
		return kb.KnowledgeBaseRepositoryAccessProfileNone
	}
}

func (s *KBService) createKBSyncToken(ctx context.Context, workspaceID, subject string, agents map[string][]AgentAccessibleKnowledgeBaseForSync) (repos.RepositorySyncTokenLease, error) {
	if s == nil || s.syncTokenIssuer == nil {
		return repos.RepositorySyncTokenLease{}, nil
	}
	repositories := kbRepositoryFullNamesForSync(agents)
	if len(repositories) == 0 {
		return repos.RepositorySyncTokenLease{}, nil
	}
	request := access.NewSyncLeaseRequest(workspaceID, subject, repos.RepositorySyncTokenDefaultProviderHost, "KB sync lease", repositories, []string{
		repos.RepositorySyncCapabilityRead,
		repos.RepositorySyncCapabilityClone,
	})
	return s.syncTokenIssuer.CreateRepositorySyncToken(ctx, repos.RepositorySyncTokenRequest{
		WorkspaceID:      request.WorkspaceID,
		ActorPrincipalID: request.ActorPrincipalID,
		ProviderHost:     request.ProviderHost,
		Repositories:     request.Repositories,
		Capabilities:     request.Capabilities,
		DisplayName:      request.DisplayName,
	})
}

func kbRepositoryFullNamesForSync(agents map[string][]AgentAccessibleKnowledgeBaseForSync) []string {
	fullNames := make([]string, 0)
	for _, bases := range agents {
		for _, base := range bases {
			for _, source := range base.Sources {
				for _, repo := range source.Repositories {
					if repo.Repository != nil {
						fullNames = append(fullNames, repo.Repository.RepoFullName)
					}
				}
			}
		}
	}
	return access.UniqueRepositoryFullNames(fullNames)
}

func (s *KBService) enqueueAgentKnowledgeBaseAccessChange(ctx context.Context, workspaceID, agentID string, occurredAt time.Time) error {
	if s == nil {
		return nil
	}
	outbox := s.assetAccessOutboxForStore(nil)
	if outbox == nil {
		return nil
	}
	_, err := access.EnqueueAccessChange(ctx, outbox, memory.AgentAssetAccessChangeInput{
		WorkspaceID: strings.TrimSpace(workspaceID),
		AgentID:     strings.TrimSpace(agentID),
		AssetType:   access.AssetTypeKB,
		OccurredAt:  occurredAt,
	})
	return err
}

// enqueueKnowledgeBaseAccessChangesForSource reuses the daemon KB asset
// access-change pipeline when source content changes without a grant mutation.
func (s *KBService) enqueueKnowledgeBaseAccessChangesForSource(ctx context.Context, tx *kb.Store, source kb.Source, occurredAt time.Time) error {
	if s == nil || s.assetAccessOutbox == nil {
		return nil
	}
	if tx == nil {
		tx = s.store
	}
	if tx == nil {
		return invalidArgument("knowledge base store is not configured")
	}
	workspaceID := strings.TrimSpace(source.WorkspaceID)
	kbID := strings.TrimSpace(source.KnowledgeBaseID)
	if workspaceID == "" {
		return invalidArgument("source workspace id is required")
	}
	if kbID == "" {
		return invalidArgument("source knowledge base id is required")
	}
	agentIDs, err := tx.ListKnowledgeBaseAgentGrantSubjectIDs(ctx, workspaceID, kbID)
	if err != nil {
		return err
	}
	outbox := s.assetAccessOutboxForStore(tx)
	for _, agentID := range agentIDs {
		_, err := access.EnqueueAccessChange(ctx, outbox, memory.AgentAssetAccessChangeInput{
			WorkspaceID: workspaceID,
			AgentID:     strings.TrimSpace(agentID),
			AssetType:   access.AssetTypeKB,
			OccurredAt:  occurredAt,
		})
		if err != nil {
			return err
		}
	}
	return nil
}

func (s *KBService) assetAccessOutboxForStore(tx *kb.Store) *memory.Store {
	if s == nil || s.assetAccessOutbox == nil {
		return nil
	}
	if tx != nil && tx.DB() != nil {
		return s.assetAccessOutbox.WithDB(tx.DB())
	}
	return s.assetAccessOutbox
}
