// Package knowledge provides persistent storage for AI-learned information about guests
package knowledge

import (
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
	"strings"
	"sync"
	"time"

	"github.com/rcourtman/pulse-go-rewrite/internal/crypto"
	"github.com/rs/zerolog/log"
)

// Note represents a single piece of learned information
type Note struct {
	ID        string    `json:"id"`
	Category  string    `json:"category"` // "service", "path", "credential", "config", "learning", "history"
	Title     string    `json:"title"`
	Content   string    `json:"content"`
	CreatedAt time.Time `json:"created_at"`
	UpdatedAt time.Time `json:"updated_at"`
}

// GuestKnowledge represents all knowledge about a specific guest
type GuestKnowledge struct {
	GuestID   string    `json:"guest_id"`
	GuestName string    `json:"guest_name"`
	GuestType string    `json:"guest_type"` // "vm", "container", "node", "host"
	Notes     []Note    `json:"notes"`
	UpdatedAt time.Time `json:"updated_at"`
}

// Store manages persistent knowledge storage with encryption
type Store struct {
	dataDir string
	mu      sync.RWMutex
	cache   map[string]*GuestKnowledge
	crypto  *crypto.CryptoManager
}

// NewStore creates a new knowledge store with encryption
func NewStore(dataDir string) (*Store, error) {
	knowledgeDir := filepath.Join(dataDir, "knowledge")
	if err := os.MkdirAll(knowledgeDir, 0700); err != nil {
		return nil, fmt.Errorf("failed to create knowledge directory: %w", err)
	}

	// Initialize crypto manager for encryption (uses same key as other Pulse secrets)
	cryptoMgr, err := crypto.NewCryptoManagerAt(dataDir)
	if err != nil {
		log.Warn().Err(err).Msg("Failed to initialize crypto for knowledge store, data will be unencrypted")
	}

	return &Store{
		dataDir: knowledgeDir,
		cache:   make(map[string]*GuestKnowledge),
		crypto:  cryptoMgr,
	}, nil
}

// guestFilePath returns the file path for a guest's knowledge
func (s *Store) guestFilePath(guestID string) string {
	// Sanitize guest ID for filesystem
	safeID := filepath.Base(guestID) // Prevent path traversal
	// Use .enc extension for encrypted files
	if s.crypto != nil {
		return filepath.Join(s.dataDir, safeID+".enc")
	}
	return filepath.Join(s.dataDir, safeID+".json")
}

// GetKnowledge retrieves knowledge for a guest
func (s *Store) GetKnowledge(guestID string) (*GuestKnowledge, error) {
	s.mu.RLock()
	if cached, ok := s.cache[guestID]; ok {
		s.mu.RUnlock()
		return cached, nil
	}
	s.mu.RUnlock()

	// Load from disk
	s.mu.Lock()
	defer s.mu.Unlock()

	// Double-check after acquiring write lock
	if cached, ok := s.cache[guestID]; ok {
		return cached, nil
	}

	filePath := s.guestFilePath(guestID)
	data, err := os.ReadFile(filePath)
	if os.IsNotExist(err) {
		// Try legacy unencrypted file
		legacyPath := filepath.Join(s.dataDir, filepath.Base(guestID)+".json")
		data, err = os.ReadFile(legacyPath)
		if os.IsNotExist(err) {
			// No knowledge yet, return empty
			knowledge := &GuestKnowledge{
				GuestID: guestID,
				Notes:   []Note{},
			}
			s.cache[guestID] = knowledge
			return knowledge, nil
		}
		if err != nil {
			return nil, fmt.Errorf("failed to read knowledge file: %w", err)
		}
		// Legacy file found - will be encrypted on next save
		log.Info().Str("guest_id", guestID).Msg("Found unencrypted knowledge file, will encrypt on next save")
	} else if err != nil {
		return nil, fmt.Errorf("failed to read knowledge file: %w", err)
	}

	// Decrypt if crypto is available and file is encrypted
	if s.crypto != nil && filepath.Ext(filePath) == ".enc" {
		decrypted, err := s.crypto.Decrypt(data)
		if err != nil {
			// Try as plain JSON (migration case)
			var knowledge GuestKnowledge
			if jsonErr := json.Unmarshal(data, &knowledge); jsonErr == nil {
				log.Info().Str("guest_id", guestID).Msg("Loaded unencrypted knowledge (will encrypt on next save)")
				s.cache[guestID] = &knowledge
				return &knowledge, nil
			}
			return nil, fmt.Errorf("failed to decrypt knowledge: %w", err)
		}
		data = decrypted
	}

	var knowledge GuestKnowledge
	if err := json.Unmarshal(data, &knowledge); err != nil {
		return nil, fmt.Errorf("failed to parse knowledge file: %w", err)
	}

	s.cache[guestID] = &knowledge
	return &knowledge, nil
}

