/*
 * ChatCLI - Command Line Interface for LLM interaction
 * Copyright (c) 2024 Edilson Freitas
 * License: Apache-2.0
 */
package ctxmgr

import (
	"context"
	"fmt"
	"regexp"
	"sort"
	"strings"
	"sync"
	"time"

	"github.com/diillson/chatcli/llm/embedding"
	"github.com/diillson/chatcli/models"
	"github.com/diillson/chatcli/utils"
	"github.com/google/uuid"
	"go.uber.org/zap"
)

// Manager gerencia contextos de forma thread-safe
type Manager struct {
	contexts         map[string]*FileContext      // ID -> FileContext
	attachedContexts map[string][]AttachedContext // SessionID -> AttachedContexts
	Storage          *Storage
	validator        *Validator
	processor        *Processor
	logger           *zap.Logger
	mu               sync.RWMutex

	// retrieval is the optional semantic-retrieval engine, wired post-construction
	// via AttachEmbeddingProvider. nil/disabled → --rag attachments degrade to the
	// legacy whole-content path, so retrieval is purely additive.
	retrieval *RetrievalEngine

	// digestMu/digestCache memoize the knowledge index card per context
	// revision: prompt assembly runs every turn and the digest walk is
	// O(corpus), so recomputing it per turn scales with the corpus for free.
	digestMu    sync.Mutex
	digestCache map[string]knowledgeDigestEntry
}

// AttachEmbeddingProvider wires (or rewires) the embedding provider that powers
// semantic /context retrieval. A Null/absent provider still yields a live
// engine: Enabled() stays false (so --rag attachments degrade to whole content
// exactly as before), while knowledge-mode hybrid retrieval keeps its keyless
// BM25 floor. Safe to call once at startup; provider-agnostic across backends.
func (m *Manager) AttachEmbeddingProvider(provider embedding.Provider) {
	m.mu.Lock()
	defer m.mu.Unlock()
	m.retrieval = NewRetrievalEngine(provider, m.Storage.basePath, m.logger)
}

// RetrievalEnabled reports whether a real embedding provider backs retrieval.
func (m *Manager) RetrievalEnabled() bool {
	m.mu.RLock()
	defer m.mu.RUnlock()
	return m.retrieval.Enabled()
}

// NewManager cria uma nova instância do gerenciador de contextos
func NewManager(logger *zap.Logger) (*Manager, error) {
	storage, err := NewStorage(logger)
	if err != nil {
		return nil, fmt.Errorf("erro ao inicializar storage: %w", err)
	}

	manager := &Manager{
		contexts:         make(map[string]*FileContext),
		attachedContexts: make(map[string][]AttachedContext),
		Storage:          storage,
		validator:        NewValidator(logger),
		processor:        NewProcessor(logger),
		logger:           logger,
	}

	// Carregar contextos existentes do disco
	if err := manager.loadContexts(); err != nil {
		logger.Warn("Erro ao carregar contextos do disco", zap.Error(err))
	}

	return manager, nil
}

