package cli

import (
	"context"
	"fmt"
	"os"
	"path/filepath"
	"regexp"
	"sort"
	"strconv"
	"strings"
	"sync/atomic"
	"time"

	"github.com/diillson/chatcli/cli/imgcompress"
	"github.com/diillson/chatcli/config"
	"github.com/diillson/chatcli/i18n"
	"github.com/diillson/chatcli/models"
	"github.com/diillson/chatcli/utils"
	"go.uber.org/zap"
)

// maxImageAttachmentBytes caps a single attached image. Vision providers
// reject very large images; this also guards memory on accidental @file of
// a huge binary.
const maxImageAttachmentBytes = 20 * 1024 * 1024 // 20 MB

func (cli *ChatCLI) processFileCommand(ctx context.Context, userInput string) (string, string, []models.ImageContent) {
	var additionalContext string
	var images []models.ImageContent

	if strings.Contains(strings.ToLower(userInput), "@file") {
		// Removida a verificação especial para o OpenAI Assistant
		// Agora, sempre usamos o mesmo processamento de arquivos, independentemente do modelo

		paths, options, err := extractFileCommandOptions(userInput)
		if err != nil {
			cli.logger.Error("Erro ao processar os comandos @file", zap.Error(err))
			return userInput, fmt.Sprintf("\nErro ao processar @file: %s\n", err.Error()), nil
		}

		// Usar o modo a partir das opções já extraídas
		mode := config.ModeFull // Modo padrão
		if modeVal, ok := options["mode"]; ok {
			mode = modeVal
		}

		// Configurar estimador de tokens e obter limite máximo de tokens do LLM atual
		tokenEstimator := cli.getTokenEstimatorForCurrentLLM()
		maxTokens := cli.getMaxTokensForCurrentLLM()

		// Processar cada caminho encontrado após @file
		for _, path := range paths {
			// Vision: se o caminho aponta para um único arquivo de imagem,
			// anexa os bytes como ImageContent (entrada multimodal) em vez de
			// inliná-lo como texto ilegível. Diretórios/globs seguem o fluxo
			// de texto normal.
			if img, ok := cli.loadImageAttachment(path); ok {
				images = append(images, img)
				continue
			}

			// Configurações de escaneamento
			scanOptions := utils.DefaultDirectoryScanOptions(cli.logger)

			// 1. Pré-escanear para obter a contagem total de arquivos
			totalFiles, err := utils.CountMatchingFiles(path, scanOptions)
			if err != nil {
				cli.logger.Error("Erro ao contar arquivos", zap.String("path", path), zap.Error(err))
				additionalContext += fmt.Sprintf("\nErro ao analisar o diretório '%s': %s\n", path, err.Error())
				continue
			}

			// 2. Inicializar contador para os arquivos processados
			var processedFiles int32 = 0

			// 3. Definir o callback para atualizar a animação com progresso rico
			scanOptions.OnFileProcessed = func(info utils.FileInfo) {
				// Usar atomic para segurança em concorrência, embora o callback seja chamado em série aqui
				atomic.AddInt32(&processedFiles, 1)
				// Atualiza a mensagem da animação com o progresso
				cli.animation.UpdateMessage(
					fmt.Sprintf("Analisando... [%d/%d] %s", atomic.LoadInt32(&processedFiles), totalFiles, info.Path),
				)
			}

			// Escolher a forma de processar (summary, chunked, smartChunk ou full)
			switch mode {
			case config.ModeSummary:
				// Atualizar a mensagem da animação para o modo summary
				cli.animation.UpdateMessage(fmt.Sprintf("Gerando resumo para %s...", path))
				summary, err := cli.processDirectorySummary(ctx, path, tokenEstimator, maxTokens)
				if err != nil {
					additionalContext += fmt.Sprintf("\nErro ao processar '%s': %s\n", path, err.Error())
				} else {
					additionalContext += summary
				}

			case config.ModeChunked:
				// Atualizar a mensagem da animação para o modo chunked
				cli.animation.UpdateMessage(fmt.Sprintf("Dividindo %s em chunks...", path))
				chunks, err := cli.processDirectoryChunked(ctx, path, tokenEstimator, maxTokens)
				if err != nil {
					additionalContext += fmt.Sprintf("\nErro ao processar '%s': %s\n", path, err.Error())
				} else {
					// Apenas o primeiro chunk é adicionado diretamente ao contexto.
					if len(chunks) > 0 {
						totalChunks := len(chunks)
						var totalFiles int
						var totalSize int64

						// Contar estimativa de arquivos e tamanho total
						for _, chunk := range chunks {
							fileCount := strings.Count(chunk.Content, "📄 ARQUIVO")
							totalFiles += fileCount
							totalSize += int64(len(chunk.Content))
						}

						chunkSummary := fmt.Sprintf(
							"📊 PROJETO DIVIDIDO EM CHUNKS\n"+
								"=============================\n"+
								"▶️ Total de chunks: %d\n"+
								"▶️ Arquivos estimados: ~%d\n"+
								"▶️ Tamanho total: %.2f MB\n"+
								"▶️ Você está no chunk 1/%d\n"+
								"▶️ Use '/nextchunk' para avançar para o próximo chunk\n\n"+
								"=============================\n\n",
							totalChunks, totalFiles, float64(totalSize)/1024/1024, totalChunks,
						)

						// Exibir o resumo no console e aguardar 5 segundos para o usuário ler
						fmt.Println()
						fmt.Println(chunkSummary)
						fmt.Println(i18n.T("file.chunk.wait_before_send"))

						// Usar timer em vez de aguardar input do usuário
						time.Sleep(5 * time.Second)

						fmt.Println(i18n.T("file.chunk.sending_first"))

						// Agora concatenamos ao contexto o resumo + o primeiro chunk
						additionalContext += chunkSummary + chunks[0].Content

						// Guardar os próximos chunks para o /nextchunk
						cli.fileChunks = chunks[1:]

						// Avisar o usuário sobre chunks pendentes
						if len(cli.fileChunks) > 0 {
							additionalContext += fmt.Sprintf(
								"\n\n⚠️ ATENÇÃO: Ainda existem %d chunks adicionais. Use /nextchunk quando terminar de analisar este chunk.\n",
								len(cli.fileChunks),
							)
						}
					}
				}

			case config.ModeSmartChunk:
				// Atualizar a mensagem da animação para o modo smart
				cli.animation.UpdateMessage(fmt.Sprintf("Analisando relevância dos arquivos em %s...", path))

				// Extrair a consulta do usuário (tudo o que vier após o @file + opções)
				query := extractUserQuery(userInput)
				relevantContent, err := cli.processDirectorySmart(ctx, path, query, tokenEstimator, maxTokens)
				if err != nil {
					additionalContext += fmt.Sprintf("\nErro ao processar '%s': %s\n", path, err.Error())
				} else {
					additionalContext += relevantContent
				}

			default: // ModeFull - comportamento atual (inclui todo o conteúdo relevante dentro de um limite)
				// Ajustar limite de tamanho com base em tokens disponíveis
				scanOptions.MaxTotalSize = estimateBytesFromTokens(maxTokens*3/4, tokenEstimator)

				files, err := utils.ProcessDirectory(ctx, path, scanOptions)
				if err != nil {
					cli.logger.Error(fmt.Sprintf("Erro ao processar '%s'", path), zap.Error(err))
					additionalContext += fmt.Sprintf("\nErro ao processar '%s': %s\n", path, err.Error())
					continue
				}

				if len(files) == 0 {
					additionalContext += fmt.Sprintf("\nNenhum arquivo relevante encontrado em '%s'\n", path)
					continue
				}

				formattedContent := utils.FormatDirectoryContent(files, scanOptions.MaxTotalSize)
				additionalContext += fmt.Sprintf("\n%s\n", formattedContent)
			}
		}

		// Remover o comando @file do input original para não poluir o prompt final
		userInput = removeAllFileCommands(userInput)
	}

	return userInput, additionalContext, images
}

