package tools

import (
	"bufio"
	"context"
	"fmt"
	"os"
	"path/filepath"
	"sort"
	"strings"
	"sync"
	"time"
)

type MemorySearchTool struct {
	workspace string
	mu        sync.RWMutex
	cache     map[string]cachedMemoryFile
}

type memoryBlock struct {
	lineNum int
	content string
	heading string
	lower   string
}

type cachedMemoryFile struct {
	modTime    time.Time
	blocks     []memoryBlock
	tokenIndex map[string][]int
}

func NewMemorySearchTool(workspace string) *MemorySearchTool {
	return &MemorySearchTool{
		workspace: workspace,
		cache:     make(map[string]cachedMemoryFile),
	}
}

func (t *MemorySearchTool) Name() string {
	return "memory_search"
}

func (t *MemorySearchTool) Description() string {
	return "Semantically search MEMORY.md and memory/*.md files for information. Returns relevant snippets (paragraphs) containing the query terms."
}

func (t *MemorySearchTool) Parameters() map[string]interface{} {
	return map[string]interface{}{
		"type": "object",
		"properties": map[string]interface{}{
			"query": map[string]interface{}{
				"type":        "string",
				"description": "Search query keywords (e.g., 'docker deploy project')",
			},
			"namespace": map[string]interface{}{
				"type":        "string",
				"description": "Optional memory namespace. Use main for workspace memory, or subagent id for isolated memory.",
				"default":     "main",
			},
			"maxResults": map[string]interface{}{
				"type":        "integer",
				"description": "Maximum number of results to return",
				"default":     5,
			},
		},
		"required": []string{"query"},
	}
}

type searchResult struct {
	file    string
	lineNum int
	content string
	score   int
}

func (t *MemorySearchTool) Execute(ctx context.Context, args map[string]interface{}) (string, error) {
	query := MapStringArg(args, "query")
	if query == "" {
		return "", fmt.Errorf("query is required")
	}
	namespace := parseMemoryNamespaceArg(args)

	maxResults := MapIntArg(args, "maxResults", 5)
	if maxResults <= 0 {
		maxResults = 5
	}

	keywords := strings.Fields(strings.ToLower(query))
	if len(keywords) == 0 {
		return "Please provide search keywords.", nil
	}

	files := t.getMemoryFiles(namespace)

	resultsChan := make(chan []searchResult, len(files))
	var wg sync.WaitGroup

	for _, file := range files {
		if ctx.Err() != nil {
			break
		}
		wg.Add(1)
		go func(f string) {
			defer wg.Done()
			if ctx.Err() != nil {
				return
			}
			matches, err := t.searchFile(f, keywords)
			if err == nil {
				resultsChan <- matches
			}
		}(file)
	}

	go func() {
		wg.Wait()
		close(resultsChan)
	}()

	var allResults []searchResult
	for matches := range resultsChan {
		allResults = append(allResults, matches...)
	}

	sort.SliceStable(allResults, func(i, j int) bool {
		if allResults[i].score != allResults[j].score {
			return allResults[i].score > allResults[j].score
		}
		if allResults[i].file != allResults[j].file {
			return allResults[i].file < allResults[j].file
		}
		return allResults[i].lineNum < allResults[j].lineNum
	})

	if len(allResults) > maxResults {
		allResults = allResults[:maxResults]
	}

	if len(allResults) == 0 {
		return fmt.Sprintf("No memory found for query: %s (namespace=%s)", query, namespace), nil
	}

	var sb strings.Builder
	sb.WriteString(fmt.Sprintf("Found %d memories for '%s' (namespace=%s):\n\n", len(allResults), query, namespace))
	for _, res := range allResults {
		relPath, err := filepath.Rel(t.workspace, res.file)
		if err != nil || strings.HasPrefix(relPath, "..") {
			relPath = res.file
		}
		lineEnd := res.lineNum + countLines(res.content) - 1
		if lineEnd < res.lineNum {
			lineEnd = res.lineNum
		}
		sb.WriteString(fmt.Sprintf("Source: %s#L%d-L%d\n%s\n\n", relPath, res.lineNum, lineEnd, res.content))
	}

	return sb.String(), nil
}

func (t *MemorySearchTool) getMemoryFiles(namespace string) []string {
	var files []string

	base := memoryNamespaceBaseDir(t.workspace, namespace)

	// Check workspace MEMORY.md first
	mainMem := filepath.Join(base, "MEMORY.md")
	if _, err := os.Stat(mainMem); err == nil {
		files = append(files, mainMem)
	}

	// Backward-compatible location: memory/MEMORY.md
	legacyMem := filepath.Join(base, "memory", "MEMORY.md")
	if _, err := os.Stat(legacyMem); err == nil {
		files = append(files, legacyMem)
	}

	// Recursively include memory/**/*.md
	memDir := filepath.Join(base, "memory")
	_ = filepath.WalkDir(memDir, func(path string, d os.DirEntry, err error) error {
		if err != nil || d == nil || d.IsDir() {
			return nil
		}
		if strings.HasSuffix(strings.ToLower(d.Name()), ".md") {
			files = append(files, path)
		}
		return nil
	})

	return dedupeStrings(files)
}

func countLines(s string) int {
	s = strings.TrimSpace(s)
	if s == "" {
		return 0
	}
	return len(strings.Split(s, "\n"))
}

func dedupeStrings(items []string) []string {
	seen := make(map[string]struct{}, len(items))
	out := make([]string, 0, len(items))
	for _, v := range items {
		if _, ok := seen[v]; ok {
			continue
		}
		seen[v] = struct{}{}
		out = append(out, v)
	}
	return out
}