// CreateContext cria um novo contexto a partir de caminhos de arquivos/diretórios
func (m *Manager) CreateContext(ctx context.Context, name, description string, paths []string, mode ProcessingMode, tags []string, force bool) (*FileContext, error) {
	m.mu.Lock()
	defer m.mu.Unlock()

	// Validar nome
	if err := m.validator.ValidateName(name); err != nil {
		return nil, err
	}

	// Verificar se já existe
	if m.contextExistsByName(name) {
		if !force {
			return nil, fmt.Errorf("já existe um contexto com o nome '%s'. Use --force caso queira sobrescrever", name)
		}

		// Se force=true, deletar o existente primeiro
		for id, ctx := range m.contexts {
			if ctx.Name == name {
				if err := m.Storage.DeleteContext(id); err != nil {
					return nil, fmt.Errorf("erro ao remover contexto existente: %w", err)
				}
				delete(m.contexts, id)
				break
			}
		}
	}

	// Processar arquivos baseado no modo. Em knowledge mode, arquivos .jsonl
	// são corpora docs-flatten e entram pelo parser nativo (um FileInfo por
	// chunk, preservando source/título/proveniência); demais caminhos seguem
	// pelo scanner normal, então diretórios de docs também viram knowledge.
	knowledgeMeta := map[string]string{}
	var files []utils.FileInfo
	var scanPaths []string
	if mode == ModeKnowledge {
		for _, p := range paths {
			if !isJSONLPath(p) {
				scanPaths = append(scanPaths, p)
				continue
			}
			expanded, expandErr := utils.ExpandPath(p)
			if expandErr != nil {
				expanded = p
			}
			kfiles, kmeta, ingestErr := ingestKnowledgeJSONL(expanded, m.logger)
			if ingestErr != nil {
				return nil, ingestErr
			}
			files = append(files, kfiles...)
			for k, v := range kmeta {
				knowledgeMeta[k] = v
			}
		}
	} else {
		scanPaths = paths
	}

	scanOpts := utils.DefaultDirectoryScanOptions(m.logger)
	if len(scanPaths) > 0 {
		scanned, opts, err := m.processor.ProcessPaths(ctx, scanPaths, mode)
		if err != nil {
			return nil, fmt.Errorf("erro ao processar arquivos: %w", err)
		}
		files = append(files, scanned...)
		scanOpts = opts
	}

	// Validar tamanho total
	var totalSize int64
	for _, f := range files {
		totalSize += f.Size
	}

	if err := m.validator.ValidateTotalSize(totalSize); err != nil {
		return nil, err
	}

	// Criar contexto
	fctx := &FileContext{
		ID:          uuid.New().String(),
		Name:        name,
		Description: description,
		Files:       files,
		Mode:        mode,
		TotalSize:   totalSize,
		FileCount:   len(files),
		CreatedAt:   time.Now(),
		UpdatedAt:   time.Now(),
		Tags:        tags,
		Metadata:    knowledgeMeta,
		ScanOptions: scanOpts,
		ScanOptionsMetadata: ScanOptionsMetadata{
			MaxTotalSize:      scanOpts.MaxTotalSize,
			MaxFilesToProcess: scanOpts.MaxFilesToProcess,
			Extensions:        scanOpts.Extensions,
			ExcludeDirs:       scanOpts.ExcludeDirs,
			ExcludePatterns:   scanOpts.ExcludePatterns,
			IncludeHidden:     scanOpts.IncludeHidden,
		},
	}

	// NOVO: Se modo chunked, dividir em chunks
	if mode == ModeChunked {
		m.logger.Info("Dividindo arquivos em chunks",
			zap.String("context_name", name),
			zap.Int("total_files", len(files)))

		chunker := NewChunker(m.logger)
		chunks, err := chunker.DivideIntoChunks(files, ChunkSmart)
		if err != nil {
			return nil, fmt.Errorf("erro ao dividir em chunks: %w", err)
		}

		fctx.Chunks = chunks
		fctx.IsChunked = true
		fctx.ChunkStrategy = string(ChunkSmart)

		m.logger.Info("Contexto dividido em chunks",
			zap.String("context_id", fctx.ID),
			zap.Int("total_chunks", len(chunks)))
	}

	// Armazenar em memória
	m.contexts[fctx.ID] = fctx

	// Persistir no disco
	if err := m.Storage.SaveContext(fctx); err != nil {
		delete(m.contexts, fctx.ID)
		return nil, fmt.Errorf("erro ao salvar contexto: %w", err)
	}

	m.logger.Info("Contexto criado com sucesso",
		zap.String("id", fctx.ID),
		zap.String("name", fctx.Name),
		zap.Int("file_count", fctx.FileCount),
		zap.Int64("total_size", fctx.TotalSize),
		zap.Bool("is_chunked", fctx.IsChunked))

	return fctx, nil
}

// AttachContext anexa um contexto a uma sessão (não envia à LLM ainda)
func (m *Manager) AttachContext(sessionID, contextID string, priority int) error {
	opts := AttachOptions{Priority: priority}
	return m.AttachContextWithOptions(sessionID, contextID, opts)
}

// DetachContext remove um contexto anexado de uma sessão
func (m *Manager) DetachContext(sessionID, contextID string) error {
	m.mu.Lock()
	defer m.mu.Unlock()

	attached := m.attachedContexts[sessionID]
	if len(attached) == 0 {
		return fmt.Errorf("nenhum contexto anexado a esta sessão")
	}

	// Encontrar e remover
	newAttached := make([]AttachedContext, 0, len(attached)-1)
	found := false
	var contextName string

	for _, a := range attached {
		if a.ContextID != contextID {
			newAttached = append(newAttached, a)
		} else {
			found = true
			if ctx, exists := m.contexts[contextID]; exists {
				contextName = ctx.Name
			}
		}
	}

	if !found {
		return fmt.Errorf("contexto não está anexado a esta sessão")
	}

	m.attachedContexts[sessionID] = newAttached

	m.logger.Info("Contexto desanexado da sessão",
		zap.String("session_id", sessionID),
		zap.String("context_id", contextID),
		zap.String("context_name", contextName))

	return nil
}

