package service

import (
	"bufio"
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
	"sort"
	"strings"
	"time"
)

type ConversationService interface {
	GetConversations() (map[string][]*Conversation, error)
	GetConversation(projectPath, sessionID string) (*Conversation, error)
	GetConversationsByProject(projectPath string) ([]*Conversation, error)
}

type conversationService struct {
	claudeProjectsPath string
}

func NewConversationService() ConversationService {
	homeDir, _ := os.UserHomeDir()
	return &conversationService{
		claudeProjectsPath: filepath.Join(homeDir, ".claude", "projects"),
	}
}

// ConversationMessage represents a single message in a Claude conversation
type ConversationMessage struct {
	ParentUUID  *string         `json:"parentUuid"`
	IsSidechain bool            `json:"isSidechain"`
	UserType    string          `json:"userType"`
	CWD         string          `json:"cwd"`
	SessionID   string          `json:"sessionId"`
	Version     string          `json:"version"`
	Type        string          `json:"type"`
	Message     json.RawMessage `json:"message"`
	UUID        string          `json:"uuid"`
	Timestamp   string          `json:"timestamp"`
	ParsedTime  time.Time       `json:"-"`
}

// Conversation represents a complete conversation session
type Conversation struct {
	SessionID    string                 `json:"sessionId"`
	ProjectPath  string                 `json:"projectPath"`
	ProjectName  string                 `json:"projectName"`
	Model        string                 `json:"model,omitempty"`
	Messages     []*ConversationMessage `json:"messages"`
	StartTime    time.Time              `json:"startTime"`
	EndTime      time.Time              `json:"endTime"`
	MessageCount int                    `json:"messageCount"`
	FileModTime  time.Time              `json:"-"` // Used for sorting, not exported
}

// GetConversations returns all conversations organized by project
func (cs *conversationService) GetConversations() (map[string][]*Conversation, error) {
	conversations := make(map[string][]*Conversation)
	var parseErrors []string
	rootPath, err := cs.projectsRoot()
	if err != nil {
		return nil, fmt.Errorf("failed to resolve claude projects root: %w", err)
	}

	err = filepath.Walk(rootPath, func(path string, info os.FileInfo, err error) error {
		if err != nil {
			// Log but don't fail the entire walk
			parseErrors = append(parseErrors, fmt.Sprintf("Error accessing %s: %v", path, err))
			return nil
		}

		if !strings.HasSuffix(path, ".jsonl") {
			return nil
		}

		// Reject symlinked files or paths that escape the projects root.
		resolvedPath, err := cs.resolveExistingPathWithinProjectsRoot(path)
		if err != nil {
			parseErrors = append(parseErrors, fmt.Sprintf("Skipping %s: %v", path, err))
			return nil
		}

		// Get the project path relative to the resolved root.
		projectDir := filepath.Dir(resolvedPath)
		projectRelPath, err := filepath.Rel(rootPath, projectDir)
		if err != nil {
			parseErrors = append(parseErrors, fmt.Sprintf("Skipping %s: %v", path, err))
			return nil
		}

		// Skip files directly in the projects directory
		if projectRelPath == "." || projectRelPath == "" {
			return nil
		}

		conv, err := cs.parseConversationFile(resolvedPath, projectRelPath)
		if err != nil {
			// Log parsing errors but continue processing other files
			parseErrors = append(parseErrors, fmt.Sprintf("Failed to parse %s: %v", path, err))
			return nil
		}

		if conv != nil {
			// Include conversations even if they have no messages (edge case)
			conversations[projectRelPath] = append(conversations[projectRelPath], conv)
		}

		return nil
	})

	if err != nil {
		return nil, fmt.Errorf("failed to walk claude projects: %w", err)
	}

	// Some parsing errors may have occurred but were handled

	// Sort conversations within each project by file modification time (newest first)
	for project := range conversations {
		sort.Slice(conversations[project], func(i, j int) bool {
			return conversations[project][i].FileModTime.After(conversations[project][j].FileModTime)
		})
	}

	return conversations, nil
}

// GetConversation returns a specific conversation by project and session ID
func (cs *conversationService) GetConversation(projectPath, sessionID string) (*Conversation, error) {
	filePath, resolvedProjectPath, err := cs.resolveConversationFile(projectPath, sessionID)
	if err != nil {
		return nil, fmt.Errorf("failed to resolve conversation path: %w", err)
	}

	conv, err := cs.parseConversationFile(filePath, resolvedProjectPath)
	if err != nil {
		return nil, fmt.Errorf("failed to parse conversation: %w", err)
	}

	return conv, nil
}

