readysite / website / internal / assist / executor.go
58.5 KB
executor.go
package assist

import (
	"bytes"
	"encoding/json"
	"fmt"
	"html/template"
	"log"

	"github.com/readysite/readysite/pkg/assistant"
	"github.com/readysite/readysite/website/internal/content"
	"github.com/readysite/readysite/website/internal/search"
	"github.com/readysite/readysite/website/models"
	"golang.org/x/crypto/bcrypt"
)

// =============================================================================
// Error Types
// =============================================================================

// ToolError provides contextual error information for tool failures.
type ToolError struct {
	Tool       string `json:"tool"`
	Code       string `json:"code"`
	Message    string `json:"message"`
	Suggestion string `json:"suggestion,omitempty"`
}

func (e *ToolError) Error() string {
	if e.Suggestion != "" {
		return fmt.Sprintf("%s: %s (hint: %s)", e.Tool, e.Message, e.Suggestion)
	}
	return fmt.Sprintf("%s: %s", e.Tool, e.Message)
}

// Common error codes
const (
	ErrCodeNotFound       = "NOT_FOUND"
	ErrCodeInvalidArgs    = "INVALID_ARGS"
	ErrCodeValidation     = "VALIDATION_ERROR"
	ErrCodeDuplicate      = "DUPLICATE"
	ErrCodeInternalError  = "INTERNAL_ERROR"
	ErrCodePermission     = "PERMISSION_DENIED"
)

// =============================================================================
// Executor
// =============================================================================

// toolHandler is a function that executes a tool call.
type toolHandler func(*Executor, assistant.ToolCall) (string, error)

// handlers maps tool names to their handler functions.
var handlers = map[string]toolHandler{
	// Page tools
	"create_page":  (*Executor).createPage,
	"update_page":  (*Executor).updatePage,
	"delete_page":  (*Executor).deletePage,
	"delete_pages": (*Executor).deletePages,
	"get_page":     (*Executor).getPage,
	"list_pages":   (*Executor).listPages,

	// Collection tools
	"create_collection":   (*Executor).createCollection,
	"update_collection":   (*Executor).updateCollection,
	"delete_collection":   (*Executor).deleteCollection,
	"delete_collections":  (*Executor).deleteCollections,
	"get_collection":      (*Executor).getCollection,
	"list_collections":    (*Executor).listCollections,

	// Document tools
	"create_document":   (*Executor).createDocument,
	"create_documents":  (*Executor).createDocuments,
	"update_document":   (*Executor).updateDocument,
	"delete_document":   (*Executor).deleteDocument,
	"get_document":      (*Executor).getDocument,
	"query_documents":   (*Executor).queryDocuments,

	// Partial tools
	"create_partial":  (*Executor).createPartial,
	"update_partial":  (*Executor).updatePartial,
	"delete_partial":  (*Executor).deletePartial,
	"get_partial":     (*Executor).getPartial,
	"list_partials":   (*Executor).listPartials,

	// File tools
	"update_file":  (*Executor).updateFile,
	"get_file":     (*Executor).getFile,
	"list_files":   (*Executor).listFiles,
	"read_file":    (*Executor).readFile,

	// Note tools
	"create_note":  (*Executor).createNote,
	"list_notes":   (*Executor).listNotes,
	"get_note":     (*Executor).getNote,
	"update_note":  (*Executor).updateNote,
	"delete_note":  (*Executor).deleteNote,

	// User tools
	"create_user": (*Executor).createUser,
	"update_user": (*Executor).updateUser,
	"delete_user": (*Executor).deleteUser,
	"list_users":  (*Executor).listUsers,

	// Utility tools
	"validate_template": (*Executor).validateTemplate,
	"navigate_user":     (*Executor).navigateUser,
	"search":            (*Executor).search,
}

// Executor executes tool calls and tracks mutations.
type Executor struct {
	message *models.Message // For mutation tracking (optional)
}

// NewExecutor creates a new tool executor.
// If message is provided, mutations will be recorded for undo/redo support.
func NewExecutor(message *models.Message) *Executor {
	return &Executor{message: message}
}

// Execute runs a tool call and returns the result as JSON.
func (e *Executor) Execute(call assistant.ToolCall) (string, error) {
	handler, ok := handlers[call.Name]
	if !ok {
		return "", &ToolError{
			Tool:       call.Name,
			Code:       ErrCodeNotFound,
			Message:    fmt.Sprintf("unknown tool: %s", call.Name),
			Suggestion: "Use one of: " + formatToolList(),
		}
	}
	return handler(e, call)
}

// formatToolList returns a comma-separated list of available tools.
func formatToolList() string {
	names := ToolNames()
	if len(names) > 10 {
		return fmt.Sprintf("%s, ... (%d total)", names[0], len(names))
	}
	return fmt.Sprintf("%v", names)
}

// =============================================================================
// Page Handlers
// =============================================================================

func (e *Executor) createPage(call assistant.ToolCall) (string, error) {
	var args CreatePageArgs
	if err := call.ParseArguments(&args); err != nil {
		return "", &ToolError{Tool: "create_page", Code: ErrCodeInvalidArgs, Message: err.Error()}
	}

	page := &models.Page{
		ParentID: args.ParentID,
	}

	// Set ID from arg or generate slug from title
	if args.ID != "" {
		page.ID = slugify(args.ID)
	} else if args.Title != "" {
		page.ID = slugify(args.Title)
	}

	id, err := models.Pages.Insert(page)
	if err != nil {
		return "", &ToolError{
			Tool:    "create_page",
			Code:    ErrCodeInternalError,
			Message: fmt.Sprintf("failed to create page: %v", err),
		}
	}

	// Determine content status
	contentStatus := models.StatusDraft
	if args.Published {
		contentStatus = models.StatusPublished
	}

	// Save content as first version
	page.ID = id
	pageContent, err := content.SavePageContent(page, args.Title, args.Description, unescapeHTML(args.HTML), "", contentStatus)
	if err != nil {
		return "", &ToolError{
			Tool:    "create_page",
			Code:    ErrCodeInternalError,
			Message: fmt.Sprintf("failed to save page content: %v", err),
		}
	}

	e.recordMutation(ActionCreate, "page", id, nil, page)
	e.recordMutation(ActionCreate, "page_content", pageContent.ID, nil, pageContent)

	return jsonResult(map[string]any{
		"id":      id,
		"title":   args.Title,
		"path":    page.Path(),
		"message": fmt.Sprintf("Created page '%s'", args.Title),
	})
}

func (e *Executor) updatePage(call assistant.ToolCall) (string, error) {
	var args UpdatePageArgs
	if err := call.ParseArguments(&args); err != nil {
		return "", &ToolError{Tool: "update_page", Code: ErrCodeInvalidArgs, Message: err.Error()}
	}

	page, err := models.Pages.Get(args.ID)
	if err != nil {
		return "", &ToolError{
			Tool:       "update_page",
			Code:       ErrCodeNotFound,
			Message:    fmt.Sprintf("page '%s' not found", args.ID),
			Suggestion: "Use list_pages to see available pages",
		}
	}

	beforeState := clonePage(page)

	// Update page metadata
	if args.ParentID != nil {
		page.ParentID = *args.ParentID
	}
	if args.Position != nil {
		page.Position = *args.Position
	}

	if err := models.Pages.Update(page); err != nil {
		return "", &ToolError{Tool: "update_page", Code: ErrCodeInternalError, Message: err.Error()}
	}

	// Handle content changes
	currentTitle := page.Title()
	currentDescription := page.Description()
	currentHTML := page.HTML()

	newTitle := currentTitle
	newDescription := currentDescription
	newHTML := currentHTML

	if args.Title != nil {
		newTitle = *args.Title
	}
	if args.Description != nil {
		newDescription = *args.Description
	}
	if args.HTML != nil {
		newHTML = unescapeHTML(*args.HTML)
	}

	// Determine content status
	contentStatus := models.StatusDraft
	if page.IsPublished() {
		contentStatus = models.StatusPublished
	}
	if args.Published != nil {
		if *args.Published {
			contentStatus = models.StatusPublished
		} else {
			contentStatus = models.StatusDraft
		}
	}

	// Create new content version if something changed
	contentChanged := newTitle != currentTitle || newDescription != currentDescription || newHTML != currentHTML
	statusChanged := args.Published != nil
	if contentChanged || statusChanged {
		pageContent, err := content.SavePageContent(page, newTitle, newDescription, newHTML, "", contentStatus)
		if err != nil {
			return "", &ToolError{Tool: "update_page", Code: ErrCodeInternalError, Message: err.Error()}
		}
		e.recordMutation(ActionCreate, "page_content", pageContent.ID, nil, pageContent)
	}

	e.recordMutation(ActionUpdate, "page", page.ID, beforeState, page)

	return jsonResult(map[string]any{
		"id":      page.ID,
		"title":   newTitle,
		"message": fmt.Sprintf("Updated page '%s'", newTitle),
	})
}

