package workspace

import (
	"encoding/json"
	"errors"
	"fmt"
	"io/fs"
	"os"
	"path/filepath"
	"strings"
	"time"
	"unicode/utf8"
)

const (
	memoryFileName                       = "MEMORY.md"
	stateDirName                         = "state"
	kbDirName                            = "kb"
	archiveDirName                       = "archive"
	legacyNotesDirName                   = "notes"
	activeTaskRelPath                    = "state/active-task.json"
	activeContextRelPath                 = "state/active-context.md"
	openLoopsRelPath                     = "state/open-loops.json"
	kbIndexRelPath                       = "kb/index.md"
	kbDomainRelPath                      = "kb/domain.md"
	kbUserPrefsRelPath                   = "kb/user-preferences.md"
	kbChannelContextRelPath              = "kb/channel-context.md"
	archiveCheckpointsPath               = "archive/checkpoints.md"
	localMemorySchema                    = 1
	maxMemoryFileCharacters              = 32768
	memoryTitleMaxCharacters             = 120
	memoryCheckpointSummaryMaxCharacters = 512
	memoryCheckpointContextMaxCharacters = 240
	archiveCheckpointsHeader             = "# Automatic Checkpoints\n\nHistorical runtime checkpoints. Latest entries are appended below.\n\n"
)

var legacyKBMigrations = []struct {
	legacyRelPath    string
	canonicalRelPath string
}{
	{
		legacyRelPath:    filepath.Join(legacyNotesDirName, "domain.md"),
		canonicalRelPath: kbDomainRelPath,
	},
	{
		legacyRelPath:    filepath.Join(legacyNotesDirName, "user-preferences.md"),
		canonicalRelPath: kbUserPrefsRelPath,
	},
	{
		legacyRelPath:    filepath.Join(legacyNotesDirName, "channels.md"),
		canonicalRelPath: kbChannelContextRelPath,
	},
}

// RecoveryCheckpoint is the compact runtime state written into MEMORY.md,
// state/active-task.json, state/active-context.md, and archive/checkpoints.md
// so an agent can recover after restart.
type RecoveryCheckpoint struct {
	Timestamp     time.Time
	Trigger       string
	Summary       string
	SessionID     string
	WakeTarget    string
	WakeMessageID string
}

type activeTaskState struct {
	SchemaVersion    int              `json:"schemaVersion"`
	UpdatedAt        string           `json:"updatedAt"`
	LastCheckpoint   *checkpointState `json:"lastCheckpoint,omitempty"`
	CurrentObjective string           `json:"currentObjective,omitempty"`
	NextStep         string           `json:"nextStep,omitempty"`
	RelevantFiles    []string         `json:"relevantFiles,omitempty"`
}

type checkpointState struct {
	Trigger       string `json:"trigger"`
	Summary       string `json:"summary"`
	SessionID     string `json:"sessionId,omitempty"`
	WakeTarget    string `json:"wakeTarget,omitempty"`
	WakeMessageID string `json:"wakeMessageId,omitempty"`
	Timestamp     string `json:"timestamp"`
}

type openLoopsState struct {
	SchemaVersion int      `json:"schemaVersion"`
	UpdatedAt     string   `json:"updatedAt"`
	Items         []string `json:"items"`
}

// MemoryLayoutReport describes non-fatal local memory maintenance issues that
// callers should surface to the user while still allowing the agent to start.
type MemoryLayoutReport struct {
	MemoryIndexOversized  bool
	MemoryIndexCharacters int
	MemoryIndexLimit      int
	Warning               string
}

type memoryIndexSizeError struct {
	limit int
	got   int
}

func (err *memoryIndexSizeError) Error() string {
	return fmt.Sprintf("MEMORY.md exceeds %d characters (got %d); move detailed content into state/, kb/, or archive/ and keep only links plus one-line summaries in MEMORY.md", err.limit, err.got)
}

type memoryActiveContextMode int

