// Package credentialstore provides a durable, dialect-abstracted repository
// for principal and token records. It selects a SQL driver from the database
// URL (PostgreSQL for postgres:// or postgresql://, SQLite for file paths),
// runs idempotent schema migrations, and exposes an opaque Close that the
// Control Plane wires to its server lifetime.
//
// The store is optional: callers pass an empty URL and receive a nil Store
// without an error, preserving the legacy startup path. Any non-empty URL
// must resolve to a working connection; otherwise the caller returns a fatal
// error before serving.
package credentialstore

import (
	"context"
	"database/sql"
	"fmt"
	"strings"

	_ "github.com/jackc/pgx/v5/stdlib"
	_ "modernc.org/sqlite"
)

const (
	dialectPostgres = "pgx"
	dialectSQLite   = "sqlite"
)

// dialect selects the driver name from a database URL.
//
//	Dialects:
//	  postgres:// or postgresql:// -> "pgx" (registered by jackc/pgx/v5/stdlib)
//	  file path or "file:..."      -> "sqlite" (registered by modernc.org/sqlite)
//	  anything else               -> "" (unknown; caller rejects)
func dialectFromURL(rawURL string) (string, error) {
	trimmed := strings.TrimSpace(rawURL)
	if trimmed == "" {
		return "", fmt.Errorf("credentialstore: empty database url")
	}
	lower := strings.ToLower(trimmed)
	switch {
	case strings.HasPrefix(lower, "postgres://") || strings.HasPrefix(lower, "postgresql://"):
		return dialectPostgres, nil
	case strings.HasPrefix(lower, "file:") || !strings.Contains(lower, "://"):
		// Treat anything without a scheme as a file path for SQLite.
		return dialectSQLite, nil
	default:
		return "", fmt.Errorf("credentialstore: unsupported database url scheme: %s", schemeOf(trimmed))
	}
}

func schemeOf(rawURL string) string {
	if i := strings.Index(rawURL, "://"); i >= 0 {
		return rawURL[:i]
	}
	return ""
}

// Store holds the underlying sql.DB handle and selected dialect. It is safe
// for concurrent use through the standard database/sql pool.
type Store struct {
	db          *sql.DB
	dialect     string
	keyRegistry EnvelopeKeyRegistry
}

// Option configures optional Store behaviors.
type Option func(*Store)

// WithEnvelopeKeyRegistry configures the store's envelope key validator.
func WithEnvelopeKeyRegistry(registry EnvelopeKeyRegistry) Option {
	return func(s *Store) {
		if registry != nil {
			s.keyRegistry = registry
		}
	}
}

// KeyRegistry returns the configured EnvelopeKeyRegistry or an unavailable default.
func (s *Store) KeyRegistry() EnvelopeKeyRegistry {
	if s == nil || s.keyRegistry == nil {
		return unavailableKeyRegistry{}
	}
	return s.keyRegistry
}

// Open connects to the database named by databaseURL, runs the idempotent
// schema migration, and verifies reachability with a ping. When
// databaseURL is empty, Open returns (nil, nil) so the caller can preserve
// the legacy startup path without a configured credential plane.
//
// Open never silently falls back between dialects: a configured URL must
// resolve to exactly one driver and succeed.
func Open(ctx context.Context, databaseURL string, opts ...Option) (*Store, error) {
	if strings.TrimSpace(databaseURL) == "" {
		return nil, nil
	}
	dia, err := dialectFromURL(databaseURL)
	if err != nil {
		return nil, err
	}
	dataSource := databaseURL
	if dia == dialectSQLite {
		dataSource = sqliteDataSourceWithBusyTimeout(databaseURL)
	}
	db, err := sql.Open(dia, dataSource)
	if err != nil {
		return nil, fmt.Errorf("credentialstore: open %s: %w", dia, err)
	}
	db.SetMaxOpenConns(10)
	db.SetMaxIdleConns(2)
	if err := db.PingContext(ctx); err != nil {
		_ = db.Close()
		return nil, fmt.Errorf("credentialstore: ping %s: %w", dia, err)
	}
	if err := migrate(ctx, db, dia); err != nil {
		_ = db.Close()
		return nil, fmt.Errorf("credentialstore: migrate: %w", err)
	}
	s := &Store{
		db:          db,
		dialect:     dia,
		keyRegistry: unavailableKeyRegistry{},
	}
	for _, opt := range opts {
		opt(s)
	}
	return s, nil
}

// sqliteDataSourceWithBusyTimeout appends the modernc SQLite busy-timeout
// pragma as the final query value. Keeping it in the DSN makes the timeout
// apply to every connection the database/sql pool opens, including separate
// Store handles used by concurrent bootstrap commands.
func sqliteDataSourceWithBusyTimeout(dataSource string) string {
	const busyTimeoutPragma = "_pragma=busy_timeout%3d5000"

	base, fragment, hasFragment := strings.Cut(dataSource, "#")
	separator := "?"
	if strings.Contains(base, "?") {
		separator = "&"
	}
	if strings.HasSuffix(base, "?") || strings.HasSuffix(base, "&") {
		separator = ""
	}
	result := base + separator + busyTimeoutPragma
	if hasFragment {
		result += "#" + fragment
	}
	return result
}

// bind converts portable question-mark placeholders to the ordinal syntax
// required by PostgreSQL. SQLite accepts question marks unchanged.
func (s *Store) bind(query string) string {
	return bindQuery(s.dialect, query)
}

func bindQuery(dialect, query string) string {
	if dialect != dialectPostgres {
		return query
	}
	var bound strings.Builder
	bound.Grow(len(query) + 8)
	ordinal := 1
	for _, r := range query {
		if r == '?' {
			fmt.Fprintf(&bound, "$%d", ordinal)
			ordinal++
			continue
		}
		bound.WriteRune(r)
	}
	return bound.String()
}

// Close releases the underlying database connection. It is safe to call on a
// nil receiver.
func (s *Store) Close() error {
	if s == nil || s.db == nil {
		return nil
	}
	return s.db.Close()
}

// RunInTransaction executes fn inside a single transaction. On success the
// transaction is committed and fn's return value is returned. On any error
// the transaction is rolled back and the error is returned unchanged.
func (s *Store) RunInTransaction(ctx context.Context, fn func(ctx context.Context, tx *sql.Tx) error) error {
	tx, err := s.db.BeginTx(ctx, nil)
	if err != nil {
		return err
	}
	if err := fn(ctx, tx); err != nil {
		_ = tx.Rollback()
		return err
	}
	return tx.Commit()
}

// DB exposes the underlying *sql.DB for callers that need direct access
// (e.g. tests inspecting row counts). Production code should prefer the
// typed methods on Store.
func (s *Store) DB() *sql.DB { return s.db }

// IsOpen reports whether the store holds an active connection.
func (s *Store) IsOpen() bool {
	if s == nil || s.db == nil {
		return false
	}
	return s.db.Ping() == nil
}
