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

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

	"github.com/diillson/chatcli/auth"
	"github.com/diillson/chatcli/i18n"
	"github.com/diillson/chatcli/llm/client"
	"github.com/diillson/chatcli/models"
	"github.com/diillson/chatcli/utils"
	"go.uber.org/zap"
)

const (
	MaxAssistantFiles       = 20
	MaxFileSizeBytes        = 512 * 1024 * 1024 // 512MB
	AssistantAPIBaseURL     = "https://api.openai.com/v1"
	DefaultPollingInterval  = 1 * time.Second
	MaxPollingInterval      = 5 * time.Second
	DefaultPollingTimeout   = 5 * time.Minute
	DefaultAssistantModel   = "gpt-4o"
	DefaultAssistantName    = "ChatCLI Assistant"
	DefaultAssistantTimeout = 10 * time.Minute
)

// OpenAIAssistantClient implementa a interface LLMClient usando a API de Assistentes da OpenAI
type OpenAIAssistantClient struct {
	provider        auth.TokenProvider
	model           string
	assistantID     string
	currentThreadID string
	logger          *zap.Logger
	client          *utils.APIClient
	fileRegistry    *FileRegistry
	assistantName   string
	pollingInterval time.Duration
	pollingTimeout  time.Duration
	activeThreads   map[string]time.Time
	mu              sync.RWMutex
	fileUploadSem   chan struct{} // Semáforo para limitar uploads paralelos
	usageState      client.UsageState
}

// LastUsage returns the token usage from the most recent API call.
func (c *OpenAIAssistantClient) LastUsage() *models.UsageInfo { return c.usageState.LastUsage() }

// LastStopReason returns the stop reason from the most recent API call.
func (c *OpenAIAssistantClient) LastStopReason() string { return c.usageState.LastStopReason() }

// FileRegistry gerencia o cache de arquivos já enviados para a OpenAI
type FileRegistry struct {
	Files       map[string]string // Mapeia caminhos locais para IDs de arquivo na OpenAI
	TotalSize   int64             // Tamanho total dos arquivos carregados
	mu          sync.RWMutex
	logger      *zap.Logger
	cachePath   string
	assistantID string
}

// NewOpenAIAssistantClient cria uma nova instância de OpenAIAssistantClient.
// The Assistants API only speaks the API-key dialect; the provider must be in
// AuthModeAPIKey. The token is snapshotted at construction since OpenAI
// platform API keys do not expire.
func NewOpenAIAssistantClient(ctx context.Context, provider auth.TokenProvider, model string, logger *zap.Logger) (*OpenAIAssistantClient, error) {
	if model == "" {
		model = DefaultAssistantModel
	}

	token, err := provider.Token(ctx)
	if err != nil {
		return nil, fmt.Errorf("%s: %w", i18n.T("llm.assistant.create_cache_dir_error"), err)
	}

	client := utils.NewAPIClient(
		logger,
		AssistantAPIBaseURL,
		map[string]string{
			"Authorization": "Bearer " + token,
			"Content-Type":  "application/json",
			"OpenAI-Beta":   "assistants=v2",
		},
	)

	// Criar o diretório de cache se não existir
	cacheDir := filepath.Join(os.TempDir(), "chatcli-openai-cache")
	if err := os.MkdirAll(cacheDir, 0o700); err != nil {
		return nil, fmt.Errorf("%s: %w", i18n.T("llm.assistant.create_cache_dir_error"), err)
	}

	assistantClient := &OpenAIAssistantClient{
		provider:        provider,
		model:           model,
		assistantID:     "",
		currentThreadID: "",
		logger:          logger,
		client:          client,
		fileRegistry:    newFileRegistry(logger, cacheDir),
		assistantName:   DefaultAssistantName,
		pollingInterval: DefaultPollingInterval,
		pollingTimeout:  DefaultPollingTimeout,
		activeThreads:   make(map[string]time.Time),
		fileUploadSem:   make(chan struct{}, 3), // Limitar a 3 uploads paralelos
	}

	// Inicializar o assistente
	if err := assistantClient.initializeAssistant(ctx); err != nil {
		return nil, err
	}

	return assistantClient, nil
}

