//go:build js && wasm

// Package wasmdb provides a WebAssembly-compatible database implementation
// using IndexedDB as the storage backend. It replicates the Badger database's
// index schema for full query compatibility.
//
// This implementation uses aperturerobotics/go-indexeddb (a fork of hack-pad/go-indexeddb)
// which provides full IndexedDB bindings with cursor/range support and transaction retry
// mechanisms to handle IndexedDB's transaction expiration issues in Go WASM.
//
// Architecture:
//   - Each index type (evt, eid, kc-, pc-, etc.) maps to an IndexedDB object store
//   - Keys are binary-encoded using the same format as the Badger implementation
//   - Range queries use IndexedDB cursors with KeyRange bounds
//   - Serial numbers are managed using a dedicated "meta" object store
package wasmdb

import (
	"context"
	"encoding/binary"
	"errors"
	"fmt"
	"sync"

	"github.com/aperturerobotics/go-indexeddb/idb"
	"github.com/hack-pad/safejs"
	"lol.mleku.dev"
	"lol.mleku.dev/chk"

	"git.mleku.dev/mleku/nostr/encoders/event"
	"git.mleku.dev/mleku/nostr/encoders/filter"
	"next.orly.dev/pkg/database"
	"next.orly.dev/pkg/database/indexes"
)

const (
	// DatabaseName is the IndexedDB database name
	DatabaseName = "orly-nostr-relay"

	// DatabaseVersion is incremented when schema changes require migration
	DatabaseVersion = 1

	// MetaStoreName holds metadata like serial counters
	MetaStoreName = "meta"

	// EventSerialKey is the key for the event serial counter in meta store
	EventSerialKey = "event_serial"

	// PubkeySerialKey is the key for the pubkey serial counter in meta store
	PubkeySerialKey = "pubkey_serial"

	// RelayIdentityKey is the key for the relay identity secret
	RelayIdentityKey = "relay_identity"
)

// Object store names matching Badger index prefixes
var objectStoreNames = []string{
	MetaStoreName,
	string(indexes.EventPrefix),            // "evt" - full events
	string(indexes.SmallEventPrefix),       // "sev" - small events inline
	string(indexes.ReplaceableEventPrefix), // "rev" - replaceable events
	string(indexes.AddressableEventPrefix), // "aev" - addressable events
	string(indexes.IdPrefix),               // "eid" - event ID index
	string(indexes.FullIdPubkeyPrefix),     // "fpc" - full ID + pubkey + timestamp
	string(indexes.CreatedAtPrefix),        // "c--" - created_at index
	string(indexes.KindPrefix),             // "kc-" - kind index
	string(indexes.PubkeyPrefix),           // "pc-" - pubkey index
	string(indexes.KindPubkeyPrefix),       // "kpc" - kind + pubkey index
	string(indexes.TagPrefix),              // "tc-" - tag index
	string(indexes.TagKindPrefix),          // "tkc" - tag + kind index
	string(indexes.TagPubkeyPrefix),        // "tpc" - tag + pubkey index
	string(indexes.TagKindPubkeyPrefix),    // "tkp" - tag + kind + pubkey index
	string(indexes.WordPrefix),             // "wrd" - word search index
	string(indexes.ExpirationPrefix),       // "exp" - expiration index
	string(indexes.VersionPrefix),          // "ver" - schema version
	string(indexes.PubkeySerialPrefix),     // "pks" - pubkey serial index
	string(indexes.SerialPubkeyPrefix),     // "spk" - serial to pubkey
	string(indexes.EventPubkeyGraphPrefix), // "epg" - event-pubkey graph
	string(indexes.PubkeyEventGraphPrefix), // "peg" - pubkey-event graph
	"markers",                              // metadata key-value storage
	"subscriptions",                        // payment subscriptions
	"nip43",                                // NIP-43 membership
	"invites",                              // invite codes
}

// W implements the database.Database interface using IndexedDB
type W struct {
	ctx    context.Context
	cancel context.CancelFunc

	dataDir string // Not really used in WASM, but kept for interface compatibility
	Logger  *logger

	db    *idb.Database
	dbMu  sync.RWMutex
	ready chan struct{}

	// Serial counters (cached in memory, persisted to IndexedDB)
	eventSerial  uint64
	pubkeySerial uint64
	serialMu     sync.Mutex
}

