package runtime

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

	"context"
	"errors"
	"path"
	"regexp"
	"strings"
	"time"

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

var (
	errStopRepositoryStream = errors.New("knowledge repository result limit reached")
	repositoryCommitPattern = regexp.MustCompile(`^[0-9a-f]{40}$`)
)

type Service struct {
	Handles    HandleStore
	Repository RepositoryPrimitive
	Clock      func() time.Time
}

// ExecutionRepository is a turn-scoped, read-only repository session. Its
// roots are authorized once before exploration starts, so model-directed tool
// calls cannot amplify handle-store or product-authorization reads.
type ExecutionRepository struct {
	handles    map[string]protocol.RootHandle
	repository RepositoryPrimitive
}

type RefSnapshot struct {
	HandleID   string
	SourceID   string
	RepoRole   string
	CurrentRef string
	TargetRef  string
	RefKind    string
}

// AuthorizeExecution validates every requested root exactly once and returns
// an immutable repository session plus the public root metadata shown to the
// hosted exploration model.
func (service *Service) AuthorizeExecution(ctx context.Context, principal protocol.Principal, handleIDs []string) (*ExecutionRepository, []RootHandleView, error) {
	if service == nil || service.Handles == nil || service.Repository == nil {
		return nil, nil, repositoryFailure("Knowledge Q&A repository tools are unavailable", nil)
	}
	principal = normalizePrincipal(principal)
	if err := validatePrincipal(principal); err != nil {
		return nil, nil, err
	}
	handleIDs = normalizeUniqueStrings(handleIDs)
	if len(handleIDs) == 0 {
		return nil, nil, invalidRequest("at least one root handle is required")
	}
	loaded, err := service.Handles.LoadRootHandles(ctx, principal, handleIDs)
	if err != nil {
		return nil, nil, handleRejected()
	}
	loadedByID := make(map[string]protocol.RootHandle, len(loaded))
	for _, handle := range loaded {
		if _, duplicate := loadedByID[handle.ID]; duplicate {
			return nil, nil, handleRejected()
		}
		loadedByID[handle.ID] = handle
	}
	handles := make(map[string]protocol.RootHandle, len(handleIDs))
	views := make([]RootHandleView, 0, len(handleIDs))
	for _, handleID := range handleIDs {
		handle, ok := loadedByID[handleID]
		if !ok {
			return nil, nil, handleRejected()
		}
		if err := service.validateHandle(principal, handleID, handle); err != nil {
			return nil, nil, err
		}
		handles[handle.ID] = handle
		views = append(views, rootHandleView(handle))
	}
	return &ExecutionRepository{handles: handles, repository: service.Repository}, views, nil
}

func rootHandleView(handle protocol.RootHandle) RootHandleView {
	return RootHandleView{
		HandleID: handle.ID, KnowledgeBaseID: handle.KnowledgeBaseID, SourceID: handle.SourceID,
		RootKind: handle.RootKind, RepoRole: handle.RepoRole, Revision: handle.Revision,
		FreshnessStatus: handle.FreshnessStatus, DefaultRef: handle.DefaultRef, SourceName: handle.SourceName,
	}
}

func (session *ExecutionRepository) handlesFor(handleIDs []string) ([]protocol.RootHandle, error) {
	handleIDs = normalizeUniqueStrings(handleIDs)
	if len(handleIDs) == 0 {
		return nil, invalidRequest("at least one root handle is required")
	}
	handles := make([]protocol.RootHandle, 0, len(handleIDs))
	for _, handleID := range handleIDs {
		handle, ok := session.handles[handleID]
		if !ok {
			return nil, handleRejected()
		}
		handles = append(handles, handle)
	}
	return handles, nil
}