const (
	memoryActiveContextFull memoryActiveContextMode = iota
	memoryActiveContextConcise
	memoryActiveContextPointer
	memoryActiveContextTruncated
)

// InitializeMemoryLayout creates or repairs the local memory directory layout
// without recording a runtime checkpoint.
func InitializeMemoryLayout(workDir, title string) error {
	_, err := InitializeMemoryLayoutWithReport(workDir, title)
	return err
}

// InitializeMemoryLayoutWithReport creates or repairs the local memory layout.
// Oversized MEMORY.md content is compacted where daemon-owned sections can be
// safely rewritten; if non-daemon content keeps the file over budget, startup
// continues and the returned report contains a warning for the user.
func InitializeMemoryLayoutWithReport(workDir, title string) (MemoryLayoutReport, error) {
	return ensureLocalMemoryScaffold(workDir, title, nil, true)
}

// RecordRecoveryCheckpoint writes the latest recovery state into state/,
// updates the bounded MEMORY.md index, and appends the historical archive entry.
func RecordRecoveryCheckpoint(workDir, title string, checkpoint RecoveryCheckpoint) error {
	if checkpoint.Timestamp.IsZero() {
		checkpoint.Timestamp = time.Now().UTC()
	}
	if _, err := ensureLocalMemoryScaffold(workDir, title, &checkpoint, true); err != nil {
		return err
	}
	if err := writeActiveTaskState(workDir, checkpoint); err != nil {
		return err
	}
	if err := writeActiveContext(workDir, checkpoint); err != nil {
		return err
	}
	if err := touchOpenLoopsState(workDir, checkpoint.Timestamp); err != nil {
		return err
	}
	if err := appendArchiveCheckpoint(workDir, checkpoint); err != nil {
		return err
	}
	_, err := updateMemoryIndex(workDir, title, &checkpoint, true)
	return err
}

// AppendArchiveCheckpoint appends a historical checkpoint without rewriting the
// active state files.
func AppendArchiveCheckpoint(workDir, trigger, summary string, timestamp time.Time) error {
	if timestamp.IsZero() {
		timestamp = time.Now().UTC()
	}
	if err := ensureArchiveImported(workDir); err != nil {
		return err
	}
	return appendArchiveCheckpoint(workDir, RecoveryCheckpoint{
		Timestamp: timestamp,
		Trigger:   trigger,
		Summary:   summary,
	})
}

func ensureLocalMemoryScaffold(workDir, title string, checkpoint *RecoveryCheckpoint, warnOnOversizedMemory bool) (MemoryLayoutReport, error) {
	if strings.TrimSpace(workDir) == "" {
		return MemoryLayoutReport{}, nil
	}
	for _, rel := range []string{
		stateDirName,
		kbDirName,
		filepath.Join(kbDirName, "tasks"),
		archiveDirName,
		filepath.Join(archiveDirName, "tasks"),
	} {
		if err := os.MkdirAll(filepath.Join(workDir, rel), 0o755); err != nil {
			return MemoryLayoutReport{}, err
		}
	}
	if err := ensureArchiveImported(workDir); err != nil {
		return MemoryLayoutReport{}, err
	}
	if err := ensureLegacyKBImported(workDir); err != nil {
		return MemoryLayoutReport{}, err
	}
	if err := ensureKBIndex(workDir); err != nil {
		return MemoryLayoutReport{}, err
	}
	if err := ensureOpenLoopsState(workDir); err != nil {
		return MemoryLayoutReport{}, err
	}
	if err := ensureActiveTaskState(workDir); err != nil {
		return MemoryLayoutReport{}, err
	}
	if err := ensureActiveContext(workDir); err != nil {
		return MemoryLayoutReport{}, err
	}
	return updateMemoryIndex(workDir, title, checkpoint, warnOnOversizedMemory)
}

