package kb

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

	"github.com/tidbcloud/AgentTeam/internal/service/content"
	storekb "github.com/tidbcloud/AgentTeam/internal/store/kb"
	"github.com/tidbcloud/AgentTeam/internal/store/resource/repos"
	"gorm.io/gorm"
)

const (
	kbContentSubjectUser  = "user"
	kbContentSubjectAgent = "agent"
)

// KBSourceContentAccessPolicy rechecks the live workspace, source ownership,
// and KB grant at every Content Store operation. The scoped principal is only
// a routing claim; revoking membership, a KB grant, or the source closes the
// native boundary immediately.
type KBSourceContentAccessPolicy struct {
	workspace  content.WorkspaceAccessVerifier
	agents     AgentWorkspaceAccessVerifier
	governance WorkspaceGovernanceAuthorizer
	store      kbSourceContentAccessStore
}

type kbSourceContentAccessStore interface {
	GetSourceByID(context.Context, string, string) (storekb.Source, error)
	GetKnowledgeBase(context.Context, string, string) (storekb.KnowledgeBase, error)
	GetKnowledgeBaseGrant(context.Context, string, string, string, string) (storekb.KnowledgeBaseGrant, error)
	ListKnowledgeBaseSourceRepositoryAccessOverridesForSources(context.Context, string, []string, []string) ([]storekb.KnowledgeBaseSourceRepositoryAccessOverride, error)
	ListSourceContentMappingsByVersions(context.Context, string, string, string, []string) ([]storekb.SourceContentMapping, error)
	ListSourceContentRepositoryRolesByContent(context.Context, string, string, string, string) ([]string, error)
}

func NewKBSourceContentAccessPolicy(
	workspace content.WorkspaceAccessVerifier,
	governance WorkspaceGovernanceAuthorizer,
	store kbSourceContentAccessStore,
) (*KBSourceContentAccessPolicy, error) {
	if workspace == nil || governance == nil || store == nil {
		return nil, fmt.Errorf("KB source content workspace verifiers and store are required")
	}
	agents, _ := workspace.(AgentWorkspaceAccessVerifier)
	return &KBSourceContentAccessPolicy{workspace: workspace, agents: agents, governance: governance, store: store}, nil
}

