package tools

import (
	"context"
	"encoding/json"
	"fmt"
	"log"

	"chat-switchboard/knowledge"
	"chat-switchboard/store"
)

// ── Late Registration ────────────────────────
// kb_search cannot use init() because it needs stores + embedder,
// which aren't available until after main.go initializes them.
// Call RegisterKBSearch() from main.go after stores init.

// RegisterKBSearch registers the kb_search tool with captured dependencies.
func RegisterKBSearch(stores store.Stores, embedder *knowledge.Embedder) {
	Register(&kbSearchTool{
		stores:   stores,
		embedder: embedder,
	})
}

// ═══════════════════════════════════════════
// kb_search
// ═══════════════════════════════════════════

type kbSearchTool struct {
	BaseTool
	stores   store.Stores
	embedder *knowledge.Embedder
}

func (t *kbSearchTool) Definition() ToolDef {
	return ToolDef{
		Name:        "kb_search",
		DisplayName: "Knowledge Base",
		Category:    "knowledge",
		Description: "Search knowledge bases for relevant information. " +
			"Returns text passages from uploaded documents that match the query. " +
			"Use this when the user asks questions that might be answered by their documents.",
		Parameters: JSONSchema(map[string]interface{}{
			"query": Prop("string", "Search query — use natural language to describe what you're looking for"),
			"max_results": map[string]interface{}{
				"type":        "integer",
				"description": "Maximum results to return (1-20, default 5)",
			},
		}, []string{"query"}),
	}
}

func (t *kbSearchTool) Execute(ctx context.Context, execCtx ExecutionContext, argsJSON string) (string, error) {
	var args struct {
		Query      string `json:"query"`
		MaxResults int    `json:"max_results"`
	}
	if err := json.Unmarshal([]byte(argsJSON), &args); err != nil {
		return "", fmt.Errorf("invalid arguments: %w", err)
	}

	if args.Query == "" {
		return "", fmt.Errorf("query is required")
	}
	if args.MaxResults <= 0 || args.MaxResults > 20 {
		args.MaxResults = 5
	}

	// Resolve user's team IDs for scoped access
	teamIDs, _ := t.stores.Teams.GetUserTeamIDs(ctx, execCtx.UserID)

	// Get active KB IDs for this channel, including persona-bound KBs
	var kbIDs []string
	var err error
	if execCtx.PersonaID != "" {
		kbIDs, err = t.stores.KnowledgeBases.GetActiveKBIDsWithPersona(
			ctx, execCtx.ChannelID, execCtx.UserID, teamIDs, execCtx.PersonaID)
	} else {
		kbIDs, err = t.stores.KnowledgeBases.GetActiveKBIDs(
			ctx, execCtx.ChannelID, execCtx.UserID, teamIDs)
	}
	if err != nil {
		return "", fmt.Errorf("failed to look up active knowledge bases: %w", err)
	}

	// Also include project-bound KBs (v0.19.0)
	if t.stores.Projects != nil {
		projID, _ := t.stores.Projects.GetProjectIDForChannel(ctx, execCtx.ChannelID)
		if projID != "" {
			projKBIDs, _ := t.stores.Projects.GetKBIDs(ctx, projID)
			projSet := make(map[string]bool, len(kbIDs))
			for _, id := range kbIDs {
				projSet[id] = true
			}
			for _, id := range projKBIDs {
				if !projSet[id] {
					kbIDs = append(kbIDs, id)
					projSet[id] = true
				}
			}
		}
	}

	// Also include personal KBs (always available to owner, even if not linked to channel)
	personalKBs, err := t.stores.KnowledgeBases.ListPersonal(ctx, execCtx.UserID)
	if err == nil {
		kbIDSet := make(map[string]bool, len(kbIDs))
		for _, id := range kbIDs {
			kbIDSet[id] = true
		}
		for _, kb := range personalKBs {
			if !kbIDSet[kb.ID] && kb.ChunkCount > 0 {
				kbIDs = append(kbIDs, kb.ID)
			}
		}
	}

	if len(kbIDs) == 0 {
		result, _ := json.Marshal(map[string]interface{}{
			"results":      []interface{}{},
			"query":        args.Query,
			"searched_kbs": []string{},
			"message":      "No knowledge bases are active on this channel. Link a knowledge base to the channel or upload documents to a personal knowledge base first.",
		})
		return string(result), nil
	}

	// Resolve team ID for embedding role resolution (pick first team)
	var teamID *string
	if len(teamIDs) > 0 {
		teamID = &teamIDs[0]
	}

	// Embed the query
	embedResult, err := t.embedder.EmbedChunks(ctx, execCtx.UserID, teamID, []string{args.Query})
	if err != nil {
		return "", fmt.Errorf("failed to embed search query: %w", err)
	}
	if len(embedResult.Vectors) == 0 {
		return "", fmt.Errorf("embedding produced no vectors")
	}

	queryVec := embedResult.Vectors[0]

	// Track embedding usage (role = "embedding")
	chanPtr := &execCtx.ChannelID
	t.embedder.LogUsage(ctx, execCtx.UserID, chanPtr, embedResult)

	// Similarity search across all active KBs
	const defaultThreshold = 0.3
	results, err := t.stores.KnowledgeBases.SimilaritySearch(ctx, kbIDs, queryVec, defaultThreshold, args.MaxResults)
	if err != nil {
		return "", fmt.Errorf("search failed: %w", err)
	}

	// Collect searched KB names for attribution
	searchedKBs := make(map[string]bool)
	for _, r := range results {
		searchedKBs[r.KBName] = true
	}
	kbNames := make([]string, 0, len(searchedKBs))
	for name := range searchedKBs {
		kbNames = append(kbNames, name)
	}

	// If no results found from search, list all searched KB names
	if len(kbNames) == 0 {
		for _, kbID := range kbIDs {
			kb, err := t.stores.KnowledgeBases.GetByID(ctx, kbID)
			if err == nil {
				kbNames = append(kbNames, kb.Name)
			}
		}
	}

	log.Printf("🔍 kb_search: %q → %d results across %d KBs", args.Query, len(results), len(kbIDs))

	out, _ := json.Marshal(map[string]interface{}{
		"results":      results,
		"query":        args.Query,
		"total":        len(results),
		"searched_kbs": kbNames,
	})
	return string(out), nil
}