func (e *Executor) deletePage(call assistant.ToolCall) (string, error) {
	var args DeletePageArgs
	if err := call.ParseArguments(&args); err != nil {
		return "", &ToolError{Tool: "delete_page", Code: ErrCodeInvalidArgs, Message: err.Error()}
	}

	page, err := models.Pages.Get(args.ID)
	if err != nil {
		return "", &ToolError{
			Tool:       "delete_page",
			Code:       ErrCodeNotFound,
			Message:    fmt.Sprintf("page '%s' not found", args.ID),
			Suggestion: "Use list_pages to see available pages",
		}
	}

	title := page.Title()
	beforeState := clonePage(page)

	if err := models.Pages.Delete(page); err != nil {
		return "", &ToolError{Tool: "delete_page", Code: ErrCodeInternalError, Message: err.Error()}
	}

	e.recordMutation(ActionDelete, "page", page.ID, beforeState, nil)

	return jsonResult(map[string]any{
		"id":      page.ID,
		"message": fmt.Sprintf("Deleted page '%s'", title),
	})
}

func (e *Executor) deletePages(call assistant.ToolCall) (string, error) {
	var args DeletePagesArgs
	if err := call.ParseArguments(&args); err != nil {
		return "", &ToolError{Tool: "delete_pages", Code: ErrCodeInvalidArgs, Message: err.Error()}
	}

	// Parse JSON array of IDs
	var ids []string
	if err := json.Unmarshal([]byte(args.IDs), &ids); err != nil {
		return "", &ToolError{
			Tool:       "delete_pages",
			Code:       ErrCodeInvalidArgs,
			Message:    fmt.Sprintf("invalid JSON array: %v", err),
			Suggestion: "Provide a JSON array of page IDs, e.g., [\"page1\", \"page2\"]",
		}
	}

	if len(ids) == 0 {
		return "", &ToolError{
			Tool:       "delete_pages",
			Code:       ErrCodeInvalidArgs,
			Message:    "ids array is empty",
			Suggestion: "Provide at least one page ID to delete",
		}
	}

	// Limit batch size
	const maxBatchSize = 50
	if len(ids) > maxBatchSize {
		return "", &ToolError{
			Tool:       "delete_pages",
			Code:       ErrCodeInvalidArgs,
			Message:    fmt.Sprintf("too many pages (%d), max is %d", len(ids), maxBatchSize),
			Suggestion: "Split into multiple batches",
		}
	}

	// Delete pages
	deletedIDs := make([]string, 0, len(ids))
	deletedTitles := make([]string, 0, len(ids))
	var errors []string

	for _, id := range ids {
		page, err := models.Pages.Get(id)
		if err != nil {
			errors = append(errors, fmt.Sprintf("page '%s' not found", id))
			continue
		}

		title := page.Title()
		beforeState := clonePage(page)

		if err := models.Pages.Delete(page); err != nil {
			errors = append(errors, fmt.Sprintf("failed to delete '%s': %v", id, err))
			continue
		}

		e.recordMutation(ActionDelete, "page", page.ID, beforeState, nil)
		deletedIDs = append(deletedIDs, id)
		deletedTitles = append(deletedTitles, title)
	}

	result := map[string]any{
		"deleted_ids":   deletedIDs,
		"deleted_count": len(deletedIDs),
		"total_count":   len(ids),
	}

	if len(errors) > 0 {
		result["errors"] = errors
		result["message"] = fmt.Sprintf("Deleted %d of %d pages", len(deletedIDs), len(ids))
	} else {
		result["message"] = fmt.Sprintf("Deleted %d pages", len(deletedIDs))
	}

	return jsonResult(result)
}

func (e *Executor) getPage(call assistant.ToolCall) (string, error) {
	var args GetPageArgs
	if err := call.ParseArguments(&args); err != nil {
		return "", &ToolError{Tool: "get_page", Code: ErrCodeInvalidArgs, Message: err.Error()}
	}

	page, err := models.Pages.Get(args.ID)
	if err != nil {
		return "", &ToolError{
			Tool:       "get_page",
			Code:       ErrCodeNotFound,
			Message:    fmt.Sprintf("page '%s' not found", args.ID),
			Suggestion: "Use list_pages to see available pages",
		}
	}

	return jsonResult(map[string]any{
		"id":          page.ID,
		"title":       page.Title(),
		"description": page.Description(),
		"html":        page.HTML(),
		"parent_id":   page.ParentID,
		"position":    page.Position,
		"published":   page.IsPublished(),
		"status":      page.Status(),
		"path":        page.Path(),
		"created_at":  page.CreatedAt,
		"updated_at":  page.UpdatedAt,
	})
}

func (e *Executor) listPages(call assistant.ToolCall) (string, error) {
	var args ListPagesArgs
	if err := call.ParseArguments(&args); err != nil {
		return "", &ToolError{Tool: "list_pages", Code: ErrCodeInvalidArgs, Message: err.Error()}
	}

	var query string
	var queryArgs []any

	if args.ParentID != "" {
		query = "WHERE ParentID = ?"
		queryArgs = []any{args.ParentID}
	} else {
		query = "WHERE ParentID = ''"
	}

	query += " ORDER BY Position, CreatedAt"

	allPages, err := models.Pages.Search(query, queryArgs...)
	if err != nil {
		return "", &ToolError{Tool: "list_pages", Code: ErrCodeInternalError, Message: err.Error()}
	}

	// Filter by published status if needed
	var pages []*models.Page
	if args.IncludeUnpublished {
		pages = allPages
	} else {
		for _, p := range allPages {
			if p.IsPublished() {
				pages = append(pages, p)
			}
		}
	}

	result := make([]map[string]any, len(pages))
	for i, p := range pages {
		result[i] = map[string]any{
			"id":        p.ID,
			"title":     p.Title(),
			"path":      p.Path(),
			"published": p.IsPublished(),
			"status":    p.Status(),
			"position":  p.Position,
		}
	}

	return jsonResult(map[string]any{
		"pages": result,
		"count": len(pages),
	})
}

// =============================================================================
// Collection Handlers
// =============================================================================

func (e *Executor) createCollection(call assistant.ToolCall) (string, error) {
	var args CreateCollectionArgs
	if err := call.ParseArguments(&args); err != nil {
		return "", &ToolError{Tool: "create_collection", Code: ErrCodeInvalidArgs, Message: err.Error()}
	}

	// Validate schema if provided
	if args.Schema != "" {
		if err := content.ValidateSchema(args.Schema); err != nil {
			return "", &ToolError{
				Tool:       "create_collection",
				Code:       ErrCodeValidation,
				Message:    fmt.Sprintf("invalid schema: %v", err),
				Suggestion: "Each field needs 'name' and valid 'type'. Select fields need 'options.values', relation fields need 'options.collection'.",
			}
		}
	}

	col := &models.Collection{
		Name:        args.Name,
		Description: args.Description,
		Schema:      args.Schema,
	}

	if args.ID != "" {
		col.ID = args.ID
	}

	id, err := models.Collections.Insert(col)
	if err != nil {
		return "", &ToolError{Tool: "create_collection", Code: ErrCodeInternalError, Message: err.Error()}
	}

	e.recordMutation(ActionCreate, "collection", id, nil, col)

	return jsonResult(map[string]any{
		"id":      id,
		"name":    col.Name,
		"message": fmt.Sprintf("Created collection '%s'", col.Name),
	})
}

