package tools

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

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

// ── Late Registration ────────────────────────
// Note tools use late registration (like kb_search) because
// note_create, note_update, and note_search need the embedder for
// vector operations. The embedder is optional — if nil, notes still
// work but semantic search degrades to full-text search.

// RegisterNoteTools registers all note tools with captured dependencies.
// Call from main.go after stores + embedder init.
func RegisterNoteTools(stores store.Stores, embedder *knowledge.Embedder) {
	Register(&noteCreateTool{stores: stores, embedder: embedder})
	Register(&noteSearchTool{stores: stores, embedder: embedder})
	Register(&noteUpdateTool{stores: stores, embedder: embedder})
	Register(&noteListTool{stores: stores})
}

// ── Shared helpers ─────────────────────────────

// embedNote generates a vector for a note's title+content and stores it.
// Silently no-ops if embedder is nil or not configured.
func embedNote(ctx context.Context, embedder *knowledge.Embedder, stores store.Stores, noteID, userID, title, content string) {
	if embedder == nil || !embedder.IsConfigured(ctx) {
		return
	}

	text := title + "\n\n" + content
	if len(text) > 8000 {
		text = text[:8000] // limit to ~2k tokens for embedding
	}

	// Resolve team for embedding role
	var teamID *string
	if stores.Teams != nil {
		ids, _ := stores.Teams.GetUserTeamIDs(ctx, userID)
		if len(ids) > 0 {
			teamID = &ids[0]
		}
	}

	result, err := embedder.EmbedChunks(ctx, userID, teamID, []string{text})
	if err != nil {
		log.Printf("⚠ note embed failed for %s: %v", noteID, err)
		return
	}
	if len(result.Vectors) == 0 {
		return
	}

	// Store the vector via store method
	err = stores.Notes.SetEmbedding(ctx, noteID, vectorToString(result.Vectors[0]))
	if err != nil {
		log.Printf("⚠ note embed store failed for %s: %v", noteID, err)
		return
	}

	// Track embedding usage
	embedder.LogUsage(ctx, userID, nil, result)
	log.Printf("📝 note %s embedded (%d tokens)", noteID, result.InputTokens)
}

// vectorToString converts a float64 slice to pgvector literal format: [0.1,0.2,...]
func vectorToString(vec []float64) string {
	parts := make([]string, len(vec))
	for i, v := range vec {
		parts[i] = fmt.Sprintf("%g", v)
	}
	return "[" + strings.Join(parts, ",") + "]"
}

// ═══════════════════════════════════════════
// note_create
// ═══════════════════════════════════════════

type noteCreateTool struct {
	visitorDeniedBase
	stores   store.Stores
	embedder *knowledge.Embedder
}

func (t *noteCreateTool) Definition() ToolDef {
	return ToolDef{
		Name:        "note_create",
		DisplayName: "Create",
		Category:    "notes",
		Description: "Create a new note for the user. Use this to save information, insights, summaries, or anything the user wants to remember. Notes are persistent and searchable.",
		Parameters: JSONSchema(map[string]interface{}{
			"title":   Prop("string", "Title of the note"),
			"content": Prop("string", "Markdown content of the note"),
			"folder":  Prop("string", "Folder path, e.g. '/projects/switchboard/'. Defaults to '/'"),
			"tags":    PropArray("Tags for categorization, e.g. ['research', 'golang']"),
		}, []string{"title", "content"}),
	}
}