func (policy *KBSourceContentAccessPolicy) AuthorizeContentAccess(ctx context.Context, request content.AccessRequest) error {
	domain := strings.TrimSpace(request.Domain)
	if policy == nil || policy.workspace == nil || policy.governance == nil || policy.store == nil ||
		(domain != kbSourceContentDomain && domain != kbUADashboardContentDomain) {
		return content.ErrAccessDenied
	}
	if err := (content.ScopedAccessPolicy{}).AuthorizeContentAccess(ctx, request); err != nil {
		return err
	}

	workspaceID := strings.TrimSpace(request.WorkspaceID)
	sourceID := strings.TrimSpace(request.BindingKey)
	subject := strings.TrimSpace(request.Principal.Subject)
	subjectType := strings.TrimSpace(request.Principal.SubjectType)
	allowed, err := policy.hasWorkspaceAccess(ctx, workspaceID, subjectType, subject)
	if err != nil {
		return fmt.Errorf("verify KB source content workspace access: %w", err)
	}
	if !allowed {
		return content.ErrAccessDenied
	}

	source, err := policy.store.GetSourceByID(ctx, workspaceID, sourceID)
	if errors.Is(err, gorm.ErrRecordNotFound) {
		return content.ErrAccessDenied
	}
	if err != nil {
		return fmt.Errorf("load KB source content owner: %w", err)
	}
	base, err := policy.store.GetKnowledgeBase(ctx, workspaceID, strings.TrimSpace(source.KnowledgeBaseID))
	if errors.Is(err, gorm.ErrRecordNotFound) {
		return content.ErrAccessDenied
	}
	if err != nil {
		return fmt.Errorf("load KB source content knowledge base: %w", err)
	}
	if strings.TrimSpace(source.KnowledgeBaseID) == "" || source.KnowledgeBaseID != base.ID ||
		base.DeletedAt != nil || !base.IsActive {
		return content.ErrAccessDenied
	}
	repoRoles, err := policy.contentRepositoryRoles(ctx, request, source)
	if err != nil {
		return err
	}

	role := ""
	repositoryProfiles := make([]string, 0, len(repoRoles))
	if subjectType == kbContentSubjectUser && strings.TrimSpace(base.CreatedBy) == subject {
		role = "owner"
		repositoryProfiles = repeatedKBRepositoryProfiles(repoRoles, storekb.KnowledgeBaseRepositoryAccessProfileManage)
	} else if subjectType == kbContentSubjectUser {
		admin, adminErr := policy.governance.CanAdministerWorkspace(ctx, workspaceID, subject)
		if adminErr != nil {
			return fmt.Errorf("authorize KB source content workspace governance: %w", adminErr)
		}
		if admin {
			role = "manage"
			repositoryProfiles = repeatedKBRepositoryProfiles(repoRoles, storekb.KnowledgeBaseRepositoryAccessProfileManage)
		} else {
			grant, grantErr := policy.store.GetKnowledgeBaseGrant(ctx, workspaceID, base.ID, subjectType, subject)
			switch {
			case grantErr == nil:
				role = strings.TrimSpace(grant.Role)
				repositoryProfiles, err = policy.effectiveRepositoryProfiles(ctx, grant, source.ID, repoRoles)
				if err != nil {
					return err
				}
			case errors.Is(grantErr, gorm.ErrRecordNotFound):
				if strings.TrimSpace(base.PublicationState) != KBPublicationDraft {
					role = normalizedKBDefaultWorkspaceMemberRole(base.DefaultWorkspaceMemberRole)
					repositoryProfiles = repeatedKBRepositoryProfiles(repoRoles, base.DefaultRepositoryAccessProfile)
				}
			default:
				return fmt.Errorf("load KB source content grant: %w", grantErr)
			}
		}
	} else if subjectType == kbContentSubjectAgent {
		grant, grantErr := policy.store.GetKnowledgeBaseGrant(ctx, workspaceID, base.ID, subjectType, subject)
		switch {
		case grantErr == nil:
			role = strings.TrimSpace(grant.Role)
			repositoryProfiles, err = policy.effectiveRepositoryProfiles(ctx, grant, source.ID, repoRoles)
			if err != nil {
				return err
			}
		case errors.Is(grantErr, gorm.ErrRecordNotFound):
			return content.ErrAccessDenied
		default:
			return fmt.Errorf("load KB source content agent grant: %w", grantErr)
		}
	} else {
		return content.ErrAccessDenied
	}
	if strings.TrimSpace(base.PublicationState) == KBPublicationDraft && !roleCanEdit(role) {
		role = ""
	}
	if subjectType == kbContentSubjectAgent && !kbSourceContentAgentReadAction(request.Action) {
		return content.ErrAccessDenied
	}
	if !kbSourceContentGrantAllows(role, request.Action) {
		return content.ErrAccessDenied
	}
	for _, repositoryProfile := range repositoryProfiles {
		if !kbSourceContentRepositoryProfileAllows(repositoryProfile, request.Action) {
			return content.ErrAccessDenied
		}
	}
	return nil
}

func (policy *KBSourceContentAccessPolicy) contentRepositoryRoles(ctx context.Context, request content.AccessRequest, source storekb.Source) ([]string, error) {
	if strings.TrimSpace(request.Domain) == kbUADashboardContentDomain {
		return []string{repos.RepoRoleInsights}, nil
	}
	if strings.TrimSpace(request.ContentID) == "" || kbSourceContentMigrationPrincipal(request.Principal.Actions) {
		// The exact mapping does not exist until the bounded importer creates the
		// lineage (and FindContentByBinding has no ID yet). Product authorization
		// is still rechecked below; product reads after projection always carry an
		// exact content ID and therefore resolve the persisted repository role.
		return []string{repos.RepoRolePrimary}, nil
	}
	if err := validateKBSourceContentVersionScope(request.Action, request.VersionIDs); err != nil {
		return nil, err
	}
	if len(request.VersionIDs) > 0 {
		return policy.contentRepositoryRolesForVersions(ctx, request, source)
	}
	roles, err := policy.store.ListSourceContentRepositoryRolesByContent(ctx, request.WorkspaceID, source.KnowledgeBaseID, source.ID, request.ContentID)
	if err != nil {
		return nil, fmt.Errorf("load KB source content repository roles: %w", err)
	}
	return normalizeKBSourceContentRepositoryRoles(roles)
}