func (e *Executor) updateCollection(call assistant.ToolCall) (string, error) {
	var args UpdateCollectionArgs
	if err := call.ParseArguments(&args); err != nil {
		return "", &ToolError{Tool: "update_collection", Code: ErrCodeInvalidArgs, Message: err.Error()}
	}

	col, err := models.Collections.Get(args.ID)
	if err != nil {
		return "", &ToolError{
			Tool:       "update_collection",
			Code:       ErrCodeNotFound,
			Message:    fmt.Sprintf("collection '%s' not found", args.ID),
			Suggestion: "Use list_collections to see available collections",
		}
	}

	// Validate schema if provided
	if args.Schema != nil && *args.Schema != "" {
		if err := content.ValidateSchema(*args.Schema); err != nil {
			return "", &ToolError{
				Tool:       "update_collection",
				Code:       ErrCodeValidation,
				Message:    fmt.Sprintf("invalid schema: %v", err),
				Suggestion: "Each field needs 'name' and valid 'type'. Select fields need 'options.values', relation fields need 'options.collection'.",
			}
		}
	}

	beforeState := cloneCollection(col)

	if args.Name != nil {
		col.Name = *args.Name
	}
	if args.Description != nil {
		col.Description = *args.Description
	}
	if args.Schema != nil {
		col.Schema = *args.Schema
	}

	if err := models.Collections.Update(col); err != nil {
		return "", &ToolError{Tool: "update_collection", Code: ErrCodeInternalError, Message: err.Error()}
	}

	e.recordMutation(ActionUpdate, "collection", col.ID, beforeState, col)

	return jsonResult(map[string]any{
		"id":      col.ID,
		"name":    col.Name,
		"message": fmt.Sprintf("Updated collection '%s'", col.Name),
	})
}

func (e *Executor) deleteCollection(call assistant.ToolCall) (string, error) {
	var args DeleteCollectionArgs
	if err := call.ParseArguments(&args); err != nil {
		return "", &ToolError{Tool: "delete_collection", Code: ErrCodeInvalidArgs, Message: err.Error()}
	}

	col, err := models.Collections.Get(args.ID)
	if err != nil {
		return "", &ToolError{
			Tool:       "delete_collection",
			Code:       ErrCodeNotFound,
			Message:    fmt.Sprintf("collection '%s' not found", args.ID),
			Suggestion: "Use list_collections to see available collections",
		}
	}

	beforeState := cloneCollection(col)

	// Delete all documents in the collection first
	docs, _ := col.Documents()
	for _, doc := range docs {
		models.Documents.Delete(doc)
	}

	if err := models.Collections.Delete(col); err != nil {
		return "", &ToolError{Tool: "delete_collection", Code: ErrCodeInternalError, Message: err.Error()}
	}

	e.recordMutation(ActionDelete, "collection", col.ID, beforeState, nil)

	return jsonResult(map[string]any{
		"id":                col.ID,
		"documents_deleted": len(docs),
		"message":           fmt.Sprintf("Deleted collection '%s' and %d documents", beforeState.Name, len(docs)),
	})
}

func (e *Executor) deleteCollections(call assistant.ToolCall) (string, error) {
	var args DeleteCollectionsArgs
	if err := call.ParseArguments(&args); err != nil {
		return "", &ToolError{Tool: "delete_collections", Code: ErrCodeInvalidArgs, Message: err.Error()}
	}

	// Parse JSON array of IDs
	var ids []string
	if err := json.Unmarshal([]byte(args.IDs), &ids); err != nil {
		return "", &ToolError{
			Tool:       "delete_collections",
			Code:       ErrCodeInvalidArgs,
			Message:    fmt.Sprintf("invalid JSON array: %v", err),
			Suggestion: "Provide a JSON array of collection IDs, e.g., [\"collection1\", \"collection2\"]",
		}
	}

	if len(ids) == 0 {
		return "", &ToolError{
			Tool:       "delete_collections",
			Code:       ErrCodeInvalidArgs,
			Message:    "ids array is empty",
			Suggestion: "Provide at least one collection ID to delete",
		}
	}

	// Limit batch size
	const maxBatchSize = 20
	if len(ids) > maxBatchSize {
		return "", &ToolError{
			Tool:       "delete_collections",
			Code:       ErrCodeInvalidArgs,
			Message:    fmt.Sprintf("too many collections (%d), max is %d", len(ids), maxBatchSize),
			Suggestion: "Split into multiple batches",
		}
	}

	// Delete collections
	deletedIDs := make([]string, 0, len(ids))
	deletedNames := make([]string, 0, len(ids))
	totalDocsDeleted := 0
	var errors []string

	for _, id := range ids {
		col, err := models.Collections.Get(id)
		if err != nil {
			errors = append(errors, fmt.Sprintf("collection '%s' not found", id))
			continue
		}

		beforeState := cloneCollection(col)

		// Delete all documents in the collection first
		docs, _ := col.Documents()
		for _, doc := range docs {
			models.Documents.Delete(doc)
		}

		if err := models.Collections.Delete(col); err != nil {
			errors = append(errors, fmt.Sprintf("failed to delete '%s': %v", id, err))
			continue
		}

		e.recordMutation(ActionDelete, "collection", col.ID, beforeState, nil)
		deletedIDs = append(deletedIDs, id)
		deletedNames = append(deletedNames, beforeState.Name)
		totalDocsDeleted += len(docs)
	}

	result := map[string]any{
		"deleted_ids":       deletedIDs,
		"deleted_count":     len(deletedIDs),
		"total_count":       len(ids),
		"documents_deleted": totalDocsDeleted,
	}

	if len(errors) > 0 {
		result["errors"] = errors
		result["message"] = fmt.Sprintf("Deleted %d of %d collections (%d documents)", len(deletedIDs), len(ids), totalDocsDeleted)
	} else {
		result["message"] = fmt.Sprintf("Deleted %d collections (%d documents)", len(deletedIDs), totalDocsDeleted)
	}

	return jsonResult(result)
}

func (e *Executor) getCollection(call assistant.ToolCall) (string, error) {
	var args GetCollectionArgs
	if err := call.ParseArguments(&args); err != nil {
		return "", &ToolError{Tool: "get_collection", Code: ErrCodeInvalidArgs, Message: err.Error()}
	}

	col, err := models.Collections.Get(args.ID)
	if err != nil {
		return "", &ToolError{
			Tool:       "get_collection",
			Code:       ErrCodeNotFound,
			Message:    fmt.Sprintf("collection '%s' not found", args.ID),
			Suggestion: "Use list_collections to see available collections",
		}
	}

	return jsonResult(map[string]any{
		"id":             col.ID,
		"name":           col.Name,
		"description":    col.Description,
		"schema":         col.Schema,
		"document_count": col.DocumentCount(),
		"created_at":     col.CreatedAt,
		"updated_at":     col.UpdatedAt,
	})
}

func (e *Executor) listCollections(call assistant.ToolCall) (string, error) {
	cols, err := models.Collections.All()
	if err != nil {
		return "", &ToolError{Tool: "list_collections", Code: ErrCodeInternalError, Message: err.Error()}
	}

	result := make([]map[string]any, len(cols))
	for i, c := range cols {
		result[i] = map[string]any{
			"id":             c.ID,
			"name":           c.Name,
			"description":    c.Description,
			"document_count": c.DocumentCount(),
		}
	}

	return jsonResult(map[string]any{
		"collections": result,
		"count":       len(cols),
	})
}

// =============================================================================
// Document Handlers
// =============================================================================