func ensureArchiveImported(workDir string) error {
	archivePath := filepath.Join(workDir, archiveCheckpointsPath)
	if _, err := os.Stat(archivePath); err == nil {
		return nil
	} else if !errors.Is(err, fs.ErrNotExist) {
		return err
	}

	legacyPath := filepath.Join(workDir, legacyNotesDirName, "checkpoints.md")
	data, err := os.ReadFile(legacyPath)
	if errors.Is(err, fs.ErrNotExist) {
		return ensureArchiveHeader(workDir)
	}
	if err != nil {
		return err
	}
	if err := os.MkdirAll(filepath.Dir(archivePath), 0o755); err != nil {
		return err
	}
	if err := os.WriteFile(archivePath, data, 0o644); err != nil {
		return err
	}
	return nil
}

func ensureArchiveHeader(workDir string) error {
	path := filepath.Join(workDir, archiveCheckpointsPath)
	if _, err := os.Stat(path); err == nil {
		return nil
	} else if !errors.Is(err, fs.ErrNotExist) {
		return err
	}
	if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
		return err
	}
	return os.WriteFile(path, []byte(archiveCheckpointsHeader), 0o644)
}

func ensureKBIndex(workDir string) error {
	path := filepath.Join(workDir, kbIndexRelPath)
	if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
		return err
	}
	if _, err := os.Stat(path); err == nil {
		return nil
	} else if !errors.Is(err, fs.ErrNotExist) {
		return err
	}
	content := strings.TrimSpace(`
# Local Knowledge Index

- Durable local knowledge for this concrete agent belongs under `+"`kb/`"+`.
- Common canonical docs include `+"`kb/domain.md`"+`, `+"`kb/user-preferences.md`"+`, and `+"`kb/channel-context.md`"+` when this agent accumulates that knowledge.
- Put task-specific carry-over notes under `+"`kb/tasks/`"+` and link them from `+"`MEMORY.md`"+` when relevant.
- If some knowledge should travel with every future spawn from the same role, promote it into the Role package instead of leaving it in local memory.
- During migration, bootstrap copies legacy `+"`notes/domain.md`"+`, `+"`notes/user-preferences.md`"+`, and `+"`notes/channels.md`"+` into those canonical `+"`kb/*`"+` paths when the destination file is still missing.
- Legacy `+"`notes/*`"+` files may still exist during migration; treat them as compatibility inputs, not the default recovery path.
`) + "\n"
	return os.WriteFile(path, []byte(content), 0o644)
}

const (
	managedKBIndexStartMarker = "<!-- agentteam-managed-kb:start -->"
	managedKBIndexEndMarker   = "<!-- agentteam-managed-kb:end -->"
)

type ManagedKnowledgeBaseIndexEntry struct {
	Name        string
	Description string
	SourceName  string
	RepoRole    string
	Path        string
	Branch      string
	Revision    string
}

// WriteManagedKnowledgeBaseIndex updates the daemon-owned managed KB section in
// kb/index.md while preserving user-maintained content elsewhere in the file.
func WriteManagedKnowledgeBaseIndex(workDir string, entries []ManagedKnowledgeBaseIndexEntry) error {
	if strings.TrimSpace(workDir) == "" {
		return nil
	}
	if err := ensureKBIndex(workDir); err != nil {
		return err
	}
	path := filepath.Join(workDir, kbIndexRelPath)
	data, err := os.ReadFile(path)
	if err != nil {
		return err
	}
	content := string(data)
	section := renderManagedKnowledgeBaseIndex(entries)
	start := strings.Index(content, managedKBIndexStartMarker)
	end := strings.Index(content, managedKBIndexEndMarker)
	if start >= 0 && end > start {
		end += len(managedKBIndexEndMarker)
		content = strings.TrimRight(content[:start], "\n") + "\n\n" + section + strings.TrimLeft(content[end:], "\n")
	} else {
		content = strings.TrimRight(content, "\n") + "\n\n" + section
	}
	if !strings.HasSuffix(content, "\n") {
		content += "\n"
	}
	return os.WriteFile(path, []byte(content), 0o644)
}

