package memorysearch

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"strings"
	"time"
)

// pendingFileName is the journal file recording an in-flight reconcile.
// Its presence at startup triggers recovery; its absence means no work
// was in progress when the previous owner died.
const pendingFileName = "pending.json"

// currentFileName is the published pointer to the serviceable generation.
// OpenGeneration reads it to resolve which generation basename to open.
// Only the basename (relative to generations/) is stored — never an
// absolute path — so the pointer cannot be forged into opening a
// generation outside the resolver-derived derivative root.
const currentFileName = "current.json"

// PendingPhase categorizes how far an in-flight reconcile progressed.
// Recovery branches on this value to decide whether to drop, finish, or
// re-publish the candidate.
type PendingPhase string

const (
	// PendingPhaseScanning: Scan finished; Build has not allocated a
	// generation yet. No candidate to clean up.
	PendingPhaseScanning PendingPhase = "scanning"
	// PendingPhaseBuilding: Build allocated a generation directory and is
	// indexing. A crash here leaves a partial generation without
	// BUILD_COMPLETE; recovery must drop it.
	PendingPhaseBuilding PendingPhase = "building"
	// PendingPhasePublishing: Build completed and the candidate carries
	// BUILD_COMPLETE, but the current pointer has not been atomically
	// swapped yet. Recovery must verify the marker and finish the swap.
	PendingPhasePublishing PendingPhase = "publishing"
	// PendingPhaseClearing: Publish succeeded; GC of older generations
	// was in progress. Recovery must finish GC and clear the journal.
	PendingPhaseClearing PendingPhase = "clearing"
)

// Pending is the journal entry written before each phase transition of a
// reconcile. It carries enough state for recovery to finish or rollback
// without re-running Scan/Build, and enough provenance (binding
// fingerprint, target revision) for an auditor to detect a stale or
// mismatched journal.
type Pending struct {
	// BindingFingerprint is the binding fingerprint at the time the
	// journal was written. Recovery refuses to act if the on-disk binding
	// no longer matches — the journal belongs to a previous binding.
	BindingFingerprint string `json:"binding_fingerprint"`
	// TargetRevision is the SourceSnapshot.Revision the reconcile is
	// trying to publish. Recovery only finishes the publish if the
	// candidate manifest's SourceRevision still matches this value.
	TargetRevision string `json:"target_revision"`
	// Phase records the last completed phase. See PendingPhase docs.
	Phase PendingPhase `json:"phase"`
	// CandidateBasename is the generation directory name (relative to
	// generations/) when Phase >= Building. Empty for Scanning.
	CandidateBasename string `json:"candidate_basename,omitempty"`
	// PreviousBasename is the generation that was current before this
	// reconcile started, retained as a rollback target. Recovery may
	// need it to keep the previous generation serviceable if publish
	// cannot complete.
	PreviousBasename string `json:"previous_basename,omitempty"`
	// StartedAt is when the reconcile began, for diagnostics and GC.
	StartedAt time.Time `json:"started_at"`
	// UpdatedAt is when the journal was last written, for diagnostics.
	UpdatedAt time.Time `json:"updated_at"`
}

// Current is the published pointer to the serviceable generation. Its
// presence means a generation has been validated, has BUILD_COMPLETE,
// and may be opened by OpenGeneration. Its absence or corruption means
// no serviceable state.
type Current struct {
	// BindingFingerprint ties the pointer to a specific binding. Readers
	// refuse to use a pointer whose fingerprint does not match the
	// caller's binding.
	BindingFingerprint string `json:"binding_fingerprint"`
	// GenerationBasename is the generation directory name relative to
	// generations/. OpenGeneration reconstructs the absolute path as
	// <derivativeRoot>/generations/<GenerationBasename>.
	GenerationBasename string `json:"generation_basename"`
	// SourceRevision is the manifest's SourceRevision at publish time.
	// Callers compare this against a fresh Scan to detect staleness.
	SourceRevision string `json:"source_revision"`
	// PublishedAt is when the pointer was swapped in.
	PublishedAt time.Time `json:"published_at"`
}