// gateImagesForModel decides what to do with attached images given the active
// model (hybrid B+A strategy). It resolves a vision mode (see resolveVisionMode)
// and then:
//   - native   → pass the images through to the provider (path B);
//   - describe → fold a textual description from a vision model into the
//     prompt (path A), returning no images;
//   - off      → ignore the images.
//
// When describe is selected but no vision model is reachable, the images are
// dropped with a user-facing warning, so a non-vision provider never receives
// an image it cannot decode.
func (cli *ChatCLI) gateImagesForModel(ctx context.Context, images []models.ImageContent) ([]models.ImageContent, string) {
	if len(images) == 0 {
		return nil, ""
	}

	switch cli.resolveVisionMode() {
	case visionNative:
		return cli.compressImagesForVision(images), ""
	case visionOff:
		cli.logger.Info("vision: input disabled by CHATCLI_VISION_INPUT=off",
			zap.String("provider", cli.Provider), zap.String("model", cli.Model))
		return nil, ""
	default: // visionDescribe
		if desc, ok := cli.describeImagesFallback(ctx, images); ok {
			cli.logger.Info("vision: native vision unavailable, using describe-fallback",
				zap.String("provider", cli.Provider), zap.String("model", cli.Model),
				zap.Int("images", len(images)))
			return nil, desc
		}
		fmt.Println(i18n.T("vision.warn.model_no_vision", cli.Model))
		cli.logger.Warn("vision: image attached but model has no vision and no fallback is available",
			zap.String("provider", cli.Provider), zap.String("model", cli.Model))
		return nil, ""
	}
}