func (t *noteCreateTool) Execute(ctx context.Context, execCtx ExecutionContext, argsJSON string) (string, error) {
	var args struct {
		Title   string   `json:"title"`
		Content string   `json:"content"`
		Folder  string   `json:"folder"`
		Tags    []string `json:"tags"`
	}
	if err := json.Unmarshal([]byte(argsJSON), &args); err != nil {
		return "", fmt.Errorf("invalid arguments: %w", err)
	}

	if args.Title == "" {
		return "", fmt.Errorf("title is required")
	}

	folder := normalizePath(args.Folder)
	tags := args.Tags
	if tags == nil {
		tags = []string{}
	}

	// source_channel_id links the note to the conversation that created it
	var sourceChannelID *string
	if execCtx.ChannelID != "" {
		sourceChannelID = &execCtx.ChannelID
	}

	note := &models.Note{
		UserID:          execCtx.UserID,
		Title:           args.Title,
		Content:         args.Content,
		FolderPath:      folder,
		Tags:            tags,
		SourceChannelID: sourceChannelID,
	}

	if err := t.stores.Notes.Create(ctx, note); err != nil {
		return "", fmt.Errorf("failed to create note: %w", err)
	}

	// Embed async — don't block the tool response
	go embedNote(context.Background(), t.embedder, t.stores, note.ID, execCtx.UserID, args.Title, args.Content)

	result, _ := json.Marshal(map[string]interface{}{
		"id":         note.ID,
		"title":      args.Title,
		"folder":     folder,
		"tags":       tags,
		"created_at": note.CreatedAt.Format("2006-01-02T15:04:05Z"),
		"message":    fmt.Sprintf("Note '%s' created successfully.", args.Title),
	})
	return string(result), nil
}

// ═══════════════════════════════════════════
// note_search
// ═══════════════════════════════════════════

type noteSearchTool struct {
	visitorDeniedBase
	stores   store.Stores
	embedder *knowledge.Embedder
}

func (t *noteSearchTool) Definition() ToolDef {
	return ToolDef{
		Name:        "note_search",
		DisplayName: "Search",
		Category:    "notes",
		Description: "Search the user's notes by keyword or semantic meaning. " +
			"Set semantic=true for meaning-based search using AI embeddings, " +
			"or omit for fast keyword matching. Returns matching notes ranked by relevance.",
		Parameters: JSONSchema(map[string]interface{}{
			"query": Prop("string", "Search query — natural language keywords or question"),
			"limit": Prop("integer", "Maximum results to return (default 10, max 50)"),
			"semantic": map[string]interface{}{
				"type":        "boolean",
				"description": "Use semantic (vector) search instead of keyword search. Better for meaning-based queries.",
			},
		}, []string{"query"}),
	}
}

func (t *noteSearchTool) Execute(ctx context.Context, execCtx ExecutionContext, argsJSON string) (string, error) {
	var args struct {
		Query    string `json:"query"`
		Limit    int    `json:"limit"`
		Semantic bool   `json:"semantic"`
	}
	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.Limit <= 0 || args.Limit > 50 {
		args.Limit = 10
	}

	// Semantic search via vector similarity
	if args.Semantic {
		return t.semanticSearch(ctx, execCtx, args.Query, args.Limit)
	}

	// Default: full-text keyword search
	return t.keywordSearch(ctx, execCtx, args.Query, args.Limit)
}

func (t *noteSearchTool) keywordSearch(ctx context.Context, execCtx ExecutionContext, query string, limit int) (string, error) {
	storeResults, err := t.stores.Notes.SearchKeyword(ctx, execCtx.UserID, query, limit)
	if err != nil {
		return "", fmt.Errorf("search failed: %w", err)
	}

	results := make([]noteSearchResult, 0, len(storeResults))
	for _, sr := range storeResults {
		results = append(results, noteSearchResult{
			ID:       sr.ID,
			Title:    sr.Title,
			Folder:   sr.FolderPath,
			Tags:     sr.Tags,
			Excerpt:  sr.Excerpt,
			Headline: sr.Headline,
			Rank:     sr.Rank,
		})
	}

	out, _ := json.Marshal(map[string]interface{}{
		"query":   query,
		"mode":    "keyword",
		"count":   len(results),
		"results": results,
	})
	return string(out), nil
}