func (policy *KBSourceContentAccessPolicy) contentRepositoryRolesForVersions(ctx context.Context, request content.AccessRequest, source storekb.Source) ([]string, error) {
	versionIDs := uniqueTrimmedContentVersionIDs(request.VersionIDs)
	if len(versionIDs) == 0 || len(versionIDs) > 2 {
		return nil, content.ErrAccessDenied
	}
	mappings, err := policy.store.ListSourceContentMappingsByVersions(ctx, request.WorkspaceID, source.ID, request.ContentID, versionIDs)
	if err != nil {
		return nil, fmt.Errorf("load exact KB source content mappings: %w", err)
	}
	if len(mappings) != len(versionIDs) {
		return nil, content.ErrAccessDenied
	}
	expected := make(map[string]struct{}, len(versionIDs))
	for _, versionID := range versionIDs {
		expected[versionID] = struct{}{}
	}
	roles := make([]string, 0, len(mappings))
	for _, mapping := range mappings {
		if mapping.WorkspaceID != request.WorkspaceID || mapping.KnowledgeBaseID != source.KnowledgeBaseID ||
			mapping.SourceID != source.ID || mapping.ContentID != request.ContentID {
			return nil, content.ErrAccessDenied
		}
		if _, ok := expected[strings.TrimSpace(mapping.ContentVersionID)]; !ok {
			return nil, content.ErrAccessDenied
		}
		delete(expected, strings.TrimSpace(mapping.ContentVersionID))
		roles = append(roles, mapping.RepositoryRole)
	}
	if len(expected) != 0 {
		return nil, content.ErrAccessDenied
	}
	return normalizeKBSourceContentRepositoryRoles(roles)
}

func validateKBSourceContentVersionScope(action content.AccessAction, versionIDs []string) error {
	switch action {
	case content.AccessReadPayload, content.AccessReadArchive:
		if len(versionIDs) != 1 {
			return content.ErrAccessDenied
		}
	case content.AccessCompare:
		if len(versionIDs) != 2 {
			return content.ErrAccessDenied
		}
	}
	if len(versionIDs) > 2 {
		return content.ErrAccessDenied
	}
	for _, versionID := range versionIDs {
		if strings.TrimSpace(versionID) == "" {
			return content.ErrAccessDenied
		}
	}
	return nil
}

func uniqueTrimmedContentVersionIDs(versionIDs []string) []string {
	unique := make([]string, 0, len(versionIDs))
	seen := make(map[string]struct{}, len(versionIDs))
	for _, versionID := range versionIDs {
		versionID = strings.TrimSpace(versionID)
		if _, ok := seen[versionID]; ok {
			continue
		}
		seen[versionID] = struct{}{}
		unique = append(unique, versionID)
	}
	return unique
}

func normalizeKBSourceContentRepositoryRoles(roles []string) ([]string, error) {
	if len(roles) == 0 || len(roles) > 3 {
		return nil, content.ErrAccessDenied
	}
	unique := make([]string, 0, len(roles))
	seen := make(map[string]struct{}, len(roles))
	for _, role := range roles {
		role = strings.TrimSpace(role)
		if !kbSourceContentRepositoryRoleKnown(role) {
			return nil, content.ErrAccessDenied
		}
		if _, ok := seen[role]; ok {
			continue
		}
		seen[role] = struct{}{}
		unique = append(unique, role)
	}
	return unique, nil
}

func kbSourceContentRepositoryRoleKnown(repoRole string) bool {
	switch strings.TrimSpace(repoRole) {
	case repos.RepoRolePrimary, repos.RepoRoleMirror, repos.RepoRoleInsights:
		return true
	default:
		return false
	}
}