// compressImagesForVision downscales/re-encodes inline image bytes before they
// reach a vision model — cutting upload bytes, latency and (above the edge cap)
// the billed vision tokens. Keyless and pure-Go. Controlled by:
//
//	CHATCLI_VISION_COMPRESS       on|off (default on)
//	CHATCLI_VISION_MAX_EDGE       longest-edge cap in px (default 1568)
//	CHATCLI_VISION_JPEG_QUALITY   1..100 (default 82)
//
// URL-only images (no inline bytes) are passed through untouched. The function
// is conservative: imgcompress never inflates a payload and never drops alpha.
func (cli *ChatCLI) compressImagesForVision(images []models.ImageContent) []models.ImageContent {
	if strings.EqualFold(strings.TrimSpace(utils.GetEnvOrDefault("CHATCLI_VISION_COMPRESS", "on")), "off") {
		return images
	}
	opts := imgcompress.DefaultOptions()
	if v := strings.TrimSpace(os.Getenv("CHATCLI_VISION_MAX_EDGE")); v != "" {
		if n, err := strconv.Atoi(v); err == nil && n > 0 {
			opts.MaxEdge = n
		}
	}
	if v := strings.TrimSpace(os.Getenv("CHATCLI_VISION_JPEG_QUALITY")); v != "" {
		if n, err := strconv.Atoi(v); err == nil && n >= 1 && n <= 100 {
			opts.JPEGQuality = n
		}
	}

	totalSaved := 0
	out := make([]models.ImageContent, len(images))
	copy(out, images)
	for i := range out {
		if len(out[i].Data) == 0 {
			continue // URL-only; nothing to recompress
		}
		data, res := imgcompress.Compress(out[i].Data, out[i].MediaType, opts)
		if !res.Changed {
			continue
		}
		out[i].Data = data
		out[i].MediaType = res.OutMediaType
		totalSaved += res.SavedBytes()
		cli.logger.Debug("vision: image recompressed",
			zap.String("file", out[i].FileName),
			zap.Int("orig_bytes", res.OrigBytes), zap.Int("new_bytes", res.NewBytes),
			zap.String("orig_dim", fmt.Sprintf("%dx%d", res.OrigW, res.OrigH)),
			zap.String("new_dim", fmt.Sprintf("%dx%d", res.NewW, res.NewH)),
			zap.String("out_type", res.OutMediaType))
	}
	if totalSaved > 0 {
		cli.logger.Info("vision: image compression saved bytes",
			zap.Int("images", len(out)), zap.Int("bytes_saved", totalSaved))
	}
	return out
}

// imageFileExtensions are the extensions we treat as vision attachments.
var imageFileExtensions = map[string]bool{
	".png": true, ".jpg": true, ".jpeg": true, ".gif": true, ".webp": true,
}

// loadImageAttachment reports whether path is a single image file and, if so,
// reads it into an ImageContent. Directories, oversized files, unreadable
// paths, and non-image content all return ok=false so the caller falls back
// to the normal text-scanning flow.
func (cli *ChatCLI) loadImageAttachment(path string) (models.ImageContent, bool) {
	expanded, err := utils.ExpandPath(path)
	if err != nil {
		expanded = path
	}

	info, err := os.Stat(expanded)
	if err != nil || info.IsDir() {
		return models.ImageContent{}, false
	}

	// Fast reject by extension only when it is clearly non-image AND large;
	// otherwise we still sniff, so an extensionless image is detected.
	ext := strings.ToLower(filepath.Ext(expanded))
	if info.Size() > maxImageAttachmentBytes {
		if imageFileExtensions[ext] {
			cli.logger.Warn("imagem excede o limite de anexo, ignorando",
				zap.String("path", expanded), zap.Int64("size", info.Size()))
		}
		return models.ImageContent{}, false
	}

	data, err := os.ReadFile(expanded) //#nosec G304 -- user-specified @file path to attach as a vision image
	if err != nil {
		return models.ImageContent{}, false
	}

	mime, ok := models.DetectImageMediaType(data)
	if !ok {
		// Not a recognized image; let the text path handle it.
		return models.ImageContent{}, false
	}

	cli.logger.Debug("imagem anexada via @file",
		zap.String("path", expanded), zap.String("mime", mime), zap.Int("bytes", len(data)))

	return models.ImageContent{
		MediaType: mime,
		Data:      data,
		FileName:  filepath.Base(expanded),
	}, true
}

