package runtime

import (
	"fmt"
	"strings"
)

type explorationReference struct {
	HandleID string
	Path     string
}

type explorationWebReference struct {
	URL     string
	Title   string
	Snippet string
	Source  string
}

type explorationRefRoot struct {
	HandleID   string
	BaseAlias  string
	RepoRole   string
	CurrentRef string
	TargetRef  string
	RefKind    string
}

type explorationReferences struct {
	rootAliases     map[string]string
	rootByHandle    map[string]string
	files           map[string]explorationReference
	fileByPath      map[string]string
	directories     map[string]explorationReference
	directoryByPath map[string]string
	webs            map[string]explorationWebReference
	webByURL        map[string]string
	refRoots        map[string]explorationRefRoot
	nextFile        int
	nextDirectory   int
	nextWeb         int
}

func newExplorationReferences(roots []RootHandleView) *explorationReferences {
	refs := &explorationReferences{
		rootAliases:     make(map[string]string, len(roots)),
		rootByHandle:    make(map[string]string, len(roots)),
		files:           make(map[string]explorationReference),
		fileByPath:      make(map[string]string),
		directories:     make(map[string]explorationReference),
		directoryByPath: make(map[string]string),
		webs:            make(map[string]explorationWebReference),
		webByURL:        make(map[string]string),
		refRoots:        make(map[string]explorationRefRoot),
	}
	for index, root := range roots {
		alias := fmt.Sprintf("r%d", index+1)
		refs.rootAliases[alias] = root.HandleID
		refs.rootByHandle[root.HandleID] = alias
	}
	return refs
}

func (refs *explorationReferences) addRefRoot(snapshot RefSnapshot, baseAlias string) string {
	if alias := refs.rootByHandle[strings.TrimSpace(snapshot.HandleID)]; alias != "" {
		return alias
	}
	alias := fmt.Sprintf("r%d", len(refs.rootAliases)+1)
	refs.rootAliases[alias] = strings.TrimSpace(snapshot.HandleID)
	refs.rootByHandle[strings.TrimSpace(snapshot.HandleID)] = alias
	refs.refRoots[alias] = explorationRefRoot{
		HandleID: snapshot.HandleID, BaseAlias: strings.TrimSpace(baseAlias),
		RepoRole:   snapshot.RepoRole,
		CurrentRef: strings.TrimSpace(snapshot.CurrentRef), TargetRef: strings.TrimSpace(snapshot.TargetRef),
		RefKind: strings.TrimSpace(snapshot.RefKind),
	}
	return alias
}

func (refs *explorationReferences) resolveRefRoot(alias string) (explorationRefRoot, bool) {
	value, ok := refs.refRoots[strings.TrimSpace(alias)]
	return value, ok
}

func (refs *explorationReferences) roots() []string {
	aliases := make([]string, 0, len(refs.rootAliases))
	for index := 1; index <= len(refs.rootAliases); index++ {
		aliases = append(aliases, fmt.Sprintf("r%d", index))
	}
	return aliases
}

func (refs *explorationReferences) resolveRoot(alias string) (string, bool) {
	handleID, ok := refs.rootAliases[strings.TrimSpace(alias)]
	return handleID, ok
}

func (refs *explorationReferences) rootAlias(handleID string) string {
	return refs.rootByHandle[strings.TrimSpace(handleID)]
}

func (refs *explorationReferences) fileRef(handleID, filePath string) string {
	key := referencePathKey(handleID, filePath)
	if existing := refs.fileByPath[key]; existing != "" {
		return existing
	}
	refs.nextFile++
	ref := fmt.Sprintf("f%d", refs.nextFile)
	refs.files[ref] = explorationReference{HandleID: handleID, Path: filePath}
	refs.fileByPath[key] = ref
	return ref
}

