package ai

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"log"
	"net/http"
	"os"
	"path/filepath"
	"regexp"
	"strings"
	"sync"
	"time"

	"golang.org/x/time/rate"

	"github.com/your-username/todo/src/config"
)

type AIService struct {
	config  *config.AIConfig
	client  *http.Client
	limiter *rate.Limiter
}

type AnalysisResult struct {
	Priority     int    `json:"priority"`
	Category     string `json:"category"`
	TimeEstimate int    `json:"estimate"`
	RelatedTasks string `json:"related"`
	Summary      string `json:"summary"`
}

type ollamaRequest struct {
	Model     string `json:"model"`
	Prompt    string `json:"prompt"`
	Stream    bool   `json:"stream"`
	MaxTokens int    `json:"max_tokens"`
}

type ollamaResponse struct {
	Response string `json:"response"`
}

var (
	analysisLogFile *os.File
	analysisLogger  *log.Logger
	logMutex        sync.Mutex
)

func InitAnalysisLogger(logPath string) error {
	var err error
	analysisLogFile, err = os.OpenFile(logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
	if err != nil {
		return fmt.Errorf("failed to open analysis log file: %w", err)
	}
	analysisLogger = log.New(analysisLogFile, "", log.LstdFlags)
	return nil
}

func CloseAnalysisLogger() error {
	if analysisLogFile != nil {
		return analysisLogFile.Close()
	}
	return nil
}

func logAnalysis(taskTitle string, result *AnalysisResult, source string) {
	if analysisLogger == nil {
		return
	}

	logMutex.Lock()
	defer logMutex.Unlock()

	analysisLogger.Printf("Task: %q\nSource: %s\nPriority: %d\nCategory: %s\nTime Estimate: %d minutes\nSummary: %s\nRelated Tasks: %s\n---\n",
		taskTitle,
		source,
		result.Priority,
		result.Category,
		result.TimeEstimate,
		result.Summary,
		result.RelatedTasks)
}

func (s *AIService) logAnalysis(taskTitle string, result *AnalysisResult, inputErr error) {
	logDir := "logs"
	if err := os.MkdirAll(logDir, 0755); err != nil {
		log.Printf("Failed to create log directory: %v", err)
		return
	}

	logFile := filepath.Join(logDir, "analysis.log")
	f, err := os.OpenFile(logFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
	if err != nil {
		log.Printf("Failed to open analysis log file: %v", err)
		return
	}
	defer f.Close()

	timestamp := time.Now().Format("2006-01-02 15:04:05")
	var logEntry string
	if inputErr != nil {
		logEntry = fmt.Sprintf("[%s] Task: %q Error: %v\n", timestamp, taskTitle, inputErr)
	} else {
		jsonResult, _ := result.ToJSON()
		logEntry = fmt.Sprintf("[%s] Task: %q Result: %s\n", timestamp, taskTitle, jsonResult)
	}

	if _, err := f.WriteString(logEntry); err != nil {
		log.Printf("Failed to write to analysis log: %v", err)
	}
}

func NewAIService(cfg *config.AIConfig) *AIService {
	client := &http.Client{
		Timeout: time.Duration(cfg.Timeout) * time.Second,
	}

	// Create rate limiter - default 10 requests per minute
	rps := rate.Every(time.Minute / time.Duration(cfg.RequestsPerMinute))
	limiter := rate.NewLimiter(rps, cfg.BurstLimit)

	return &AIService{
		config:  cfg,
		client:  client,
		limiter: limiter,
	}
}

func (s *AIService) AnalyzeTask(ctx context.Context, taskTitle string) (*AnalysisResult, error) {
	var lastErr error

	// Wait for rate limiter
	if err := s.limiter.Wait(ctx); err != nil {
		return s.fallbackAnalysis(taskTitle), nil
	}

	// Try each configured model in order
	for _, model := range s.config.Models {
		var analyzer TaskAnalyzer
		switch model.Provider {
		case "ollama":
			analyzer = &OllamaAnalyzer{
				baseURL: s.config.OllamaBaseURL,
				model:   model.Name,
				client:  s.client,
				config:  s.config,
			}
		default:
			continue
		}

		// Try with retries
		for attempt := 0; attempt < s.config.MaxRetries; attempt++ {
			select {
			case <-ctx.Done():
				return s.fallbackAnalysis(taskTitle), nil
			default:
				result, err := analyzer.Analyze(ctx, taskTitle)
				if err == nil {
					s.logAnalysis(taskTitle, result, nil)
					return result, nil
				}

				// Check if error is retryable
				var retryable bool
				if errors.Is(err, context.DeadlineExceeded) ||
					errors.Is(err, context.Canceled) ||
					strings.Contains(err.Error(), "connection refused") ||
					strings.Contains(err.Error(), "timeout") {
					retryable = true
				}

				if !retryable {
					break
				}

				lastErr = err
				backoff := time.Duration(s.config.RetryDelay*(1<<attempt)) * time.Millisecond
				time.Sleep(backoff)
			}
		}
	}

	// If all attempts failed, use fallback
	result := s.fallbackAnalysis(taskTitle)
	s.logAnalysis(taskTitle, result, lastErr)
	return result, lastErr
}

func (s *AIService) fallbackAnalysis(taskTitle string) *AnalysisResult {
	result := &AnalysisResult{
		Priority:     3,
		Category:     inferCategory(taskTitle),
		TimeEstimate: 30,
		Summary:      taskTitle,
	}
	logAnalysis(taskTitle, result, "fallback")
	return result
}

type TaskAnalyzer interface {
	Analyze(ctx context.Context, taskTitle string) (*AnalysisResult, error)
}

type OllamaAnalyzer struct {
	baseURL string
	model   string
	client  *http.Client
	config  *config.AIConfig
}

func (a *OllamaAnalyzer) Analyze(ctx context.Context, taskTitle string) (*AnalysisResult, error) {
	prompt := fmt.Sprintf(`Analyze this task and provide JSON output with these fields:
- priority (1-5, where 1 is highest)
- category (work, personal, shopping, etc)
- estimate (time estimate in minutes)
- related (related tasks or follow-ups)
- summary (brief task description)

Task: %s

Please ensure the response is valid JSON.`, taskTitle)

	req := ollamaRequest{
		Model:     a.model,
		Prompt:    prompt,
		Stream:    false,
		MaxTokens: a.config.MaxTokens,
	}

	body, err := json.Marshal(req)
	if err != nil {
		return nil, fmt.Errorf("failed to marshal request: %w", err)
	}

	reqURL := fmt.Sprintf("%s/api/generate", strings.TrimRight(a.baseURL, "/"))
	httpReq, err := http.NewRequestWithContext(ctx, "POST", reqURL, bytes.NewBuffer(body))
	if err != nil {
		return nil, fmt.Errorf("failed to create request: %w", err)
	}

	httpReq.Header.Set("Content-Type", "application/json")

	resp, err := a.client.Do(httpReq)
	if err != nil {
		// Check if error matches any retryable patterns
		for _, pattern := range a.config.RetryableErrors {
			if strings.Contains(err.Error(), pattern) {
				return nil, fmt.Errorf("retryable error: %w", err)
			}
		}
		return nil, fmt.Errorf("failed to make request: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode == http.StatusTooManyRequests {
		return nil, fmt.Errorf("retryable error: rate limit exceeded")
	}

	if resp.StatusCode >= 500 {
		return nil, fmt.Errorf("retryable error: server error %d", resp.StatusCode)
	}

	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
	}

	var ollamaResp ollamaResponse
	if err := json.NewDecoder(resp.Body).Decode(&ollamaResp); err != nil {
		return nil, fmt.Errorf("failed to decode response: %w", err)
	}

	// Clean up response text by removing ANSI escape sequences and markers
	cleanText := stripAnsiCodes(ollamaResp.Response)

	// Try to extract JSON from the response text
	var result AnalysisResult
	jsonStart := strings.Index(cleanText, "{")
	jsonEnd := strings.LastIndex(cleanText, "}")

	if jsonStart >= 0 && jsonEnd > jsonStart {
		jsonStr := cleanText[jsonStart : jsonEnd+1]
		if err := json.Unmarshal([]byte(jsonStr), &result); err != nil {
			log.Printf("JSON parse error: %v\nContent: %q", err, jsonStr)
			// Try to extract structured data from text
			extractedResult := a.extractFromText(cleanText, taskTitle)
			logAnalysis(taskTitle, extractedResult, "text-extraction")
			return extractedResult, nil
		}

		validatedResult := a.validateResult(&result, taskTitle)
		logAnalysis(taskTitle, validatedResult, "ollama-json")
		return validatedResult, nil
	}

	// If no JSON found, try to extract structured data from text
	extractedResult := a.extractFromText(cleanText, taskTitle)
	logAnalysis(taskTitle, extractedResult, "text-extraction")
	return extractedResult, nil
}

// stripAnsiCodes removes ANSI escape sequences and code block markers from text
func stripAnsiCodes(text string) string {
	// Remove ANSI escape sequences
	ansiRegex := regexp.MustCompile(`\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])`)
	text = ansiRegex.ReplaceAllString(text, "")

	// Remove code block markers and "json" language identifier
	text = regexp.MustCompile("(?m)^```json\\s*$").ReplaceAllString(text, "")
	text = regexp.MustCompile("(?m)^```\\s*$").ReplaceAllString(text, "")

	// Clean up any remaining whitespace
	text = strings.TrimSpace(text)

	return text
}

func (a *OllamaAnalyzer) extractFromText(text string, taskTitle string) *AnalysisResult {
	result := &AnalysisResult{
		Summary: taskTitle,
	}

	// Extract priority (1-5)
	if idx := strings.Index(text, "priority"); idx >= 0 {
		for i := idx; i < len(text) && i < idx+20; i++ {
			if p := text[i]; p >= '1' && p <= '5' {
				result.Priority = int(p - '0')
				break
			}
		}
	}

	// Extract category
	categories := []string{"work", "personal", "shopping", "health", "education"}
	for _, cat := range categories {
		if strings.Contains(strings.ToLower(text), cat) {
			result.Category = cat
			break
		}
	}

	// Extract time estimate
	if idx := strings.Index(text, "estimate"); idx >= 0 {
		var estimate int
		fmt.Sscanf(text[idx:], "estimate: %d", &estimate)
		if estimate > 0 {
			result.TimeEstimate = estimate
		}
	}

	return a.validateResult(result, taskTitle)
}

func (a *OllamaAnalyzer) validateResult(result *AnalysisResult, taskTitle string) *AnalysisResult {
	if result == nil {
		result = &AnalysisResult{}
	}

	// Validate and set defaults
	if result.Priority < 1 || result.Priority > 5 {
		// Try to infer priority from keywords
		priority := 3 // default medium priority
		lowPriority := []string{"whenever", "someday", "maybe", "if possible", "low"}
		highPriority := []string{"urgent", "asap", "important", "critical", "due", "deadline"}

		taskLower := strings.ToLower(taskTitle)
		for _, word := range lowPriority {
			if strings.Contains(taskLower, word) {
				priority = 4
				break
			}
		}
		for _, word := range highPriority {
			if strings.Contains(taskLower, word) {
				priority = 2
				break
			}
		}
		result.Priority = priority
	}

	if result.TimeEstimate <= 0 {
		// Set default based on typical task duration
		switch {
		case len(taskTitle) < 20:
			result.TimeEstimate = 15 // Quick tasks
		case len(taskTitle) < 50:
			result.TimeEstimate = 30 // Medium tasks
		default:
			result.TimeEstimate = 60 // Longer tasks
		}
	}

	if result.Category == "" {
		result.Category = inferCategory(taskTitle)
	}

	if result.Summary == "" {
		result.Summary = taskTitle
	}

	return result
}

func (r *AnalysisResult) ToJSON() (string, error) {
	data, err := json.Marshal(r)
	if err != nil {
		return "", err
	}
	return string(data), nil
}

func inferCategory(taskTitle string) string {
	taskTitle = strings.ToLower(taskTitle)

	categories := map[string][]string{
		"work":          {"report", "meeting", "project", "email", "presentation", "deadline"},
		"education":     {"study", "learn", "read", "homework", "assignment", "course"},
		"shopping":      {"buy", "purchase", "grocery", "store", "market", "shop"},
		"health":        {"exercise", "gym", "workout", "doctor", "medical", "appointment"},
		"maintenance":   {"clean", "fix", "repair", "maintain", "organize"},
		"communication": {"call", "email", "message", "contact", "meet"},
	}

	for category, keywords := range categories {
		for _, keyword := range keywords {
			if strings.Contains(taskTitle, keyword) {
				return category
			}
		}
	}

	return "general"
}
