package tools

import (
	"context"
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
	"regexp"
	"sort"
	"strings"
	"time"
)

type RepoMapTool struct {
	workspace string
}

func NewRepoMapTool(workspace string) *RepoMapTool {
	return &RepoMapTool{workspace: workspace}
}

func (t *RepoMapTool) Name() string {
	return "repo_map"
}

func (t *RepoMapTool) Description() string {
	return "Build and query repository map to quickly locate target files/symbols before reading source."
}

func (t *RepoMapTool) Parameters() map[string]interface{} {
	return map[string]interface{}{
		"type": "object",
		"properties": map[string]interface{}{
			"query": map[string]interface{}{
				"type":        "string",
				"description": "Search file path or symbol keyword",
			},
			"max_results": map[string]interface{}{
				"type":        "integer",
				"default":     20,
				"description": "Maximum results to return",
			},
			"refresh": map[string]interface{}{
				"type":        "boolean",
				"description": "Force rebuild map cache",
				"default":     false,
			},
		},
	}
}

type repoMapCache struct {
	Workspace string         `json:"workspace"`
	UpdatedAt int64          `json:"updated_at"`
	Files     []repoMapEntry `json:"files"`
}

type repoMapEntry struct {
	Path    string   `json:"path"`
	Lang    string   `json:"lang"`
	Size    int64    `json:"size"`
	ModTime int64    `json:"mod_time"`
	Symbols []string `json:"symbols,omitempty"`
}

func (t *RepoMapTool) Execute(_ context.Context, args map[string]interface{}) (string, error) {
	query := MapStringArg(args, "query")
	maxResults := MapIntArg(args, "max_results", 20)
	forceRefresh, _ := MapBoolArg(args, "refresh")

	cache, err := t.loadOrBuildMap(forceRefresh)
	if err != nil {
		return "", err
	}
	if len(cache.Files) == 0 {
		return "Repo map is empty.", nil
	}

	results := t.filterRepoMap(cache.Files, query)
	if len(results) > maxResults {
		results = results[:maxResults]
	}

	var sb strings.Builder
	sb.WriteString(fmt.Sprintf("Repo Map (updated: %s)\n", time.UnixMilli(cache.UpdatedAt).Format("2006-01-02 15:04:05")))
	sb.WriteString(fmt.Sprintf("Workspace: %s\n", cache.Workspace))
	sb.WriteString(fmt.Sprintf("Matched files: %d\n\n", len(results)))

	for _, item := range results {
		sb.WriteString(fmt.Sprintf("- %s [%s] (%d bytes)\n", item.Path, item.Lang, item.Size))
		if len(item.Symbols) > 0 {
			sb.WriteString(fmt.Sprintf("  symbols: %s\n", strings.Join(item.Symbols, ", ")))
		}
	}
	if len(results) == 0 {
		return "No files matched query.", nil
	}
	return sb.String(), nil
}

func (t *RepoMapTool) cachePath() string {
	return filepath.Join(t.workspace, ".clawgo", "repo_map.json")
}

func (t *RepoMapTool) loadOrBuildMap(force bool) (*repoMapCache, error) {
	if !force {
		if data, err := os.ReadFile(t.cachePath()); err == nil {
			var cache repoMapCache
			if err := json.Unmarshal(data, &cache); err == nil {
				if cache.Workspace == t.workspace && (time.Now().UnixMilli()-cache.UpdatedAt) < int64((10*time.Minute)/time.Millisecond) {
					return &cache, nil
				}
			}
		}
	}

	cache := &repoMapCache{
		Workspace: t.workspace,
		UpdatedAt: time.Now().UnixMilli(),
		Files:     []repoMapEntry{},
	}

	err := filepath.Walk(t.workspace, func(path string, info os.FileInfo, err error) error {
		if err != nil || info == nil {
			return nil
		}
		if info.IsDir() {
			name := info.Name()
			if name == ".git" || name == "node_modules" || name == ".clawgo" || name == "vendor" {
				return filepath.SkipDir
			}
			return nil
		}
		if info.Size() > 512*1024 {
			return nil
		}

		rel, err := filepath.Rel(t.workspace, path)
		if err != nil {
			return nil
		}
		lang := langFromPath(rel)
		if lang == "" {
			return nil
		}

		entry := repoMapEntry{
			Path:    rel,
			Lang:    lang,
			Size:    info.Size(),
			ModTime: info.ModTime().UnixMilli(),
			Symbols: extractSymbols(path, lang),
		}
		cache.Files = append(cache.Files, entry)
		return nil
	})
	if err != nil {
		return nil, err
	}

	sort.Slice(cache.Files, func(i, j int) bool {
		return cache.Files[i].Path < cache.Files[j].Path
	})

	if err := os.MkdirAll(filepath.Dir(t.cachePath()), 0755); err == nil {
		if data, err := json.Marshal(cache); err == nil {
			_ = os.WriteFile(t.cachePath(), data, 0644)
		}
	}
	return cache, nil
}