// DeleteContext remove um contexto permanentemente
func (m *Manager) DeleteContext(contextID string) error {
	m.mu.Lock()
	defer m.mu.Unlock()

	ctx, exists := m.contexts[contextID]
	if !exists {
		return fmt.Errorf("contexto '%s' não encontrado", contextID)
	}

	// Verificar se está anexado a alguma sessão
	for sessionID, attached := range m.attachedContexts {
		for _, a := range attached {
			if a.ContextID == contextID {
				return fmt.Errorf("contexto '%s' está anexado à sessão '%s'. Desanexe antes de deletar", ctx.Name, sessionID)
			}
		}
	}

	// Deletar do disco
	if err := m.Storage.DeleteContext(contextID); err != nil {
		return fmt.Errorf("erro ao deletar contexto do disco: %w", err)
	}

	// Deletar da memória + caches derivados (lexical/vetorial em RAM, vetores
	// persistidos em disco e o digest memoizado) — nada órfão sobrevive.
	delete(m.contexts, contextID)
	m.retrieval.DropCache(contextID)
	m.digestMu.Lock()
	delete(m.digestCache, contextID)
	m.digestMu.Unlock()

	m.logger.Info("Contexto deletado",
		zap.String("id", contextID),
		zap.String("name", ctx.Name))

	return nil
}

// MergeContexts mescla múltiplos contextos em um novo
func (m *Manager) MergeContexts(name, description string, contextIDs []string, opts MergeOptions) (*FileContext, error) {
	m.mu.Lock()
	defer m.mu.Unlock()

	if len(contextIDs) < 2 {
		return nil, fmt.Errorf("é necessário pelo menos 2 contextos para mesclar")
	}

	// Validar nome do novo contexto
	if err := m.validator.ValidateName(name); err != nil {
		return nil, err
	}

	if m.contextExistsByName(name) {
		return nil, fmt.Errorf("já existe um contexto com o nome '%s'", name)
	}

	// Coletar todos os arquivos
	allFiles := make([]utils.FileInfo, 0)
	seenPaths := make(map[string]utils.FileInfo)

	for _, ctxID := range contextIDs {
		ctx, exists := m.contexts[ctxID]
		if !exists {
			return nil, fmt.Errorf("contexto '%s' não encontrado", ctxID)
		}

		for _, file := range ctx.Files {
			if opts.RemoveDuplicates {
				if existing, seen := seenPaths[file.Path]; seen {
					// Preferir versão mais recente
					if opts.PreferNewer {
						// Comparar tamanho como heurística
						if file.Size > existing.Size {
							seenPaths[file.Path] = file
						}
					}
					continue
				}
				seenPaths[file.Path] = file
			} else {
				allFiles = append(allFiles, file)
			}
		}
	}

	// Aplicar opções pós-processamento
	if opts.RemoveDuplicates {
		allFiles = make([]utils.FileInfo, 0, len(seenPaths))
		for _, file := range seenPaths {
			allFiles = append(allFiles, file)
		}
	}

	if opts.SortByPath {
		sort.Slice(allFiles, func(i, j int) bool {
			return allFiles[i].Path < allFiles[j].Path
		})
	}

	// Calcular tamanho total
	var totalSize int64
	for _, f := range allFiles {
		totalSize += f.Size
	}

	// Validar tamanho
	if err := m.validator.ValidateTotalSize(totalSize); err != nil {
		return nil, err
	}

	// Criar novo contexto mesclado
	mergedCtx := &FileContext{
		ID:          uuid.New().String(),
		Name:        name,
		Description: description,
		Files:       allFiles,
		Mode:        ModeFull, // Contextos mesclados sempre em modo full
		TotalSize:   totalSize,
		FileCount:   len(allFiles),
		CreatedAt:   time.Now(),
		UpdatedAt:   time.Now(),
		Tags:        opts.Tags,
		Metadata: map[string]string{
			"merged_from": fmt.Sprintf("%d contexts", len(contextIDs)),
		},
		ScanOptions: utils.DirectoryScanOptions{}, // Vazio para contextos mesclados
	}

	// Armazenar
	m.contexts[mergedCtx.ID] = mergedCtx
	if err := m.Storage.SaveContext(mergedCtx); err != nil {
		delete(m.contexts, mergedCtx.ID)
		return nil, fmt.Errorf("erro ao salvar contexto mesclado: %w", err)
	}

	m.logger.Info("Contextos mesclados com sucesso",
		zap.String("new_context_id", mergedCtx.ID),
		zap.String("new_context_name", mergedCtx.Name),
		zap.Int("source_contexts", len(contextIDs)),
		zap.Int("total_files", mergedCtx.FileCount))

	return mergedCtx, nil
}

