// Package recorder owns decision records and their local persistence.
package recorder

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

const (
	fileInterval   = time.Hour
	fileTimeLayout = "2006-01-02T15"
	filePrefix     = "decisions-"
	fileSuffix     = ".jsonl"
)

// Recorder persists finalized decision records.
type Recorder interface {
	Record(*Decision) error
}

// JSONLRecorder appends decision records to hourly UTC JSONL files.
type JSONLRecorder struct {
	Dir string

	mu sync.Mutex
}

var _ Recorder = (*JSONLRecorder)(nil)

// NewJSONLRecorder creates the recorder and its landing directory.
func NewJSONLRecorder(dir string) (*JSONLRecorder, error) {
	if dir == "" {
		return nil, fmt.Errorf("decision record directory is empty")
	}
	if err := os.MkdirAll(dir, 0o755); err != nil {
		return nil, fmt.Errorf("create decision record directory: %w", err)
	}
	return &JSONLRecorder{Dir: dir}, nil
}

// FilePath returns the hourly UTC file for a decision occurrence time.
func FilePath(dir string, decisionTime time.Time) string {
	fileStart := decisionTime.UTC().Truncate(fileInterval)
	return filepath.Join(dir, filePrefix+fileStart.Format(fileTimeLayout)+fileSuffix)
}

// ClosedFiles returns hourly files whose interval end plus grace is not after now.
func ClosedFiles(dir string, now time.Time, grace time.Duration) ([]string, error) {
	entries, err := os.ReadDir(dir)
	if err != nil {
		return nil, fmt.Errorf("read decision record directory: %w", err)
	}

	closed := make([]string, 0, len(entries))
	for _, entry := range entries {
		if entry.IsDir() {
			continue
		}
		fileStart, ok := fileStartTime(entry.Name())
		if !ok || fileStart.Add(fileInterval+grace).After(now.UTC()) {
			continue
		}
		closed = append(closed, filepath.Join(dir, entry.Name()))
	}
	sort.Strings(closed)
	return closed, nil
}

// Record appends one JSON-encoded decision record as one line.
func (w *JSONLRecorder) Record(decision *Decision) error {
	line, err := json.Marshal(decision)
	if err != nil {
		return fmt.Errorf("marshal decision record: %w", err)
	}

	w.mu.Lock()
	defer w.mu.Unlock()

	file, err := os.OpenFile(FilePath(w.Dir, decision.DecisionTime), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
	if err != nil {
		return fmt.Errorf("open decision record file: %w", err)
	}
	defer func() { _ = file.Close() }()
	if _, err := file.Write(append(line, '\n')); err != nil {
		return fmt.Errorf("write decision record: %w", err)
	}
	return nil
}

func fileStartTime(name string) (time.Time, bool) {
	if !strings.HasPrefix(name, filePrefix) || !strings.HasSuffix(name, fileSuffix) {
		return time.Time{}, false
	}
	value := strings.TrimSuffix(strings.TrimPrefix(name, filePrefix), fileSuffix)
	fileStart, err := time.Parse(fileTimeLayout, value)
	return fileStart, err == nil
}