// writePendingAtomic crash-safely writes the pending journal: marshal →
// temp → fsync → rename → fsync dir. The phase transition is durable
// before the caller proceeds to the next phase, so a crash mid-phase
// leaves a journal that recovery can interpret.
//
// All basenames in p are validated before any byte hits disk. pending.json
// is attacker-writable state in the threat model — a forged entry with
// CandidateBasename = "../../memory" must never reach os.RemoveAll in
// recovery, so the writer refuses to record such a value in the first
// place. Recovery re-validates on load to defend against corruption that
// bypassed this writer (manual edit, partial flush from an older binary).
func writePendingAtomic(derivativeRoot string, p Pending) error {
	if err := validateBasename(p.CandidateBasename); err != nil {
		return fmt.Errorf("memory search: write pending: candidate basename: %w", err)
	}
	if err := validateBasename(p.PreviousBasename); err != nil {
		return fmt.Errorf("memory search: write pending: previous basename: %w", err)
	}
	pendingPath := filepath.Join(derivativeRoot, pendingFileName)
	return writeJSONAtomic(pendingPath, p)
}

// clearPending removes the journal. Called after a successful publish+GC
// or after recovery drops a stale candidate. Removal is via direct os.Remove
// followed by an fsync of derivativeRoot: the unlink-then-fsync ordering
// makes the "no work in flight" signal durable so a crash immediately after
// clearPending cannot restore a stale journal that recovery would then
// act on twice. Without the dir fsync the comment "absence of the file IS
// the signal" would be a lie across a power loss.
func clearPending(derivativeRoot string) error {
	pendingPath := filepath.Join(derivativeRoot, pendingFileName)
	if err := os.Remove(pendingPath); err != nil {
		if errors.Is(err, os.ErrNotExist) {
			return nil
		}
		return fmt.Errorf("memory search: clear pending: %w", err)
	}
	if err := syncDir(derivativeRoot); err != nil {
		return fmt.Errorf("memory search: clear pending fsync dir: %w", err)
	}
	return nil
}

// loadPending reads the journal. Returns an os.ErrNotExist-wrapped error
// if no journal is present, which is the normal "no work in flight"
// state.
func loadPending(derivativeRoot string) (*Pending, error) {
	pendingPath := filepath.Join(derivativeRoot, pendingFileName)
	raw, err := os.ReadFile(pendingPath)
	if err != nil {
		return nil, err
	}
	var p Pending
	if err := json.Unmarshal(raw, &p); err != nil {
		return nil, fmt.Errorf("memory search: parse pending: %w", err)
	}
	return &p, nil
}

// writeCurrentAtomic crash-safely publishes a new current pointer. The
// previous pointer is overwritten; the previous generation directory is
// retained on disk as a rollback target until GC sheds it. GenerationBasename
// is validated before the write so a forged pointer cannot name a path
// outside generations/.
func writeCurrentAtomic(derivativeRoot string, c Current) error {
	if err := validateBasename(c.GenerationBasename); err != nil {
		return fmt.Errorf("memory search: write current: generation basename: %w", err)
	}
	currentPath := filepath.Join(derivativeRoot, currentFileName)
	return writeJSONAtomic(currentPath, c)
}

// loadCurrent reads the published pointer. Returns an os.ErrNotExist-
// wrapped error if no generation has ever been published. The returned
// Current is NOT validated; callers that go on to dereference
// GenerationBasename must call validateBasename themselves (or use
// CurrentBasenameForOpen, which does it).
func loadCurrent(derivativeRoot string) (*Current, error) {
	currentPath := filepath.Join(derivativeRoot, currentFileName)
	raw, err := os.ReadFile(currentPath)
	if err != nil {
		return nil, err
	}
	var c Current
	if err := json.Unmarshal(raw, &c); err != nil {
		return nil, fmt.Errorf("memory search: parse current: %w", err)
	}
	return &c, nil
}