func (e *Executor) createDocument(call assistant.ToolCall) (string, error) {
	var args CreateDocumentArgs
	if err := call.ParseArguments(&args); err != nil {
		return "", &ToolError{Tool: "create_document", Code: ErrCodeInvalidArgs, Message: err.Error()}
	}

	// Verify collection exists
	col, err := models.Collections.Get(args.CollectionID)
	if err != nil {
		return "", &ToolError{
			Tool:       "create_document",
			Code:       ErrCodeNotFound,
			Message:    fmt.Sprintf("collection '%s' not found", args.CollectionID),
			Suggestion: "Use list_collections to see available collections",
		}
	}

	// Parse and validate data against schema
	var data map[string]any
	if args.Data != "" {
		if err := json.Unmarshal([]byte(args.Data), &data); err != nil {
			return "", &ToolError{
				Tool:       "create_document",
				Code:       ErrCodeInvalidArgs,
				Message:    fmt.Sprintf("invalid JSON data: %v", err),
				Suggestion: "Ensure the data is valid JSON object",
			}
		}
	}

	if err := content.ValidateDocument(col, data); err != nil {
		return "", &ToolError{
			Tool:       "create_document",
			Code:       ErrCodeValidation,
			Message:    fmt.Sprintf("validation failed: %v", err),
			Suggestion: "Check the collection schema for required fields",
		}
	}

	doc := &models.Document{
		CollectionID: args.CollectionID,
		Data:         args.Data,
	}

	id, err := models.Documents.Insert(doc)
	if err != nil {
		return "", &ToolError{Tool: "create_document", Code: ErrCodeInternalError, Message: err.Error()}
	}

	e.recordMutation(ActionCreate, "document", id, nil, doc)

	return jsonResult(map[string]any{
		"id":            id,
		"collection_id": args.CollectionID,
		"message":       fmt.Sprintf("Created document in collection '%s'", col.Name),
	})
}

func (e *Executor) createDocuments(call assistant.ToolCall) (string, error) {
	var args CreateDocumentsArgs
	if err := call.ParseArguments(&args); err != nil {
		return "", &ToolError{Tool: "create_documents", Code: ErrCodeInvalidArgs, Message: err.Error()}
	}

	// Verify collection exists
	col, err := models.Collections.Get(args.CollectionID)
	if err != nil {
		return "", &ToolError{
			Tool:       "create_documents",
			Code:       ErrCodeNotFound,
			Message:    fmt.Sprintf("collection '%s' not found", args.CollectionID),
			Suggestion: "Use list_collections to see available collections",
		}
	}

	// Parse documents array
	var documents []map[string]any
	if err := json.Unmarshal([]byte(args.Documents), &documents); err != nil {
		return "", &ToolError{
			Tool:       "create_documents",
			Code:       ErrCodeInvalidArgs,
			Message:    fmt.Sprintf("invalid JSON array: %v", err),
			Suggestion: "Provide a JSON array of document objects, e.g., [{...}, {...}]",
		}
	}

	if len(documents) == 0 {
		return "", &ToolError{
			Tool:       "create_documents",
			Code:       ErrCodeInvalidArgs,
			Message:    "documents array is empty",
			Suggestion: "Provide at least one document object",
		}
	}

	// Limit batch size
	const maxBatchSize = 100
	if len(documents) > maxBatchSize {
		return "", &ToolError{
			Tool:       "create_documents",
			Code:       ErrCodeInvalidArgs,
			Message:    fmt.Sprintf("too many documents (%d), max is %d", len(documents), maxBatchSize),
			Suggestion: "Split into multiple batches",
		}
	}

	// Validate all documents first
	for i, data := range documents {
		if err := content.ValidateDocument(col, data); err != nil {
			return "", &ToolError{
				Tool:       "create_documents",
				Code:       ErrCodeValidation,
				Message:    fmt.Sprintf("document %d validation failed: %v", i+1, err),
				Suggestion: "Check the collection schema for required fields",
			}
		}
	}

	// Create all documents
	createdIDs := make([]string, 0, len(documents))
	for _, data := range documents {
		dataJSON, _ := json.Marshal(data)
		doc := &models.Document{
			CollectionID: args.CollectionID,
			Data:         string(dataJSON),
		}

		id, err := models.Documents.Insert(doc)
		if err != nil {
			// Return partial success
			return jsonResult(map[string]any{
				"created_ids":   createdIDs,
				"created_count": len(createdIDs),
				"total_count":   len(documents),
				"error":         fmt.Sprintf("failed at document %d: %v", len(createdIDs)+1, err),
				"message":       fmt.Sprintf("Partially created %d of %d documents", len(createdIDs), len(documents)),
			})
		}

		createdIDs = append(createdIDs, id)
		e.recordMutation(ActionCreate, "document", id, nil, doc)
	}

	return jsonResult(map[string]any{
		"created_ids":   createdIDs,
		"created_count": len(createdIDs),
		"collection_id": args.CollectionID,
		"message":       fmt.Sprintf("Created %d documents in collection '%s'", len(createdIDs), col.Name),
	})
}

func (e *Executor) updateDocument(call assistant.ToolCall) (string, error) {
	var args UpdateDocumentArgs
	if err := call.ParseArguments(&args); err != nil {
		return "", &ToolError{Tool: "update_document", Code: ErrCodeInvalidArgs, Message: err.Error()}
	}

	doc, err := models.Documents.Get(args.ID)
	if err != nil {
		return "", &ToolError{
			Tool:       "update_document",
			Code:       ErrCodeNotFound,
			Message:    fmt.Sprintf("document '%s' not found", args.ID),
			Suggestion: "Use query_documents to find documents in a collection",
		}
	}

	// Get collection for validation
	col, err := models.Collections.Get(doc.CollectionID)
	if err != nil {
		log.Printf("Warning: collection %s not found for document %s", doc.CollectionID, doc.ID)
	}

	beforeState := cloneDocument(doc)

	// Parse existing data and merge with new data
	var existingData, newData map[string]any
	if doc.Data != "" {
		if err := json.Unmarshal([]byte(doc.Data), &existingData); err != nil {
			log.Printf("Warning: failed to parse existing document data: %v", err)
			existingData = make(map[string]any)
		}
	} else {
		existingData = make(map[string]any)
	}

	if err := json.Unmarshal([]byte(args.Data), &newData); err != nil {
		return "", &ToolError{
			Tool:       "update_document",
			Code:       ErrCodeInvalidArgs,
			Message:    fmt.Sprintf("invalid JSON data: %v", err),
			Suggestion: "Ensure the data is valid JSON object",
		}
	}

	// Merge new data into existing
	for k, v := range newData {
		existingData[k] = v
	}

	// Validate merged data against schema
	if col != nil {
		if err := content.ValidateDocument(col, existingData); err != nil {
			return "", &ToolError{
				Tool:       "update_document",
				Code:       ErrCodeValidation,
				Message:    fmt.Sprintf("validation failed: %v", err),
				Suggestion: "Check the collection schema for field requirements",
			}
		}
	}

	mergedData, err := json.Marshal(existingData)
	if err != nil {
		return "", &ToolError{Tool: "update_document", Code: ErrCodeInternalError, Message: err.Error()}
	}
	doc.Data = string(mergedData)

	if err := models.Documents.Update(doc); err != nil {
		return "", &ToolError{Tool: "update_document", Code: ErrCodeInternalError, Message: err.Error()}
	}

	e.recordMutation(ActionUpdate, "document", doc.ID, beforeState, doc)

	return jsonResult(map[string]any{
		"id":      doc.ID,
		"message": "Updated document",
	})
}

func (e *Executor) deleteDocument(call assistant.ToolCall) (string, error) {
	var args DeleteDocumentArgs
	if err := call.ParseArguments(&args); err != nil {
		return "", &ToolError{Tool: "delete_document", Code: ErrCodeInvalidArgs, Message: err.Error()}
	}

	doc, err := models.Documents.Get(args.ID)
	if err != nil {
		return "", &ToolError{
			Tool:       "delete_document",
			Code:       ErrCodeNotFound,
			Message:    fmt.Sprintf("document '%s' not found", args.ID),
			Suggestion: "Use query_documents to find documents in a collection",
		}
	}

	beforeState := cloneDocument(doc)

	if err := models.Documents.Delete(doc); err != nil {
		return "", &ToolError{Tool: "delete_document", Code: ErrCodeInternalError, Message: err.Error()}
	}

	e.recordMutation(ActionDelete, "document", doc.ID, beforeState, nil)

	return jsonResult(map[string]any{
		"id":      doc.ID,
		"message": "Deleted document",
	})
}

