// app_search.go contains the search-related application layer code.
//
// This file provides:
//   - Search index initialization and rebuild logic
//   - Wails bindings for frontend search functionality
//   - File and collection indexing helpers
//
// The search feature uses Bleve for local full-text search. Each user has their
// own isolated search index stored in their local application data directory.
// Search results are deduplicated by filename to avoid showing the same file
// multiple times when it exists in multiple collections.
package app

import (
	"fmt"
	"time"

	"go.uber.org/zap"

	"codeberg.org/mapleopentech/monorepo/native/desktop/maplefile/internal/domain/file"
	"codeberg.org/mapleopentech/monorepo/native/desktop/maplefile/internal/service/search"
)

// =============================================================================
// SEARCH INDEXING OPERATIONS
// These functions are called internally when files/collections are created,
// updated, or when the index needs to be rebuilt.
// =============================================================================

// indexFileForSearch indexes a single file in the search index.
// This is called when a new file is uploaded to add it to the search index immediately.
func (a *Application) indexFileForSearch(fileID, collectionID, filename string, tags []string, size int64) error {
	// Get collection name for denormalization
	collectionName := ""
	collectionRepo := a.getCollectionRepo()
	if collectionRepo != nil {
		collection, err := collectionRepo.Get(collectionID)
		if err == nil && collection != nil {
			collectionName = collection.Name
		}
	}

	// Create file document for search
	fileDoc := &search.FileDocument{
		ID:             fileID,
		Filename:       filename,
		Description:    "", // No description field in current implementation
		CollectionID:   collectionID,
		CollectionName: collectionName,
		Tags:           tags,
		Size:           size,
		CreatedAt:      time.Now(),
		Type:           "file",
	}

	return a.searchService.IndexFile(fileDoc)
}

// indexCollectionForSearch indexes a collection in the search index
func (a *Application) indexCollectionForSearch(collectionID, name string, tags []string, fileCount int) error {
	// Create collection document for search
	collectionDoc := &search.CollectionDocument{
		ID:          collectionID,
		Name:        name,
		Description: "", // No description field in current implementation
		Tags:        tags,
		FileCount:   fileCount,
		CreatedAt:   time.Now(),
		Type:        "collection",
	}

	return a.searchService.IndexCollection(collectionDoc)
}

// InitializeSearchIndex initializes the search index for the current user.
// This can be called manually if the index needs to be initialized.
func (a *Application) InitializeSearchIndex() error {
	a.logger.Info("Manually initializing search index")

	// Get current session to get user email
	session, err := a.authService.GetCurrentSession(a.ctx)
	if err != nil || session == nil {
		return fmt.Errorf("no active session found")
	}

	// Initialize the search service
	if err := a.searchService.Initialize(a.ctx, session.Email); err != nil {
		a.logger.Error("Failed to initialize search index", zap.Error(err))
		return fmt.Errorf("failed to initialize search index: %w", err)
	}

	a.logger.Info("Search index initialized successfully")

	// Rebuild index from local data
	if err := a.rebuildSearchIndexForUser(session.Email); err != nil {
		a.logger.Warn("Failed to rebuild search index", zap.Error(err))
		return fmt.Errorf("failed to rebuild search index: %w", err)
	}

	return nil
}

// rebuildSearchIndexForUser rebuilds the entire search index from the local file repository.
// This is called on app startup and after login to ensure the search index is up-to-date.
// The rebuild process:
//  1. Lists all files from the local repository
//  2. Deduplicates files by ID (in case of repository corruption)
//  3. Skips deleted files
//  4. Passes all files to the search service for batch indexing
func (a *Application) rebuildSearchIndexForUser(userEmail string) error {
	a.logger.Info("Rebuilding search index from local data", zap.String("email", userEmail))

	fileRepo := a.getFileRepo()
	if fileRepo == nil {
		return fmt.Errorf("file repository not available")
	}

	// Get all local files
	localFiles, err := fileRepo.List()
	if err != nil {
		a.logger.Error("Failed to list files for search index rebuild", zap.Error(err))
		return fmt.Errorf("failed to list files: %w", err)
	}

	// Convert to search documents - use map to deduplicate by ID
	fileDocumentsMap := make(map[string]*search.FileDocument)
	for _, f := range localFiles {
		// Skip deleted files
		if f.State == file.StateDeleted {
			continue
		}

		// Check for duplicates in local file repo
		if _, exists := fileDocumentsMap[f.ID]; exists {
			a.logger.Warn("Duplicate file found in local repository",
				zap.String("id", f.ID),
				zap.String("name", f.Name))
			continue
		}

		// Get collection name if available
		collectionName := ""
		collectionRepo := a.getCollectionRepo()
		if collectionRepo != nil {
			collection, err := collectionRepo.Get(f.CollectionID)
			if err == nil && collection != nil {
				collectionName = collection.Name
			}
		}

		fileDoc := &search.FileDocument{
			ID:             f.ID,
			Filename:       f.Name,
			Description:    "",
			CollectionID:   f.CollectionID,
			CollectionName: collectionName,
			Tags:           []string{}, // Tags not stored in file entity currently
			Size:           f.DecryptedSizeInBytes,
			CreatedAt:      f.CreatedAt,
			Type:           "file",
		}
		fileDocumentsMap[f.ID] = fileDoc
	}

	// Convert map to slice
	fileDocuments := make([]*search.FileDocument, 0, len(fileDocumentsMap))
	for _, doc := range fileDocumentsMap {
		fileDocuments = append(fileDocuments, doc)
	}

	a.logger.Info("Prepared files for indexing",
		zap.Int("total_from_repo", len(localFiles)),
		zap.Int("unique_files", len(fileDocuments)))

	// For now, we don't index collections separately since they're fetched from cloud
	// Collections will be indexed when they're explicitly created/updated
	collectionDocuments := []*search.CollectionDocument{}

	// Rebuild the index
	if err := a.searchService.RebuildIndex(userEmail, fileDocuments, collectionDocuments); err != nil {
		a.logger.Error("Failed to rebuild search index", zap.Error(err))
		return fmt.Errorf("failed to rebuild search index: %w", err)
	}

	a.logger.Info("Search index rebuilt successfully",
		zap.Int("files_indexed", len(fileDocuments)),
		zap.Int("collections_indexed", len(collectionDocuments)))

	return nil
}