// writeJSONAtomic is the shared crash-safe writer for pending.json and
// current.json. Marshal → temp file in the same dir → fsync file →
// rename over target → fsync dir. The temp-in-same-dir guarantee means
// the rename is atomic on the same filesystem. The dir fsync makes the
// rename durable so a crash after return cannot lose the new entry.
func writeJSONAtomic(targetPath string, v any) error {
	data, err := json.MarshalIndent(v, "", "  ")
	if err != nil {
		return fmt.Errorf("memory search: marshal: %w", err)
	}
	dir := filepath.Dir(targetPath)
	tmp, err := os.CreateTemp(dir, ".json-*.tmp")
	if err != nil {
		return fmt.Errorf("memory search: create temp: %w", err)
	}
	tmpName := tmp.Name()
	cleanup := func() { _ = os.Remove(tmpName) }
	if _, err := tmp.Write(data); err != nil {
		_ = tmp.Close()
		cleanup()
		return fmt.Errorf("memory search: write temp: %w", err)
	}
	if err := tmp.Sync(); err != nil {
		_ = tmp.Close()
		cleanup()
		return fmt.Errorf("memory search: fsync temp: %w", err)
	}
	if err := tmp.Close(); err != nil {
		cleanup()
		return fmt.Errorf("memory search: close temp: %w", err)
	}
	if err := os.Rename(tmpName, targetPath); err != nil {
		cleanup()
		return fmt.Errorf("memory search: rename: %w", err)
	}
	if err := syncDir(dir); err != nil {
		return fmt.Errorf("memory search: fsync dir: %w", err)
	}
	return nil
}

// CurrentBasenameForOpen resolves the current pointer's generation
// basename into an absolute path under the derivative root. Callers
// pass this to OpenGeneration. Returns ErrNotReady if no current
// pointer exists or the binding fingerprint does not match. The loaded
// basename is validated before the Join: a forged current.json with
// GenerationBasename = "../../memory" is rejected here rather than
// turned into a path that points outside generations/.
func CurrentBasenameForOpen(ctx context.Context, derivativeRoot, expectedFingerprint string) (string, error) {
	if err := ctx.Err(); err != nil {
		return "", err
	}
	c, err := loadCurrent(derivativeRoot)
	if err != nil {
		if errors.Is(err, os.ErrNotExist) {
			return "", ErrNotReady
		}
		return "", err
	}
	if c.BindingFingerprint != expectedFingerprint {
		return "", fmt.Errorf("%w: current pointer fingerprint %q does not match expected %q",
			ErrNotReady, c.BindingFingerprint, expectedFingerprint)
	}
	if c.GenerationBasename == "" {
		return "", fmt.Errorf("%w: current pointer missing generation basename", ErrNotReady)
	}
	if err := validateBasename(c.GenerationBasename); err != nil {
		return "", fmt.Errorf("%w: current pointer generation basename: %v", ErrNotReady, err)
	}
	return filepath.Join(derivativeRoot, generationsDirName, c.GenerationBasename), nil
}

// validateBasename enforces that s is a single path component — a name
// with no separator, no parent reference, and no ambiguity. Empty is
// allowed (optional journal fields). Any non-empty value must satisfy
// filepath.Base(s) == s and must not be "." or "..".
//
// This is the trust boundary for every on-disk path derived from a journal
// or current pointer: pending.json and current.json are attacker-writable
// state (a process that crashes mid-write leaves them intact on disk, and
// an attacker with write access to the derivative root can plant a forged
// value). A forged CandidateBasename of "../../memory" must never reach
// os.Remove / os.Open. Every Join(generationsDir, basename),
// Join(derivativeRoot, current.json), and analogous call site MUST gate
// on this check.
func validateBasename(s string) error {
	if s == "" {
		return nil
	}
	if s == "." || s == ".." {
		return fmt.Errorf("%w: basename is %q", ErrInvalidBasename, s)
	}
	if strings.ContainsAny(s, `/\`) {
		return fmt.Errorf("%w: %q contains a path separator", ErrInvalidBasename, s)
	}
	if filepath.Base(s) != s {
		return fmt.Errorf("%w: %q is not a single path component", ErrInvalidBasename, s)
	}
	return nil
}

// ErrNotReady signals that no serviceable generation exists for the
// binding. Query paths return this so callers can report "degraded" to
// the user.
var ErrNotReady = errors.New("memory search: not ready")

// ErrInvalidBasename signals that a journal or current pointer carried a
// basename that was not a single path component. Recovery and query paths
// treat this as fail-closed: they refuse to act on the entry and return
// the error to the operator.
var ErrInvalidBasename = errors.New("memory search: invalid generation basename")
