// persistor.go — JSONL append-only persistence for pathway memory.
//
// Each event is one JSON line. Append is O(1) (open append, write,
// close — Go's *os.File default fsync policy is "rely on OS" which
// is fine here; correctness on power-loss is best-effort, not
// transactional). Replay reads the file once at startup.
//
// Corruption recovery: malformed lines log a warn (counted in
// Replay's return) but do not stop the load. Partial state is
// better than no state for an agent substrate.
//
// What's NOT here:
//   - Compaction. JSONL grows linearly with mutations; below 100K
//     traces this is fine. Compaction will land when needed and
//     will emit a snapshot file + tail JSONL.
//   - fsync per write. We rely on the OS's eventual fsync; trace
//     loss on hard crash is acceptable for the substrate's
//     "remember most things" guarantee.

package pathway

import (
	"bufio"
	"encoding/json"
	"errors"
	"fmt"
	"io/fs"
	"log/slog"
	"os"
	"path/filepath"
)

// Persistor wraps a single JSONL file. Construct with NewPersistor;
// it does NOT load on construction — callers must call Store.Load()
// to replay.
type Persistor struct {
	path string
}

// NewPersistor returns a persistor for the given file path. The
// parent directory is created on demand. The file is created lazily
// on first Append.
func NewPersistor(path string) (*Persistor, error) {
	if path == "" {
		return nil, errors.New("pathway: persistor path is empty")
	}
	if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
		return nil, fmt.Errorf("pathway: create dir: %w", err)
	}
	return &Persistor{path: path}, nil
}

// Path returns the underlying file path. Useful for tests + logs.
func (p *Persistor) Path() string { return p.path }

// Append writes one event to the JSONL log. Each call opens the
// file in append mode, writes one line, and closes — simple but
// correct. A pooled persistent fd is a future optimization if
// profiling shows append-rate matters.
func (p *Persistor) Append(e event) error {
	line, err := json.Marshal(e)
	if err != nil {
		return fmt.Errorf("pathway: marshal event: %w", err)
	}
	f, err := os.OpenFile(p.path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
	if err != nil {
		return fmt.Errorf("pathway: open log: %w", err)
	}
	defer f.Close()
	if _, err := f.Write(line); err != nil {
		return fmt.Errorf("pathway: write event: %w", err)
	}
	if _, err := f.Write([]byte{'\n'}); err != nil {
		return fmt.Errorf("pathway: write newline: %w", err)
	}
	return nil
}

// Replay reads the log line-by-line and invokes apply for each
// event. Returns the count of events successfully applied. A
// missing file is NOT an error (means "no prior state"); a
// partially-corrupt file logs warns and continues.
func (p *Persistor) Replay(apply func(event) error) (int, error) {
	f, err := os.Open(p.path)
	if errors.Is(err, fs.ErrNotExist) {
		return 0, nil
	}
	if err != nil {
		return 0, fmt.Errorf("pathway: open log: %w", err)
	}
	defer f.Close()

	scanner := bufio.NewScanner(f)
	// Big buffer for unusually long content — 1 MiB per line cap.
	buf := make([]byte, 0, 64*1024)
	scanner.Buffer(buf, 1<<20)

	applied := 0
	skipped := 0
	lineNo := 0
	for scanner.Scan() {
		lineNo++
		raw := scanner.Bytes()
		if len(raw) == 0 {
			continue
		}
		var e event
		if err := json.Unmarshal(raw, &e); err != nil {
			slog.Warn("pathway: replay skipped malformed line",
				"path", p.path, "line", lineNo, "err", err.Error())
			skipped++
			continue
		}
		if err := apply(e); err != nil {
			slog.Warn("pathway: replay event apply failed",
				"path", p.path, "line", lineNo, "op", e.Op, "err", err.Error())
			skipped++
			continue
		}
		applied++
	}
	if err := scanner.Err(); err != nil {
		return applied, fmt.Errorf("pathway: scan log: %w", err)
	}
	if skipped > 0 {
		slog.Info("pathway: replay completed with skips",
			"path", p.path, "applied", applied, "skipped", skipped)
	}
	return applied, nil
}