func renderManagedKnowledgeBaseIndex(entries []ManagedKnowledgeBaseIndexEntry) string {
	var b strings.Builder
	b.WriteString(managedKBIndexStartMarker)
	b.WriteString("\n")
	b.WriteString("## Managed Knowledge Bases\n\n")
	if len(entries) == 0 {
		b.WriteString("No managed MeshX knowledge bases are currently mounted for this agent.\n")
	} else {
		for _, entry := range entries {
			name := firstNonEmptyKBIndexString(entry.Name, "Untitled KB")
			b.WriteString("- **")
			b.WriteString(escapeManagedKBIndexText(name))
			b.WriteString("**")
			if desc := strings.TrimSpace(entry.Description); desc != "" {
				b.WriteString(": ")
				b.WriteString(escapeManagedKBIndexText(desc))
			}
			b.WriteString("\n")
			b.WriteString("  - Path: `")
			b.WriteString(filepath.ToSlash(strings.TrimSpace(entry.Path)))
			b.WriteString("`\n")
			if source := strings.TrimSpace(entry.SourceName); source != "" {
				b.WriteString("  - Source: ")
				b.WriteString(escapeManagedKBIndexText(source))
				b.WriteString("\n")
			}
			if role := strings.TrimSpace(entry.RepoRole); role != "" {
				b.WriteString("  - Repository role: `")
				b.WriteString(role)
				b.WriteString("`\n")
			}
			if branch := strings.TrimSpace(entry.Branch); branch != "" {
				b.WriteString("  - Branch: `")
				b.WriteString(branch)
				b.WriteString("`\n")
			}
		}
	}
	b.WriteString(managedKBIndexEndMarker)
	b.WriteString("\n")
	return b.String()
}

func firstNonEmptyKBIndexString(values ...string) string {
	for _, value := range values {
		if strings.TrimSpace(value) != "" {
			return strings.TrimSpace(value)
		}
	}
	return ""
}

func escapeManagedKBIndexText(value string) string {
	value = strings.ReplaceAll(strings.TrimSpace(value), "\n", " ")
	value = strings.ReplaceAll(value, "\r", " ")
	return value
}

func ensureLegacyKBImported(workDir string) error {
	for _, migration := range legacyKBMigrations {
		targetPath := filepath.Join(workDir, migration.canonicalRelPath)
		if _, err := os.Stat(targetPath); err == nil {
			continue
		} else if !errors.Is(err, fs.ErrNotExist) {
			return err
		}

		sourcePath := filepath.Join(workDir, migration.legacyRelPath)
		data, err := os.ReadFile(sourcePath)
		if errors.Is(err, fs.ErrNotExist) {
			continue
		}
		if err != nil {
			return err
		}
		if err := os.MkdirAll(filepath.Dir(targetPath), 0o755); err != nil {
			return err
		}
		if err := os.WriteFile(targetPath, data, 0o644); err != nil {
			return err
		}
	}
	return nil
}

func ensureOpenLoopsState(workDir string) error {
	path := filepath.Join(workDir, openLoopsRelPath)
	if _, err := os.Stat(path); err == nil {
		return nil
	} else if !errors.Is(err, fs.ErrNotExist) {
		return err
	}
	return touchOpenLoopsState(workDir, time.Now().UTC())
}

func ensureActiveTaskState(workDir string) error {
	path := filepath.Join(workDir, activeTaskRelPath)
	if _, err := os.Stat(path); err == nil {
		return nil
	} else if !errors.Is(err, fs.ErrNotExist) {
		return err
	}
	return writeJSONFile(path, activeTaskState{
		SchemaVersion:    localMemorySchema,
		UpdatedAt:        time.Now().UTC().Format(time.RFC3339),
		CurrentObjective: "No active task recorded yet.",
		NextStep:         "Recover only the context needed for the work at hand from this workspace; start with the configured memory index and the recovery files named in the Workspace & Memory instructions, then inspect relevant files, git state, task context, or recent history if needed.",
		RelevantFiles: []string{
			openLoopsRelPath,
			activeContextRelPath,
			kbIndexRelPath,
			archiveCheckpointsPath,
		},
	})
}