// GetContext retorna um contexto pelo ID
func (m *Manager) GetContext(contextID string) (*FileContext, error) {
	m.mu.RLock()
	defer m.mu.RUnlock()

	ctx, exists := m.contexts[contextID]
	if !exists {
		return nil, fmt.Errorf("contexto '%s' não encontrado", contextID)
	}

	return ctx, nil
}

// GetContextByName retorna um contexto pelo nome
func (m *Manager) GetContextByName(name string) (*FileContext, error) {
	m.mu.RLock()
	defer m.mu.RUnlock()

	for _, ctx := range m.contexts {
		if ctx.Name == name {
			return ctx, nil
		}
	}

	return nil, fmt.Errorf("contexto com nome '%s' não encontrado", name)
}

// ListContexts lista todos os contextos com filtro opcional
func (m *Manager) ListContexts(filter *ContextFilter) ([]*FileContext, error) {
	m.mu.RLock()
	defer m.mu.RUnlock()

	result := make([]*FileContext, 0)

	for _, ctx := range m.contexts {
		if filter != nil {
			if !m.matchesFilter(ctx, filter) {
				continue
			}
		}
		result = append(result, ctx)
	}

	// Ordenar por data de criação (mais recente primeiro)
	sort.Slice(result, func(i, j int) bool {
		return result[i].CreatedAt.After(result[j].CreatedAt)
	})

	return result, nil
}

// GetAttachedContexts retorna os contextos anexados a uma sessão
func (m *Manager) GetAttachedContexts(sessionID string) ([]*FileContext, error) {
	m.mu.RLock()
	defer m.mu.RUnlock()

	attached := m.attachedContexts[sessionID]
	result := make([]*FileContext, 0, len(attached))

	for _, a := range attached {
		if ctx, exists := m.contexts[a.ContextID]; exists {
			result = append(result, ctx)
		}
	}

	return result, nil
}