// GetConversationsByProject returns all conversations for a specific project
func (cs *conversationService) GetConversationsByProject(projectPath string) ([]*Conversation, error) {
	var conversations []*Conversation
	projectDir, resolvedProjectPath, err := cs.resolveProjectDir(projectPath)
	if err != nil {
		return nil, fmt.Errorf("failed to resolve project path: %w", err)
	}

	files, err := os.ReadDir(projectDir)
	if err != nil {
		return nil, fmt.Errorf("failed to read project directory: %w", err)
	}

	for _, file := range files {
		if !strings.HasSuffix(file.Name(), ".jsonl") {
			continue
		}

		filePath := filepath.Join(projectDir, file.Name())
		conv, err := cs.parseConversationFile(filePath, resolvedProjectPath)
		if err != nil {
			continue
		}

		if conv != nil && len(conv.Messages) > 0 {
			conversations = append(conversations, conv)
		}
	}

	// Sort by file modification time (newest first)
	sort.Slice(conversations, func(i, j int) bool {
		return conversations[i].FileModTime.After(conversations[j].FileModTime)
	})

	return conversations, nil
}

func (cs *conversationService) projectsRoot() (string, error) {
	root, err := filepath.Abs(cs.claudeProjectsPath)
	if err != nil {
		return "", fmt.Errorf("failed to make projects root absolute: %w", err)
	}

	resolvedRoot, err := filepath.EvalSymlinks(root)
	if err != nil {
		if os.IsNotExist(err) {
			return root, nil
		}
		return "", fmt.Errorf("failed to resolve projects root symlinks: %w", err)
	}

	return resolvedRoot, nil
}

func (cs *conversationService) resolveProjectDir(projectPath string) (string, string, error) {
	cleanedProjectPath, err := cleanRelativeConversationPath(projectPath)
	if err != nil {
		return "", "", err
	}

	rootPath, err := cs.projectsRoot()
	if err != nil {
		return "", "", err
	}

	candidate := filepath.Join(rootPath, cleanedProjectPath)
	resolvedCandidate, err := cs.resolveExistingPathWithinProjectsRoot(candidate)
	if err != nil {
		return "", "", err
	}

	return resolvedCandidate, cleanedProjectPath, nil
}

func (cs *conversationService) resolveConversationFile(projectPath, sessionID string) (string, string, error) {
	if sessionID == "" {
		return "", "", fmt.Errorf("session ID is required")
	}

	if sessionID != filepath.Base(sessionID) || sessionID == "." || sessionID == ".." {
		return "", "", fmt.Errorf("invalid session ID: %s", sessionID)
	}

	projectDir, cleanedProjectPath, err := cs.resolveProjectDir(projectPath)
	if err != nil {
		return "", "", err
	}

	candidate := filepath.Join(projectDir, sessionID+".jsonl")
	resolvedCandidate, err := cs.resolveExistingPathWithinProjectsRoot(candidate)
	if err != nil {
		return "", "", err
	}

	return resolvedCandidate, cleanedProjectPath, nil
}

func (cs *conversationService) resolveExistingPathWithinProjectsRoot(path string) (string, error) {
	rootPath, err := cs.projectsRoot()
	if err != nil {
		return "", err
	}

	absolutePath, err := filepath.Abs(path)
	if err != nil {
		return "", fmt.Errorf("failed to make path absolute: %w", err)
	}

	normalizedPath := filepath.Clean(absolutePath)
	if !pathWithinRoot(normalizedPath, rootPath) {
		return "", fmt.Errorf("path escapes projects root: %s", path)
	}

	resolvedPath, err := filepath.EvalSymlinks(normalizedPath)
	if err != nil {
		return "", fmt.Errorf("failed to resolve path symlinks: %w", err)
	}

	if !pathWithinRoot(resolvedPath, rootPath) {
		return "", fmt.Errorf("path escapes projects root after symlink resolution: %s", path)
	}

	return resolvedPath, nil
}

func cleanRelativeConversationPath(p string) (string, error) {
	if p == "" {
		return "", fmt.Errorf("path is required")
	}

	if filepath.IsAbs(p) {
		return "", fmt.Errorf("absolute paths are not allowed: %s", p)
	}

	cleaned := filepath.Clean(p)
	if cleaned == "." || cleaned == ".." || strings.HasPrefix(cleaned, ".."+string(os.PathSeparator)) {
		return "", fmt.Errorf("path escapes projects root: %s", p)
	}

	return cleaned, nil
}