// SaveNote adds or updates a note for a guest
func (s *Store) SaveNote(guestID, guestName, guestType, category, title, content string) error {
	s.mu.Lock()
	defer s.mu.Unlock()

	// Get or create knowledge
	knowledge, ok := s.cache[guestID]
	if !ok {
		// Try to load from disk first
		knowledge = &GuestKnowledge{
			GuestID:   guestID,
			GuestName: guestName,
			GuestType: guestType,
			Notes:     []Note{},
		}
		
		// Check for existing file
		filePath := s.guestFilePath(guestID)
		if data, err := os.ReadFile(filePath); err == nil {
			// Decrypt if needed
			if s.crypto != nil && filepath.Ext(filePath) == ".enc" {
				if decrypted, err := s.crypto.Decrypt(data); err == nil {
					data = decrypted
				}
			}
			if err := json.Unmarshal(data, &knowledge); err != nil {
				log.Warn().Err(err).Str("guest_id", guestID).Msg("Failed to parse existing knowledge, starting fresh")
			}
		}
		s.cache[guestID] = knowledge
	}

	// Update guest info if provided
	if guestName != "" {
		knowledge.GuestName = guestName
	}
	if guestType != "" {
		knowledge.GuestType = guestType
	}

	now := time.Now()

	// Check if note with same title exists in category
	found := false
	for i, note := range knowledge.Notes {
		if note.Category == category && note.Title == title {
			// Update existing note
			knowledge.Notes[i].Content = content
			knowledge.Notes[i].UpdatedAt = now
			found = true
			break
		}
	}

	if !found {
		// Add new note
		note := Note{
			ID:        fmt.Sprintf("%s-%d", category, len(knowledge.Notes)+1),
			Category:  category,
			Title:     title,
			Content:   content,
			CreatedAt: now,
			UpdatedAt: now,
		}
		knowledge.Notes = append(knowledge.Notes, note)
	}

	knowledge.UpdatedAt = now

	// Save to disk (encrypted)
	return s.saveToFile(guestID, knowledge)
}

// DeleteNote removes a note
func (s *Store) DeleteNote(guestID, noteID string) error {
	s.mu.Lock()
	defer s.mu.Unlock()

	knowledge, ok := s.cache[guestID]
	if !ok {
		return fmt.Errorf("guest not found: %s", guestID)
	}

	// Find and remove note
	for i, note := range knowledge.Notes {
		if note.ID == noteID {
			knowledge.Notes = append(knowledge.Notes[:i], knowledge.Notes[i+1:]...)
			knowledge.UpdatedAt = time.Now()
			return s.saveToFile(guestID, knowledge)
		}
	}

	return fmt.Errorf("note not found: %s", noteID)
}

// GetNotesByCategory returns notes filtered by category
func (s *Store) GetNotesByCategory(guestID, category string) ([]Note, error) {
	knowledge, err := s.GetKnowledge(guestID)
	if err != nil {
		return nil, err
	}

	var notes []Note
	for _, note := range knowledge.Notes {
		if category == "" || note.Category == category {
			notes = append(notes, note)
		}
	}
	return notes, nil
}