// UpdateContext atualiza um contexto existente
func (m *Manager) UpdateContext(ctx context.Context, name string, newPaths []string, newMode ProcessingMode, newTags []string, newDescription string) (*FileContext, error) {
	m.mu.Lock()
	defer m.mu.Unlock()

	// Buscar contexto existente
	var existingCtx *FileContext
	for _, fc := range m.contexts {
		if fc.Name == name {
			existingCtx = fc
			break
		}
	}

	if existingCtx == nil {
		return nil, fmt.Errorf("contexto '%s' não encontrado", name)
	}

	// Processar novos arquivos se paths foram fornecidos
	var files []utils.FileInfo
	var scanOpts utils.DirectoryScanOptions
	var totalSize int64

	if len(newPaths) > 0 {
		mode := newMode
		if mode == "" {
			mode = existingCtx.Mode // Manter modo anterior se não especificado
		}

		// Mesmo gate de knowledge mode do CreateContext: corpora .jsonl do
		// docs-flatten entram pelo parser nativo (um FileInfo por chunk,
		// preservando source/título/proveniência), enquanto os demais caminhos
		// seguem pelo scanner normal. Sem isto, um .jsonl seria ingerido como um
		// único arquivo monolítico (units: 1) e a proveniência kb.* ficaria stale.
		knowledgeMeta := map[string]string{}
		var scanPaths []string
		if mode == ModeKnowledge {
			for _, p := range newPaths {
				if !isJSONLPath(p) {
					scanPaths = append(scanPaths, p)
					continue
				}
				expanded, expandErr := utils.ExpandPath(p)
				if expandErr != nil {
					expanded = p
				}
				kfiles, kmeta, ingestErr := ingestKnowledgeJSONL(expanded, m.logger)
				if ingestErr != nil {
					return nil, ingestErr
				}
				files = append(files, kfiles...)
				for k, v := range kmeta {
					knowledgeMeta[k] = v
				}
			}
		} else {
			scanPaths = newPaths
		}

		scanOpts = utils.DefaultDirectoryScanOptions(m.logger)
		if len(scanPaths) > 0 {
			scanned, opts, err := m.processor.ProcessPaths(ctx, scanPaths, mode)
			if err != nil {
				return nil, fmt.Errorf("erro ao processar arquivos: %w", err)
			}
			files = append(files, scanned...)
			scanOpts = opts
		}

		for _, f := range files {
			totalSize += f.Size
		}

		if err := m.validator.ValidateTotalSize(totalSize); err != nil {
			return nil, err
		}

		existingCtx.Files = files
		existingCtx.Mode = mode
		existingCtx.TotalSize = totalSize
		existingCtx.FileCount = len(files)
		existingCtx.ScanOptions = scanOpts
		existingCtx.ScanOptionsMetadata = ScanOptionsMetadata{
			MaxTotalSize:      scanOpts.MaxTotalSize,
			MaxFilesToProcess: scanOpts.MaxFilesToProcess,
			Extensions:        scanOpts.Extensions,
			ExcludeDirs:       scanOpts.ExcludeDirs,
			ExcludePatterns:   scanOpts.ExcludePatterns,
			IncludeHidden:     scanOpts.IncludeHidden,
		}

		// Refrescar a proveniência kb.* a partir do novo corpus — commit e
		// contagem de sources mudam a cada re-ingestão. Merge (não overwrite)
		// preserva quaisquer outras chaves de metadata já gravadas no contexto.
		if len(knowledgeMeta) > 0 {
			if existingCtx.Metadata == nil {
				existingCtx.Metadata = map[string]string{}
			}
			for k, v := range knowledgeMeta {
				existingCtx.Metadata[k] = v
			}
		}
	}

	// Atualizar descrição se fornecida
	if newDescription != "" {
		existingCtx.Description = newDescription
	}

	// Atualizar tags se fornecidas
	if len(newTags) > 0 {
		existingCtx.Tags = newTags
	}

	// IMPORTANTE: Atualizar timestamp
	existingCtx.UpdatedAt = time.Now()

	// Re-dividir em chunks se necessário
	if existingCtx.Mode == ModeChunked {
		// Só re-chunka quando há arquivos novos; um update apenas de descrição/
		// tags preserva os chunks existentes (conteúdo inalterado).
		if len(files) > 0 {
			m.logger.Info("Re-dividindo arquivos em chunks após atualização",
				zap.String("context_name", name),
				zap.Int("total_files", len(files)))

			chunker := NewChunker(m.logger)
			chunks, err := chunker.DivideIntoChunks(files, ChunkSmart)
			if err != nil {
				return nil, fmt.Errorf("erro ao dividir em chunks: %w", err)
			}

			existingCtx.Chunks = chunks
			existingCtx.IsChunked = true
			existingCtx.ChunkStrategy = string(ChunkSmart)
		}
	} else if len(files) > 0 {
		// O modo deixou de ser chunked nesta atualização (ex.: chunked→knowledge):
		// descartar o estado de chunk órfão para que um contexto knowledge/full
		// nunca carregue um índice de chunks fantasma (IsChunked stale é landmine
		// para qualquer caminho que ramifique em fc.IsChunked).
		existingCtx.Chunks = nil
		existingCtx.IsChunked = false
		existingCtx.ChunkStrategy = ""
	}

	// Salvar no disco
	if err := m.Storage.SaveContext(existingCtx); err != nil {
		return nil, fmt.Errorf("erro ao salvar contexto atualizado: %w", err)
	}

	m.logger.Info("Contexto atualizado com sucesso",
		zap.String("id", existingCtx.ID),
		zap.String("name", existingCtx.Name),
		zap.Int("file_count", existingCtx.FileCount),
		zap.Int64("total_size", existingCtx.TotalSize))

	return existingCtx, nil
}

