package sessions

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

	"github.com/nextlevelbuilder/goclaw/internal/providers"
)

// Session stores conversation history for one agent+scope combination.
type Session struct {
	Key      string              `json:"key"`       // agent:{agentId}:{sessionKey}
	Messages []providers.Message `json:"messages"`
	Summary  string              `json:"summary,omitempty"`
	Created  time.Time           `json:"created"`
	Updated  time.Time           `json:"updated"`

	// Metadata (matching TS SessionEntry subset)
	Model           string `json:"model,omitempty"`
	Provider        string `json:"provider,omitempty"`
	Channel         string `json:"channel,omitempty"`
	InputTokens     int64  `json:"inputTokens,omitempty"`
	OutputTokens    int64  `json:"outputTokens,omitempty"`
	CompactionCount             int    `json:"compactionCount,omitempty"`
	MemoryFlushCompactionCount int    `json:"memoryFlushCompactionCount,omitempty"`
	MemoryFlushAt              int64  `json:"memoryFlushAt,omitempty"` // unix ms
	Label                      string `json:"label,omitempty"`
	SpawnedBy       string `json:"spawnedBy,omitempty"`
	SpawnDepth      int    `json:"spawnDepth,omitempty"`

	ContextWindow    int `json:"contextWindow,omitempty"`
	LastPromptTokens int `json:"lastPromptTokens,omitempty"`
	LastMessageCount int `json:"lastMessageCount,omitempty"`
}

// Manager handles session lifecycle, persistence, and lookup.
type Manager struct {
	sessions map[string]*Session
	mu       sync.RWMutex
	storage  string
}

func NewManager(storage string) *Manager {
	m := &Manager{
		sessions: make(map[string]*Session),
		storage:  storage,
	}
	if storage != "" {
		os.MkdirAll(storage, 0755)
		m.loadAll()
	}
	return m
}

// SessionKey builds a composite session key: agent:{agentId}:{scopeKey}
func SessionKey(agentID, scopeKey string) string {
	return fmt.Sprintf("agent:%s:%s", agentID, scopeKey)
}

// GetOrCreate returns an existing session or creates a new one.
func (m *Manager) GetOrCreate(key string) *Session {
	m.mu.Lock()
	defer m.mu.Unlock()

	if s, ok := m.sessions[key]; ok {
		return s
	}

	s := &Session{
		Key:      key,
		Messages: []providers.Message{},
		Created:  time.Now(),
		Updated:  time.Now(),
	}
	m.sessions[key] = s
	return s
}

// AddMessage appends a message to a session.
func (m *Manager) AddMessage(key string, msg providers.Message) {
	m.mu.Lock()
	defer m.mu.Unlock()

	s, ok := m.sessions[key]
	if !ok {
		s = &Session{
			Key:      key,
			Messages: []providers.Message{},
			Created:  time.Now(),
		}
		m.sessions[key] = s
	}

	s.Messages = append(s.Messages, msg)
	s.Updated = time.Now()
}

// GetHistory returns a copy of the message history.
func (m *Manager) GetHistory(key string) []providers.Message {
	m.mu.RLock()
	defer m.mu.RUnlock()

	s, ok := m.sessions[key]
	if !ok {
		return nil
	}

	msgs := make([]providers.Message, len(s.Messages))
	copy(msgs, s.Messages)
	return msgs
}

// GetSummary returns the session summary.
func (m *Manager) GetSummary(key string) string {
	m.mu.RLock()
	defer m.mu.RUnlock()
	if s, ok := m.sessions[key]; ok {
		return s.Summary
	}
	return ""
}

// SetSummary updates the session summary.
func (m *Manager) SetSummary(key, summary string) {
	m.mu.Lock()
	defer m.mu.Unlock()
	if s, ok := m.sessions[key]; ok {
		s.Summary = summary
		s.Updated = time.Now()
	}
}

// SetLabel updates the session label.
func (m *Manager) SetLabel(key, label string) {
	m.mu.Lock()
	defer m.mu.Unlock()
	if s, ok := m.sessions[key]; ok {
		s.Label = label
		s.Updated = time.Now()
	}
}

// UpdateMetadata sets model/provider/channel metadata on a session.
func (m *Manager) UpdateMetadata(key, model, provider, channel string) {
	m.mu.Lock()
	defer m.mu.Unlock()
	if s, ok := m.sessions[key]; ok {
		if model != "" {
			s.Model = model
		}
		if provider != "" {
			s.Provider = provider
		}
		if channel != "" {
			s.Channel = channel
		}
	}
}

// AccumulateTokens adds token counts from a completed run.
func (m *Manager) AccumulateTokens(key string, inputTokens, outputTokens int64) {
	m.mu.Lock()
	defer m.mu.Unlock()
	if s, ok := m.sessions[key]; ok {
		s.InputTokens += inputTokens
		s.OutputTokens += outputTokens
	}
}