func kbSourceContentMigrationPrincipal(actions []content.AccessAction) bool {
	for _, action := range actions {
		if action == content.AccessCreateLineage {
			return true
		}
	}
	return false
}

func (policy *KBSourceContentAccessPolicy) effectiveRepositoryProfiles(ctx context.Context, grant storekb.KnowledgeBaseGrant, sourceID string, repoRoles []string) ([]string, error) {
	profiles := repeatedKBRepositoryProfiles(repoRoles, grant.RepositoryAccessProfile)
	overrides, err := policy.store.ListKnowledgeBaseSourceRepositoryAccessOverridesForSources(ctx, grant.WorkspaceID, []string{grant.ID}, []string{sourceID})
	if err != nil {
		return nil, fmt.Errorf("load KB source repository access overrides: %w", err)
	}
	roleIndex := make(map[string]int, len(repoRoles))
	for index, repoRole := range repoRoles {
		roleIndex[strings.TrimSpace(repoRole)] = index
	}
	for _, override := range overrides {
		if override.WorkspaceID != grant.WorkspaceID || override.KnowledgeBaseID != grant.KnowledgeBaseID || override.GrantID != grant.ID ||
			override.SourceID != sourceID || override.SubjectType != grant.SubjectType || override.SubjectID != grant.SubjectID ||
			!kbSourceContentRepositoryRoleKnown(override.RepoRole) {
			return nil, content.ErrAccessDenied
		}
		if index, ok := roleIndex[strings.TrimSpace(override.RepoRole)]; ok {
			profiles[index] = storekb.NormalizeKnowledgeBaseRepositoryAccessProfile(override.RepositoryAccessProfile)
		}
	}
	return profiles, nil
}

func repeatedKBRepositoryProfiles(repoRoles []string, profile string) []string {
	profiles := make([]string, len(repoRoles))
	for index := range profiles {
		profiles[index] = storekb.NormalizeKnowledgeBaseRepositoryAccessProfile(profile)
	}
	return profiles
}

func (policy *KBSourceContentAccessPolicy) hasWorkspaceAccess(ctx context.Context, workspaceID, subjectType, subject string) (bool, error) {
	switch subjectType {
	case kbContentSubjectUser:
		return policy.workspace.HasWorkspaceAccess(ctx, workspaceID, subject)
	case kbContentSubjectAgent:
		if policy.agents == nil {
			return false, nil
		}
		return policy.agents.HasAgentWorkspaceAccess(ctx, workspaceID, subject)
	default:
		return false, nil
	}
}

func kbSourceContentAgentReadAction(action content.AccessAction) bool {
	switch action {
	case content.AccessReadMetadata, content.AccessReadPayload,
		content.AccessReadHistory, content.AccessCompare,
		content.AccessReadArchive:
		return true
	default:
		return false
	}
}

func kbSourceContentGrantAllows(role string, action content.AccessAction) bool {
	role = strings.TrimSpace(role)
	switch action {
	case content.AccessReadMetadata, content.AccessReadPayload,
		content.AccessReadHistory, content.AccessCompare,
		content.AccessReadArchive:
		return role != "" && role != "none"
	case content.AccessCreateLineage, content.AccessPublish:
		return roleCanEdit(role)
	case content.AccessManagePin:
		return roleCanManage(role)
	default:
		return false
	}
}

func kbSourceContentRepositoryProfileAllows(profile string, action content.AccessAction) bool {
	switch storekb.NormalizeKnowledgeBaseRepositoryAccessProfile(profile) {
	case storekb.KnowledgeBaseRepositoryAccessProfileRead:
		return kbSourceContentAgentReadAction(action)
	case storekb.KnowledgeBaseRepositoryAccessProfileWrite:
		return action != content.AccessManagePin && action != content.AccessAction("")
	case storekb.KnowledgeBaseRepositoryAccessProfileManage:
		return true
	default:
		return false
	}
}

var _ content.ContentAccessPolicy = (*KBSourceContentAccessPolicy)(nil)