// CORREÇÃO 2: Refatorada para usar a estrutura de dados correta e lidar com chunks selecionados.
// BuildPromptMessages agora considera chunks selecionados
func (m *Manager) BuildPromptMessages(sessionID string, opts FormatOptions) ([]models.Message, error) {
	m.mu.RLock()
	defer m.mu.RUnlock()

	attachments := m.attachedContexts[sessionID]
	if len(attachments) == 0 {
		return nil, nil
	}

	// Ordenar por prioridade (menor primeiro)
	sort.Slice(attachments, func(i, j int) bool {
		return attachments[i].Priority < attachments[j].Priority
	})

	messages := make([]models.Message, 0, len(attachments))

	for _, attachment := range attachments {
		ctx, exists := m.contexts[attachment.ContextID]
		if !exists {
			m.logger.Warn("Contexto anexado não encontrado durante a construção do prompt",
				zap.String("contextID", attachment.ContextID))
			continue
		}

		// Knowledge contexts inject only their index card here (stable, cheap,
		// cacheable); the corpus itself is reached per turn via the volatile
		// hybrid-retrieval block. This is what keeps a multi-MB corpus at a
		// fixed few-hundred-token cost.
		if ctx.Mode == ModeKnowledge {
			messages = append(messages, models.Message{
				Role:    promptRole(opts.Role),
				Content: m.KnowledgeDigest(ctx),
			})
			continue
		}

		// Retrieval-mode attachments are query-driven and therefore volatile;
		// they are injected separately via BuildRetrievedContextMessages so they
		// never poison the cached prefix this method feeds. Only skip them when
		// retrieval is actually enabled — otherwise a --rag attachment with no
		// embedding provider must still appear here as whole content.
		if attachment.RetrievalTopK > 0 && m.retrieval.Enabled() {
			continue
		}

		var content string
		// Se tem chunks selecionados, usar apenas eles
		if len(attachment.SelectedChunks) > 0 {
			content = fmt.Sprintf("📦 CONTEXTO: %s (Chunks: %v)\n", ctx.Name, attachment.SelectedChunks)
			if opts.IncludeMetadata {
				content += fmt.Sprintf("Modo: %s | Chunks Selecionados: %d de %d\n\n",
					ctx.Mode, len(attachment.SelectedChunks), len(ctx.Chunks))
			}

			// Incluir apenas chunks selecionados
			for _, chunkNum := range attachment.SelectedChunks {
				if chunkNum < 1 || chunkNum > len(ctx.Chunks) {
					continue // Ignora chunks inválidos
				}
				chunk := ctx.Chunks[chunkNum-1] // Índice é 0-based
				content += m.formatChunk(chunk, opts)
			}
		} else {
			// Usar formatação normal (todos os arquivos ou todos os chunks)
			content = m.formatContextContent(ctx, opts)
		}

		messages = append(messages, models.Message{
			Role:    promptRole(opts.Role),
			Content: content,
		})
	}

	return messages, nil
}

// promptRole normaliza o role das mensagens de contexto: default seguro "user"
// (contexto não deve competir com system prompt) e somente user|system passam.
func promptRole(role string) string {
	role = strings.TrimSpace(strings.ToLower(role))
	if role != "user" && role != "system" {
		return "user"
	}
	return role
}

