// ClawGo - Ultra-lightweight personal AI agent
// Inspired by and based on nanobot: https://github.com/HKUDS/nanobot
// License: MIT
//
// Copyright (c) 2026 ClawGo contributors

package agent

import (
	"fmt"
	"os"
	"path/filepath"
	"strings"
	"time"
)

// MemoryStore manages persistent memory for the agent.
// - Long-term memory: MEMORY.md (workspace root, compatible with OpenClaw)
// - Daily notes: memory/YYYY-MM-DD.md
// It also supports legacy locations for backward compatibility.
type MemoryStore struct {
	workspace        string
	namespace        string
	memoryDir        string
	memoryFile       string
	legacyMemoryFile string
}

// NewMemoryStore creates a new MemoryStore with the given workspace path.
// It ensures the memory directory exists.
func NewMemoryStore(workspace string) *MemoryStore {
	return NewMemoryStoreWithNamespace(workspace, "main")
}

func NewMemoryStoreWithNamespace(workspace, namespace string) *MemoryStore {
	ns := normalizeMemoryNamespace(namespace)
	baseDir := workspace
	if ns != "main" {
		baseDir = filepath.Join(workspace, "agents", ns)
	}

	memoryDir := filepath.Join(baseDir, "memory")
	memoryFile := filepath.Join(baseDir, "MEMORY.md")
	legacyMemoryFile := filepath.Join(memoryDir, "MEMORY.md")

	// Ensure memory directory exists
	os.MkdirAll(memoryDir, 0755)

	return &MemoryStore{
		workspace:        workspace,
		namespace:        ns,
		memoryDir:        memoryDir,
		memoryFile:       memoryFile,
		legacyMemoryFile: legacyMemoryFile,
	}
}

// getTodayFile returns the path to today's daily note file (memory/YYYY-MM-DD.md).
func (ms *MemoryStore) getTodayFile() string {
	return filepath.Join(ms.memoryDir, time.Now().Format("2006-01-02")+".md")
}

// ReadLongTerm reads the long-term memory (MEMORY.md).
// Returns empty string if the file doesn't exist.
func (ms *MemoryStore) ReadLongTerm() string {
	if data, err := os.ReadFile(ms.memoryFile); err == nil {
		return string(data)
	}
	if data, err := os.ReadFile(ms.legacyMemoryFile); err == nil {
		return string(data)
	}
	return ""
}

// WriteLongTerm writes content to the long-term memory file (MEMORY.md).
func (ms *MemoryStore) WriteLongTerm(content string) error {
	return os.WriteFile(ms.memoryFile, []byte(content), 0644)
}

// ReadToday reads today's daily note.
// Returns empty string if the file doesn't exist.
func (ms *MemoryStore) ReadToday() string {
	todayFile := ms.getTodayFile()
	if data, err := os.ReadFile(todayFile); err == nil {
		return string(data)
	}
	return ""
}

// AppendToday appends content to today's daily note.
// If the file doesn't exist, it creates a new file with a date header.
func (ms *MemoryStore) AppendToday(content string) error {
	todayFile := ms.getTodayFile()

	// Ensure memory directory exists
	os.MkdirAll(ms.memoryDir, 0755)

	var existingContent string
	if data, err := os.ReadFile(todayFile); err == nil {
		existingContent = string(data)
	}

	var newContent string
	if existingContent == "" {
		// Add header for new day
		header := fmt.Sprintf("# %s\n\n", time.Now().Format("2006-01-02"))
		newContent = header + content
	} else {
		// Append to existing content
		newContent = existingContent + "\n" + content
	}

	return os.WriteFile(todayFile, []byte(newContent), 0644)
}

// GetRecentDailyNotes returns daily notes from the last N days.
// Contents are joined with "---" separator.
func (ms *MemoryStore) GetRecentDailyNotes(days int) string {
	var notes []string

	for i := 0; i < days; i++ {
		date := time.Now().AddDate(0, 0, -i)

		// Preferred format: memory/YYYY-MM-DD.md
		newPath := filepath.Join(ms.memoryDir, date.Format("2006-01-02")+".md")
		if data, err := os.ReadFile(newPath); err == nil {
			notes = append(notes, string(data))
			continue
		}

		// Backward-compatible format: memory/YYYYMM/YYYYMMDD.md
		legacyDate := date.Format("20060102")
		legacyPath := filepath.Join(ms.memoryDir, legacyDate[:6], legacyDate+".md")
		if data, err := os.ReadFile(legacyPath); err == nil {
			notes = append(notes, string(data))
		}
	}

	if len(notes) == 0 {
		return ""
	}

	// Join with separator
	var result string
	for i, note := range notes {
		if i > 0 {
			result += "\n\n---\n\n"
		}
		result += note
	}
	return result
}

// GetMemoryContext returns formatted memory context for the agent prompt.
// Includes long-term memory and recent daily notes.
func (ms *MemoryStore) GetMemoryContext() string {
	var parts []string

	// Recent daily notes first (today + yesterday), to prioritize fresh context
	recentNotes := ms.GetRecentDailyNotes(2)
	if recentNotes != "" {
		parts = append(parts, "## Recent Daily Notes\n\n"+recentNotes)
	}

	// Long-term memory
	longTerm := ms.ReadLongTerm()
	if longTerm != "" {
		parts = append(parts, "## Long-term Memory\n\n"+longTerm)
	}

	if len(parts) == 0 {
		return ""
	}

	// Join parts with separator
	var result string
	for i, part := range parts {
		if i > 0 {
			result += "\n\n---\n\n"
		}
		result += part
	}
	return fmt.Sprintf("# Memory\n\n%s", result)
}

func normalizeMemoryNamespace(namespace string) string {
	namespace = strings.TrimSpace(strings.ToLower(namespace))
	if namespace == "" || namespace == "main" {
		return "main"
	}
	var sb strings.Builder
	for _, r := range namespace {
		switch {
		case r >= 'a' && r <= 'z':
			sb.WriteRune(r)
		case r >= '0' && r <= '9':
			sb.WriteRune(r)
		case r == '-' || r == '_' || r == '.':
			sb.WriteRune(r)
		case r == ' ':
			sb.WriteRune('-')
		}
	}
	out := strings.Trim(sb.String(), "-_.")
	if out == "" {
		return "main"
	}
	return out
}