// Grep searches the already-authorized immutable roots without reloading their
// durable handles.
func (session *ExecutionRepository) Grep(ctx context.Context, request GrepRequest, emit func(GrepMatch) error) (GrepCompletion, error) {
	if session == nil || session.repository == nil {
		return GrepCompletion{}, repositoryFailure("Knowledge Q&A repository tools are unavailable", nil)
	}
	handles, err := session.handlesFor(request.HandleIDs)
	if err != nil {
		return GrepCompletion{}, err
	}
	return grepAuthorized(ctx, session.repository, handles, request, emit)
}

// List enumerates one directory level in an already-authorized immutable root.
func (session *ExecutionRepository) List(ctx context.Context, request ListRequest, emit func(ListEntry) error) error {
	if session == nil || session.repository == nil {
		return repositoryFailure("Knowledge Q&A repository tools are unavailable", nil)
	}
	handles, err := session.handlesFor([]string{request.HandleID})
	if err != nil {
		return err
	}
	return listAuthorized(ctx, session.repository, handles[0], request, emit)
}

// Read streams one explicit line range from an already-authorized immutable
// root.
func (session *ExecutionRepository) Read(ctx context.Context, request ReadRequest, emit func(ReadChunk) error) error {
	if session == nil || session.repository == nil {
		return repositoryFailure("Knowledge Q&A repository tools are unavailable", nil)
	}
	handles, err := session.handlesFor([]string{request.HandleID})
	if err != nil {
		return err
	}
	return readAuthorized(ctx, session.repository, handles[0], request, emit)
}

// OpenRef resolves one branch or tag inside an already-authorized primary Source
// repository and binds the resulting immutable commit to this execution only.
// It cannot cross repositories or widen the requester's Source authorization.
func (session *ExecutionRepository) OpenRef(ctx context.Context, handleID, refName, refKind string) (RefSnapshot, error) {
	if session == nil || session.repository == nil {
		return RefSnapshot{}, repositoryFailure("Knowledge Q&A repository tools are unavailable", nil)
	}
	handleID = strings.TrimSpace(handleID)
	refName = strings.TrimSpace(refName)
	refKind = strings.TrimSpace(refKind)
	handle, ok := session.handles[handleID]
	if !ok {
		return RefSnapshot{}, handleRejected()
	}
	if handle.RootKind != RootKindPrimary || handle.SourceID == "" ||
		(handle.RepoRole != RepoRolePrimary && handle.RepoRole != RepoRoleMirror) {
		return RefSnapshot{}, invalidRequest("ref snapshots require a primary Source repository root")
	}
	if refName == "" || len(refName) > 255 || (refKind != RefKindBranch && refKind != RefKindTag) {
		return RefSnapshot{}, invalidRequest("repository ref is invalid")
	}
	resolver, ok := session.repository.(RepositoryRefPrimitive)
	if !ok {
		return RefSnapshot{}, repositoryFailure("Knowledge Q&A ref snapshots are unavailable", nil)
	}
	revision, err := resolver.ResolveRef(ctx, RepositorySnapshot{
		RepositoryRef: handle.RepositoryRef,
		Revision:      handle.Revision,
		WorkspaceID:   handle.WorkspaceID,
		RequesterID:   handle.RequesterID,
		SourceID:      handle.SourceID,
	}, refName, refKind)
	if err != nil {
		return RefSnapshot{}, err
	}
	revision = strings.TrimSpace(revision)
	if !repositoryCommitPattern.MatchString(revision) {
		return RefSnapshot{}, repositoryFailure("Repository primitive returned an invalid ref snapshot", nil)
	}
	derivedID, err := identifiers.NewEntityID("kqf")
	if err != nil {
		return RefSnapshot{}, repositoryFailure("Knowledge Q&A ref snapshot could not be allocated", err)
	}
	derived := handle
	derived.ID = derivedID
	derived.Revision = revision
	derived.ResolvedRef = refName
	derived.ResolvedRefKind = refKind
	derived.FreshnessStatus = "ref_snapshot"
	session.handles[derived.ID] = derived
	return RefSnapshot{
		HandleID: derived.ID, SourceID: handle.SourceID, RepoRole: handle.RepoRole,
		CurrentRef: strings.TrimSpace(handle.DefaultRef), TargetRef: refName, RefKind: refKind,
	}, nil
}