func pathWithinRoot(candidatePath, rootPath string) bool {
	relPath, err := filepath.Rel(rootPath, candidatePath)
	if err != nil {
		return false
	}

	if relPath == "." {
		return true
	}

	return relPath != ".." && !strings.HasPrefix(relPath, ".."+string(os.PathSeparator))
}

// parseConversationFile reads and parses a JSONL conversation file
func (cs *conversationService) parseConversationFile(filePath, projectPath string) (*Conversation, error) {
	// Get file info for modification time
	fileInfo, err := os.Stat(filePath)
	if err != nil {
		return nil, fmt.Errorf("failed to stat file: %w", err)
	}

	file, err := os.Open(filePath)
	if err != nil {
		return nil, fmt.Errorf("failed to open file: %w", err)
	}
	defer file.Close()

	var messages []*ConversationMessage
	var parseErrors int
	lineNum := 0
	conversationModel := ""

	scanner := bufio.NewScanner(file)

	// Increase buffer size for large messages
	const maxScanTokenSize = 10 * 1024 * 1024 // 10MB
	buf := make([]byte, maxScanTokenSize)
	scanner.Buffer(buf, maxScanTokenSize)

	for scanner.Scan() {
		lineNum++
		line := scanner.Bytes()

		// Skip empty lines
		if len(line) == 0 {
			continue
		}

		var msg ConversationMessage
		if err := json.Unmarshal(line, &msg); err != nil {
			parseErrors++
			// Log only first few errors to avoid spam
			if parseErrors <= 3 {
				// Skip malformed line
			}
			continue
		}

		// Parse timestamp
		if msg.Timestamp != "" {
			parsedTime, err := time.Parse(time.RFC3339, msg.Timestamp)
			if err != nil {
				// Try alternative timestamp formats
				parsedTime, err = time.Parse(time.RFC3339Nano, msg.Timestamp)
				if err != nil {
					// Skip message with invalid timestamp
				}
			}
			msg.ParsedTime = parsedTime
		}

		messages = append(messages, &msg)

		// Claude conversation JSONL records the assistant model inside the nested message object.
		// Track the latest model we see so the list view can filter by the active model tier.
		var messageMeta struct {
			Model string `json:"model"`
		}
		if err := json.Unmarshal(msg.Message, &messageMeta); err == nil && messageMeta.Model != "" {
			conversationModel = messageMeta.Model
		}
	}

	if err := scanner.Err(); err != nil {
		return nil, fmt.Errorf("scanner error: %w", err)
	}

	if parseErrors > 3 {
		// Some lines failed to parse but were skipped
	}

	// Return empty conversation if no messages (caller can decide what to do)
	if len(messages) == 0 {
		// Extract session ID from filename
		sessionID := filepath.Base(filePath)
		sessionID = strings.TrimSuffix(sessionID, ".jsonl")

		// Use the full project path as provided
		projectName := projectPath
		// If it looks like a file path, extract the last component
		if strings.Contains(projectPath, "-") {
			// This handles cases like "-Users-seifghazi-dev-llm-proxy"
			projectName = projectPath
		}

		return &Conversation{
			SessionID:    sessionID,
			ProjectPath:  projectPath,
			ProjectName:  projectName,
			Model:        conversationModel,
			Messages:     messages,
			StartTime:    time.Time{},
			EndTime:      time.Time{},
			MessageCount: 0,
			FileModTime:  fileInfo.ModTime(),
		}, nil
	}

	// Sort messages by timestamp
	sort.Slice(messages, func(i, j int) bool {
		return messages[i].ParsedTime.Before(messages[j].ParsedTime)
	})

	// Extract session ID from filename
	sessionID := filepath.Base(filePath)
	sessionID = strings.TrimSuffix(sessionID, ".jsonl")

	// Use the full project path as provided
	projectName := projectPath

	// Find first and last valid timestamps
	var startTime, endTime time.Time
	for _, msg := range messages {
		if !msg.ParsedTime.IsZero() {
			if startTime.IsZero() || msg.ParsedTime.Before(startTime) {
				startTime = msg.ParsedTime
			}
			if endTime.IsZero() || msg.ParsedTime.After(endTime) {
				endTime = msg.ParsedTime
			}
		}
	}

	// If no valid timestamps found, use file modification time
	if startTime.IsZero() {
		startTime = fileInfo.ModTime()
		endTime = fileInfo.ModTime()
	}

	return &Conversation{
		SessionID:    sessionID,
		ProjectPath:  projectPath,
		ProjectName:  projectName,
		Model:        conversationModel,
		Messages:     messages,
		StartTime:    startTime,
		EndTime:      endTime,
		MessageCount: len(messages),
		FileModTime:  fileInfo.ModTime(),
	}, nil
}