// GetModelName retorna o nome do modelo utilizado
func (c *OpenAIAssistantClient) GetModelName() string {
	return fmt.Sprintf("%s (Assistant)", c.model)
}

// SendPrompt envia uma mensagem para o thread atual e retorna a resposta
func (c *OpenAIAssistantClient) SendPrompt(ctx context.Context, prompt string, history []models.Message, maxTokens int) (string, error) {
	start := time.Now()
	client.LogRequestStart(c.logger, "OPENAI_ASSISTANT", c.model,
		zap.Int("payload_bytes", len(prompt)),
		zap.Int("history_len", len(history)),
		zap.Int("max_tokens", maxTokens),
	)

	c.mu.Lock()

	// Verificar se já temos um thread ativo, se não, criar um novo
	if c.currentThreadID == "" {
		threadID, err := c.createThread(ctx)
		if err != nil {
			c.mu.Unlock()
			client.LogRequestFinish(c.logger, "OPENAI_ASSISTANT", c.model, "error", time.Since(start))
			return "", fmt.Errorf("%s: %w", i18n.T("llm.assistant.create_thread_error"), err)
		}
		c.currentThreadID = threadID
		c.activeThreads[threadID] = time.Now()
		c.logger.Info(i18n.T("llm.assistant.thread_created"), zap.String("threadID", threadID))
	}

	threadID := c.currentThreadID
	c.mu.Unlock()

	// Adicionar a mensagem ao thread
	if err := c.addMessageToThread(ctx, threadID, prompt); err != nil {
		c.logger.Error(i18n.T("llm.assistant.add_message_error"),
			zap.String("threadID", threadID),
			zap.Error(err))
		client.LogRequestFinish(c.logger, "OPENAI_ASSISTANT", c.model, "error", time.Since(start))
		return "", fmt.Errorf("%s: %w", i18n.T("llm.assistant.add_message_error"), err)
	}

	// Executar o assistente no thread
	runID, err := c.runAssistant(ctx, threadID)
	if err != nil {
		c.logger.Error(i18n.T("llm.assistant.run_error"),
			zap.String("threadID", threadID),
			zap.Error(err))
		client.LogRequestFinish(c.logger, "OPENAI_ASSISTANT", c.model, "error", time.Since(start))
		return "", fmt.Errorf("%s: %w", i18n.T("llm.assistant.run_error"), err)
	}

	c.logger.Debug("Assistente executando",
		zap.String("threadID", threadID),
		zap.String("runID", runID))

	// Aguardar a conclusão da execução
	runStatus, err := c.waitForRunCompletion(ctx, threadID, runID)
	if err != nil {
		c.logger.Error(i18n.T("llm.assistant.wait_response_error"),
			zap.String("threadID", threadID),
			zap.String("runID", runID),
			zap.Error(err))

		// Se for um erro de timeout, podemos tentar recuperar parcialmente
		if strings.Contains(err.Error(), "timeout") {
			c.logger.Warn(i18n.T("llm.assistant.timeout_recovery"),
				zap.String("threadID", threadID))

			// Tentar obter a última resposta mesmo com timeout
			partialResponse, getErr := c.getLatestResponse(ctx, threadID)
			if getErr == nil && partialResponse != "" {
				c.logger.Info(i18n.T("llm.assistant.partial_response"))
				client.LogRequestFinish(c.logger, "OPENAI_ASSISTANT", c.model, "success", time.Since(start),
					zap.Int("response_chars", len(partialResponse)),
					zap.String("kind", "partial"),
				)
				return i18n.T("llm.assistant.partial_response_prefix", partialResponse), nil
			}
		}

		client.LogRequestFinish(c.logger, "OPENAI_ASSISTANT", c.model, "error", time.Since(start))
		return "", fmt.Errorf("%s: %w", i18n.T("llm.assistant.wait_response_error"), err)
	}

	if runStatus != "completed" {
		c.logger.Error(i18n.T("llm.assistant.run_failed", runStatus),
			zap.String("threadID", threadID),
			zap.String("runID", runID),
			zap.String("status", runStatus))
		client.LogRequestFinish(c.logger, "OPENAI_ASSISTANT", c.model, "error", time.Since(start))
		return "", fmt.Errorf("%s", i18n.T("llm.assistant.run_failed", runStatus))
	}

	// Obter a resposta
	response, err := c.getLatestResponse(ctx, threadID)
	if err != nil {
		c.logger.Error(i18n.T("llm.assistant.get_response_error"),
			zap.String("threadID", threadID),
			zap.Error(err))
		client.LogRequestFinish(c.logger, "OPENAI_ASSISTANT", c.model, "error", time.Since(start))
		return "", fmt.Errorf("%s: %w", i18n.T("llm.assistant.get_response_error"), err)
	}

	client.LogRequestFinish(c.logger, "OPENAI_ASSISTANT", c.model, "success", time.Since(start),
		zap.Int("response_chars", len(response)),
	)
	return response, nil
}