func grepAuthorized(ctx context.Context, repository RepositoryPrimitive, handles []protocol.RootHandle, request GrepRequest, emit func(GrepMatch) error) (GrepCompletion, error) {
	pattern := strings.TrimSpace(request.Pattern)
	if pattern == "" {
		return GrepCompletion{}, invalidRequest("grep pattern is required")
	}
	if _, err := regexp.CompilePOSIX(pattern); err != nil {
		return GrepCompletion{}, invalidRequest("grep pattern is not a valid POSIX extended regular expression")
	}
	pathGlobs, err := normalizePathGlobs(request.PathGlobs)
	if err != nil {
		return GrepCompletion{}, err
	}
	if emit == nil {
		return GrepCompletion{}, invalidRequest("grep result consumer is required")
	}
	for _, handle := range handles {
		snapshot := RepositorySnapshot{
			RepositoryRef: handle.RepositoryRef, Revision: handle.Revision,
			WorkspaceID: handle.WorkspaceID, RequesterID: handle.RequesterID, SourceID: handle.SourceID,
		}
		err := repository.Grep(ctx, snapshot, RepositoryGrepRequest{
			Pattern: pattern, PathGlobs: pathGlobs,
		}, func(candidate RepositoryGrepMatch) error {
			if err := contextError(ctx); err != nil {
				return err
			}
			candidatePath, err := normalizeRelativePath(candidate.Path)
			if err != nil || candidate.Line < 1 {
				return repositoryFailure("Repository primitive returned an invalid grep result", err)
			}
			return emit(GrepMatch{
				HandleID: handle.ID,
				Path:     candidatePath,
				Line:     candidate.Line,
				Preview:  candidate.Preview,
			})
		})
		if err != nil {
			if errors.Is(err, errStopRepositoryStream) {
				return GrepCompletion{Complete: false, Reason: "result_limit"}, nil
			}
			mapped := mapRepositoryError(err)
			var toolErr *Error
			if errors.As(mapped, &toolErr) && (toolErr.Code == CodeCanceled || toolErr.Code == CodeResourceExhausted) {
				return GrepCompletion{Complete: false, Reason: toolErr.Code}, mapped
			}
			return GrepCompletion{}, mapped
		}
	}
	return GrepCompletion{Complete: true}, nil
}

func listAuthorized(ctx context.Context, repository RepositoryPrimitive, handle protocol.RootHandle, request ListRequest, emit func(ListEntry) error) error {
	directory := strings.TrimSpace(request.Path)
	if directory != "" {
		var err error
		directory, err = normalizeRelativePath(directory)
		if err != nil {
			return err
		}
	}
	if emit == nil {
		return invalidRequest("list result consumer is required")
	}
	err := repository.List(ctx, RepositorySnapshot{
		RepositoryRef: handle.RepositoryRef, Revision: handle.Revision,
		WorkspaceID: handle.WorkspaceID, RequesterID: handle.RequesterID, SourceID: handle.SourceID,
	}, RepositoryListRequest{Path: directory}, func(candidate RepositoryListEntry) error {
		if err := contextError(ctx); err != nil {
			return err
		}
		entryPath, err := normalizeRelativePath(candidate.Path)
		if err != nil || (candidate.Kind != "file" && candidate.Kind != "directory") {
			return repositoryFailure("Repository primitive returned an invalid list entry", err)
		}
		return emit(ListEntry{HandleID: handle.ID, Path: entryPath, Kind: candidate.Kind})
	})
	if errors.Is(err, errStopRepositoryStream) {
		return nil
	}
	return mapRepositoryError(err)
}

