// Package store is maven's persistent state layer.
//
// The layer is append-only: a wrong fact is superseded, not overwritten.
// Current value for a key = the latest non-voided fact row.
//
// Schema lives in schema.sql and is applied idempotently on Open.
package store

import (
	"context"
	"database/sql"
	_ "embed" // schema.sql
	"errors"
	"fmt"
	"time"

	_ "modernc.org/sqlite"
)

//go:embed schema.sql
var schemaSQL string

// FactKind — self | env | config. The loop only evaluates predicates against
// `self` and `config` rows; `env` feeds env predicates (calendar/weather/health).
type FactKind string

const (
	KindSelf   FactKind = "self"
	KindEnv    FactKind = "env"
	KindConfig FactKind = "config"
)

// Fact — one observation. ts is valid-time (true-as-of), not insert-time.
type Fact struct {
	ID         int64
	Ts         time.Time
	Kind       FactKind
	Key        string
	Value      string // raw json if structured
	Source     string // tap:* | infer:* | poll:* | ambient | promote | feedback
	Confidence float64
	VoidsID    sql.NullInt64

	// Subject, EntityID, ResolutionState — entity-aware memory (Vikunja #279).
	// Subject is the free-text "who/what this fact is about" supplied at write
	// time by WriteFactAboutSubject; empty means the fact isn't about a
	// resolvable entity (ResolutionState stays "none"). EntityID is the
	// canonical Nexus entity_id once the enrichment worker resolves Subject.
	Subject         string
	EntityID        sql.NullString
	ResolutionState FactResolutionState
}

// FactResolutionState — where a fact's Subject stands in Nexus entity
// resolution. "none" = no subject given (most facts). "pending" = subject
// given, not yet resolved. Terminal states: "resolved", "ambiguous" (Nexus
// returned candidates, not stored — matches the ecosystem's ambiguity-blocks
// invariant), "not_found".
type FactResolutionState string

const (
	ResolutionNone      FactResolutionState = "none"
	ResolutionPending   FactResolutionState = "pending"
	ResolutionResolved  FactResolutionState = "resolved"
	ResolutionAmbiguous FactResolutionState = "ambiguous"
	ResolutionNotFound  FactResolutionState = "not_found"
)

// Bucket — presence hysteresis state.
type Bucket string

const (
	Present Bucket = "present"
	Away    Bucket = "away"
)

// Store is the persistent state layer. All writes are append-only; nothing
// here performs an UPDATE of a fact value (status-flips on reminders/nudges
// are the documented exceptions — they mutate small state-machine columns).
type Store struct {
	db  *sql.DB
	enc *encState // nil ⇒ plaintext store (dev/CI); set ⇒ re-encrypt on Close
}

// openAt opens or creates the sqlite database at path and applies schema +
// migrations. This is the raw plaintext open used by both Open (plaintext
// store) and OpenEncrypted (the tmpfs working copy).
//
// Pragmas (WAL, NORMAL, FK on, busy_timeout) are set in schema.sql and re-applied
// per connection on open via the modernc driver DSN.
func openAt(ctx context.Context, path string) (*sql.DB, error) {
	// `_pragma=busy_timeout(5000)` etc. embed cleanly; schema.sql sets them too.
	dsn := fmt.Sprintf("file:%s?_pragma=busy_timeout(5000)&_pragma=foreign_keys(1)", path)
	db, err := sql.Open("sqlite", dsn)
	if err != nil {
		return nil, fmt.Errorf("open %s: %w", path, err)
	}
	// One connection, so every statement is serialised at the database and no
	// caller above needs a lock of its own. internal/ipc's Server relies on
	// exactly this, which is why the cap is an invariant rather than a tuning
	// knob: raising it moves the serialisation guarantee somewhere it is not
	// written down.
	//
	// Measured on 07-08-2026 (V-642, docs/evals/2026-08-07-store-connection-cap.md).
	// WAL exists to let readers run beside one writer, and the cap gives that
	// up, but reads do not queue: p50 594µs against 525µs at a cap of four,
	// while write throughput more than halves. The one thing the cap cannot
	// survive is a long-lived transaction, which holds the only connection and
	// stalls every read for its lifetime. So the store begins none, and
	// TestConnCap_ReadBlocksBehindOpenSnapshot is the standing measurement of
	// what re-adding one would cost.
	db.SetMaxOpenConns(1)
	if _, err := db.ExecContext(ctx, schemaSQL); err != nil {
		if closeErr := db.Close(); closeErr != nil {
			return nil, fmt.Errorf("apply schema: %w (close: %v)", err, closeErr)
		}
		return nil, fmt.Errorf("apply schema: %w", err)
	}
	if err := migrate(ctx, db); err != nil {
		if closeErr := db.Close(); closeErr != nil {
			return nil, fmt.Errorf("migrate: %w (close: %v)", err, closeErr)
		}
		return nil, fmt.Errorf("migrate: %w", err)
	}
	return db, nil
}

// Open opens or creates a PLAINTEXT sqlite database at path. Used by tests and
// by any deployment that keeps the db unencrypted (CI, dev). Production goes
// through OpenEncrypted.
func Open(ctx context.Context, path string) (*Store, error) {
	db, err := openAt(ctx, path)
	if err != nil {
		return nil, err
	}
	return &Store{db: db}, nil
}

// Close checkpoints, releases the database handle, and — for an encrypted
// store — re-encrypts the tmpfs working copy back to the on-disk ciphertext
// file atomically, then wipes the plaintext copy and zeroes the key.
func (s *Store) Close() error {
	if s.enc == nil {
		return s.db.Close()
	}
	return s.enc.closeAndSeal(s.db)
}

var (
	// ErrNoFact — no non-voided row exists for this key.
	ErrNoFact = errors.New("store: no fact for key")
	// ErrConfidence — a write attempted to use a confidence outside (0,1.0].
	ErrConfidence = errors.New("store: confidence must be in (0.0, 1.0]")
	// ErrVoidsMissing — a correction pointed at a nonexistent fact.
	ErrVoidsMissing = errors.New("store: voids_id does not reference an existing fact")
)