// Método para limpar threads ao fim da app
func (c *OpenAIAssistantClient) Cleanup(ctx context.Context) error {
	c.mu.RLock()
	threadID := c.currentThreadID
	c.mu.RUnlock()

	if threadID == "" {
		return nil // Nada para limpar
	}

	// Criar contexto com timeout para a limpeza
	ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
	defer cancel()

	// Tenta finalizar qualquer run ativo na thread
	c.finishActiveRuns(ctx, threadID)

	c.logger.Info(i18n.T("llm.assistant.cleanup_done"))
	return nil
}

// finishActiveRuns tenta encontrar e finalizar runs ativos em uma thread
func (c *OpenAIAssistantClient) finishActiveRuns(ctx context.Context, threadID string) {
	// Listar runs ativos
	endpoint := fmt.Sprintf("/threads/%s/runs?limit=10", threadID)
	resp, err := c.client.Get(ctx, endpoint)
	if err != nil {
		c.logger.Warn(i18n.T("llm.assistant.list_runs_error"), zap.Error(err))
		return
	}

	var runsResponse struct {
		Data []struct {
			ID     string `json:"id"`
			Status string `json:"status"`
		} `json:"data"`
	}

	if err := json.Unmarshal(resp, &runsResponse); err != nil {
		c.logger.Warn(i18n.T("llm.assistant.decode_runs_error"), zap.Error(err))
		return
	}

	// Cancelar runs ativos
	for _, run := range runsResponse.Data {
		if run.Status == "in_progress" || run.Status == "queued" {
			cancelEndpoint := fmt.Sprintf("/threads/%s/runs/%s/cancel", threadID, run.ID)
			_, err := c.client.Post(ctx, cancelEndpoint, nil)
			if err != nil {
				c.logger.Warn(i18n.T("llm.assistant.cancel_run_error"),
					zap.String("runID", run.ID),
					zap.Error(err))
			} else {
				c.logger.Info(i18n.T("llm.assistant.run_canceled"), zap.String("runID", run.ID))
			}
		}
	}
}