func (refs *explorationReferences) prospectiveFileRef(handleID, filePath string) string {
	if existing := refs.fileByPath[referencePathKey(handleID, filePath)]; existing != "" {
		return existing
	}
	return fmt.Sprintf("f%d", refs.nextFile+1)
}

func (refs *explorationReferences) directoryRef(handleID, directoryPath string) string {
	key := referencePathKey(handleID, directoryPath)
	if existing := refs.directoryByPath[key]; existing != "" {
		return existing
	}
	refs.nextDirectory++
	ref := fmt.Sprintf("d%d", refs.nextDirectory)
	refs.directories[ref] = explorationReference{HandleID: handleID, Path: directoryPath}
	refs.directoryByPath[key] = ref
	return ref
}

func (refs *explorationReferences) prospectiveDirectoryRef(handleID, directoryPath string) string {
	if existing := refs.directoryByPath[referencePathKey(handleID, directoryPath)]; existing != "" {
		return existing
	}
	return fmt.Sprintf("d%d", refs.nextDirectory+1)
}

func (refs *explorationReferences) resolveFile(ref string) (explorationReference, bool) {
	value, ok := refs.files[strings.TrimSpace(ref)]
	return value, ok
}

func (refs *explorationReferences) resolveDirectory(ref string) (explorationReference, bool) {
	value, ok := refs.directories[strings.TrimSpace(ref)]
	return value, ok
}

func (refs *explorationReferences) webRef(candidate explorationWebReference) string {
	key := strings.TrimSpace(candidate.URL)
	if existing := refs.webByURL[key]; existing != "" {
		return existing
	}
	refs.nextWeb++
	ref := fmt.Sprintf("w%d", refs.nextWeb)
	refs.webs[ref] = candidate
	refs.webByURL[key] = ref
	return ref
}

func (refs *explorationReferences) resolveWeb(ref string) (explorationWebReference, bool) {
	value, ok := refs.webs[strings.TrimSpace(ref)]
	return value, ok
}

func referencePathKey(handleID, path string) string {
	return strings.TrimSpace(handleID) + "\x00" + strings.TrimSpace(path)
}

func normalizeExplorationActionReferences(refs *explorationReferences, action *explorationAction) string {
	switch action.Name {
	case knowledgeSearchFunction:
		for _, alias := range action.Search.RootAliases {
			if _, ok := refs.resolveRoot(alias); !ok {
				return CodeHandleRejected
			}
		}
	case knowledgeListFunction:
		rootHandleID := ""
		if action.List.RootAlias != "" {
			var ok bool
			rootHandleID, ok = refs.resolveRoot(action.List.RootAlias)
			if !ok {
				return CodeHandleRejected
			}
		}
		if action.List.DirectoryRef != "" {
			directory, ok := refs.resolveDirectory(action.List.DirectoryRef)
			if !ok {
				return CodeHandleRejected
			}
			if rootHandleID != "" && directory.HandleID != rootHandleID {
				return CodeHandleRejected
			}
			// A matching root alias is redundant once the attempt-local directory
			// reference has been resolved. Normalize it before duplicate detection.
			action.List.RootAlias = ""
		}
	case knowledgeReadFunction:
		if _, ok := refs.resolveFile(action.Read.FileRef); !ok {
			return CodeHandleRejected
		}
	case knowledgeWebReadFunction:
		if _, ok := refs.resolveWeb(action.WebRead.WebRef); !ok {
			return CodeHandleRejected
		}
	case knowledgeRefFunction:
		if _, ok := refs.resolveRoot(action.Ref.RootAlias); !ok {
			return CodeHandleRejected
		}
	case knowledgeAnswerFunction:
		if action.Answer.SourceBranchChange != nil {
			refRoot, ok := refs.resolveRefRoot(action.Answer.SourceBranchChange.BranchRootAlias)
			if !ok || refRoot.RepoRole != RepoRoleMirror || refRoot.RefKind != RefKindBranch {
				return CodeHandleRejected
			}
		}
	}
	return ""
}
