//go:build js && wasm

package wasmdb

import (
	"bytes"
	"errors"

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

	"git.mleku.dev/mleku/nostr/encoders/event"
	"next.orly.dev/pkg/database/indexes"
	"next.orly.dev/pkg/database/indexes/types"
	"next.orly.dev/pkg/interfaces/store"
)

// FetchEventBySerial retrieves an event by its serial number
func (w *W) FetchEventBySerial(ser *types.Uint40) (ev *event.E, err error) {
	if ser == nil {
		return nil, errors.New("nil serial")
	}

	// First try small event store (sev prefix)
	ev, err = w.fetchSmallEvent(ser)
	if err == nil && ev != nil {
		return ev, nil
	}

	// Then try large event store (evt prefix)
	ev, err = w.fetchLargeEvent(ser)
	if err == nil && ev != nil {
		return ev, nil
	}

	return nil, errors.New("event not found")
}

// fetchSmallEvent fetches an event from the small event store
func (w *W) fetchSmallEvent(ser *types.Uint40) (*event.E, error) {
	// Build the key prefix
	keyBuf := new(bytes.Buffer)
	if err := indexes.SmallEventEnc(ser).MarshalWrite(keyBuf); chk.E(err) {
		return nil, err
	}
	prefix := keyBuf.Bytes()

	// Open transaction
	tx, err := w.db.Transaction(idb.TransactionReadOnly, string(indexes.SmallEventPrefix))
	if err != nil {
		return nil, err
	}

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

	// Use cursor to find matching key
	cursorReq, err := store.OpenCursor(idb.CursorNext)
	if err != nil {
		return nil, err
	}

	var foundEvent *event.E
	err = cursorReq.Iter(w.ctx, func(cursor *idb.CursorWithValue) error {
		keyVal, keyErr := cursor.Key()
		if keyErr != nil {
			return keyErr
		}

		keyBytes := safeValueToBytes(keyVal)
		if len(keyBytes) >= len(prefix) && bytes.HasPrefix(keyBytes, prefix) {
			// Found matching key
			// Format: sev|serial(5)|size(2)|data(variable)
			if len(keyBytes) > 10 { // 3 + 5 + 2 = 10 minimum
				sizeOffset := 8 // 3 prefix + 5 serial
				if len(keyBytes) > sizeOffset+2 {
					size := int(keyBytes[sizeOffset])<<8 | int(keyBytes[sizeOffset+1])
					dataStart := sizeOffset + 2
					if len(keyBytes) >= dataStart+size {
						eventData := keyBytes[dataStart : dataStart+size]
						ev := new(event.E)
						if unmarshalErr := ev.UnmarshalBinary(bytes.NewReader(eventData)); unmarshalErr == nil {
							foundEvent = ev
							return errors.New("found") // Stop iteration
						}
					}
				}
			}
		}

		return cursor.Continue()
	})

	if foundEvent != nil {
		return foundEvent, nil
	}
	if err != nil && err.Error() != "found" {
		return nil, err
	}

	return nil, errors.New("small event not found")
}

// fetchLargeEvent fetches an event from the large event store
func (w *W) fetchLargeEvent(ser *types.Uint40) (*event.E, error) {
	// Build the key
	keyBuf := new(bytes.Buffer)
	if err := indexes.EventEnc(ser).MarshalWrite(keyBuf); chk.E(err) {
		return nil, err
	}

	// Open transaction
	tx, err := w.db.Transaction(idb.TransactionReadOnly, string(indexes.EventPrefix))
	if err != nil {
		return nil, err
	}

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

	// Get the value directly
	keyJS := bytesToSafeValue(keyBuf.Bytes())
	req, err := store.Get(keyJS)
	if err != nil {
		return nil, err
	}

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

	if val.IsUndefined() || val.IsNull() {
		return nil, errors.New("large event not found")
	}

	eventData := safeValueToBytes(val)
	if len(eventData) == 0 {
		return nil, errors.New("empty event data")
	}

	ev := new(event.E)
	if err := ev.UnmarshalBinary(bytes.NewReader(eventData)); err != nil {
		return nil, err
	}

	return ev, nil
}

// FetchEventsBySerials retrieves multiple events by their serial numbers
func (w *W) FetchEventsBySerials(serials []*types.Uint40) (events map[uint64]*event.E, err error) {
	events = make(map[uint64]*event.E)

	for _, ser := range serials {
		if ser == nil {
			continue
		}
		ev, fetchErr := w.FetchEventBySerial(ser)
		if fetchErr == nil && ev != nil {
			events[ser.Get()] = ev
		}
	}

	return events, nil
}

// GetFullIdPubkeyBySerial retrieves the ID, pubkey hash, and timestamp for a serial
func (w *W) GetFullIdPubkeyBySerial(ser *types.Uint40) (fidpk *store.IdPkTs, err error) {
	if ser == nil {
		return nil, errors.New("nil serial")
	}

	// Build the prefix to search for
	keyBuf := new(bytes.Buffer)
	indexes.FullIdPubkeyEnc(ser, nil, nil, nil).MarshalWrite(keyBuf)
	prefix := keyBuf.Bytes()[:8] // 3 prefix + 5 serial

	// Search in the fpc object store
	tx, err := w.db.Transaction(idb.TransactionReadOnly, string(indexes.FullIdPubkeyPrefix))
	if err != nil {
		return nil, err
	}

	objStore, err := tx.ObjectStore(string(indexes.FullIdPubkeyPrefix))
	if err != nil {
		return nil, err
	}

	// Use cursor to find matching key
	cursorReq, err := objStore.OpenCursor(idb.CursorNext)
	if err != nil {
		return nil, err
	}

	err = cursorReq.Iter(w.ctx, func(cursor *idb.CursorWithValue) error {
		keyVal, keyErr := cursor.Key()
		if keyErr != nil {
			return keyErr
		}

		keyBytes := safeValueToBytes(keyVal)
		if len(keyBytes) >= len(prefix) && bytes.HasPrefix(keyBytes, prefix) {
			// Found matching key
			// Format: fpc|serial(5)|id(32)|pubkey_hash(8)|timestamp(8)
			if len(keyBytes) >= 56 { // 3 + 5 + 32 + 8 + 8 = 56
				fidpk = &store.IdPkTs{
					Id:  make([]byte, 32),
					Pub: make([]byte, 8),
					Ts:  0,
				}
				copy(fidpk.Id, keyBytes[8:40])
				copy(fidpk.Pub, keyBytes[40:48])
				// Parse timestamp (big-endian uint64)
				var ts int64
				for i := 0; i < 8; i++ {
					ts = (ts << 8) | int64(keyBytes[48+i])
				}
				fidpk.Ts = ts
				fidpk.Ser = ser.Get()
				return errors.New("found") // Stop iteration
			}
		}

		return cursor.Continue()
	})

	if fidpk != nil {
		return fidpk, nil
	}
	if err != nil && err.Error() != "found" {
		return nil, err
	}

	return nil, errors.New("full id pubkey not found")
}

// GetFullIdPubkeyBySerials retrieves ID/pubkey/timestamp for multiple serials
func (w *W) GetFullIdPubkeyBySerials(sers []*types.Uint40) (fidpks []*store.IdPkTs, err error) {
	fidpks = make([]*store.IdPkTs, 0, len(sers))

	for _, ser := range sers {
		if ser == nil {
			continue
		}
		fidpk, fetchErr := w.GetFullIdPubkeyBySerial(ser)
		if fetchErr == nil && fidpk != nil {
			fidpks = append(fidpks, fidpk)
		}
	}

	return fidpks, nil
}