// initializeAssistant cria ou recupera um assistente OpenAI
func (c *OpenAIAssistantClient) initializeAssistant(ctx context.Context) error {
	// Tentar carregar assistantID do cache
	assistantID, err := c.loadAssistantIDFromCache()
	if err == nil && assistantID != "" {
		// Verificar se o assistente ainda existe
		if c.verifyAssistantExists(ctx, assistantID) {
			c.assistantID = assistantID
			c.logger.Info(i18n.T("llm.assistant.recovered_from_cache"), zap.String("assistantID", assistantID))
			return nil
		}
	}

	// Criar um novo assistente
	ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
	defer cancel()

	payload := map[string]interface{}{
		"model":       c.model,
		"name":        c.assistantName,
		"description": "ChatCLI Assistant para análise de código e assistência em desenvolvimento",
		"instructions": "Você é um assistente especializado em desenvolvimento de software. " +
			"Analise arquivos de código com atenção aos detalhes, fornecendo explicações " +
			"claras e sugestões de melhorias. Considere boas práticas de programação, " +
			"padrões de projeto e possíveis problemas de segurança.",
		"tools": []map[string]string{
			{"type": "code_interpreter"},
			{"type": "file_search"},
		},
	}

	resp, err := c.client.Post(ctx, "/assistants", payload)
	if err != nil {
		return fmt.Errorf("%s: %w", i18n.T("llm.assistant.create_error"), err)
	}

	var assistant struct {
		ID string `json:"id"`
	}

	if err := json.Unmarshal(resp, &assistant); err != nil {
		return fmt.Errorf("%s: %w", i18n.T("llm.assistant.decode_response_error"), err)
	}

	c.assistantID = assistant.ID

	// Salvar no cache
	if err := c.saveAssistantIDToCache(assistant.ID); err != nil {
		c.logger.Warn(i18n.T("llm.assistant.cache_save_failed"), zap.Error(err))
	}

	c.logger.Info(i18n.T("llm.assistant.new_created"), zap.String("assistantID", assistant.ID))
	return nil
}

// verifyAssistantExists verifica se um assistente ainda existe na API
func (c *OpenAIAssistantClient) verifyAssistantExists(ctx context.Context, assistantID string) bool {
	_, err := c.client.Get(ctx, fmt.Sprintf("/assistants/%s", assistantID))
	return err == nil
}

// loadAssistantIDFromCache carrega o ID do assistente do cache
func (c *OpenAIAssistantClient) loadAssistantIDFromCache() (string, error) {
	cacheFile := filepath.Join(os.TempDir(), "chatcli-openai-cache", "assistant.json")
	data, err := os.ReadFile(cacheFile) //#nosec G304 -- path supplied by user/agent through validated tool surface (boundary check upstream)
	if err != nil {
		return "", err
	}

	var cache struct {
		AssistantID string `json:"assistant_id"`
		Model       string `json:"model"`
	}

	if err := json.Unmarshal(data, &cache); err != nil {
		return "", err
	}

	// Verificar se o modelo mudou - se sim, criar novo assistente
	if cache.Model != c.model {
		return "", errors.New(i18n.T("llm.assistant.model_different"))
	}

	return cache.AssistantID, nil
}

// saveAssistantIDToCache salva o ID do assistente no cache
func (c *OpenAIAssistantClient) saveAssistantIDToCache(assistantID string) error {
	cacheDir := filepath.Join(os.TempDir(), "chatcli-openai-cache")
	if err := os.MkdirAll(cacheDir, 0o700); err != nil {
		return err
	}

	cacheFile := filepath.Join(cacheDir, "assistant.json")
	cache := struct {
		AssistantID string `json:"assistant_id"`
		Model       string `json:"model"`
	}{
		AssistantID: assistantID,
		Model:       c.model,
	}

	data, err := json.Marshal(cache)
	if err != nil {
		return err
	}

	return os.WriteFile(cacheFile, data, 0o600)
}

// newFileRegistry cria um novo registro de arquivos
func newFileRegistry(logger *zap.Logger, cacheDir string) *FileRegistry {
	cachePath := filepath.Join(cacheDir, "file_registry.json")

	registry := &FileRegistry{
		Files:     make(map[string]string),
		TotalSize: 0,
		logger:    logger,
		cachePath: cachePath,
	}

	// Carregar cache existente, se houver
	registry.loadCache()

	return registry
}

// AddFile adiciona um arquivo ao registro se ele já não existir
func (r *FileRegistry) AddFile(filePath, fileID string, fileSize int64) {
	r.mu.Lock()
	defer r.mu.Unlock()

	if _, exists := r.Files[filePath]; !exists {
		r.Files[filePath] = fileID
		r.TotalSize += fileSize
		r.saveCache()
	}
}