func (e *Executor) getDocument(call assistant.ToolCall) (string, error) {
	var args GetDocumentArgs
	if err := call.ParseArguments(&args); err != nil {
		return "", &ToolError{Tool: "get_document", Code: ErrCodeInvalidArgs, Message: err.Error()}
	}

	doc, err := models.Documents.Get(args.ID)
	if err != nil {
		return "", &ToolError{
			Tool:       "get_document",
			Code:       ErrCodeNotFound,
			Message:    fmt.Sprintf("document '%s' not found", args.ID),
			Suggestion: "Use query_documents to find documents in a collection",
		}
	}

	var data map[string]any
	if doc.Data != "" {
		json.Unmarshal([]byte(doc.Data), &data)
	}

	return jsonResult(map[string]any{
		"id":            doc.ID,
		"collection_id": doc.CollectionID,
		"data":          data,
		"created_at":    doc.CreatedAt,
		"updated_at":    doc.UpdatedAt,
	})
}

func (e *Executor) queryDocuments(call assistant.ToolCall) (string, error) {
	var args QueryDocumentsArgs
	if err := call.ParseArguments(&args); err != nil {
		return "", &ToolError{Tool: "query_documents", Code: ErrCodeInvalidArgs, Message: err.Error()}
	}

	// Build query
	query := "WHERE CollectionID = ?"
	queryArgs := []any{args.CollectionID}

	if args.Filter != "" {
		// Validate filter to prevent SQL injection
		if err := validateSQLFilter(args.Filter); err != nil {
			return "", &ToolError{
				Tool:       "query_documents",
				Code:       ErrCodeInvalidArgs,
				Message:    fmt.Sprintf("invalid filter: %v", err),
				Suggestion: "Use simple conditions like: Data LIKE '%\"field\":\"value\"%'",
			}
		}
		query += " AND " + args.Filter
	}

	if args.OrderBy != "" {
		if err := validateSQLOrderBy(args.OrderBy); err != nil {
			return "", &ToolError{
				Tool:       "query_documents",
				Code:       ErrCodeInvalidArgs,
				Message:    fmt.Sprintf("invalid order_by: %v", err),
				Suggestion: "Use simple ordering like: CreatedAt DESC",
			}
		}
		query += " ORDER BY " + args.OrderBy
	} else {
		query += " ORDER BY CreatedAt DESC"
	}

	// Enforce maximum limit
	limit := args.Limit
	if limit <= 0 || limit > 100 {
		limit = 100
	}
	query += fmt.Sprintf(" LIMIT %d", limit)

	docs, err := models.Documents.Search(query, queryArgs...)
	if err != nil {
		return "", &ToolError{Tool: "query_documents", Code: ErrCodeInternalError, Message: err.Error()}
	}

	result := make([]map[string]any, len(docs))
	for i, d := range docs {
		var data map[string]any
		if d.Data != "" {
			json.Unmarshal([]byte(d.Data), &data)
		}
		result[i] = map[string]any{
			"id":         d.ID,
			"data":       data,
			"created_at": d.CreatedAt,
			"updated_at": d.UpdatedAt,
		}
	}

	return jsonResult(map[string]any{
		"documents": result,
		"count":     len(docs),
	})
}

// =============================================================================
// Partial Handlers
// =============================================================================

func (e *Executor) createPartial(call assistant.ToolCall) (string, error) {
	var args CreatePartialArgs
	if err := call.ParseArguments(&args); err != nil {
		return "", &ToolError{Tool: "create_partial", Code: ErrCodeInvalidArgs, Message: err.Error()}
	}

	partial := &models.Partial{
		Name:        args.Name,
		Description: args.Description,
		HTML:        unescapeHTML(args.HTML),
		Published:   args.Published,
	}

	// Set ID from arg or generate slug from name
	if args.ID != "" {
		partial.ID = slugify(args.ID)
	} else if args.Name != "" {
		partial.ID = slugify(args.Name)
	}

	id, err := models.Partials.Insert(partial)
	if err != nil {
		return "", &ToolError{Tool: "create_partial", Code: ErrCodeInternalError, Message: err.Error()}
	}

	e.recordMutation(ActionCreate, "partial", id, nil, partial)

	return jsonResult(map[string]any{
		"id":      id,
		"name":    args.Name,
		"message": fmt.Sprintf("Created partial '%s'. Use {{partial \"%s\"}} in page HTML to include it.", args.Name, id),
	})
}

func (e *Executor) updatePartial(call assistant.ToolCall) (string, error) {
	var args UpdatePartialArgs
	if err := call.ParseArguments(&args); err != nil {
		return "", &ToolError{Tool: "update_partial", Code: ErrCodeInvalidArgs, Message: err.Error()}
	}

	partial, err := models.Partials.Get(args.ID)
	if err != nil {
		return "", &ToolError{
			Tool:       "update_partial",
			Code:       ErrCodeNotFound,
			Message:    fmt.Sprintf("partial '%s' not found", args.ID),
			Suggestion: "Use list_partials to see available partials",
		}
	}

	beforeState := clonePartial(partial)

	if args.Name != nil {
		partial.Name = *args.Name
	}
	if args.Description != nil {
		partial.Description = *args.Description
	}
	if args.HTML != nil {
		partial.HTML = unescapeHTML(*args.HTML)
	}
	if args.Published != nil {
		partial.Published = *args.Published
	}

	if err := models.Partials.Update(partial); err != nil {
		return "", &ToolError{Tool: "update_partial", Code: ErrCodeInternalError, Message: err.Error()}
	}

	e.recordMutation(ActionUpdate, "partial", partial.ID, beforeState, partial)

	return jsonResult(map[string]any{
		"id":      partial.ID,
		"name":    partial.Name,
		"message": fmt.Sprintf("Updated partial '%s'", partial.Name),
	})
}

func (e *Executor) deletePartial(call assistant.ToolCall) (string, error) {
	var args DeletePartialArgs
	if err := call.ParseArguments(&args); err != nil {
		return "", &ToolError{Tool: "delete_partial", Code: ErrCodeInvalidArgs, Message: err.Error()}
	}

	partial, err := models.Partials.Get(args.ID)
	if err != nil {
		return "", &ToolError{
			Tool:       "delete_partial",
			Code:       ErrCodeNotFound,
			Message:    fmt.Sprintf("partial '%s' not found", args.ID),
			Suggestion: "Use list_partials to see available partials",
		}
	}

	name := partial.Name
	beforeState := clonePartial(partial)

	if err := models.Partials.Delete(partial); err != nil {
		return "", &ToolError{Tool: "delete_partial", Code: ErrCodeInternalError, Message: err.Error()}
	}

	e.recordMutation(ActionDelete, "partial", partial.ID, beforeState, nil)

	return jsonResult(map[string]any{
		"id":      partial.ID,
		"message": fmt.Sprintf("Deleted partial '%s'", name),
	})
}

func (e *Executor) getPartial(call assistant.ToolCall) (string, error) {
	var args GetPartialArgs
	if err := call.ParseArguments(&args); err != nil {
		return "", &ToolError{Tool: "get_partial", Code: ErrCodeInvalidArgs, Message: err.Error()}
	}

	partial, err := models.Partials.Get(args.ID)
	if err != nil {
		return "", &ToolError{
			Tool:       "get_partial",
			Code:       ErrCodeNotFound,
			Message:    fmt.Sprintf("partial '%s' not found", args.ID),
			Suggestion: "Use list_partials to see available partials",
		}
	}

	return jsonResult(map[string]any{
		"id":          partial.ID,
		"name":        partial.Name,
		"description": partial.Description,
		"html":        partial.HTML,
		"published":   partial.Published,
		"usage":       fmt.Sprintf("{{partial \"%s\"}}", partial.ID),
		"created_at":  partial.CreatedAt,
		"updated_at":  partial.UpdatedAt,
	})
}