// IncrementCompaction bumps the compaction counter after summarization.
func (m *Manager) IncrementCompaction(key string) {
	m.mu.Lock()
	defer m.mu.Unlock()
	if s, ok := m.sessions[key]; ok {
		s.CompactionCount++
	}
}

// GetCompactionCount returns the current compaction count for a session.
func (m *Manager) GetCompactionCount(key string) int {
	m.mu.RLock()
	defer m.mu.RUnlock()
	if s, ok := m.sessions[key]; ok {
		return s.CompactionCount
	}
	return 0
}

// GetMemoryFlushCompactionCount returns the compaction count at which memory flush last ran.
func (m *Manager) GetMemoryFlushCompactionCount(key string) int {
	m.mu.RLock()
	defer m.mu.RUnlock()
	if s, ok := m.sessions[key]; ok {
		return s.MemoryFlushCompactionCount
	}
	return -1 // never flushed
}

// SetMemoryFlushDone records that memory flush completed at the current compaction count.
func (m *Manager) SetMemoryFlushDone(key string) {
	m.mu.Lock()
	defer m.mu.Unlock()
	if s, ok := m.sessions[key]; ok {
		s.MemoryFlushCompactionCount = s.CompactionCount
		s.MemoryFlushAt = time.Now().UnixMilli()
	}
}

// SetSpawnInfo sets subagent origin metadata on a session.
func (m *Manager) SetSpawnInfo(key, spawnedBy string, depth int) {
	m.mu.Lock()
	defer m.mu.Unlock()
	if s, ok := m.sessions[key]; ok {
		s.SpawnedBy = spawnedBy
		s.SpawnDepth = depth
	}
}

// SetContextWindow caches the agent's context window on the session.
func (m *Manager) SetContextWindow(key string, cw int) {
	m.mu.Lock()
	defer m.mu.Unlock()
	if s, ok := m.sessions[key]; ok {
		s.ContextWindow = cw
	}
}

// GetContextWindow returns the cached context window for a session (0 if unset).
func (m *Manager) GetContextWindow(key string) int {
	m.mu.RLock()
	defer m.mu.RUnlock()
	if s, ok := m.sessions[key]; ok {
		return s.ContextWindow
	}
	return 0
}

// SetLastPromptTokens records actual prompt tokens from the last LLM response.
func (m *Manager) SetLastPromptTokens(key string, tokens, msgCount int) {
	m.mu.Lock()
	defer m.mu.Unlock()
	if s, ok := m.sessions[key]; ok {
		s.LastPromptTokens = tokens
		s.LastMessageCount = msgCount
	}
}

// GetLastPromptTokens returns the last known prompt tokens and message count.
func (m *Manager) GetLastPromptTokens(key string) (int, int) {
	m.mu.RLock()
	defer m.mu.RUnlock()
	if s, ok := m.sessions[key]; ok {
		return s.LastPromptTokens, s.LastMessageCount
	}
	return 0, 0
}

// TruncateHistory keeps only the last N messages.
func (m *Manager) TruncateHistory(key string, keepLast int) {
	m.mu.Lock()
	defer m.mu.Unlock()

	s, ok := m.sessions[key]
	if !ok {
		return
	}

	if keepLast <= 0 {
		s.Messages = []providers.Message{}
	} else if len(s.Messages) > keepLast {
		s.Messages = s.Messages[len(s.Messages)-keepLast:]
	}
	s.Updated = time.Now()
}

// SetHistory replaces a session's message history with the given slice.
func (m *Manager) SetHistory(key string, msgs []providers.Message) {
	m.mu.Lock()
	defer m.mu.Unlock()

	if s, ok := m.sessions[key]; ok {
		s.Messages = msgs
		s.Updated = time.Now()
	}
}

// Reset clears a session's history and summary.
func (m *Manager) Reset(key string) {
	m.mu.Lock()
	defer m.mu.Unlock()

	if s, ok := m.sessions[key]; ok {
		s.Messages = []providers.Message{}
		s.Summary = ""
		s.Updated = time.Now()
	}
}

// Delete removes a session entirely.
func (m *Manager) Delete(key string) error {
	m.mu.Lock()
	delete(m.sessions, key)
	m.mu.Unlock()

	if m.storage != "" {
		filename := sanitizeFilename(key) + ".json"
		path := filepath.Join(m.storage, filename)
		if err := os.Remove(path); err != nil && !os.IsNotExist(err) {
			return err
		}
	}
	return nil
}