// FormatForContext formats knowledge for injection into AI context
func (s *Store) FormatForContext(guestID string) string {
	knowledge, err := s.GetKnowledge(guestID)
	if err != nil {
		log.Warn().Err(err).Str("guest_id", guestID).Msg("Failed to load guest knowledge")
		return ""
	}

	if len(knowledge.Notes) == 0 {
		return ""
	}

	// Group notes by category
	byCategory := make(map[string][]Note)
	for _, note := range knowledge.Notes {
		byCategory[note.Category] = append(byCategory[note.Category], note)
	}

	// Build formatted output with guidance on using this knowledge
	var result string
	result = fmt.Sprintf("\n## Previously Learned Information about %s\n", knowledge.GuestName)
	result += "**If relevant to the current task, use this saved information directly instead of rediscovering it.**\n"

	categoryOrder := []string{"credential", "service", "path", "config", "learning", "history"}
	categoryNames := map[string]string{
		"credential": "Credentials",
		"service":    "Services",
		"path":       "Important Paths",
		"config":     "Configuration",
		"learning":   "Learnings",
		"history":    "Session History",
	}

	for _, cat := range categoryOrder {
		notes, ok := byCategory[cat]
		if !ok || len(notes) == 0 {
			continue
		}

		result += fmt.Sprintf("\n### %s\n", categoryNames[cat])
		for _, note := range notes {
			result += fmt.Sprintf("- **%s**: %s\n", note.Title, note.Content)
		}
	}

	return result
}

// saveToFile persists knowledge to disk with encryption
func (s *Store) saveToFile(guestID string, knowledge *GuestKnowledge) error {
	data, err := json.MarshalIndent(knowledge, "", "  ")
	if err != nil {
		return fmt.Errorf("failed to marshal knowledge: %w", err)
	}

	// Encrypt if crypto manager is available
	if s.crypto != nil {
		encrypted, err := s.crypto.Encrypt(data)
		if err != nil {
			return fmt.Errorf("failed to encrypt knowledge: %w", err)
		}
		data = encrypted
	}

	filePath := s.guestFilePath(guestID)
	if err := os.WriteFile(filePath, data, 0600); err != nil {
		return fmt.Errorf("failed to write knowledge file: %w", err)
	}

	// Remove legacy unencrypted file if it exists
	if s.crypto != nil {
		legacyPath := filepath.Join(s.dataDir, filepath.Base(guestID)+".json")
		if _, err := os.Stat(legacyPath); err == nil {
			os.Remove(legacyPath)
			log.Info().Str("guest_id", guestID).Msg("Removed legacy unencrypted knowledge file")
		}
	}

	log.Debug().
		Str("guest_id", guestID).
		Int("notes", len(knowledge.Notes)).
		Bool("encrypted", s.crypto != nil).
		Msg("Saved guest knowledge")

	return nil
}

// ListGuests returns all guests that have knowledge stored
func (s *Store) ListGuests() ([]string, error) {
	files, err := os.ReadDir(s.dataDir)
	if err != nil {
		return nil, fmt.Errorf("failed to read knowledge directory: %w", err)
	}

	var guests []string
	for _, file := range files {
		ext := filepath.Ext(file.Name())
		if ext == ".json" || ext == ".enc" {
			guestID := file.Name()[:len(file.Name())-len(ext)]
			guests = append(guests, guestID)
		}
	}
	return guests, nil
}