func (e *Executor) listPartials(call assistant.ToolCall) (string, error) {
	var args ListPartialsArgs
	if err := call.ParseArguments(&args); err != nil {
		return "", &ToolError{Tool: "list_partials", Code: ErrCodeInvalidArgs, Message: err.Error()}
	}

	var query string
	if args.IncludeUnpublished {
		query = "ORDER BY Name"
	} else {
		query = "WHERE Published = true ORDER BY Name"
	}

	partials, err := models.Partials.Search(query)
	if err != nil {
		return "", &ToolError{Tool: "list_partials", Code: ErrCodeInternalError, Message: err.Error()}
	}

	result := make([]map[string]any, len(partials))
	for i, p := range partials {
		result[i] = map[string]any{
			"id":          p.ID,
			"name":        p.Name,
			"description": p.Description,
			"published":   p.Published,
			"usage":       fmt.Sprintf("{{partial \"%s\"}}", p.ID),
		}
	}

	return jsonResult(map[string]any{
		"partials": result,
		"count":    len(partials),
	})
}

// =============================================================================
// File Handlers
// =============================================================================

func (e *Executor) updateFile(call assistant.ToolCall) (string, error) {
	var args UpdateFileArgs
	if err := call.ParseArguments(&args); err != nil {
		return "", &ToolError{Tool: "update_file", Code: ErrCodeInvalidArgs, Message: err.Error()}
	}

	file, err := models.Files.Get(args.ID)
	if err != nil {
		return "", &ToolError{
			Tool:       "update_file",
			Code:       ErrCodeNotFound,
			Message:    fmt.Sprintf("file '%s' not found", args.ID),
			Suggestion: "Use list_files to see available files",
		}
	}

	beforeState := cloneFile(file)

	if args.Published != nil {
		file.Published = *args.Published
	}
	if args.Path != nil {
		newPath := *args.Path
		// Validate path uniqueness
		if file.Published && newPath != "" && newPath != file.Path {
			existing, _ := models.Files.First("WHERE Path = ? AND ID != ?", newPath, file.ID)
			if existing != nil {
				return "", &ToolError{
					Tool:       "update_file",
					Code:       ErrCodeDuplicate,
					Message:    fmt.Sprintf("path '%s' is already in use", newPath),
					Suggestion: "Choose a different path",
				}
			}
		}
		file.Path = newPath
	}

	if err := models.Files.Update(file); err != nil {
		return "", &ToolError{Tool: "update_file", Code: ErrCodeInternalError, Message: err.Error()}
	}

	e.recordMutation(ActionUpdate, "file", file.ID, beforeState, file)

	return jsonResult(map[string]any{
		"id":         file.ID,
		"name":       file.Name,
		"published":  file.Published,
		"path":       file.Path,
		"public_url": file.PublicURL(),
		"message":    fmt.Sprintf("Updated file '%s'", file.Name),
	})
}

func (e *Executor) getFile(call assistant.ToolCall) (string, error) {
	var args GetFileArgs
	if err := call.ParseArguments(&args); err != nil {
		return "", &ToolError{Tool: "get_file", Code: ErrCodeInvalidArgs, Message: err.Error()}
	}

	file, err := models.Files.Get(args.ID)
	if err != nil {
		return "", &ToolError{
			Tool:       "get_file",
			Code:       ErrCodeNotFound,
			Message:    fmt.Sprintf("file '%s' not found", args.ID),
			Suggestion: "Use list_files to see available files",
		}
	}

	return jsonResult(map[string]any{
		"id":         file.ID,
		"name":       file.Name,
		"mime_type":  file.MimeType,
		"size":       file.Size,
		"published":  file.Published,
		"path":       file.Path,
		"public_url": file.PublicURL(),
		"file_url":   "/files/" + file.ID,
		"created_at": file.CreatedAt,
		"updated_at": file.UpdatedAt,
	})
}

func (e *Executor) listFiles(call assistant.ToolCall) (string, error) {
	var args ListFilesArgs
	if err := call.ParseArguments(&args); err != nil {
		return "", &ToolError{Tool: "list_files", Code: ErrCodeInvalidArgs, Message: err.Error()}
	}

	var query string
	var queryArgs []any

	if args.MimeType != "" {
		if args.IncludeUnpublished {
			query = "WHERE MimeType LIKE ? ORDER BY CreatedAt DESC"
			queryArgs = []any{args.MimeType + "%"}
		} else {
			query = "WHERE Published = true AND MimeType LIKE ? ORDER BY CreatedAt DESC"
			queryArgs = []any{args.MimeType + "%"}
		}
	} else {
		if args.IncludeUnpublished {
			query = "ORDER BY CreatedAt DESC"
		} else {
			query = "WHERE Published = true ORDER BY CreatedAt DESC"
		}
	}

	files, err := models.Files.Search(query, queryArgs...)
	if err != nil {
		return "", &ToolError{Tool: "list_files", Code: ErrCodeInternalError, Message: err.Error()}
	}

	result := make([]map[string]any, len(files))
	for i, f := range files {
		result[i] = map[string]any{
			"id":         f.ID,
			"name":       f.Name,
			"mime_type":  f.MimeType,
			"size":       f.Size,
			"published":  f.Published,
			"path":       f.Path,
			"public_url": f.PublicURL(),
			"file_url":   "/files/" + f.ID,
		}
	}

	return jsonResult(map[string]any{
		"files": result,
		"count": len(files),
	})
}

func (e *Executor) readFile(call assistant.ToolCall) (string, error) {
	var args ReadFileArgs
	if err := call.ParseArguments(&args); err != nil {
		return "", &ToolError{Tool: "read_file", Code: ErrCodeInvalidArgs, Message: err.Error()}
	}

	file, err := models.Files.Get(args.ID)
	if err != nil {
		return "", &ToolError{
			Tool:       "read_file",
			Code:       ErrCodeNotFound,
			Message:    fmt.Sprintf("file '%s' not found", args.ID),
			Suggestion: "Use list_files to see available files",
		}
	}

	// Get cached or extract content
	fileContent, err := GetFileContent(args.ID)
	if err != nil {
		return "", &ToolError{
			Tool:       "read_file",
			Code:       ErrCodeValidation,
			Message:    err.Error(),
			Suggestion: "This tool only works with text files under 100KB",
		}
	}

	return jsonResult(map[string]any{
		"id":        file.ID,
		"name":      file.Name,
		"mime_type": file.MimeType,
		"size":      file.Size,
		"content":   fileContent,
	})
}

// =============================================================================
// Note Handlers
// =============================================================================

func (e *Executor) createNote(call assistant.ToolCall) (string, error) {
	var args CreateNoteArgs
	if err := call.ParseArguments(&args); err != nil {
		return "", &ToolError{Tool: "create_note", Code: ErrCodeInvalidArgs, Message: err.Error()}
	}

	// Validate type
	switch args.Type {
	case models.NoteTypePreference, models.NoteTypeConvention, models.NoteTypeLearned:
		// Valid
	default:
		return "", &ToolError{
			Tool:       "create_note",
			Code:       ErrCodeInvalidArgs,
			Message:    fmt.Sprintf("invalid note type: %s", args.Type),
			Suggestion: "Use 'preference', 'convention', or 'learned'",
		}
	}

	// Validate category
	switch args.Category {
	case models.NoteCategoryStyle, models.NoteCategoryContent, models.NoteCategoryStructure, models.NoteCategoryBehavior:
		// Valid
	default:
		return "", &ToolError{
			Tool:       "create_note",
			Code:       ErrCodeInvalidArgs,
			Message:    fmt.Sprintf("invalid note category: %s", args.Category),
			Suggestion: "Use 'style', 'content', 'structure', or 'behavior'",
		}
	}

	note := &models.Note{
		Type:     args.Type,
		Category: args.Category,
		Title:    args.Title,
		Content:  args.Content,
		Source:   models.NoteSourceAI,
		Active:   true,
	}

	id, err := models.Notes.Insert(note)
	if err != nil {
		return "", &ToolError{Tool: "create_note", Code: ErrCodeInternalError, Message: err.Error()}
	}

	return jsonResult(map[string]any{
		"id":      id,
		"title":   note.Title,
		"message": fmt.Sprintf("Created note '%s'", note.Title),
	})
}