func ensureActiveContext(workDir string) error {
	path := filepath.Join(workDir, activeContextRelPath)
	if _, err := os.Stat(path); err == nil {
		return nil
	} else if !errors.Is(err, fs.ErrNotExist) {
		return err
	}

	var builder strings.Builder
	builder.WriteString("# Active Context\n\n")
	builder.WriteString("- Updated: ")
	builder.WriteString(time.Now().UTC().Format(time.RFC3339))
	builder.WriteString("\n- Status: No runtime checkpoint recorded yet.\n")
	builder.WriteString("- Structured task state: `")
	builder.WriteString(activeTaskRelPath)
	builder.WriteString("`\n- Open loops: `")
	builder.WriteString(openLoopsRelPath)
	builder.WriteString("`\n- Durable local knowledge index: `")
	builder.WriteString(kbIndexRelPath)
	builder.WriteString("`\n- Historical checkpoints: `")
	builder.WriteString(archiveCheckpointsPath)
	builder.WriteString("` (fallback only)\n")
	return os.WriteFile(path, []byte(builder.String()), 0o644)
}

func touchOpenLoopsState(workDir string, ts time.Time) error {
	path := filepath.Join(workDir, openLoopsRelPath)
	state := openLoopsState{
		SchemaVersion: localMemorySchema,
		UpdatedAt:     ts.UTC().Format(time.RFC3339),
		Items:         []string{},
	}
	if data, err := os.ReadFile(path); err == nil {
		var existing openLoopsState
		if json.Unmarshal(data, &existing) == nil && len(existing.Items) > 0 {
			state.Items = existing.Items
		}
	}
	return writeJSONFile(path, state)
}

func writeActiveTaskState(workDir string, checkpoint RecoveryCheckpoint) error {
	state := activeTaskState{
		SchemaVersion:    localMemorySchema,
		UpdatedAt:        checkpoint.Timestamp.UTC().Format(time.RFC3339),
		CurrentObjective: checkpoint.Summary,
		NextStep:         "Recover the context needed to continue the current or pending work from this workspace; start with the configured memory index and the recovery files named in the Workspace & Memory instructions, then inspect relevant files, git state, task context, or recent history if needed.",
		RelevantFiles: []string{
			openLoopsRelPath,
			activeContextRelPath,
			kbIndexRelPath,
			archiveCheckpointsPath,
		},
		LastCheckpoint: &checkpointState{
			Trigger:       checkpoint.Trigger,
			Summary:       checkpoint.Summary,
			SessionID:     checkpoint.SessionID,
			WakeTarget:    checkpoint.WakeTarget,
			WakeMessageID: checkpoint.WakeMessageID,
			Timestamp:     checkpoint.Timestamp.UTC().Format(time.RFC3339),
		},
	}
	return writeJSONFile(filepath.Join(workDir, activeTaskRelPath), state)
}

func writeActiveContext(workDir string, checkpoint RecoveryCheckpoint) error {
	var builder strings.Builder
	builder.WriteString("# Active Context\n\n")
	builder.WriteString("- Updated: ")
	builder.WriteString(checkpoint.Timestamp.UTC().Format(time.RFC3339))
	builder.WriteString("\n- Trigger: ")
	builder.WriteString(strings.TrimSpace(checkpoint.Trigger))
	builder.WriteString("\n- Summary: ")
	builder.WriteString(strings.TrimSpace(checkpoint.Summary))
	builder.WriteString("\n- Structured task state: `")
	builder.WriteString(activeTaskRelPath)
	builder.WriteString("`\n- Open loops: `")
	builder.WriteString(openLoopsRelPath)
	builder.WriteString("`\n- Durable local knowledge index: `")
	builder.WriteString(kbIndexRelPath)
	builder.WriteString("`\n- Historical checkpoints: `")
	builder.WriteString(archiveCheckpointsPath)
	builder.WriteString("` (fallback only)\n")
	if trimmed := strings.TrimSpace(checkpoint.SessionID); trimmed != "" {
		builder.WriteString("- Session: `")
		builder.WriteString(trimmed)
		builder.WriteString("`\n")
	}
	if trimmed := strings.TrimSpace(checkpoint.WakeTarget); trimmed != "" {
		builder.WriteString("- Wake target: `")
		builder.WriteString(trimmed)
		builder.WriteString("`\n")
	}
	if trimmed := strings.TrimSpace(checkpoint.WakeMessageID); trimmed != "" {
		builder.WriteString("- Wake message: `")
		builder.WriteString(trimmed)
		builder.WriteString("`\n")
	}
	return os.WriteFile(filepath.Join(workDir, activeContextRelPath), []byte(builder.String()), 0o644)
}