// FormatAllForContext returns a summary of all saved knowledge across all guests
// This is used when no specific target is selected to give the AI full context
// To prevent context bloat, it limits output to maxGuests and maxBytes
func (s *Store) FormatAllForContext() string {
	const maxGuests = 10   // Only include the 10 most recently updated guests
	const maxBytes = 8000  // Cap total output at ~8KB to leave room for other context

	guests, err := s.ListGuests()
	if err != nil || len(guests) == 0 {
		return ""
	}

	// Load all guests with notes and sort by most recently updated
	type guestWithTime struct {
		id        string
		knowledge *GuestKnowledge
	}
	var guestsWithNotes []guestWithTime

	for _, guestID := range guests {
		knowledge, err := s.GetKnowledge(guestID)
		if err != nil || len(knowledge.Notes) == 0 {
			continue
		}
		guestsWithNotes = append(guestsWithNotes, guestWithTime{id: guestID, knowledge: knowledge})
	}

	if len(guestsWithNotes) == 0 {
		return ""
	}

	// Sort by UpdatedAt descending (most recent first)
	for i := 0; i < len(guestsWithNotes)-1; i++ {
		for j := i + 1; j < len(guestsWithNotes); j++ {
			if guestsWithNotes[j].knowledge.UpdatedAt.After(guestsWithNotes[i].knowledge.UpdatedAt) {
				guestsWithNotes[i], guestsWithNotes[j] = guestsWithNotes[j], guestsWithNotes[i]
			}
		}
	}

	// Track how many guests and notes we're including vs total
	totalGuests := len(guestsWithNotes)
	totalNotes := 0
	for _, g := range guestsWithNotes {
		totalNotes += len(g.knowledge.Notes)
	}

	// Limit to maxGuests
	truncatedGuests := false
	if len(guestsWithNotes) > maxGuests {
		guestsWithNotes = guestsWithNotes[:maxGuests]
		truncatedGuests = true
	}

	var sections []string
	includedNotes := 0
	currentBytes := 0

	for _, g := range guestsWithNotes {
		knowledge := g.knowledge

		// Build a summary for this guest
		guestName := knowledge.GuestName
		if guestName == "" {
			guestName = g.id
		}

		// Group notes by category
		byCategory := make(map[string][]Note)
		for _, note := range knowledge.Notes {
			byCategory[note.Category] = append(byCategory[note.Category], note)
		}

		var guestSection string
		guestSection = fmt.Sprintf("\n### %s (%s)", guestName, knowledge.GuestType)

		categoryOrder := []string{"credential", "service", "path", "config", "learning"}
		for _, cat := range categoryOrder {
			notes, ok := byCategory[cat]
			if !ok || len(notes) == 0 {
				continue
			}
			for _, note := range notes {
				// Mask credentials in the summary
				content := note.Content
				if cat == "credential" && len(content) > 6 {
					content = content[:2] + "****" + content[len(content)-2:]
				}
				noteLine := fmt.Sprintf("\n- **%s**: %s", note.Title, content)
				
				// Check if adding this note would exceed our byte limit
				if currentBytes+len(guestSection)+len(noteLine) > maxBytes {
					// Stop adding notes, we've hit the limit
					if includedNotes > 0 {
						log.Warn().
							Int("total_notes", totalNotes).
							Int("included_notes", includedNotes).
							Int("total_guests", totalGuests).
							Int("max_bytes", maxBytes).
							Msg("Knowledge context truncated to prevent bloat - consider cleaning up old notes")
					}
					goto finalize
				}
				guestSection += noteLine
				includedNotes++
			}
		}

		currentBytes += len(guestSection)
		sections = append(sections, guestSection)
	}

finalize:
	if len(sections) == 0 {
		return ""
	}

	// Build result with info about truncation if applicable
	var header string
	if truncatedGuests || includedNotes < totalNotes {
		header = fmt.Sprintf("\n\n## Saved Knowledge (%d/%d notes from %d/%d guests, most recent)\n",
			includedNotes, totalNotes, len(sections), totalGuests)
	} else {
		header = fmt.Sprintf("\n\n## Saved Knowledge (%d notes across %d guests)\n", totalNotes, len(sections))
	}
	result := header
	result += "This is information learned from previous sessions. Use it to avoid rediscovery.\n"
	result += strings.Join(sections, "\n")

	return result
}