// Ensure W implements database.Database interface at compile time
var _ database.Database = (*W)(nil)

// init registers the wasmdb database factory
func init() {
	database.RegisterWasmDBFactory(func(
		ctx context.Context,
		cancel context.CancelFunc,
		cfg *database.DatabaseConfig,
	) (database.Database, error) {
		return NewWithConfig(ctx, cancel, cfg)
	})
}

// NewWithConfig creates a new IndexedDB-based database instance
func NewWithConfig(
	ctx context.Context, cancel context.CancelFunc, cfg *database.DatabaseConfig,
) (*W, error) {
	w := &W{
		ctx:     ctx,
		cancel:  cancel,
		dataDir: cfg.DataDir,
		Logger:  NewLogger(lol.GetLogLevel(cfg.LogLevel)),
		ready:   make(chan struct{}),
	}

	// Open or create the IndexedDB database
	if err := w.openDatabase(); err != nil {
		return nil, fmt.Errorf("failed to open IndexedDB: %w", err)
	}

	// Load serial counters from storage
	if err := w.loadSerialCounters(); err != nil {
		return nil, fmt.Errorf("failed to load serial counters: %w", err)
	}

	// Start warmup goroutine
	go w.warmup()

	// Setup shutdown handler
	go func() {
		<-w.ctx.Done()
		w.cancel()
		w.Close()
	}()

	return w, nil
}

// New creates a new IndexedDB-based database instance with default configuration
func New(
	ctx context.Context, cancel context.CancelFunc, dataDir, logLevel string,
) (*W, error) {
	cfg := &database.DatabaseConfig{
		DataDir:  dataDir,
		LogLevel: logLevel,
	}
	return NewWithConfig(ctx, cancel, cfg)
}

// openDatabase opens or creates the IndexedDB database with all required object stores
func (w *W) openDatabase() error {
	w.dbMu.Lock()
	defer w.dbMu.Unlock()

	// Get the IndexedDB factory (panics if not available)
	factory := idb.Global()

	// Open the database with upgrade handler
	openReq, err := factory.Open(w.ctx, DatabaseName, DatabaseVersion, func(db *idb.Database, oldVersion, newVersion uint) error {
		// This is called when the database needs to be created or upgraded
		w.Logger.Infof("IndexedDB upgrade: version %d -> %d", oldVersion, newVersion)

		// Create all object stores
		for _, storeName := range objectStoreNames {
			// Check if store already exists
			if !w.hasObjectStore(db, storeName) {
				// Create object store without auto-increment (we manage keys manually)
				opts := idb.ObjectStoreOptions{}
				if _, err := db.CreateObjectStore(storeName, opts); err != nil {
					return fmt.Errorf("failed to create object store %s: %w", storeName, err)
				}
				w.Logger.Debugf("created object store: %s", storeName)
			}
		}
		return nil
	})
	if err != nil {
		return fmt.Errorf("failed to open IndexedDB: %w", err)
	}

	db, err := openReq.Await(w.ctx)
	if err != nil {
		return fmt.Errorf("failed to await IndexedDB open: %w", err)
	}

	w.db = db
	return nil
}

// hasObjectStore checks if an object store exists in the database
func (w *W) hasObjectStore(db *idb.Database, name string) bool {
	names, err := db.ObjectStoreNames()
	if err != nil {
		return false
	}
	for _, n := range names {
		if n == name {
			return true
		}
	}
	return false
}

// loadSerialCounters loads the event and pubkey serial counters from IndexedDB
func (w *W) loadSerialCounters() error {
	w.serialMu.Lock()
	defer w.serialMu.Unlock()

	// Load event serial
	eventSerialBytes, err := w.getMeta(EventSerialKey)
	if err != nil {
		return err
	}
	if eventSerialBytes != nil && len(eventSerialBytes) == 8 {
		w.eventSerial = binary.BigEndian.Uint64(eventSerialBytes)
	}

	// Load pubkey serial
	pubkeySerialBytes, err := w.getMeta(PubkeySerialKey)
	if err != nil {
		return err
	}
	if pubkeySerialBytes != nil && len(pubkeySerialBytes) == 8 {
		w.pubkeySerial = binary.BigEndian.Uint64(pubkeySerialBytes)
	}

	w.Logger.Infof("loaded serials: event=%d, pubkey=%d", w.eventSerial, w.pubkeySerial)
	return nil
}