func (t *RepoMapTool) filterRepoMap(files []repoMapEntry, query string) []repoMapEntry {
	q := strings.ToLower(strings.TrimSpace(query))
	if q == "" {
		return files
	}

	type scored struct {
		item  repoMapEntry
		score int
	}
	items := []scored{}
	for _, file := range files {
		score := 0
		p := strings.ToLower(file.Path)
		if strings.Contains(p, q) {
			score += 5
		}
		for _, sym := range file.Symbols {
			if strings.Contains(strings.ToLower(sym), q) {
				score += 3
			}
		}
		if score > 0 {
			items = append(items, scored{item: file, score: score})
		}
	}
	sort.Slice(items, func(i, j int) bool {
		if items[i].score == items[j].score {
			return items[i].item.Path < items[j].item.Path
		}
		return items[i].score > items[j].score
	})

	out := make([]repoMapEntry, 0, len(items))
	for _, item := range items {
		out = append(out, item.item)
	}
	return out
}

func langFromPath(path string) string {
	switch strings.ToLower(filepath.Ext(path)) {
	case ".go":
		return "go"
	case ".md":
		return "markdown"
	case ".json":
		return "json"
	case ".yaml", ".yml":
		return "yaml"
	case ".sh":
		return "shell"
	case ".py":
		return "python"
	case ".js":
		return "javascript"
	case ".ts":
		return "typescript"
	default:
		return ""
	}
}

func extractSymbols(path, lang string) []string {
	data, err := os.ReadFile(path)
	if err != nil {
		return nil
	}
	content := string(data)
	out := []string{}

	switch lang {
	case "go":
		// Top-level functions
		reFunc := regexp.MustCompile(`(?m)^func\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(`)
		for _, m := range reFunc.FindAllStringSubmatch(content, 20) {
			if len(m) > 1 {
				out = append(out, m[1])
			}
		}
		// Methods: func (r *Receiver) MethodName(...)
		reMethod := regexp.MustCompile(`(?m)^func\s+\([^)]+\)\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(`)
		for _, m := range reMethod.FindAllStringSubmatch(content, 20) {
			if len(m) > 1 {
				out = append(out, m[1])
			}
		}
		// Types
		reType := regexp.MustCompile(`(?m)^type\s+([A-Za-z_][A-Za-z0-9_]*)\s+`)
		for _, m := range reType.FindAllStringSubmatch(content, 20) {
			if len(m) > 1 {
				out = append(out, m[1])
			}
		}
	case "python":
		// Functions and Classes
		re := regexp.MustCompile(`(?m)^(?:def|class)\s+([A-Za-z_][A-Za-z0-9_]*)`)
		for _, m := range re.FindAllStringSubmatch(content, 30) {
			if len(m) > 1 {
				out = append(out, m[1])
			}
		}
	case "javascript", "typescript":
		// function Name(...) or class Name ... or const Name = (...) =>
		re := regexp.MustCompile(`(?m)^(?:export\s+)?(?:function|class|const|let|var)\s+([A-Za-z_][A-Za-z0-9_]*)`)
		for _, m := range re.FindAllStringSubmatch(content, 30) {
			if len(m) > 1 {
				out = append(out, m[1])
			}
		}
	case "markdown":
		// Headers as symbols
		re := regexp.MustCompile(`(?m)^#+\s+(.+)$`)
		for _, m := range re.FindAllStringSubmatch(content, 20) {
			if len(m) > 1 {
				out = append(out, strings.TrimSpace(m[1]))
			}
		}
	}

	if len(out) == 0 {
		return nil
	}
	sort.Strings(out)
	uniq := []string{}
	seen := make(map[string]bool)
	for _, s := range out {
		if !seen[s] {
			uniq = append(uniq, s)
			seen[s] = true
		}
	}
	return uniq
}