func readAuthorized(ctx context.Context, repository RepositoryPrimitive, handle protocol.RootHandle, request ReadRequest, emit func(ReadChunk) error) error {
	filePath, err := normalizeRelativePath(request.Path)
	if err != nil {
		return err
	}
	if request.LineStart < 1 || request.LineEnd < request.LineStart {
		return invalidRequest("read line range is invalid")
	}
	if emit == nil {
		return invalidRequest("read result consumer is required")
	}

	snapshot := RepositorySnapshot{
		RepositoryRef: handle.RepositoryRef, Revision: handle.Revision,
		WorkspaceID: handle.WorkspaceID, RequesterID: handle.RequesterID, SourceID: handle.SourceID,
	}
	err = repository.Read(ctx, snapshot, RepositoryReadRequest{
		Path: filePath, LineStart: request.LineStart, LineEnd: request.LineEnd,
	}, func(candidate RepositoryReadChunk) error {
		if err := contextError(ctx); err != nil {
			return err
		}
		if candidate.LineStart < request.LineStart ||
			candidate.LineEnd < candidate.LineStart ||
			candidate.LineEnd > request.LineEnd {
			return repositoryFailure("Repository primitive returned an invalid read range", nil)
		}
		return emit(ReadChunk{
			HandleID:  handle.ID,
			Content:   append([]byte(nil), candidate.Content...),
			LineStart: candidate.LineStart,
			LineEnd:   candidate.LineEnd,
			Citation:  citationFor(handle, filePath, candidate.LineStart, candidate.LineEnd),
		})
	})
	if errors.Is(err, errStopRepositoryStream) {
		return nil
	}
	return mapRepositoryError(err)
}

func (service *Service) validateHandle(principal protocol.Principal, handleID string, handle protocol.RootHandle) error {
	if handle.ID != handleID ||
		strings.TrimSpace(handle.OrganizationID) != principal.OrganizationID ||
		strings.TrimSpace(handle.WorkspaceID) != principal.WorkspaceID ||
		strings.TrimSpace(handle.RequesterID) != principal.RequesterID ||
		strings.TrimSpace(handle.TurnID) != principal.TurnID ||
		strings.TrimSpace(handle.KnowledgeBaseID) == "" ||
		!validRootKindRole(handle.RootKind, handle.RepoRole) ||
		strings.TrimSpace(handle.Revision) == "" ||
		strings.TrimSpace(handle.RepositoryRef) == "" ||
		!validResolvedRef(handle.ResolvedRef, handle.ResolvedRefKind) ||
		!handle.ExpiresAt.After(service.now()) {
		return handleRejected()
	}
	return nil
}

func validResolvedRef(refName, refKind string) bool {
	refName = strings.TrimSpace(refName)
	refKind = strings.TrimSpace(refKind)
	if refName == "" || refKind == "" {
		return refName == "" && refKind == ""
	}
	return len(refName) <= 255 && (refKind == RefKindBranch || refKind == RefKindTag)
}

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

func citationFor(handle protocol.RootHandle, filePath string, start, end int) CitationMetadata {
	return CitationMetadata{
		KnowledgeBaseID: handle.KnowledgeBaseID,
		SourceID:        handle.SourceID,
		RootKind:        handle.RootKind,
		RepoRole:        handle.RepoRole,
		Revision:        handle.Revision,
		ResolvedRef:     handle.ResolvedRef,
		ResolvedRefKind: handle.ResolvedRefKind,
		Path:            filePath,
		LineStart:       start,
		LineEnd:         end,
	}
}

func normalizePrincipal(principal protocol.Principal) protocol.Principal {
	principal.OrganizationID = strings.TrimSpace(principal.OrganizationID)
	principal.WorkspaceID = strings.TrimSpace(principal.WorkspaceID)
	principal.RequesterID = strings.TrimSpace(principal.RequesterID)
	principal.TurnID = strings.TrimSpace(principal.TurnID)
	return principal
}