// List returns metadata for all sessions, optionally filtered by agent ID.
func (m *Manager) List(agentID string) []SessionInfo {
	m.mu.RLock()
	defer m.mu.RUnlock()

	var result []SessionInfo
	prefix := ""
	if agentID != "" {
		prefix = "agent:" + agentID + ":"
	}

	for key, s := range m.sessions {
		if prefix != "" && !strings.HasPrefix(key, prefix) {
			continue
		}
		result = append(result, SessionInfo{
			Key:          key,
			MessageCount: len(s.Messages),
			Created:      s.Created,
			Updated:      s.Updated,
			Label:        s.Label,
			Channel:      s.Channel,
		})
	}
	return result
}

// LastUsedChannel finds the most recently updated channel session for an agent
// and extracts channel + chatID from the key. Returns ("", "") if none found.
func (m *Manager) LastUsedChannel(agentID string) (channel, chatID string) {
	m.mu.RLock()
	defer m.mu.RUnlock()

	prefix := "agent:" + agentID + ":"
	var bestKey string
	var bestUpdated time.Time

	for key, s := range m.sessions {
		if !strings.HasPrefix(key, prefix) {
			continue
		}
		// Skip non-channel sessions (cron, subagent)
		rest := key[len(prefix):]
		if strings.HasPrefix(rest, "cron:") || strings.HasPrefix(rest, "subagent:") {
			continue
		}
		if s.Updated.After(bestUpdated) {
			bestUpdated = s.Updated
			bestKey = key
		}
	}

	if bestKey == "" {
		return "", ""
	}

	// Parse: agent:{agentId}:{channel}:{peerKind}:{chatId}
	parts := strings.SplitN(bestKey, ":", 5)
	if len(parts) >= 5 {
		return parts[2], parts[4]
	}
	return "", ""
}

// SessionInfo is a lightweight session descriptor for listing.
type SessionInfo struct {
	Key          string    `json:"key"`
	MessageCount int       `json:"messageCount"`
	Created      time.Time `json:"created"`
	Updated      time.Time `json:"updated"`
	Label        string    `json:"label,omitempty"`
	Channel      string    `json:"channel,omitempty"`
}

// Save persists a session to disk atomically.
func (m *Manager) Save(key string) error {
	if m.storage == "" {
		return nil
	}

	m.mu.RLock()
	s, ok := m.sessions[key]
	if !ok {
		m.mu.RUnlock()
		return nil
	}

	// Snapshot under lock
	snapshot := Session{
		Key:             s.Key,
		Summary:         s.Summary,
		Created:         s.Created,
		Updated:         s.Updated,
		Model:           s.Model,
		Provider:        s.Provider,
		Channel:         s.Channel,
		InputTokens:     s.InputTokens,
		OutputTokens:    s.OutputTokens,
		CompactionCount: s.CompactionCount,
		MemoryFlushCompactionCount: s.MemoryFlushCompactionCount,
		MemoryFlushAt:   s.MemoryFlushAt,
		Label:           s.Label,
		SpawnedBy:       s.SpawnedBy,
		SpawnDepth:      s.SpawnDepth,
		ContextWindow:    s.ContextWindow,
		LastPromptTokens: s.LastPromptTokens,
		LastMessageCount: s.LastMessageCount,
	}
	if len(s.Messages) > 0 {
		snapshot.Messages = make([]providers.Message, len(s.Messages))
		copy(snapshot.Messages, s.Messages)
	} else {
		snapshot.Messages = []providers.Message{}
	}
	m.mu.RUnlock()

	data, err := json.MarshalIndent(snapshot, "", "  ")
	if err != nil {
		return err
	}

	filename := sanitizeFilename(key)
	if filename == "." || !filepath.IsLocal(filename) || strings.ContainsAny(filename, `/\`) {
		return os.ErrInvalid
	}

	sessionPath := filepath.Join(m.storage, filename+".json")

	// Atomic write: temp file → rename
	tmpFile, err := os.CreateTemp(m.storage, "session-*.tmp")
	if err != nil {
		return err
	}
	tmpPath := tmpFile.Name()
	cleanup := true
	defer func() {
		if cleanup {
			os.Remove(tmpPath)
		}
	}()

	if _, err := tmpFile.Write(data); err != nil {
		tmpFile.Close()
		return err
	}
	if err := tmpFile.Sync(); err != nil {
		tmpFile.Close()
		return err
	}
	tmpFile.Close()

	if err := os.Rename(tmpPath, sessionPath); err != nil {
		return err
	}
	cleanup = false
	return nil
}

func (m *Manager) loadAll() {
	files, err := os.ReadDir(m.storage)
	if err != nil {
		return
	}

	for _, f := range files {
		if f.IsDir() || filepath.Ext(f.Name()) != ".json" {
			continue
		}

		data, err := os.ReadFile(filepath.Join(m.storage, f.Name()))
		if err != nil {
			continue
		}

		var s Session
		if err := json.Unmarshal(data, &s); err != nil {
			continue
		}

		m.sessions[s.Key] = &s
	}
}

func sanitizeFilename(key string) string {
	return strings.ReplaceAll(key, ":", "_")
}