// searchFile searches parsed markdown blocks with cache by file modtime.
func (t *MemorySearchTool) searchFile(path string, keywords []string) ([]searchResult, error) {
	blocks, tokenIndex, err := t.getOrParseBlocks(path)
	if err != nil {
		return nil, err
	}
	candidate := candidateBlockIndexes(tokenIndex, keywords, len(blocks))
	results := make([]searchResult, 0, 8)
	for _, idx := range candidate {
		if idx < 0 || idx >= len(blocks) {
			continue
		}
		b := blocks[idx]
		score := 0
		for _, kw := range keywords {
			if strings.Contains(b.lower, kw) {
				score++
			}
			if b.heading != "" && strings.Contains(strings.ToLower(b.heading), kw) {
				score++
			}
		}
		if score == 0 {
			continue
		}
		content := b.content
		if b.heading != "" && !strings.HasPrefix(strings.TrimSpace(content), "#") {
			content = fmt.Sprintf("[%s]\n%s", b.heading, content)
		}
		results = append(results, searchResult{file: path, lineNum: b.lineNum, content: content, score: score})
	}
	return results, nil
}

func (t *MemorySearchTool) getOrParseBlocks(path string) ([]memoryBlock, map[string][]int, error) {
	st, err := os.Stat(path)
	if err != nil {
		return nil, nil, err
	}
	mod := st.ModTime()
	t.mu.RLock()
	if c, ok := t.cache[path]; ok && c.modTime.Equal(mod) {
		blocks := c.blocks
		idx := c.tokenIndex
		t.mu.RUnlock()
		return blocks, idx, nil
	}
	t.mu.RUnlock()

	blocks, tokenIndex, err := parseMarkdownBlocks(path)
	if err != nil {
		return nil, nil, err
	}
	t.mu.Lock()
	t.cache[path] = cachedMemoryFile{modTime: mod, blocks: blocks, tokenIndex: tokenIndex}
	t.mu.Unlock()
	return blocks, tokenIndex, nil
}

func parseMarkdownBlocks(path string) ([]memoryBlock, map[string][]int, error) {
	file, err := os.Open(path)
	if err != nil {
		return nil, nil, err
	}
	defer file.Close()

	blocks := make([]memoryBlock, 0, 32)
	scanner := bufio.NewScanner(file)
	var current strings.Builder
	blockStartLine := 1
	currentLine := 0
	lastHeading := ""

	flush := func() {
		content := strings.TrimSpace(current.String())
		if content == "" {
			current.Reset()
			return
		}
		blocks = append(blocks, memoryBlock{lineNum: blockStartLine, content: content, heading: lastHeading, lower: strings.ToLower(content)})
		current.Reset()
	}

	for scanner.Scan() {
		currentLine++
		line := scanner.Text()
		trimmed := strings.TrimSpace(line)
		isHeader := strings.HasPrefix(trimmed, "#")
		isEmpty := trimmed == ""
		isList := strings.HasPrefix(trimmed, "- ") || strings.HasPrefix(trimmed, "* ") || (len(trimmed) > 3 && trimmed[1] == '.' && trimmed[2] == ' ')

		if isHeader {
			flush()
			lastHeading = strings.TrimLeft(trimmed, "# ")
			blockStartLine = currentLine
			current.WriteString(line + "\n")
			flush()
			continue
		}
		if isEmpty {
			flush()
			blockStartLine = currentLine + 1
			continue
		}
		if isList {
			flush()
			blockStartLine = currentLine
		}
		if current.Len() == 0 {
			blockStartLine = currentLine
		}
		current.WriteString(line + "\n")
	}
	flush()
	tokenIndex := buildTokenIndex(blocks)
	return blocks, tokenIndex, nil
}

func buildTokenIndex(blocks []memoryBlock) map[string][]int {
	idx := make(map[string][]int, 256)
	for i, b := range blocks {
		seen := map[string]struct{}{}
		for _, tok := range tokenizeForIndex(b.lower + " " + strings.ToLower(b.heading)) {
			if _, ok := seen[tok]; ok {
				continue
			}
			seen[tok] = struct{}{}
			idx[tok] = append(idx[tok], i)
		}
	}
	return idx
}

func tokenizeForIndex(s string) []string {
	s = strings.ToLower(strings.TrimSpace(s))
	if s == "" {
		return nil
	}
	s = strings.NewReplacer("\n", " ", "\t", " ", ",", " ", ".", " ", ":", " ", ";", " ", "(", " ", ")", " ", "[", " ", "]", " ", "{", " ", "}", " ", "`", " ", "\"", " ", "'", " ").Replace(s)
	parts := strings.Fields(s)
	out := make([]string, 0, len(parts))
	for _, p := range parts {
		if len(p) < 2 {
			continue
		}
		out = append(out, p)
	}
	return out
}

func candidateBlockIndexes(tokenIndex map[string][]int, keywords []string, total int) []int {
	if total <= 0 {
		return nil
	}
	if len(keywords) == 0 || len(tokenIndex) == 0 {
		out := make([]int, 0, total)
		for i := 0; i < total; i++ {
			out = append(out, i)
		}
		return out
	}
	candMap := map[int]int{}
	for _, kw := range keywords {
		kw = strings.TrimSpace(strings.ToLower(kw))
		if kw == "" {
			continue
		}
		for tok, ids := range tokenIndex {
			if strings.Contains(tok, kw) {
				for _, id := range ids {
					candMap[id]++
				}
			}
		}
	}
	if len(candMap) == 0 {
		out := make([]int, 0, total)
		for i := 0; i < total; i++ {
			out = append(out, i)
		}
		return out
	}
	out := make([]int, 0, len(candMap))
	for id := range candMap {
		out = append(out, id)
	}
	sort.Slice(out, func(i, j int) bool { return candMap[out[i]] > candMap[out[j]] })
	return out
}