func (e *Executor) listNotes(call assistant.ToolCall) (string, error) {
	var args ListNotesArgs
	if err := call.ParseArguments(&args); err != nil {
		return "", &ToolError{Tool: "list_notes", Code: ErrCodeInvalidArgs, Message: err.Error()}
	}

	// Build query
	var conditions []string
	var queryArgs []any

	if !args.IncludeInactive {
		conditions = append(conditions, "Active = true")
	}
	if args.Type != "" {
		conditions = append(conditions, "Type = ?")
		queryArgs = append(queryArgs, args.Type)
	}
	if args.Category != "" {
		conditions = append(conditions, "Category = ?")
		queryArgs = append(queryArgs, args.Category)
	}

	query := "ORDER BY UpdatedAt DESC"
	if len(conditions) > 0 {
		query = "WHERE " + conditions[0]
		for i := 1; i < len(conditions); i++ {
			query += " AND " + conditions[i]
		}
		query += " ORDER BY UpdatedAt DESC"
	}

	notes, err := models.Notes.Search(query, queryArgs...)
	if err != nil {
		return "", &ToolError{Tool: "list_notes", Code: ErrCodeInternalError, Message: err.Error()}
	}

	result := make([]map[string]any, len(notes))
	for i, n := range notes {
		result[i] = map[string]any{
			"id":       n.ID,
			"type":     n.Type,
			"category": n.Category,
			"title":    n.Title,
			"content":  n.Content,
			"source":   n.Source,
			"active":   n.Active,
		}
	}

	return jsonResult(map[string]any{
		"notes": result,
		"count": len(notes),
	})
}

func (e *Executor) getNote(call assistant.ToolCall) (string, error) {
	var args GetNoteArgs
	if err := call.ParseArguments(&args); err != nil {
		return "", &ToolError{Tool: "get_note", Code: ErrCodeInvalidArgs, Message: err.Error()}
	}

	note, err := models.Notes.Get(args.ID)
	if err != nil {
		return "", &ToolError{
			Tool:       "get_note",
			Code:       ErrCodeNotFound,
			Message:    fmt.Sprintf("note '%s' not found", args.ID),
			Suggestion: "Use list_notes to see available notes",
		}
	}

	return jsonResult(map[string]any{
		"id":         note.ID,
		"type":       note.Type,
		"category":   note.Category,
		"title":      note.Title,
		"content":    note.Content,
		"source":     note.Source,
		"active":     note.Active,
		"created_at": note.CreatedAt,
		"updated_at": note.UpdatedAt,
	})
}

func (e *Executor) updateNote(call assistant.ToolCall) (string, error) {
	var args UpdateNoteArgs
	if err := call.ParseArguments(&args); err != nil {
		return "", &ToolError{Tool: "update_note", Code: ErrCodeInvalidArgs, Message: err.Error()}
	}

	note, err := models.Notes.Get(args.ID)
	if err != nil {
		return "", &ToolError{
			Tool:       "update_note",
			Code:       ErrCodeNotFound,
			Message:    fmt.Sprintf("note '%s' not found", args.ID),
			Suggestion: "Use list_notes to see available notes",
		}
	}

	if args.Title != nil {
		note.Title = *args.Title
	}
	if args.Content != nil {
		note.Content = *args.Content
	}
	if args.Active != nil {
		note.Active = *args.Active
	}

	if err := models.Notes.Update(note); err != nil {
		return "", &ToolError{Tool: "update_note", Code: ErrCodeInternalError, Message: err.Error()}
	}

	return jsonResult(map[string]any{
		"id":      note.ID,
		"title":   note.Title,
		"message": fmt.Sprintf("Updated note '%s'", note.Title),
	})
}

func (e *Executor) deleteNote(call assistant.ToolCall) (string, error) {
	var args DeleteNoteArgs
	if err := call.ParseArguments(&args); err != nil {
		return "", &ToolError{Tool: "delete_note", Code: ErrCodeInvalidArgs, Message: err.Error()}
	}

	note, err := models.Notes.Get(args.ID)
	if err != nil {
		return "", &ToolError{
			Tool:       "delete_note",
			Code:       ErrCodeNotFound,
			Message:    fmt.Sprintf("note '%s' not found", args.ID),
			Suggestion: "Use list_notes to see available notes",
		}
	}

	title := note.Title

	if err := models.Notes.Delete(note); err != nil {
		return "", &ToolError{Tool: "delete_note", Code: ErrCodeInternalError, Message: err.Error()}
	}

	return jsonResult(map[string]any{
		"id":      args.ID,
		"message": fmt.Sprintf("Deleted note '%s'", title),
	})
}

// =============================================================================
// User Handlers
// =============================================================================

func (e *Executor) createUser(call assistant.ToolCall) (string, error) {
	var args CreateUserArgs
	if err := call.ParseArguments(&args); err != nil {
		return "", &ToolError{Tool: "create_user", Code: ErrCodeInvalidArgs, Message: err.Error()}
	}

	if args.Email == "" {
		return "", &ToolError{Tool: "create_user", Code: ErrCodeInvalidArgs, Message: "email is required"}
	}
	if args.Password == "" {
		return "", &ToolError{Tool: "create_user", Code: ErrCodeInvalidArgs, Message: "password is required"}
	}
	if len(args.Password) < 8 {
		return "", &ToolError{Tool: "create_user", Code: ErrCodeInvalidArgs, Message: "password must be at least 8 characters"}
	}

	// Check if email already exists
	existing, _ := models.Users.First("WHERE Email = ?", args.Email)
	if existing != nil {
		return "", &ToolError{Tool: "create_user", Code: ErrCodeDuplicate, Message: "a user with this email already exists"}
	}

	// Hash password
	hash, err := bcrypt.GenerateFromPassword([]byte(args.Password), bcrypt.DefaultCost)
	if err != nil {
		return "", &ToolError{Tool: "create_user", Code: ErrCodeInternalError, Message: "failed to hash password"}
	}

	role := args.Role
	if role == "" {
		role = "user"
	}

	user := &models.User{
		Email:        args.Email,
		Name:         args.Name,
		PasswordHash: string(hash),
		Role:         role,
		Verified:     true, // Admin-created users are auto-verified
	}

	id, err := models.Users.Insert(user)
	if err != nil {
		return "", &ToolError{Tool: "create_user", Code: ErrCodeInternalError, Message: err.Error()}
	}

	e.recordMutation(ActionCreate, "user", id, nil, user)
	e.publishEvent(ActionCreate, "user", id, user)

	return jsonResult(map[string]any{
		"id":      id,
		"email":   user.Email,
		"name":    user.Name,
		"role":    user.Role,
		"message": fmt.Sprintf("Created user '%s'", user.Email),
	})
}

func (e *Executor) updateUser(call assistant.ToolCall) (string, error) {
	var args UpdateUserArgs
	if err := call.ParseArguments(&args); err != nil {
		return "", &ToolError{Tool: "update_user", Code: ErrCodeInvalidArgs, Message: err.Error()}
	}

	if args.ID == "" {
		return "", &ToolError{Tool: "update_user", Code: ErrCodeInvalidArgs, Message: "id is required"}
	}

	user, err := models.Users.Get(args.ID)
	if err != nil {
		return "", &ToolError{Tool: "update_user", Code: ErrCodeNotFound, Message: "user not found"}
	}

	before := *user

	if args.Name != "" {
		user.Name = args.Name
	}
	if args.Email != "" {
		// Check if new email already exists
		existing, _ := models.Users.First("WHERE Email = ? AND ID != ?", args.Email, user.ID)
		if existing != nil {
			return "", &ToolError{Tool: "update_user", Code: ErrCodeDuplicate, Message: "a user with this email already exists"}
		}
		user.Email = args.Email
	}
	if args.Role != "" {
		user.Role = args.Role
	}
	if args.Verified != nil {
		user.Verified = *args.Verified
	}

	if err := models.Users.Update(user); err != nil {
		return "", &ToolError{Tool: "update_user", Code: ErrCodeInternalError, Message: err.Error()}
	}

	e.recordMutation(ActionUpdate, "user", user.ID, before, user)
	e.publishEvent(ActionUpdate, "user", user.ID, user)

	return jsonResult(map[string]any{
		"id":      user.ID,
		"email":   user.Email,
		"name":    user.Name,
		"role":    user.Role,
		"message": fmt.Sprintf("Updated user '%s'", user.Email),
	})
}