// GetFileID retorna o ID do arquivo se estiver no registro
func (r *FileRegistry) GetFileID(filePath string) (string, bool) {
	r.mu.RLock()
	defer r.mu.RUnlock()

	id, exists := r.Files[filePath]
	return id, exists
}

// loadCache carrega o cache do disco
func (r *FileRegistry) loadCache() {
	r.mu.Lock()
	defer r.mu.Unlock()

	// Verificar se o arquivo de cache existe
	if _, err := os.Stat(r.cachePath); os.IsNotExist(err) {
		r.logger.Info(i18n.T("llm.assistant.cache_not_found"),
			zap.String("path", r.cachePath))
		return
	}

	// Abrir e ler o arquivo
	data, err := os.ReadFile(r.cachePath)
	if err != nil {
		r.logger.Warn(i18n.T("llm.assistant.cache_read_error"), zap.Error(err))
		return
	}

	// Decodificar o conteúdo
	var cache struct {
		Files       map[string]string `json:"files"`
		TotalSize   int64             `json:"total_size"`
		AssistantID string            `json:"assistant_id"`
		LastUpdated string            `json:"last_updated"`
	}

	if err := json.Unmarshal(data, &cache); err != nil {
		r.logger.Warn(i18n.T("llm.assistant.cache_decode_error"), zap.Error(err))
		return
	}

	// Verificar se os arquivos ainda existem
	validFiles := make(map[string]string)
	var validSize int64

	for path, id := range cache.Files {
		if fi, err := os.Stat(path); err == nil {
			validFiles[path] = id
			validSize += fi.Size()
		}
	}

	r.Files = validFiles
	r.TotalSize = validSize
	r.assistantID = cache.AssistantID

	r.logger.Info(i18n.T("llm.assistant.cache_loaded"),
		zap.Int("file_count", len(r.Files)),
		zap.Int64("total_size", r.TotalSize))
}

// saveCache salva o cache no disco
func (r *FileRegistry) saveCache() {
	r.mu.RLock()
	defer r.mu.RUnlock()

	// Criar o diretório do cache se não existir
	cacheDir := filepath.Dir(r.cachePath)
	if err := os.MkdirAll(cacheDir, 0o700); err != nil {
		r.logger.Warn(i18n.T("llm.assistant.cache_dir_error"),
			zap.String("dir", cacheDir),
			zap.Error(err))
		return
	}

	// Preparar os dados para salvar
	cache := struct {
		Files       map[string]string `json:"files"`
		TotalSize   int64             `json:"total_size"`
		AssistantID string            `json:"assistant_id"`
		LastUpdated string            `json:"last_updated"`
	}{
		Files:       r.Files,
		TotalSize:   r.TotalSize,
		AssistantID: r.assistantID,
		LastUpdated: time.Now().Format(time.RFC3339),
	}

	// Codificar os dados
	data, err := json.MarshalIndent(cache, "", "  ")
	if err != nil {
		r.logger.Warn(i18n.T("llm.assistant.cache_encode_error"), zap.Error(err))
		return
	}

	// Salvar no arquivo
	if err := os.WriteFile(r.cachePath, data, 0o600); err != nil {
		r.logger.Warn(i18n.T("llm.assistant.cache_save_error"), zap.Error(err))
		return
	}

	r.logger.Debug(i18n.T("llm.assistant.cache_saved"),
		zap.String("path", r.cachePath),
		zap.Int("file_count", len(r.Files)))
}

// Criar um thread
func (c *OpenAIAssistantClient) createThread(ctx context.Context) (string, error) {
	payload := map[string]interface{}{}

	resp, err := c.client.Post(ctx, "/threads", payload)
	if err != nil {
		return "", err
	}

	var response struct {
		ID string `json:"id"`
	}

	if err := json.Unmarshal(resp, &response); err != nil {
		return "", fmt.Errorf("%s: %w", i18n.T("llm.assistant.decode_response"), err)
	}

	return response.ID, nil
}