// extractFileCommandOptions extrai caminhos e opções do comando @file
func extractFileCommandOptions(input string) ([]string, map[string]string, error) {
	var paths []string
	options := make(map[string]string)

	// Regex atualizada para encontrar blocos @file com opções e caminho
	// Aceita tanto "--key=value" quanto "--key value"
	re := regexp.MustCompile(`@file((?:\s+--\w+(?:(?:=|\s+)\S+)?)*\s+[\w~/.-]+/?[\w.-]*)`)
	matches := re.FindAllStringSubmatch(input, -1)

	for _, match := range matches {
		if len(match) < 2 {
			continue
		}

		// Divide o bloco de comando em tokens
		tokens := strings.Fields(match[1])
		var currentPath string

		// Itera sobre os tokens para separar opções do caminho
		i := 0
		for i < len(tokens) {
			token := tokens[i]
			if strings.HasPrefix(token, "--") {
				key := strings.TrimPrefix(token, "--")
				// Formato --key=value
				if parts := strings.SplitN(key, "=", 2); len(parts) == 2 {
					options[parts[0]] = parts[1]
					i++
					// Formato --key value
				} else if i+1 < len(tokens) && !strings.HasPrefix(tokens[i+1], "--") {
					options[key] = tokens[i+1]
					i += 2 // Pula a chave e o valor
				} else {
					// Opção sem valor (flag booleana)
					options[key] = "true"
					i++
				}
			} else {
				// O primeiro token que não é opção é o caminho do arquivo
				currentPath = token
				break // Para a análise de opções para este comando @file
			}
		}
		if currentPath != "" {
			paths = append(paths, currentPath)
		}
	}

	if len(paths) == 0 && len(matches) > 0 {
		return nil, nil, fmt.Errorf("comando @file encontrado, mas nenhum caminho válido foi especificado")
	}

	return paths, options, nil
}

func (cli *ChatCLI) processDirectorySummary(ctx context.Context, path string, tokenEstimator func(string) int, maxTokens int) (string, error) {
	path, err := utils.ExpandPath(path)
	if err != nil {
		return "", fmt.Errorf("erro ao expandir o caminho: %w", err)
	}

	fileInfo, err := os.Stat(path)
	if err != nil {
		return "", fmt.Errorf("erro ao acessar o caminho: %w", err)
	}

	// Se for um arquivo único
	if !fileInfo.IsDir() {
		extension := filepath.Ext(path)
		fileType := utils.DetectFileType(path)
		size := fileInfo.Size()

		return fmt.Sprintf("📄 %s (%s, %.2f KB)\nTipo: %s\nTamanho: %d bytes\n",
			path, extension, float64(size)/1024, fileType, size), nil
	}

	// Se for um diretório, escanear a estrutura
	var builder strings.Builder
	builder.WriteString(fmt.Sprintf("📁 ESTRUTURA DO DIRETÓRIO: %s\n\n", path))

	// Mapeamentos para estatísticas
	fileTypes := make(map[string]int)
	var totalSize int64
	var totalFiles int
	var totalDirs int

	// Função recursiva para construir árvore de diretórios
	var buildTree func(dir string, prefix string, depth int) error
	buildTree = func(dir string, prefix string, depth int) error {
		if depth > 3 { // Limitar profundidade para evitar estruturas gigantes
			builder.WriteString(prefix + "...\n")
			return nil
		}

		entries, err := os.ReadDir(dir)
		if err != nil {
			return err
		}

		for i, entry := range entries {
			// Verificar se estamos dentro do limite de tokens
			if tokenEstimator(builder.String()) > maxTokens/2 {
				builder.WriteString(prefix + "... (truncado por limite de tokens)\n")
				return nil
			}

			isLast := i == len(entries)-1

			var newPrefix string
			if isLast {
				builder.WriteString(prefix + "└── ")
				newPrefix = prefix + "    "
			} else {
				builder.WriteString(prefix + "├── ")
				newPrefix = prefix + "│   "
			}

			entryPath := filepath.Join(dir, entry.Name())

			if entry.IsDir() {
				// Verificar se é um diretório que normalmente seria ignorado
				if utils.ShouldSkipDir(entry.Name()) {
					builder.WriteString(entry.Name() + "/ (ignorado)\n")
					continue
				}

				totalDirs++
				builder.WriteString(entry.Name() + "/\n")

				// Recursivamente processar subdiretórios
				err := buildTree(entryPath, newPrefix, depth+1)
				if err != nil {
					return err
				}
			} else {
				totalFiles++
				fInfo, err := entry.Info()
				if err == nil {
					totalSize += fInfo.Size()
					fileExt := filepath.Ext(entry.Name())
					fileType := utils.DetectFileType(entry.Name())
					fileTypes[fileType]++

					// Adicionar informações do arquivo, incluindo a extensão
					builder.WriteString(fmt.Sprintf("%s (%s, %.1f KB, %s)\n",
						entry.Name(), fileExt, float64(fInfo.Size())/1024, fileType))
				} else {
					builder.WriteString(entry.Name() + "\n")
				}
			}
		}
		return nil
	}

	err = buildTree(path, "", 0)
	if err != nil {
		return "", fmt.Errorf("erro ao construir árvore de diretórios: %w", err)
	}

	// Adicionar estatísticas
	builder.WriteString("\n📊 ESTATÍSTICAS:\n")
	builder.WriteString(fmt.Sprintf("Total de Diretórios: %d\n", totalDirs))
	builder.WriteString(fmt.Sprintf("Total de Arquivos: %d\n", totalFiles))
	builder.WriteString(fmt.Sprintf("Tamanho Total: %.2f MB\n", float64(totalSize)/1024/1024))

	builder.WriteString("\n🔍 TIPOS DE ARQUIVO:\n")
	for fileType, count := range fileTypes {
		builder.WriteString(fmt.Sprintf("%s: %d arquivos\n", fileType, count))
	}

	return builder.String(), nil
}