// BuildRetrievedContextMessages runs per-turn retrieval for every attachment
// that is query-driven — knowledge contexts (always; hybrid BM25+vectors, no
// API key required) and --rag attachments (vector-only, needs a provider) —
// and returns one message per context holding only the passages relevant to
// query. Returns nil when nothing opted in or the query is empty, so the
// caller can skip the volatile block entirely.
//
// A failure on a single context is logged and skipped, never fatal: a flaky
// embedding call must not break the turn. The query embedding happens outside
// the manager lock because it does network I/O.
func (m *Manager) BuildRetrievedContextMessages(ctx context.Context, sessionID, query string) ([]models.Message, error) {
	m.mu.RLock()
	engine := m.retrieval
	attachments := append([]AttachedContext(nil), m.attachedContexts[sessionID]...)
	wanted := make(map[string]*FileContext)
	for _, a := range attachments {
		c, ok := m.contexts[a.ContextID]
		if !ok {
			continue
		}
		if c.Mode == ModeKnowledge || a.RetrievalTopK > 0 {
			wanted[a.ContextID] = c
		}
	}
	m.mu.RUnlock()

	if engine == nil || len(wanted) == 0 || strings.TrimSpace(query) == "" {
		return nil, nil
	}

	sort.Slice(attachments, func(i, j int) bool {
		return attachments[i].Priority < attachments[j].Priority
	})

	messages := make([]models.Message, 0, len(wanted))
	for _, a := range attachments {
		fc, ok := wanted[a.ContextID]
		if !ok {
			continue
		}
		var block string
		if fc.Mode == ModeKnowledge {
			segs, err := engine.RetrieveHybrid(ctx, fc, query, a.RetrievalTopK)
			if err != nil {
				m.logger.Warn("knowledge retrieval failed; skipping",
					zap.String("context", fc.Name), zap.Error(err))
				continue
			}
			block = FormatKnowledgeSegmentsBlock(fc.Name, segs)
		} else {
			if !engine.Enabled() {
				continue // --rag sem provider: degradou para conteúdo inteiro no prefixo
			}
			segs, err := engine.Retrieve(ctx, fc, query, a.RetrievalTopK)
			if err != nil {
				m.logger.Warn("context semantic retrieval failed; skipping",
					zap.String("context", fc.Name), zap.Error(err))
				continue
			}
			block = FormatSegmentsBlock(fc.Name, query, segs)
		}
		if block == "" {
			continue
		}
		messages = append(messages, models.Message{Role: "system", Content: block})
	}
	return messages, nil
}

// GetMetrics retorna métricas sobre os contextos
func (m *Manager) GetMetrics() *ContextMetrics {
	m.mu.RLock()
	defer m.mu.RUnlock()

	metrics := &ContextMetrics{
		TotalContexts:    len(m.contexts),
		AttachedContexts: 0,
		TotalFiles:       0,
		TotalSizeBytes:   0,
		ContextsByMode:   make(map[string]int),
		LastUpdated:      time.Now(),
		StoragePath:      m.Storage.basePath,
	}

	for _, ctx := range m.contexts {
		metrics.TotalFiles += ctx.FileCount
		metrics.TotalSizeBytes += ctx.TotalSize
		metrics.ContextsByMode[string(ctx.Mode)]++
	}

	// Contar contextos anexados (unique)
	uniqueAttached := make(map[string]bool)
	for _, attached := range m.attachedContexts {
		for _, a := range attached {
			uniqueAttached[a.ContextID] = true
		}
	}
	metrics.AttachedContexts = len(uniqueAttached)

	return metrics
}

// GetSessionsForContext returns all session IDs that have the given context attached.
func (m *Manager) GetSessionsForContext(contextID string) []string {
	m.mu.RLock()
	defer m.mu.RUnlock()

	var sessions []string
	for sessionID, attached := range m.attachedContexts {
		for _, a := range attached {
			if a.ContextID == contextID {
				sessions = append(sessions, sessionID)
				break
			}
		}
	}

	sort.Strings(sessions)
	return sessions
}

// Helper methods

func (m *Manager) contextExistsByName(name string) bool {
	for _, ctx := range m.contexts {
		if ctx.Name == name {
			return true
		}
	}
	return false
}

func (m *Manager) matchesFilter(ctx *FileContext, filter *ContextFilter) bool {
	// Filtrar por tags
	if len(filter.Tags) > 0 {
		hasTag := false
		for _, filterTag := range filter.Tags {
			for _, ctxTag := range ctx.Tags {
				if ctxTag == filterTag {
					hasTag = true
					break
				}
			}
			if hasTag {
				break
			}
		}
		if !hasTag {
			return false
		}
	}

	// Filtrar por modo
	if filter.Mode != "" && ctx.Mode != filter.Mode {
		return false
	}

	// Filtrar por tamanho
	if filter.MinSize > 0 && ctx.TotalSize < filter.MinSize {
		return false
	}
	if filter.MaxSize > 0 && ctx.TotalSize > filter.MaxSize {
		return false
	}

	// Filtrar por data
	if filter.CreatedAfter != nil && ctx.CreatedAt.Before(*filter.CreatedAfter) {
		return false
	}
	if filter.CreatedBefore != nil && ctx.CreatedAt.After(*filter.CreatedBefore) {
		return false
	}

	// Filtrar por padrão de nome
	if filter.NamePattern != "" {
		matched, err := regexp.MatchString(filter.NamePattern, ctx.Name)
		if err != nil || !matched {
			return false
		}
	}

	return true
}