func appendArchiveCheckpoint(workDir string, checkpoint RecoveryCheckpoint) error {
	path := filepath.Join(workDir, archiveCheckpointsPath)
	if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
		return err
	}
	if _, err := os.Stat(path); errors.Is(err, fs.ErrNotExist) {
		if err := os.WriteFile(path, []byte(archiveCheckpointsHeader), 0o644); err != nil {
			return err
		}
	} else if err != nil {
		return err
	}
	f, err := os.OpenFile(path, os.O_APPEND|os.O_WRONLY, 0o644)
	if err != nil {
		return err
	}
	defer func() { _ = f.Close() }()
	_, err = f.WriteString("- " + checkpoint.Timestamp.UTC().Format(time.RFC3339) + " [" + strings.TrimSpace(checkpoint.Trigger) + "] " + strings.TrimSpace(checkpoint.Summary) + "\n")
	return err
}

func updateMemoryIndex(workDir, title string, checkpoint *RecoveryCheckpoint, warnOnOversizedMemory bool) (MemoryLayoutReport, error) {
	path := filepath.Join(workDir, memoryFileName)
	data, err := os.ReadFile(path)
	if err != nil && !errors.Is(err, fs.ErrNotExist) {
		return MemoryLayoutReport{}, err
	}
	content := string(data)
	if strings.TrimSpace(content) == "" {
		content = "# " + BoundedMemoryTitle(title) + "\n"
	}

	var lastErr error
	var sizeErr *memoryIndexSizeError
	for _, mode := range []memoryActiveContextMode{
		memoryActiveContextFull,
		memoryActiveContextConcise,
		memoryActiveContextPointer,
		memoryActiveContextTruncated,
	} {
		rendered := renderMemoryIndexContent(content, checkpoint, mode)
		if err := validateMemoryIndexSize(rendered); err != nil {
			lastErr = err
			if errors.As(err, &sizeErr) {
				// Keep trying more compact daemon-owned Active Context renderings.
				continue
			}
			continue
		}
		return MemoryLayoutReport{}, os.WriteFile(path, []byte(rendered), 0o644)
	}
	if warnOnOversizedMemory && sizeErr != nil {
		return memoryLayoutOversizedReport(sizeErr), nil
	}
	return MemoryLayoutReport{}, lastErr
}

func renderMemoryIndexContent(content string, checkpoint *RecoveryCheckpoint, mode memoryActiveContextMode) string {
	content = replaceMarkdownSection(content, "## Active Context", renderMemoryActiveContext(checkpoint, mode))
	content = replaceOrInsertMarkdownSection(content, []string{"## Recovery Read Order", "## Recovery Layout"}, "## Recovery Read Order", renderRecoveryReadOrder())
	content = ensureMarkdownSection(content, "## Memory Map", renderMemoryMap())
	content = replaceMarkdownSection(content, "## Historical Fallback", renderHistoricalFallback())
	content = replaceMarkdownSection(content, "## Portability Boundary", renderPortabilityBoundary())
	return content
}