// processDirectoryChunked processa um diretório e divide o conteúdo em chunks
func (cli *ChatCLI) processDirectoryChunked(ctx context.Context, path string, tokenEstimator func(string) int, maxTokens int) ([]FileChunk, error) {
	path, err := utils.ExpandPath(path)
	if err != nil {
		return nil, fmt.Errorf("erro ao expandir o caminho: %w", err)
	}

	// Configurar opções de processamento de diretório
	scanOptions := utils.DefaultDirectoryScanOptions(cli.logger)
	scanOptions.OnFileProcessed = func(info utils.FileInfo) {
		cli.animation.UpdateMessage(fmt.Sprintf("Processando %s", info.Path))
	}

	// Sem limite de tamanho, vamos coletar tudo e depois dividir
	files, err := utils.ProcessDirectory(ctx, path, scanOptions)
	if err != nil {
		return nil, err
	}

	if len(files) == 0 {
		return nil, fmt.Errorf("nenhum arquivo relevante encontrado em '%s'", path)
	}

	cli.animation.UpdateMessage(fmt.Sprintf("Analisando e dividindo projeto em chunks (%d arquivos encontrados)", len(files)))

	// Dividir os arquivos em chunks
	var chunks []FileChunk
	var currentChunk strings.Builder
	filesInCurrentChunk := []utils.FileInfo{}

	// Função para finalizar o chunk atual
	finishCurrentChunk := func() {
		if currentChunk.Len() > 0 {
			formattedContent := utils.FormatDirectoryContent(filesInCurrentChunk, int64(currentChunk.Len()))
			chunks = append(chunks, FileChunk{
				Index:   len(chunks) + 1,
				Content: formattedContent,
			})

			// Resetar para o próximo chunk
			currentChunk.Reset()
			filesInCurrentChunk = []utils.FileInfo{}
		}
	}

	// Processar cada arquivo
	for _, file := range files {
		// Estimar tokens do conteúdo do arquivo
		fileTokens := tokenEstimator(file.Content)

		// Se o arquivo for maior que metade do limite, criar um chunk só para ele
		if fileTokens > maxTokens/2 {
			// Finalizar chunk anterior se existir
			finishCurrentChunk()

			// Criar um chunk separado só para este arquivo grande
			chunks = append(chunks, FileChunk{
				Index:   len(chunks) + 1,
				Content: utils.FormatDirectoryContent([]utils.FileInfo{file}, file.Size),
			})
			continue
		}

		// Verificar se adicionar este arquivo excederia o limite de tokens
		currentTokens := tokenEstimator(currentChunk.String())
		if currentTokens+fileTokens > maxTokens*3/4 {
			finishCurrentChunk()
		}

		// Adicionar arquivo ao chunk atual
		currentChunk.WriteString(file.Content)
		filesInCurrentChunk = append(filesInCurrentChunk, file)
	}

	// Finalizar o último chunk se necessário
	finishCurrentChunk()

	// Atualizar total em cada chunk
	for i := range chunks {
		chunks[i].Total = len(chunks)
	}

	return chunks, nil
}