// Adicionar mensagem a um thread
func (c *OpenAIAssistantClient) addMessageToThread(ctx context.Context, threadID, content string) error {
	payload := map[string]interface{}{
		"role":    "user",
		"content": content,
	}

	_, err := c.client.Post(ctx, fmt.Sprintf("/threads/%s/messages", threadID), payload)
	return err
}

// Executar o assistente em um thread
func (c *OpenAIAssistantClient) runAssistant(ctx context.Context, threadID string) (string, error) {
	payload := map[string]interface{}{
		"assistant_id": c.assistantID,
	}

	resp, err := c.client.Post(ctx, fmt.Sprintf("/threads/%s/runs", threadID), payload)
	if err != nil {
		return "", err
	}

	var response struct {
		ID string `json:"id"`
	}

	if err := json.Unmarshal(resp, &response); err != nil {
		return "", fmt.Errorf("%s: %w", i18n.T("llm.assistant.decode_response"), err)
	}

	return response.ID, nil
}

// Aguardar pela conclusão da execução
// Aguardar pela conclusão da execução
func (c *OpenAIAssistantClient) waitForRunCompletion(ctx context.Context, threadID, runID string) (string, error) {
	interval := c.pollingInterval
	endTime := time.Now().Add(c.pollingTimeout)

	for time.Now().Before(endTime) {
		resp, err := c.client.Get(ctx, fmt.Sprintf("/threads/%s/runs/%s", threadID, runID))
		if err != nil {
			return "", err
		}

		var runStatus struct {
			Status string `json:"status"`
		}

		if err := json.Unmarshal(resp, &runStatus); err != nil {
			return "", fmt.Errorf("%s: %w", i18n.T("llm.assistant.decode_status"), err)
		}

		// Extract usage from the run object
		var rawRun map[string]interface{}
		if err := json.Unmarshal(resp, &rawRun); err == nil {
			if usage := client.ParseOpenAIUsage(rawRun); usage != nil {
				c.usageState.StoreUsage(usage)
			}
			if status, ok := rawRun["status"].(string); ok && status != "" {
				c.usageState.StoreStopReason(status)
			}
		}

		switch runStatus.Status {
		case "completed":
			return "completed", nil
		case "failed", "canceled", "expired":
			return runStatus.Status, fmt.Errorf("%s", i18n.T("llm.assistant.execution_failed", runStatus.Status))
		}

		// Backoff exponencial com limite máximo
		time.Sleep(interval)
		if interval < MaxPollingInterval {
			// Correção aqui: converter para float64, multiplicar, e converter de volta
			interval = time.Duration(float64(interval) * 1.5)
			if interval > MaxPollingInterval {
				interval = MaxPollingInterval
			}
		}
	}

	return "", errors.New(i18n.T("llm.assistant.timeout_waiting"))
}

// Obter a última resposta do assistente
func (c *OpenAIAssistantClient) getLatestResponse(ctx context.Context, threadID string) (string, error) {
	resp, err := c.client.Get(ctx, fmt.Sprintf("/threads/%s/messages?limit=1&order=desc", threadID))
	if err != nil {
		return "", err
	}

	var response struct {
		Data []struct {
			Role    string `json:"role"`
			Content []struct {
				Type string `json:"type"`
				Text struct {
					Value string `json:"value"`
				} `json:"text"`
			} `json:"content"`
		} `json:"data"`
	}

	if err := json.Unmarshal(resp, &response); err != nil {
		return "", fmt.Errorf("%s: %w", i18n.T("llm.assistant.decode_messages"), err)
	}

	if len(response.Data) == 0 || response.Data[0].Role != "assistant" {
		return "", errors.New(i18n.T("llm.assistant.no_assistant_response"))
	}

	var fullResponse strings.Builder
	for _, content := range response.Data[0].Content {
		if content.Type == "text" {
			fullResponse.WriteString(content.Text.Value)
		}
	}

	return fullResponse.String(), nil
}