func renderRecoveryReadOrder() string {
	return strings.TrimSpace(`
1. Read ` + "`state/active-task.json`" + ` for structured current-task state.
2. Read ` + "`state/open-loops.json`" + ` for pending follow-ups that still need closure.
3. Read ` + "`state/active-context.md`" + ` for the latest runtime-written context summary.
4. Read relevant ` + "`kb/*`" + ` files from the memory map below.
`)
}

func renderMemoryMap() string {
	return strings.TrimSpace(`
- ` + "`kb/index.md`" + `: durable local knowledge index for this concrete agent.
- ` + "`kb/domain.md`" + `: stable domain facts when this agent accumulates them.
- ` + "`kb/user-preferences.md`" + `: stable user preferences when this agent accumulates them.
- ` + "`kb/channel-context.md`" + `: durable channel context when this agent accumulates it.
- ` + "`kb/tasks/<task-id>.md`" + `: task-specific carry-over notes linked when relevant.
`)
}

func renderHistoricalFallback() string {
	return strings.TrimSpace(`
- ` + "`archive/checkpoints.md`" + `: append-only checkpoint history; read only when current state is insufficient.
- Legacy ` + "`notes/*`" + ` files may still exist during migration; treat them as compatibility inputs, not the default recovery path.
`)
}

func renderPortabilityBoundary() string {
	return strings.TrimSpace(`
- Portable role knowledge belongs in the Cloud Memory identity document (` + "`memory/system/identity.md`" + `), ` + "`.agents/skills/*/SKILL.md`" + `, and ` + "`.agents/knowledge/*`" + `.
- Agent-local memory does not travel during agent creation or Role changes.
`)
}

func renderMemoryActiveContext(checkpoint *RecoveryCheckpoint, mode memoryActiveContextMode) string {
	if checkpoint == nil {
		return strings.TrimSpace(`
- First startup.
- Structured recovery: ` + "`state/active-task.json`" + `
- Open loops: ` + "`state/open-loops.json`" + `
- Runtime summary: ` + "`state/active-context.md`" + `
`)
	}
	return strings.TrimSpace(`
- Last runtime update: ` + checkpoint.Timestamp.UTC().Format(time.RFC3339) + ` [` + strings.TrimSpace(checkpoint.Trigger) + `]
- Current focus: ` + memoryCheckpointSummary(checkpoint.Summary, mode) + `
- Structured recovery: ` + "`state/active-task.json`" + `
- Open loops: ` + "`state/open-loops.json`" + `
- Runtime summary: ` + "`state/active-context.md`" + `
`)
}

func replaceMarkdownSection(content, heading, body string) string {
	return replaceOrInsertMarkdownSection(content, []string{heading}, heading, body)
}

func replaceOrInsertMarkdownSection(content string, replaceHeadings []string, heading, body string) string {
	lines := strings.Split(content, "\n")
	headingIndex := -1
	for i, line := range lines {
		if containsHeading(replaceHeadings, strings.TrimSpace(line)) {
			headingIndex = i
			break
		}
	}

	bodyLines := strings.Split(strings.TrimRight(body, "\n"), "\n")
	if headingIndex < 0 {
		content = strings.TrimRight(content, "\n")
		if content == "" {
			return heading + "\n" + strings.Join(bodyLines, "\n") + "\n"
		}
		return content + "\n\n" + heading + "\n" + strings.Join(bodyLines, "\n") + "\n"
	}

	endIndex := len(lines)
	for i := headingIndex + 1; i < len(lines); i++ {
		if strings.HasPrefix(strings.TrimSpace(lines[i]), "## ") {
			endIndex = i
			break
		}
	}

	replaced := make([]string, 0, len(lines)-(endIndex-headingIndex)+1+len(bodyLines))
	replaced = append(replaced, lines[:headingIndex]...)
	replaced = append(replaced, heading)
	replaced = append(replaced, bodyLines...)
	replaced = append(replaced, lines[endIndex:]...)
	return strings.Join(replaced, "\n")
}