// handleNextChunk processa o próximo chunk de arquivos com tratamento de falhas
func (cli *ChatCLI) handleNextChunk(ctx context.Context) bool {
	if len(cli.fileChunks) == 0 {
		if len(cli.failedChunks) > 0 {
			fmt.Println(i18n.T("chunk.no_more_pending_but_failed", len(cli.failedChunks)))
		} else {
			fmt.Println(i18n.T("chunk.no_more_chunks"))
		}
		return false
	}

	nextChunk := cli.fileChunks[0]
	totalChunks := nextChunk.Total
	currentChunk := nextChunk.Index
	remainingChunks := len(cli.fileChunks) - 1

	fmt.Println(i18n.T("chunk.sending", currentChunk, totalChunks, remainingChunks))

	progressInfo := fmt.Sprintf(i18n.T("chunk.progress_header", currentChunk, totalChunks)+"\n"+
		"=============================\n"+
		i18n.T("chunk.progress_processed", currentChunk-1)+"\n"+
		i18n.T("chunk.progress_pending", remainingChunks)+"\n"+
		i18n.T("chunk.progress_failed", len(cli.failedChunks))+"\n"+
		i18n.T("chunk.progress_usage")+"\n\n"+
		"=============================\n\n",
		currentChunk, totalChunks, currentChunk-1, remainingChunks, len(cli.failedChunks))

	prompt := i18n.T("chunk.prompt", currentChunk, totalChunks)

	cli.history = append(cli.history, models.Message{
		Role:    "user",
		Content: prompt + "\n\n" + progressInfo + nextChunk.Content,
	})

	cli.animation.ShowThinkingAnimation(cli.Client.GetModelName())
	ctx, cancel := context.WithTimeout(ctx, 5*time.Minute)
	defer cancel()
	aiResponse, err := cli.Client.SendPrompt(ctx, prompt+"\n\n"+nextChunk.Content, cli.history, 0)
	cli.animation.StopThinkingAnimation()

	if err != nil {
		cli.logger.Error("Erro ao processar chunk com a LLM", zap.Int("chunkIndex", nextChunk.Index), zap.Int("totalChunks", nextChunk.Total), zap.Error(err))
		cli.lastFailedChunk = &cli.fileChunks[0]
		cli.failedChunks = append(cli.failedChunks, cli.fileChunks[0])
		cli.fileChunks = cli.fileChunks[1:]

		fmt.Println(i18n.T("chunk.error_processing", currentChunk, totalChunks, err.Error()))
		fmt.Println(i18n.T("chunk.moved_to_failed_queue"))
		fmt.Println(i18n.T("chunk.retry_or_next"))
		return false
	}

	cli.history = append(cli.history, models.Message{Role: "assistant", Content: aiResponse})
	renderedResponse := cli.renderMarkdown(aiResponse)
	cli.typewriterEffect(fmt.Sprintf("\n%s:\n%s\n", cli.Client.GetModelName(), renderedResponse), 2*time.Millisecond)
	cli.fileChunks = cli.fileChunks[1:]

	if len(cli.fileChunks) > 0 || len(cli.failedChunks) > 0 {
		cli.printChunkStatus()
	} else {
		fmt.Println(i18n.T("chunk.all_processed_success"))
	}
	return false
}