// =============================================================================
// WAILS BINDINGS - Exposed to Frontend
// =============================================================================

// SearchInput represents the input for search
type SearchInput struct {
	Query string `json:"query"`
	Limit int    `json:"limit,omitempty"`
}

// SearchResultData represents search results for the frontend
type SearchResultData struct {
	Files            []FileSearchResult       `json:"files"`
	Collections      []CollectionSearchResult `json:"collections"`
	TotalFiles       int                      `json:"total_files"`
	TotalCollections int                      `json:"total_collections"`
	TotalHits        uint64                   `json:"total_hits"`
	MaxScore         float64                  `json:"max_score"`
	Query            string                   `json:"query"`
}

// FileSearchResult represents a file in search results
type FileSearchResult struct {
	ID             string   `json:"id"`
	Filename       string   `json:"filename"`
	CollectionID   string   `json:"collection_id"`
	CollectionName string   `json:"collection_name"`
	Tags           []string `json:"tags"`
	Size           int64    `json:"size"`
	CreatedAt      string   `json:"created_at"`
}

// CollectionSearchResult represents a collection in search results
type CollectionSearchResult struct {
	ID          string   `json:"id"`
	Name        string   `json:"name"`
	Tags        []string `json:"tags"`
	FileCount   int      `json:"file_count"`
	CreatedAt   string   `json:"created_at"`
}

// Search performs a full-text search across files and collections.
// This is the main Wails binding exposed to the frontend for search functionality.
//
// Features:
//   - Case-insensitive substring matching (e.g., "mesh" finds "meshtastic")
//   - Deduplication by filename (same filename in multiple collections shows once)
//   - Auto-initialization if search index is not ready
//   - Support for Bleve query syntax (+, -, "", *, ?)
func (a *Application) Search(input SearchInput) (*SearchResultData, error) {
	a.logger.Info("Performing search", zap.String("query", input.Query))

	// Validate input
	if input.Query == "" {
		return nil, fmt.Errorf("search query cannot be empty")
	}

	// Set default limit if not specified
	limit := input.Limit
	if limit == 0 {
		limit = 50
	}

	// Perform search
	result, err := a.searchService.Search(input.Query, limit)
	if err != nil {
		// If search index is not initialized, try to initialize it automatically
		if err.Error() == "search index not initialized" {
			a.logger.Warn("Search index not initialized, attempting to initialize now")
			if initErr := a.InitializeSearchIndex(); initErr != nil {
				a.logger.Error("Failed to auto-initialize search index", zap.Error(initErr))
				return nil, fmt.Errorf("search index not initialized. Please log out and log back in, or contact support")
			}
			// Retry search after initialization
			result, err = a.searchService.Search(input.Query, limit)
			if err != nil {
				a.logger.Error("Search failed after auto-initialization", zap.String("query", input.Query), zap.Error(err))
				return nil, fmt.Errorf("search failed: %w", err)
			}
		} else {
			a.logger.Error("Search failed", zap.String("query", input.Query), zap.Error(err))
			return nil, fmt.Errorf("search failed: %w", err)
		}
	}

	// Convert to frontend format with deduplication by filename
	// Only show one file per unique filename (first occurrence wins)
	files := make([]FileSearchResult, 0, len(result.Files))
	seenFilenames := make(map[string]bool)
	for _, f := range result.Files {
		// Skip if we've already seen this filename
		if seenFilenames[f.Filename] {
			continue
		}
		seenFilenames[f.Filename] = true

		files = append(files, FileSearchResult{
			ID:             f.ID,
			Filename:       f.Filename,
			CollectionID:   f.CollectionID,
			CollectionName: f.CollectionName,
			Tags:           f.Tags,
			Size:           f.Size,
			CreatedAt:      f.CreatedAt.Format(time.RFC3339),
		})
	}

	collections := make([]CollectionSearchResult, 0, len(result.Collections))
	for _, c := range result.Collections {
		collections = append(collections, CollectionSearchResult{
			ID:        c.ID,
			Name:      c.Name,
			Tags:      c.Tags,
			FileCount: c.FileCount,
			CreatedAt: c.CreatedAt.Format(time.RFC3339),
		})
	}

	a.logger.Info("Search completed",
		zap.String("query", input.Query),
		zap.Int("files_found", len(files)),
		zap.Int("collections_found", len(collections)))

	return &SearchResultData{
		Files:            files,
		Collections:      collections,
		TotalFiles:       len(files),
		TotalCollections: len(collections),
		TotalHits:        result.TotalHits,
		MaxScore:         result.MaxScore,
		Query:            input.Query,
	}, nil
}