func ensureMarkdownSection(content, heading, body string) string {
	lines := strings.Split(content, "\n")
	for _, line := range lines {
		if strings.TrimSpace(line) == heading {
			return content
		}
	}
	return replaceMarkdownSection(content, heading, body)
}

func containsHeading(headings []string, heading string) bool {
	for _, candidate := range headings {
		if candidate == heading {
			return true
		}
	}
	return false
}

func validateMemoryIndexSize(content string) error {
	characters := utf8.RuneCountInString(strings.TrimSpace(content))
	if characters <= maxMemoryFileCharacters {
		return nil
	}
	return &memoryIndexSizeError{limit: maxMemoryFileCharacters, got: characters}
}

func memoryLayoutOversizedReport(sizeErr *memoryIndexSizeError) MemoryLayoutReport {
	if sizeErr == nil {
		return MemoryLayoutReport{}
	}
	return MemoryLayoutReport{
		MemoryIndexOversized:  true,
		MemoryIndexCharacters: sizeErr.got,
		MemoryIndexLimit:      sizeErr.limit,
		Warning: fmt.Sprintf(
			"MEMORY.md is still over the recommended startup budget after daemon compaction (%d/%d characters). The agent will continue to start, but reading an oversized MEMORY.md can consume excessive context. Move detailed content into state/, kb/, or archive/ and keep only links plus one-line summaries in MEMORY.md.",
			sizeErr.got,
			sizeErr.limit,
		),
	}
}

// BoundedMemoryTitle returns a MEMORY.md H1-safe title within the daemon-owned
// inline title budget.
func BoundedMemoryTitle(title string) string {
	return truncateRunes(fallbackMemoryTitle(title), memoryTitleMaxCharacters, "…")
}

func memoryCheckpointSummary(summary string, mode memoryActiveContextMode) string {
	trimmed := strings.TrimSpace(summary)
	switch mode {
	case memoryActiveContextFull:
		return trimmed
	case memoryActiveContextConcise:
		return conciseMemoryCheckpointSummary(trimmed)
	case memoryActiveContextPointer:
		return memoryCheckpointPointerSummary()
	case memoryActiveContextTruncated:
		return truncateRunes(trimmed, memoryCheckpointSummaryMaxCharacters, "… (full text in state/active-task.json and state/active-context.md)")
	default:
		return trimmed
	}
}

func conciseMemoryCheckpointSummary(summary string) string {
	if summary == "" {
		return memoryCheckpointPointerSummary()
	}
	if prefix, _, ok := strings.Cut(summary, ": "); ok {
		prefix = strings.TrimSpace(prefix)
		if prefix != "" && utf8.RuneCountInString(prefix) <= memoryCheckpointContextMaxCharacters {
			return prefix + ". Full text in state/active-task.json and state/active-context.md."
		}
	}
	return memoryCheckpointPointerSummary()
}

func memoryCheckpointPointerSummary() string {
	return "Long checkpoint summary omitted from MEMORY.md; full text in state/active-task.json and state/active-context.md."
}

func truncateRunes(value string, maxCharacters int, suffix string) string {
	trimmed := strings.TrimSpace(value)
	if maxCharacters <= 0 || utf8.RuneCountInString(trimmed) <= maxCharacters {
		return trimmed
	}
	suffixRunes := []rune(suffix)
	if len(suffixRunes) >= maxCharacters {
		return string(suffixRunes[:maxCharacters])
	}
	keep := maxCharacters - len(suffixRunes)
	valueRunes := []rune(trimmed)
	return string(valueRunes[:keep]) + suffix
}

func writeJSONFile(path string, value any) error {
	data, err := json.MarshalIndent(value, "", "  ")
	if err != nil {
		return err
	}
	data = append(data, '\n')
	return os.WriteFile(path, data, 0o644)
}

func fallbackMemoryTitle(title string) string {
	trimmed := strings.TrimSpace(title)
	if trimmed == "" {
		return "Local Memory"
	}
	return trimmed
}