// Novo método para reprocessar o último chunk que falhou
func (cli *ChatCLI) handleRetryLastChunk(ctx context.Context) bool {
	if cli.lastFailedChunk == nil || len(cli.failedChunks) == 0 {
		fmt.Println(i18n.T("chunk.no_failed_to_retry"))
		return false
	}

	lastFailedIndex := len(cli.failedChunks) - 1
	chunk := cli.failedChunks[lastFailedIndex]
	cli.failedChunks = cli.failedChunks[:lastFailedIndex]

	fmt.Println(i18n.T("chunk.retrying_last_failed", chunk.Index, chunk.Total))

	progressInfo := fmt.Sprintf(i18n.T("chunk.retry_header", chunk.Index, chunk.Total)+"\n"+
		"=============================\n"+
		i18n.T("chunk.retry_status_retrying")+"\n"+
		i18n.T("chunk.progress_pending", len(cli.fileChunks))+"\n"+
		i18n.T("chunk.retry_status_other_failed", len(cli.failedChunks))+"\n"+
		"=============================\n\n",
		chunk.Index, chunk.Total, len(cli.fileChunks), len(cli.failedChunks))

	prompt := i18n.T("chunk.retry_prompt", chunk.Index, chunk.Total)

	cli.history = append(cli.history, models.Message{Role: "user", Content: prompt + "\n\n" + progressInfo + chunk.Content})
	cli.animation.ShowThinkingAnimation(cli.Client.GetModelName())
	ctx, cancel := context.WithTimeout(ctx, 5*time.Minute)
	defer cancel()
	aiResponse, err := cli.Client.SendPrompt(ctx, prompt+"\n\n"+chunk.Content, cli.history, 0)
	cli.animation.StopThinkingAnimation()

	if err != nil {
		cli.logger.Error("Erro ao reprocessar chunk com a LLM", zap.Int("chunkIndex", chunk.Index), zap.Error(err))
		cli.failedChunks = append(cli.failedChunks, chunk)
		fmt.Println(i18n.T("chunk.retry_error", chunk.Index, chunk.Total, err.Error()))
		fmt.Println(i18n.T("chunk.retry_remains_failed"))
		return false
	}

	cli.history = append(cli.history, models.Message{Role: "assistant", Content: aiResponse})
	renderedResponse := cli.renderMarkdown(aiResponse)
	cli.typewriterEffect(fmt.Sprintf("\n%s:\n%s\n", cli.Client.GetModelName(), renderedResponse), 2*time.Millisecond)

	if len(cli.failedChunks) > 0 {
		cli.lastFailedChunk = &cli.failedChunks[len(cli.failedChunks)-1]
	} else {
		cli.lastFailedChunk = nil
	}
	fmt.Println(i18n.T("chunk.retry_success"))
	cli.printChunkStatus()
	return false
}

// Método para reprocessar todos os chunks com falha
func (cli *ChatCLI) handleRetryAllChunks() bool {
	if len(cli.failedChunks) == 0 {
		fmt.Println(i18n.T("chunk.no_failed_to_retry"))
		return false
	}
	fmt.Println(i18n.T("chunk.retrying_all_failed", len(cli.failedChunks)))
	cli.fileChunks = append(cli.failedChunks, cli.fileChunks...)
	cli.failedChunks = []FileChunk{}
	cli.lastFailedChunk = nil
	return cli.handleNextChunk(cli.sessionCtx)
}

// Método para pular explicitamente um chunk
func (cli *ChatCLI) handleSkipChunk() bool {
	if len(cli.fileChunks) == 0 {
		fmt.Println(i18n.T("chunk.no_pending_to_skip"))
		return false
	}
	skippedChunk := cli.fileChunks[0]
	cli.fileChunks = cli.fileChunks[1:]
	fmt.Println(i18n.T("chunk.skipping", skippedChunk.Index, skippedChunk.Total))
	cli.printChunkStatus()
	return false
}

// Método auxiliar para imprimir o status dos chunks
func (cli *ChatCLI) printChunkStatus() {
	fmt.Println(i18n.T("chunk.status_header"))
	if len(cli.fileChunks) > 0 {
		fmt.Println(i18n.T("chunk.status_pending", len(cli.fileChunks)))
	} else {
		fmt.Println(i18n.T("chunk.status_no_pending"))
	}
	if len(cli.failedChunks) > 0 {
		fmt.Println(i18n.T("chunk.status_failed", len(cli.failedChunks)))
	} else {
		fmt.Println(i18n.T("chunk.status_no_failed"))
	}
}

// extractUserQuery extrai a consulta do usuário do input
func extractUserQuery(input string) string {
	// Remover o comando @file e qualquer opção
	cleaned := removeAllFileCommands(input)
	return strings.TrimSpace(cleaned)
}

