package runtime

import (
	"github.com/tidbcloud/AgentTeam/internal/knowledgeqa/protocol"

	"context"
	"fmt"
	"strings"
	"time"

	"github.com/tidbcloud/AgentTeam/internal/core/identifiers"
)

type Resolver struct {
	Catalog   AuthorizedRootCatalog
	Handles   HandleStore
	Clock     func() time.Time
	HandleTTL time.Duration
}

// Resolve creates one immutable, requester-bound root snapshot. The catalog is
// responsible for current requester authorization and ready-revision
// expansion; the resolver rejects malformed candidates and persists only
// opaque handles, never repository paths or credentials in the runtime view.
func (resolver *Resolver) Resolve(ctx context.Context, request ScopeRequest) ([]RootHandleView, error) {
	views, _, err := resolver.ResolveWithDiagnostics(ctx, request)
	return views, err
}

// ResolveWithDiagnostics mints the same requester-bound handles as Resolve and
// also carries requester-safe coverage diagnostics when the catalog supports
// them.
func (resolver *Resolver) ResolveWithDiagnostics(ctx context.Context, request ScopeRequest) ([]RootHandleView, ScopeDiagnostics, error) {
	if resolver == nil || resolver.Catalog == nil || resolver.Handles == nil {
		return nil, ScopeDiagnostics{}, repositoryFailure("Knowledge root resolution is unavailable", nil)
	}
	request.Principal = normalizePrincipal(request.Principal)
	if err := validatePrincipal(request.Principal); err != nil {
		return nil, ScopeDiagnostics{}, err
	}
	request.KnowledgeBaseIDs = normalizeUniqueStrings(request.KnowledgeBaseIDs)
	request.SourceIDs = normalizeUniqueStrings(request.SourceIDs)
	var (
		candidates  []AuthorizedRootCandidate
		diagnostics ScopeDiagnostics
		err         error
	)
	if catalog, ok := resolver.Catalog.(interface {
		ResolveAuthorizedScope(context.Context, ScopeRequest) (AuthorizedScopeResolution, error)
	}); ok {
		var resolution AuthorizedScopeResolution
		resolution, err = catalog.ResolveAuthorizedScope(ctx, request)
		candidates = resolution.Roots
		diagnostics = resolution.Diagnostics
	} else {
		candidates, err = resolver.Catalog.ResolveAuthorizedRoots(ctx, request)
	}
	if err != nil {
		return nil, ScopeDiagnostics{}, repositoryFailure("Authorized knowledge roots are unavailable", err)
	}
	now := resolver.now()
	expiresAt := now.Add(resolver.handleTTL())
	handles := make([]protocol.RootHandle, 0, len(candidates))
	views := make([]RootHandleView, 0, len(candidates))
	seen := make(map[string]struct{}, len(candidates))
	for _, candidate := range candidates {
		candidate = normalizeCandidate(candidate)
		if candidate.KnowledgeBaseID == "" ||
			!validRootKindRole(candidate.RootKind, candidate.RepoRole) ||
			candidate.Revision == "" ||
			candidate.RepositoryRef == "" {
			return nil, ScopeDiagnostics{}, repositoryFailure("Authorized root catalog returned an invalid root", nil)
		}
		if candidate.RootKind == RootKindLexicon && candidate.SourceID != "" {
			return nil, ScopeDiagnostics{}, repositoryFailure("KB lexicon root cannot impersonate a Source", nil)
		}
		if candidate.RootKind != RootKindLexicon && candidate.SourceID == "" {
			return nil, ScopeDiagnostics{}, repositoryFailure("Source root is missing Source identity", nil)
		}
		key := strings.Join([]string{
			candidate.KnowledgeBaseID, candidate.SourceID, candidate.RootKind,
			candidate.RepoRole, candidate.Revision,
		}, "\x00")
		if _, ok := seen[key]; ok {
			continue
		}
		seen[key] = struct{}{}
		handleID, err := identifiers.NewEntityID("kqr")
		if err != nil {
			return nil, ScopeDiagnostics{}, fmt.Errorf("generate Knowledge Q&A root handle: %w", err)
		}
		handle := protocol.RootHandle{
			ID:              handleID,
			OrganizationID:  request.Principal.OrganizationID,
			WorkspaceID:     request.Principal.WorkspaceID,
			RequesterID:     request.Principal.RequesterID,
			TurnID:          request.Principal.TurnID,
			KnowledgeBaseID: candidate.KnowledgeBaseID,
			SourceID:        candidate.SourceID,
			SourceName:      candidate.SourceName,
			RootKind:        candidate.RootKind,
			RepoRole:        candidate.RepoRole,
			Revision:        candidate.Revision,
			RepositoryRef:   candidate.RepositoryRef,
			DefaultRef:      candidate.DefaultRef,
			FreshnessStatus: candidate.FreshnessStatus,
			ExpiresAt:       expiresAt,
			CreatedAt:       now,
		}
		if handle.RootKind == RootKindPrimary && handle.DefaultRef != "" {
			handle.ResolvedRef = handle.DefaultRef
			handle.ResolvedRefKind = RefKindBranch
		}
		handles = append(handles, handle)
		views = append(views, RootHandleView{
			HandleID:        handle.ID,
			KnowledgeBaseID: handle.KnowledgeBaseID,
			SourceID:        handle.SourceID,
			SourceName:      handle.SourceName,
			RootKind:        handle.RootKind,
			RepoRole:        handle.RepoRole,
			Revision:        handle.Revision,
			FreshnessStatus: handle.FreshnessStatus,
			DefaultRef:      handle.DefaultRef,
		})
	}
	if err := resolver.Handles.PutRootHandles(ctx, handles); err != nil {
		return nil, ScopeDiagnostics{}, repositoryFailure("Knowledge root snapshot could not be persisted", err)
	}
	return views, diagnostics, nil
}

func (resolver *Resolver) now() time.Time {
	if resolver != nil && resolver.Clock != nil {
		return resolver.Clock().UTC()
	}
	return time.Now().UTC()
}

func (resolver *Resolver) handleTTL() time.Duration {
	if resolver != nil && resolver.HandleTTL > 0 {
		return resolver.HandleTTL
	}
	return DefaultRootHandleTTL
}

func normalizeCandidate(candidate AuthorizedRootCandidate) AuthorizedRootCandidate {
	candidate.KnowledgeBaseID = strings.TrimSpace(candidate.KnowledgeBaseID)
	candidate.SourceID = strings.TrimSpace(candidate.SourceID)
	candidate.SourceName = strings.TrimSpace(candidate.SourceName)
	candidate.RootKind = strings.TrimSpace(candidate.RootKind)
	candidate.RepoRole = strings.TrimSpace(candidate.RepoRole)
	candidate.Revision = strings.TrimSpace(candidate.Revision)
	candidate.RepositoryRef = strings.TrimSpace(candidate.RepositoryRef)
	candidate.DefaultRef = strings.TrimSpace(candidate.DefaultRef)
	candidate.FreshnessStatus = strings.TrimSpace(candidate.FreshnessStatus)
	return candidate
}