func (e *Executor) deleteUser(call assistant.ToolCall) (string, error) {
	var args DeleteUserArgs
	if err := call.ParseArguments(&args); err != nil {
		return "", &ToolError{Tool: "delete_user", Code: ErrCodeInvalidArgs, Message: err.Error()}
	}

	if args.ID == "" {
		return "", &ToolError{Tool: "delete_user", Code: ErrCodeInvalidArgs, Message: "id is required"}
	}

	user, err := models.Users.Get(args.ID)
	if err != nil {
		return "", &ToolError{Tool: "delete_user", Code: ErrCodeNotFound, Message: "user not found"}
	}

	email := user.Email

	if err := models.Users.Delete(user); err != nil {
		return "", &ToolError{Tool: "delete_user", Code: ErrCodeInternalError, Message: err.Error()}
	}

	e.recordMutation(ActionDelete, "user", args.ID, user, nil)
	e.publishEvent(ActionDelete, "user", args.ID, nil)

	return jsonResult(map[string]any{
		"id":      args.ID,
		"message": fmt.Sprintf("Deleted user '%s'", email),
	})
}

func (e *Executor) listUsers(call assistant.ToolCall) (string, error) {
	users, err := models.Users.Search("ORDER BY CreatedAt DESC")
	if err != nil {
		return "", &ToolError{Tool: "list_users", Code: ErrCodeInternalError, Message: err.Error()}
	}

	result := make([]map[string]any, len(users))
	for i, u := range users {
		result[i] = map[string]any{
			"id":       u.ID,
			"email":    u.Email,
			"name":     u.Name,
			"role":     u.Role,
			"verified": u.Verified,
			"created":  u.CreatedAt.Format("2006-01-02"),
		}
	}

	return jsonResult(map[string]any{
		"users": result,
		"count": len(users),
	})
}

// =============================================================================
// Utility Handlers
// =============================================================================

func (e *Executor) validateTemplate(call assistant.ToolCall) (string, error) {
	var args ValidateTemplateArgs
	if err := call.ParseArguments(&args); err != nil {
		return "", &ToolError{Tool: "validate_template", Code: ErrCodeInvalidArgs, Message: err.Error()}
	}

	if args.HTML == "" {
		return jsonResult(map[string]any{
			"valid":   true,
			"message": "Empty template is valid",
		})
	}

	// Parse the template
	tmpl, err := template.New("validate").Funcs(content.AllFuncs()).Parse(args.HTML)
	if err != nil {
		return jsonResult(map[string]any{
			"valid":      false,
			"error":      err.Error(),
			"suggestion": "Check for unclosed tags, missing braces, or invalid template syntax",
			"message":    "Template has syntax errors",
		})
	}

	// Try to execute with nil data to catch runtime errors
	var buf bytes.Buffer
	execErr := tmpl.Execute(&buf, nil)

	// Some execution errors are expected (nil pointer, missing data) - that's OK
	// We mainly care about parse errors which we already caught above

	return jsonResult(map[string]any{
		"valid":    true,
		"warning":  execErr != nil,
		"message":  "Template syntax is valid",
		"template": tmpl.Name(),
	})
}

func (e *Executor) navigateUser(call assistant.ToolCall) (string, error) {
	var args NavigateUserArgs
	if err := call.ParseArguments(&args); err != nil {
		return "", &ToolError{Tool: "navigate_user", Code: ErrCodeInvalidArgs, Message: err.Error()}
	}
	if args.URL == "" {
		return "", &ToolError{
			Tool:       "navigate_user",
			Code:       ErrCodeInvalidArgs,
			Message:    "url is required",
			Suggestion: "Provide a URL like /admin/page/about",
		}
	}
	return jsonResult(map[string]any{
		"status": "navigated",
		"url":    args.URL,
	})
}

// =============================================================================
// Search Handler
// =============================================================================

func (e *Executor) search(call assistant.ToolCall) (string, error) {
	var args SearchArgs
	if err := call.ParseArguments(&args); err != nil {
		return "", &ToolError{Tool: "search", Code: ErrCodeInvalidArgs, Message: err.Error()}
	}

	if args.Query == "" {
		return "", &ToolError{
			Tool:    "search",
			Code:    ErrCodeInvalidArgs,
			Message: "query is required",
		}
	}

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

	results, total, err := search.Search(search.SearchOptions{
		Query:        args.Query,
		EntityType:   args.Type,
		CollectionID: args.CollectionID,
		Page:         1,
		PerPage:      perPage,
	})
	if err != nil {
		return "", &ToolError{Tool: "search", Code: ErrCodeInternalError, Message: err.Error()}
	}

	items := make([]map[string]any, len(results))
	for i, r := range results {
		item := map[string]any{
			"id":    r.EntityID,
			"type":  r.EntityType,
			"title": r.Title,
			"rank":  r.Rank,
		}
		if r.Snippet != "" {
			item["snippet"] = r.Snippet
		}
		if r.CollectionID != "" {
			item["collection_id"] = r.CollectionID
		}
		items[i] = item
	}

	return jsonResult(map[string]any{
		"results": items,
		"total":   total,
		"message": fmt.Sprintf("Found %d results for '%s'", total, args.Query),
	})
}

// =============================================================================
// Helpers
// =============================================================================

func (e *Executor) recordMutation(action, entityType, entityID string, before, after any) {
	if e.message == nil {
		return
	}

	mutation := &models.Mutation{
		MessageID:  e.message.ID,
		Action:     action,
		EntityType: entityType,
		EntityID:   entityID,
	}

	if before != nil {
		data, _ := json.Marshal(before)
		mutation.BeforeState = string(data)
	}
	if after != nil {
		data, _ := json.Marshal(after)
		mutation.AfterState = string(data)
	}

	models.Mutations.Insert(mutation)

	// Publish events for real-time dashboard updates
	e.publishEvent(action, entityType, entityID, after)
}

// publishEvent sends events to the event bus for real-time updates.
func (e *Executor) publishEvent(action, entityType, entityID string, entity any) {
	var eventType content.EventType
	switch action {
	case ActionCreate:
		eventType = content.EventCreate
	case ActionUpdate:
		eventType = content.EventUpdate
	case ActionDelete:
		eventType = content.EventDelete
	default:
		return
	}

	switch entityType {
	case "page", "page_content":
		if entityType == "page_content" {
			// For page_content, we send a page_update event
			if pc, ok := entity.(*models.PageContent); ok && action == ActionCreate {
				data := map[string]any{
					"id":    pc.PageID,
					"title": pc.Title,
				}
				content.CollectionEvents.PublishPageUpdate(pc.PageID, data)
			}
			return
		}
		data := make(map[string]any)
		if p, ok := entity.(*models.Page); ok {
			data["id"] = p.ID
			data["title"] = p.Title()
			data["published"] = p.IsPublished()
			data["path"] = p.Path()
		}
		switch eventType {
		case content.EventCreate:
			content.CollectionEvents.PublishPageCreate(entityID, data)
		case content.EventUpdate:
			content.CollectionEvents.PublishPageUpdate(entityID, data)
		case content.EventDelete:
			content.CollectionEvents.PublishPageDelete(entityID)
		}

	case "collection":
		data := make(map[string]any)
		if c, ok := entity.(*models.Collection); ok {
			data["id"] = c.ID
			data["name"] = c.Name
			data["documentCount"] = c.DocumentCount()
		}
		switch eventType {
		case content.EventCreate:
			content.CollectionEvents.PublishCollectionCreate(entityID, data)
		case content.EventUpdate:
			content.CollectionEvents.PublishCollectionUpdate(entityID, data)
		case content.EventDelete:
			content.CollectionEvents.PublishCollectionDelete(entityID)
		}

	case "document":
		var collectionID string
		if d, ok := entity.(*models.Document); ok {
			collectionID = d.CollectionID
		}
		switch eventType {
		case content.EventCreate:
			content.CollectionEvents.PublishCreate(collectionID, entityID, nil)
		case content.EventUpdate:
			content.CollectionEvents.PublishUpdate(collectionID, entityID, nil)
		case content.EventDelete:
			content.CollectionEvents.PublishDelete(collectionID, entityID)
		}
	}
}
← Back