// processDirectorySmart processa um diretório e seleciona partes relevantes para a consulta
func (cli *ChatCLI) processDirectorySmart(ctx context.Context, path string, query string, tokenEstimator func(string) int, maxTokens int) (string, error) {
	path, err := utils.ExpandPath(path)
	if err != nil {
		return "", fmt.Errorf("erro ao expandir o caminho: %w", err)
	}

	// Se a consulta estiver vazia, usar o modo de resumo
	if query == "" {
		return cli.processDirectorySummary(ctx, path, tokenEstimator, maxTokens)
	}

	// Configurar opções de processamento de diretório
	scanOptions := utils.DefaultDirectoryScanOptions(cli.logger)
	scanOptions.OnFileProcessed = func(info utils.FileInfo) {
		cli.animation.UpdateMessage(fmt.Sprintf("Analisando %s", info.Path))
	}

	files, err := utils.ProcessDirectory(ctx, path, scanOptions)
	if err != nil {
		return "", err
	}

	if len(files) == 0 {
		return "", fmt.Errorf("nenhum arquivo relevante encontrado em '%s'", path)
	}

	// Avaliar relevância de cada arquivo para a consulta
	type ScoredFile struct {
		File  utils.FileInfo
		Score float64
	}

	scoredFiles := make([]ScoredFile, 0, len(files))

	// Termos importantes da consulta
	queryTerms := strings.Fields(strings.ToLower(query))

	for _, file := range files {
		// Cálculo simples de relevância baseado em correspondência de palavras-chave
		fileContent := strings.ToLower(file.Content)
		fileName := strings.ToLower(filepath.Base(file.Path))

		var score float64

		// Pontuação por nome de arquivo
		for _, term := range queryTerms {
			if strings.Contains(fileName, term) {
				score += 5.0 // Maior peso para correspondência no nome
			}
		}

		// Pontuação por conteúdo
		for _, term := range queryTerms {
			count := strings.Count(fileContent, term)
			score += float64(count) * 0.5
		}

		// Normalizar pela extensão do arquivo (favorecendo arquivos de código)
		ext := filepath.Ext(file.Path)
		if ext == ".go" || ext == ".java" || ext == ".py" || ext == ".js" || ext == ".ts" {
			score *= 1.2 // Bonus para arquivos de código
		}

		// Penalizar arquivos muito grandes
		if file.Size > 1024*50 { // Maior que 50KB
			score *= 0.9
		}

		scoredFiles = append(scoredFiles, ScoredFile{File: file, Score: score})
	}

	// Ordenar arquivos por relevância
	sort.Slice(scoredFiles, func(i, j int) bool {
		return scoredFiles[i].Score > scoredFiles[j].Score
	})

	// Selecionar os arquivos mais relevantes até atingir o limite de tokens
	var selectedFiles []utils.FileInfo
	var currentTokens int
	var builder strings.Builder

	builder.WriteString(fmt.Sprintf("📁 ARQUIVOS MAIS RELEVANTES PARA: \"%s\"\n\n", query))

	for _, scored := range scoredFiles {
		fileTokens := tokenEstimator(scored.File.Content)

		if currentTokens+fileTokens > maxTokens*3/4 {
			// Verificar se podemos incluir pelo menos um arquivo
			if len(selectedFiles) == 0 && fileTokens < maxTokens*3/4 {
				selectedFiles = append(selectedFiles, scored.File)
				currentTokens += fileTokens
				builder.WriteString(fmt.Sprintf("📄 %s (Pontuação de relevância: %.2f)\n",
					scored.File.Path, scored.Score))
			} else {
				break
			}
		} else {
			selectedFiles = append(selectedFiles, scored.File)
			currentTokens += fileTokens
			builder.WriteString(fmt.Sprintf("📄 %s (Pontuação de relevância: %.2f)\n",
				scored.File.Path, scored.Score))
		}
	}

	builder.WriteString(fmt.Sprintf("\n🔍 Foram selecionados %d/%d arquivos mais relevantes para sua consulta.\n\n",
		len(selectedFiles), len(files)))

	// Se não houver arquivos relevantes, retornar resumo
	if len(selectedFiles) == 0 {
		builder.WriteString("Nenhum arquivo teve pontuação suficiente. Aqui está um resumo estrutural:\n\n")
		summary, err := cli.processDirectorySummary(ctx, path, tokenEstimator, maxTokens)
		if err != nil {
			return builder.String(), nil
		}
		builder.WriteString(summary)
		return builder.String(), nil
	}

	// Formatar o conteúdo selecionado
	formattedContent := utils.FormatDirectoryContent(selectedFiles, int64(currentTokens))
	builder.WriteString(formattedContent)

	return builder.String(), nil
}

// removeAllFileCommands remove todos os comandos @file da entrada do usuário
func removeAllFileCommands(input string) string {
	// Usa uma regex similar à de extração para encontrar e remover todos os blocos @file
	re := regexp.MustCompile(`@file((?:\s+--\w+(?:(?:=|\s+)\S+)?)*\s+[\w~/.-]+/?[\w.-]*)`)
	cleaned := re.ReplaceAllString(input, "")

	// Limpa espaços em branco extras que podem ter sido deixados para trás
	return strings.TrimSpace(regexp.MustCompile(`\s+`).ReplaceAllString(cleaned, " "))
}

// filterEmptyLines remove linhas vazias
func filterEmptyLines(lines []string) []string {
	var filtered []string
	for _, line := range lines {
		if strings.TrimSpace(line) != "" {
			filtered = append(filtered, line)
		}
	}
	return filtered
}