// getMeta retrieves a value from the meta object store
func (w *W) getMeta(key string) ([]byte, error) {
	tx, err := w.db.Transaction(idb.TransactionReadOnly, MetaStoreName)
	if err != nil {
		return nil, err
	}

	store, err := tx.ObjectStore(MetaStoreName)
	if err != nil {
		return nil, err
	}

	keyVal, err := safejs.ValueOf(key)
	if err != nil {
		return nil, err
	}

	req, err := store.Get(keyVal)
	if err != nil {
		return nil, err
	}

	val, err := req.Await(w.ctx)
	if err != nil {
		// Key not found is not an error
		return nil, nil
	}

	if val.IsUndefined() || val.IsNull() {
		return nil, nil
	}

	// Convert safejs.Value to []byte
	return safeValueToBytes(val), nil
}

// setMeta stores a value in the meta object store
func (w *W) setMeta(key string, value []byte) error {
	tx, err := w.db.Transaction(idb.TransactionReadWrite, MetaStoreName)
	if err != nil {
		return err
	}

	store, err := tx.ObjectStore(MetaStoreName)
	if err != nil {
		return err
	}

	// Convert value to Uint8Array for IndexedDB storage
	valueJS := bytesToSafeValue(value)

	// Put with key - using PutKey since we're managing keys
	keyVal, err := safejs.ValueOf(key)
	if err != nil {
		return err
	}

	_, err = store.PutKey(keyVal, valueJS)
	if err != nil {
		return err
	}

	return tx.Await(w.ctx)
}

// nextEventSerial returns the next event serial number and persists it
func (w *W) nextEventSerial() (uint64, error) {
	w.serialMu.Lock()
	defer w.serialMu.Unlock()

	w.eventSerial++
	serial := w.eventSerial

	// Persist to IndexedDB
	buf := make([]byte, 8)
	binary.BigEndian.PutUint64(buf, serial)
	if err := w.setMeta(EventSerialKey, buf); err != nil {
		return 0, err
	}

	return serial, nil
}

// nextPubkeySerial returns the next pubkey serial number and persists it
func (w *W) nextPubkeySerial() (uint64, error) {
	w.serialMu.Lock()
	defer w.serialMu.Unlock()

	w.pubkeySerial++
	serial := w.pubkeySerial

	// Persist to IndexedDB
	buf := make([]byte, 8)
	binary.BigEndian.PutUint64(buf, serial)
	if err := w.setMeta(PubkeySerialKey, buf); err != nil {
		return 0, err
	}

	return serial, nil
}

// warmup performs database warmup and closes the ready channel when complete
func (w *W) warmup() {
	defer close(w.ready)
	// IndexedDB is ready immediately after opening
	w.Logger.Infof("IndexedDB database warmup complete, ready to serve requests")
}

// Path returns the database path (not used in WASM)
func (w *W) Path() string { return w.dataDir }

// Init initializes the database (no-op, done in New)
func (w *W) Init(path string) error { return nil }

// Sync flushes pending writes (IndexedDB handles persistence automatically)
func (w *W) Sync() error { return nil }

// Close closes the database
func (w *W) Close() error {
	w.dbMu.Lock()
	defer w.dbMu.Unlock()

	if w.db != nil {
		w.db.Close()
		w.db = nil
	}
	return nil
}

// Wipe removes all data and recreates object stores
func (w *W) Wipe() error {
	w.dbMu.Lock()
	defer w.dbMu.Unlock()

	// Close the current database
	if w.db != nil {
		w.db.Close()
		w.db = nil
	}

	// Delete the database
	factory := idb.Global()
	delReq, err := factory.DeleteDatabase(DatabaseName)
	if err != nil {
		return fmt.Errorf("failed to delete IndexedDB: %w", err)
	}
	if err := delReq.Await(w.ctx); err != nil {
		return fmt.Errorf("failed to await IndexedDB delete: %w", err)
	}

	// Reset serial counters
	w.serialMu.Lock()
	w.eventSerial = 0
	w.pubkeySerial = 0
	w.serialMu.Unlock()

	// Reopen the database (this will recreate all object stores)
	w.dbMu.Unlock()
	err = w.openDatabase()
	w.dbMu.Lock()

	return err
}

// SetLogLevel sets the logging level
func (w *W) SetLogLevel(level string) {
	w.Logger.SetLogLevel(lol.GetLogLevel(level))
}