func (t *noteSearchTool) semanticSearch(ctx context.Context, execCtx ExecutionContext, query string, limit int) (string, error) {
	// Check embedder availability — fall back to keyword search if not configured
	if t.embedder == nil || !t.embedder.IsConfigured(ctx) {
		log.Printf("⚠ note semantic search: embedder not configured, falling back to keyword")
		return t.keywordSearch(ctx, execCtx, query, limit)
	}

	// Resolve team for embedding role
	var teamID *string
	if t.stores.Teams != nil {
		ids, _ := t.stores.Teams.GetUserTeamIDs(ctx, execCtx.UserID)
		if len(ids) > 0 {
			teamID = &ids[0]
		}
	}

	// Embed the query
	embedResult, err := t.embedder.EmbedChunks(ctx, execCtx.UserID, teamID, []string{query})
	if err != nil {
		log.Printf("⚠ note semantic search embed failed: %v — falling back to keyword", err)
		return t.keywordSearch(ctx, execCtx, query, limit)
	}
	if len(embedResult.Vectors) == 0 {
		return t.keywordSearch(ctx, execCtx, query, limit)
	}

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

	queryVec := vectorToString(embedResult.Vectors[0])

	// Vector similarity search via store
	storeResults, err := t.stores.Notes.SearchSemantic(ctx, execCtx.UserID, queryVec, limit)
	if err != nil {
		log.Printf("⚠ note semantic search query failed: %v — falling back to keyword", err)
		return t.keywordSearch(ctx, execCtx, query, limit)
	}

	results := make([]noteSearchResult, 0, len(storeResults))
	for _, sr := range storeResults {
		results = append(results, noteSearchResult{
			ID:         sr.ID,
			Title:      sr.Title,
			Folder:     sr.FolderPath,
			Tags:       sr.Tags,
			Excerpt:    sr.Excerpt,
			Similarity: sr.Rank,
		})
	}

	out, _ := json.Marshal(map[string]interface{}{
		"query":   query,
		"mode":    "semantic",
		"count":   len(results),
		"results": results,
	})
	return string(out), nil
}

type noteSearchResult struct {
	ID         string   `json:"id"`
	Title      string   `json:"title"`
	Folder     string   `json:"folder"`
	Tags       []string `json:"tags"`
	Excerpt    string   `json:"excerpt"`
	Headline   string   `json:"headline,omitempty"`
	Rank       float64  `json:"rank,omitempty"`
	Similarity float64  `json:"similarity,omitempty"`
}

// ═══════════════════════════════════════════
// note_update
// ═══════════════════════════════════════════

type noteUpdateTool struct {
	visitorDeniedBase
	stores   store.Stores
	embedder *knowledge.Embedder
}

func (t *noteUpdateTool) Definition() ToolDef {
	return ToolDef{
		Name:        "note_update",
		DisplayName: "Update",
		Category:    "notes",
		Description: "Update an existing note. Can replace content entirely, append to it, or prepend. Use note_search first to find the note ID.",
		Parameters: JSONSchema(map[string]interface{}{
			"note_id": Prop("string", "UUID of the note to update (from note_search results)"),
			"title":   Prop("string", "New title (omit to keep current)"),
			"content": Prop("string", "New content or text to append/prepend"),
			"mode":    PropEnum("How to apply content", "replace", "append", "prepend"),
			"tags":    PropArray("New tags (replaces existing tags)"),
		}, []string{"note_id"}),
	}
}

func (t *noteUpdateTool) Execute(ctx context.Context, execCtx ExecutionContext, argsJSON string) (string, error) {
	var args struct {
		NoteID  string   `json:"note_id"`
		Title   *string  `json:"title"`
		Content *string  `json:"content"`
		Mode    string   `json:"mode"`
		Tags    []string `json:"tags"`
	}
	if err := json.Unmarshal([]byte(argsJSON), &args); err != nil {
		return "", fmt.Errorf("invalid arguments: %w", err)
	}

	if args.NoteID == "" {
		return "", fmt.Errorf("note_id is required")
	}

	// Verify ownership + get existing content for append/prepend
	existing, err := t.stores.Notes.GetByID(ctx, args.NoteID)
	if err != nil || existing.UserID != execCtx.UserID {
		return "", fmt.Errorf("note not found or update failed")
	}

	// Build fields map for store.Update
	fields := map[string]interface{}{}

	if args.Title != nil {
		fields["title"] = *args.Title
	}

	if args.Content != nil {
		mode := strings.ToLower(args.Mode)
		switch mode {
		case "append":
			fields["content"] = existing.Content + *args.Content
		case "prepend":
			fields["content"] = *args.Content + existing.Content
		default:
			fields["content"] = *args.Content
		}
	}

	if args.Tags != nil {
		fields["tags"] = args.Tags
	}

	if len(fields) == 0 {
		return "", fmt.Errorf("no fields to update — provide at least title, content, or tags")
	}

	if err := t.stores.Notes.Update(ctx, args.NoteID, fields); err != nil {
		return "", fmt.Errorf("note not found or update failed")
	}

	// Re-fetch to get updated values
	updated, err := t.stores.Notes.GetByID(ctx, args.NoteID)
	if err != nil {
		return "", fmt.Errorf("note not found or update failed")
	}

	// Re-embed async with updated content
	go embedNote(context.Background(), t.embedder, t.stores, updated.ID, execCtx.UserID, updated.Title, updated.Content)

	result, _ := json.Marshal(map[string]interface{}{
		"id":         updated.ID,
		"title":      updated.Title,
		"updated_at": updated.UpdatedAt.Format("2006-01-02T15:04:05Z"),
		"message":    fmt.Sprintf("Note '%s' updated successfully.", updated.Title),
	})
	return string(result), nil
}