func validatePrincipal(principal protocol.Principal) error {
	if principal.OrganizationID == "" || principal.WorkspaceID == "" ||
		principal.RequesterID == "" || principal.TurnID == "" {
		return handleRejected()
	}
	return nil
}

func normalizeRelativePath(raw string) (string, error) {
	raw = strings.TrimSpace(raw)
	if raw == "" || strings.ContainsRune(raw, '\x00') || strings.Contains(raw, `\`) || strings.HasPrefix(raw, "/") {
		return "", invalidRequest("repository path must be relative")
	}
	for _, segment := range strings.Split(raw, "/") {
		if segment == ".." {
			return "", invalidRequest("repository path must stay inside the authorized root")
		}
	}
	cleaned := path.Clean(raw)
	if cleaned == "." || cleaned == ".." || strings.HasPrefix(cleaned, "../") {
		return "", invalidRequest("repository path must stay inside the authorized root")
	}
	return cleaned, nil
}

func normalizePathGlobs(input []string) ([]string, error) {
	globs := normalizeUniqueStrings(input)
	for index, glob := range globs {
		normalized, err := normalizeRelativePath(glob)
		if err != nil {
			return nil, invalidRequest("grep path filter must stay inside the authorized root")
		}
		if _, err := path.Match(normalized, "placeholder"); err != nil {
			return nil, invalidRequest("grep path filter is invalid")
		}
		globs[index] = normalized
	}
	return globs, nil
}

func validRootKindRole(rootKind, repoRole string) bool {
	switch strings.TrimSpace(rootKind) {
	case RootKindLexicon:
		return strings.TrimSpace(repoRole) == RepoRoleWiki
	case RootKindInsights:
		return strings.TrimSpace(repoRole) == RepoRoleInsights
	case RootKindPrimary:
		switch strings.TrimSpace(repoRole) {
		case RepoRolePrimary, RepoRoleMirror:
			return true
		}
	}
	return false
}

func normalizeUniqueStrings(input []string) []string {
	seen := make(map[string]struct{}, len(input))
	result := make([]string, 0, len(input))
	for _, item := range input {
		item = strings.TrimSpace(item)
		if item == "" {
			continue
		}
		if _, ok := seen[item]; ok {
			continue
		}
		seen[item] = struct{}{}
		result = append(result, item)
	}
	return result
}

func invalidRequest(message string) error {
	return &Error{Code: CodeInvalidRequest, Message: message}
}

func handleRejected() error {
	// Keep the message intentionally uniform so unauthorized root IDs do not
	// leak whether an object exists, expired, or belongs to another principal.
	return &Error{Code: CodeHandleRejected, Message: "Knowledge root handle was rejected"}
}

func repositoryFailure(message string, cause error) error {
	return &Error{Code: CodeRepositoryFailed, Message: message, Cause: cause}
}

func contextError(ctx context.Context) error {
	if ctx == nil {
		return nil
	}
	if err := ctx.Err(); err != nil {
		return &Error{Code: CodeCanceled, Message: "Knowledge Q&A operation was canceled", Cause: err}
	}
	return nil
}

func mapRepositoryError(err error) error {
	if err == nil {
		return nil
	}
	var toolErr *Error
	if errors.As(err, &toolErr) {
		return toolErr
	}
	switch {
	case errors.Is(err, context.Canceled), errors.Is(err, context.DeadlineExceeded):
		return &Error{Code: CodeCanceled, Message: "Knowledge Q&A operation was canceled", Cause: err}
	case errors.Is(err, ErrRepositoryExhausted):
		return &Error{Code: CodeResourceExhausted, Message: "Knowledge repository resources were exhausted", Cause: err}
	case errors.Is(err, ErrRepositoryEscape):
		return repositoryFailure("Repository primitive rejected an unsafe path", err)
	default:
		return repositoryFailure("Knowledge repository operation failed", err)
	}
}