// Ready returns a channel that closes when the database is ready
func (w *W) Ready() <-chan struct{} { return w.ready }

// RunMigrations runs database migrations (handled by IndexedDB upgrade)
func (w *W) RunMigrations() {}

// EventIdsBySerial retrieves event IDs by serial range
func (w *W) EventIdsBySerial(start uint64, count int) ([]uint64, error) {
	return nil, errors.New("not implemented")
}

// Query cache methods (simplified for WASM - no caching)
func (w *W) GetCachedJSON(f *filter.F) ([][]byte, bool)             { return nil, false }
func (w *W) CacheMarshaledJSON(f *filter.F, marshaledJSON [][]byte) {}
func (w *W) GetCachedEvents(f *filter.F) (event.S, bool)            { return nil, false }
func (w *W) CacheEvents(f *filter.F, events event.S)                {}
func (w *W) InvalidateQueryCache()                                  {}

// Placeholder implementations for remaining interface methods
// Query methods are implemented in query-events.go
// Delete methods are implemented in delete-event.go

// Import, Export, and ImportEvents methods are implemented in import-export.go

func (w *W) GetRelayIdentitySecret() (skb []byte, err error) {
	return w.getMeta(RelayIdentityKey)
}

func (w *W) SetRelayIdentitySecret(skb []byte) error {
	return w.setMeta(RelayIdentityKey, skb)
}

func (w *W) GetOrCreateRelayIdentitySecret() (skb []byte, err error) {
	skb, err = w.GetRelayIdentitySecret()
	if err != nil {
		return nil, err
	}
	if skb != nil {
		return skb, nil
	}
	// Generate new secret key (32 random bytes)
	// In WASM, we use crypto.getRandomValues
	skb = make([]byte, 32)
	if err := cryptoRandom(skb); err != nil {
		return nil, err
	}
	if err := w.SetRelayIdentitySecret(skb); err != nil {
		return nil, err
	}
	return skb, nil
}

func (w *W) SetMarker(key string, value []byte) error {
	return w.setStoreValue("markers", key, value)
}

func (w *W) GetMarker(key string) (value []byte, err error) {
	return w.getStoreValue("markers", key)
}

func (w *W) HasMarker(key string) bool {
	val, err := w.GetMarker(key)
	return err == nil && val != nil
}

func (w *W) DeleteMarker(key string) error {
	return w.deleteStoreValue("markers", key)
}

// Subscription methods are implemented in subscriptions.go
// NIP-43 methods are implemented in nip43.go

// Helper methods for object store operations

func (w *W) setStoreValue(storeName, key string, value []byte) error {
	tx, err := w.db.Transaction(idb.TransactionReadWrite, storeName)
	if err != nil {
		return err
	}

	store, err := tx.ObjectStore(storeName)
	if err != nil {
		return err
	}

	keyVal, err := safejs.ValueOf(key)
	if err != nil {
		return err
	}

	valueJS := bytesToSafeValue(value)

	_, err = store.PutKey(keyVal, valueJS)
	if err != nil {
		return err
	}

	return tx.Await(w.ctx)
}

func (w *W) getStoreValue(storeName, key string) ([]byte, error) {
	tx, err := w.db.Transaction(idb.TransactionReadOnly, storeName)
	if err != nil {
		return nil, err
	}

	store, err := tx.ObjectStore(storeName)
	if err != nil {
		return nil, err
	}

	keyVal, err := safejs.ValueOf(key)
	if err != nil {
		return nil, err
	}

	req, err := store.Get(keyVal)
	if err != nil {
		return nil, err
	}

	val, err := req.Await(w.ctx)
	if err != nil {
		return nil, nil
	}

	if val.IsUndefined() || val.IsNull() {
		return nil, nil
	}

	return safeValueToBytes(val), nil
}

func (w *W) deleteStoreValue(storeName, key string) error {
	tx, err := w.db.Transaction(idb.TransactionReadWrite, storeName)
	if err != nil {
		return err
	}

	store, err := tx.ObjectStore(storeName)
	if err != nil {
		return err
	}

	keyVal, err := safejs.ValueOf(key)
	if err != nil {
		return err
	}

	_, err = store.Delete(keyVal)
	if err != nil {
		return err
	}

	return tx.Await(w.ctx)
}

// Placeholder for unused variable
var _ = chk.E