// ═══════════════════════════════════════════
// note_list
// ═══════════════════════════════════════════

type noteListTool struct {
	visitorDeniedBase
	stores store.Stores
}

func (t *noteListTool) Definition() ToolDef {
	return ToolDef{
		Name:        "note_list",
		DisplayName: "List",
		Category:    "notes",
		Description: "List the user's notes, optionally filtered by folder or tag. Shows titles and metadata without full content. Use note_search for keyword searching.",
		Parameters: JSONSchema(map[string]interface{}{
			"folder": Prop("string", "Filter by folder path, e.g. '/projects/' (omit for all)"),
			"tag":    Prop("string", "Filter by tag (omit for all)"),
			"limit":  Prop("integer", "Maximum results (default 20, max 100)"),
		}, nil),
	}
}

func (t *noteListTool) Execute(ctx context.Context, execCtx ExecutionContext, argsJSON string) (string, error) {
	var args struct {
		Folder string `json:"folder"`
		Tag    string `json:"tag"`
		Limit  int    `json:"limit"`
	}
	if err := json.Unmarshal([]byte(argsJSON), &args); err != nil {
		return "", fmt.Errorf("invalid arguments: %w", err)
	}

	if args.Limit <= 0 || args.Limit > 100 {
		args.Limit = 20
	}

	opts := store.NoteListOptions{
		ListOptions: store.ListOptions{Limit: args.Limit},
	}
	if args.Folder != "" {
		opts.FolderPath = normalizePath(args.Folder)
	}
	if args.Tag != "" {
		opts.Tag = args.Tag
	}

	notes, _, err := t.stores.Notes.ListForUser(ctx, execCtx.UserID, opts)
	if err != nil {
		return "", fmt.Errorf("failed to list notes: %w", err)
	}

	type item struct {
		ID        string   `json:"id"`
		Title     string   `json:"title"`
		Folder    string   `json:"folder"`
		Tags      []string `json:"tags"`
		Preview   string   `json:"preview"`
		CreatedAt string   `json:"created_at"`
		UpdatedAt string   `json:"updated_at"`
	}

	items := make([]item, 0, len(notes))
	for _, n := range notes {
		tags := n.Tags
		if tags == nil {
			tags = []string{}
		}
		preview := n.Content
		if len(preview) > 100 {
			preview = preview[:100]
		}
		items = append(items, item{
			ID:        n.ID,
			Title:     n.Title,
			Folder:    n.FolderPath,
			Tags:      tags,
			Preview:   preview,
			CreatedAt: n.CreatedAt.Format("2006-01-02T15:04:05Z"),
			UpdatedAt: n.UpdatedAt.Format("2006-01-02T15:04:05Z"),
		})
	}

	out, _ := json.Marshal(map[string]interface{}{
		"count": len(items),
		"notes": items,
	})
	return string(out), nil
}

// ── Path Helper ─────────────────────────────

func normalizePath(p string) string {
	p = strings.TrimSpace(p)
	if p == "" {
		return "/"
	}
	if !strings.HasPrefix(p, "/") {
		p = "/" + p
	}
	if !strings.HasSuffix(p, "/") {
		p = p + "/"
	}
	for strings.Contains(p, "//") {
		p = strings.ReplaceAll(p, "//", "/")
	}
	return p
}