func (m *Manager) formatContextContent(ctx *FileContext, opts FormatOptions) string {
	var builder strings.Builder

	// Cabeçalho do contexto
	if opts.IncludeMetadata {
		builder.WriteString(fmt.Sprintf("📦 CONTEXT: %s\n", ctx.Name))
		if ctx.Description != "" {
			builder.WriteString(fmt.Sprintf("Description: %s\n", ctx.Description))
		}
		if opts.IncludeTimestamp {
			builder.WriteString(fmt.Sprintf("Created: %s\n", ctx.CreatedAt.Format(time.RFC3339)))
		}
		builder.WriteString(fmt.Sprintf("Mode: %s | Files: %d | Size: %.2f MB\n",
			ctx.Mode, ctx.FileCount, float64(ctx.TotalSize)/1024/1024))
		if len(ctx.Tags) > 0 {
			builder.WriteString(fmt.Sprintf("Tags: %s\n", strings.Join(ctx.Tags, ", ")))
		}
		builder.WriteString("\n")
	}

	// Formatar arquivos usando a função já existente do utils
	formattedContent := utils.FormatDirectoryContent(ctx.Files, ctx.TotalSize)
	builder.WriteString(formattedContent)

	return builder.String()
}

func (m *Manager) loadContexts() error {
	contexts, err := m.Storage.LoadAllContexts()
	if err != nil {
		return err
	}

	m.mu.Lock()
	defer m.mu.Unlock()

	for _, ctx := range contexts {
		m.contexts[ctx.ID] = ctx
	}

	m.logger.Info("Contextos carregados do disco",
		zap.Int("count", len(contexts)))

	return nil
}

// CORREÇÃO 1: Função refatorada para usar a estrutura de dados correta do Manager.
// AttachContextWithOptions anexa contexto com opções avançadas
func (m *Manager) AttachContextWithOptions(sessionID, contextID string, opts AttachOptions) error {
	m.mu.Lock()
	defer m.mu.Unlock()

	ctx, exists := m.contexts[contextID]
	if !exists {
		return fmt.Errorf("contexto '%s' não encontrado", contextID)
	}

	// Verificar se já está anexado
	for _, a := range m.attachedContexts[sessionID] {
		if a.ContextID == contextID {
			return fmt.Errorf("contexto '%s' já está anexado a esta sessão", ctx.Name)
		}
	}

	// Criar o anexo com todas as opções
	attachment := AttachedContext{
		ContextID:      contextID,
		AttachedAt:     time.Now(),
		Priority:       opts.Priority,
		SelectedChunks: opts.SelectedChunks,
		RetrievalTopK:  opts.RetrievalTopK,
	}

	// Adicionar à lista de anexos da sessão
	m.attachedContexts[sessionID] = append(m.attachedContexts[sessionID], attachment)

	// Ordenar por prioridade
	sort.Slice(m.attachedContexts[sessionID], func(i, j int) bool {
		return m.attachedContexts[sessionID][i].Priority < m.attachedContexts[sessionID][j].Priority
	})

	m.logger.Info("Contexto anexado à sessão com opções",
		zap.String("session_id", sessionID),
		zap.String("context_id", contextID),
		zap.String("context_name", ctx.Name),
		zap.Int("priority", opts.Priority),
		zap.Ints("selected_chunks", opts.SelectedChunks))

	return nil
}

// CORREÇÃO 3: Corrigido o tipo do parâmetro 'chunk' de 'Chunk' para 'FileChunk'.
// formatChunk formata um chunk individual
func (m *Manager) formatChunk(chunk FileChunk, opts FormatOptions) string {
	var b strings.Builder

	b.WriteString(fmt.Sprintf("\n📦 CHUNK %d/%d: %s\n",
		chunk.Index, chunk.TotalChunks, chunk.Description))
	b.WriteString(strings.Repeat("=", 80) + "\n\n")

	for _, file := range chunk.Files {
		b.WriteString(fmt.Sprintf("📄 ARQUIVO: %s\n", file.Path))
		if opts.IncludeMetadata {
			b.WriteString(fmt.Sprintf("Tipo: %s | Tamanho: %.2f KB\n",
				file.Type, float64(file.Size)/1024))
		}
		b.WriteString("```\n")
		b.WriteString(file.Content)
		b.WriteString("\n```\n\n")
	}

	return b.String()
}
