// Package store is the SQLite persistence layer (modernc.org/sqlite,
// no CGo). It owns the schema, exposes typed accessors, and hides
// the database/sql plumbing from the rest of the server.
package store

import (
	"context"
	"database/sql"
	"errors"
	"fmt"
	"net/url"
	"time"

	_ "modernc.org/sqlite" // register the "sqlite" driver
)

// ErrNotFound is returned by accessors when a lookup misses.
var ErrNotFound = errors.New("store: not found")

// Store is a thin wrapper around *sql.DB that exposes the typed
// accessors used by the rest of the server. Callers should use the
// provided methods rather than reaching into DB() directly.
type Store struct {
	db *sql.DB
}

// Open opens (or creates) the SQLite database at path, applies all
// pending migrations, and returns a ready-to-use Store.
//
// The DSN sets:
//   - _pragma=foreign_keys(1) — referential integrity is on
//   - _pragma=journal_mode(WAL) — concurrent reads vs writes
//   - _pragma=busy_timeout(5000) — wait 5s on lock contention
//   - _time_format=sqlite — RFC 3339 read/write of TEXT timestamps
//
// Empty path uses an in-memory DB (useful for tests).
func Open(ctx context.Context, path string) (*Store, error) {
	dsn := buildDSN(path)
	db, err := sql.Open("sqlite", dsn)
	if err != nil {
		return nil, fmt.Errorf("open %q: %w", path, err)
	}
	// modernc.org/sqlite is not safe for arbitrary high parallelism on
	// a single file. WAL helps, but 1 writer + multiple readers is the
	// only safe shape. Cap connections to keep that property explicit.
	db.SetMaxOpenConns(8)
	db.SetMaxIdleConns(4)
	db.SetConnMaxLifetime(time.Hour)

	pingCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
	defer cancel()
	if err := db.PingContext(pingCtx); err != nil {
		_ = db.Close()
		return nil, fmt.Errorf("ping: %w", err)
	}

	if err := migrate(ctx, db); err != nil {
		_ = db.Close()
		return nil, fmt.Errorf("migrate: %w", err)
	}

	return &Store{db: db}, nil
}

// Close releases the underlying DB handle.
func (s *Store) Close() error { return s.db.Close() }

// DB returns the underlying *sql.DB. Reserved for tests and migrations
// — production code should add a typed method to this package instead.
func (s *Store) DB() *sql.DB { return s.db }

func buildDSN(path string) string {
	if path == "" {
		// Shared cache + named in-memory db so multiple connections see
		// the same data — needed because we cap MaxOpenConns above.
		return "file::memory:?cache=shared&_pragma=foreign_keys(1)&_pragma=journal_mode(WAL)&_pragma=busy_timeout(5000)"
	}
	q := url.Values{}
	q.Add("_pragma", "foreign_keys(1)")
	q.Add("_pragma", "journal_mode(WAL)")
	q.Add("_pragma", "busy_timeout(5000)")
	q.Add("_pragma", "synchronous(NORMAL)")
	return "file:" + path + "?" + q.Encode()
}
