//go:build js && wasm

package wasmdb

import (
	"bytes"
	"context"
	"fmt"
	"sort"
	"strconv"
	"time"

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

	"git.mleku.dev/mleku/nostr/encoders/event"
	"git.mleku.dev/mleku/nostr/encoders/filter"
	hexenc "git.mleku.dev/mleku/nostr/encoders/hex"
	"git.mleku.dev/mleku/nostr/encoders/ints"
	"git.mleku.dev/mleku/nostr/encoders/kind"
	"git.mleku.dev/mleku/nostr/encoders/tag"
	"git.mleku.dev/mleku/nostr/encoders/tag/atag"
	"next.orly.dev/pkg/database"
	"next.orly.dev/pkg/database/indexes"
	"next.orly.dev/pkg/database/indexes/types"
	"next.orly.dev/pkg/interfaces/store"
	"next.orly.dev/pkg/utils"
)

// DeleteEvent removes an event from the database identified by `eid`.
func (w *W) DeleteEvent(c context.Context, eid []byte) (err error) {
	w.Logger.Warnf("deleting event %0x", eid)

	// Get the serial number for the event ID
	var ser *types.Uint40
	ser, err = w.GetSerialById(eid)
	if chk.E(err) {
		return
	}
	if ser == nil {
		// Event wasn't found, nothing to delete
		return
	}

	// Fetch the event to get its data
	var ev *event.E
	ev, err = w.FetchEventBySerial(ser)
	if chk.E(err) {
		return
	}
	if ev == nil {
		// Event wasn't found, nothing to delete
		return
	}

	if err = w.DeleteEventBySerial(c, ser, ev); chk.E(err) {
		return
	}
	return
}

// DeleteEventBySerial removes an event and all its indexes by serial number.
func (w *W) DeleteEventBySerial(c context.Context, ser *types.Uint40, ev *event.E) (err error) {
	w.Logger.Infof("DeleteEventBySerial: deleting event %0x (serial %d)", ev.ID, ser.Get())

	// Get all indexes for the event
	var idxs [][]byte
	idxs, err = database.GetIndexesForEvent(ev, ser.Get())
	if chk.E(err) {
		w.Logger.Errorf("DeleteEventBySerial: failed to get indexes for event %0x: %v", ev.ID, err)
		return
	}
	w.Logger.Infof("DeleteEventBySerial: found %d indexes for event %0x", len(idxs), ev.ID)

	// Collect all unique store names we need to access
	storeNames := make(map[string]struct{})
	for _, key := range idxs {
		if len(key) >= 3 {
			storeNames[string(key[:3])] = struct{}{}
		}
	}

	// Also include event stores
	storeNames[string(indexes.EventPrefix)] = struct{}{}
	storeNames[string(indexes.SmallEventPrefix)] = struct{}{}

	// Convert to slice
	storeList := make([]string, 0, len(storeNames))
	for name := range storeNames {
		storeList = append(storeList, name)
	}

	if len(storeList) == 0 {
		return nil
	}

	// Start a transaction to delete the event and all its indexes
	tx, err := w.db.Transaction(idb.TransactionReadWrite, storeList[0], storeList[1:]...)
	if err != nil {
		return fmt.Errorf("failed to start delete transaction: %w", err)
	}

	// Delete all indexes
	for _, key := range idxs {
		if len(key) < 3 {
			continue
		}
		storeName := string(key[:3])
		objStore, storeErr := tx.ObjectStore(storeName)
		if storeErr != nil {
			w.Logger.Warnf("DeleteEventBySerial: failed to get object store %s: %v", storeName, storeErr)
			continue
		}

		keyJS := bytesToSafeValue(key)
		if _, delErr := objStore.Delete(keyJS); delErr != nil {
			w.Logger.Warnf("DeleteEventBySerial: failed to delete index from %s: %v", storeName, delErr)
		}
	}

	// Delete from small event store
	sevKeyBuf := new(bytes.Buffer)
	if err = indexes.SmallEventEnc(ser).MarshalWrite(sevKeyBuf); err == nil {
		if objStore, storeErr := tx.ObjectStore(string(indexes.SmallEventPrefix)); storeErr == nil {
			// For small events, the key includes size and data, so we need to scan
			w.deleteKeysByPrefix(objStore, sevKeyBuf.Bytes())
		}
	}

	// Delete from large event store
	evtKeyBuf := new(bytes.Buffer)
	if err = indexes.EventEnc(ser).MarshalWrite(evtKeyBuf); err == nil {
		if objStore, storeErr := tx.ObjectStore(string(indexes.EventPrefix)); storeErr == nil {
			keyJS := bytesToSafeValue(evtKeyBuf.Bytes())
			objStore.Delete(keyJS)
		}
	}

	// Commit transaction
	if err = tx.Await(c); err != nil {
		return fmt.Errorf("failed to commit delete transaction: %w", err)
	}

	w.Logger.Infof("DeleteEventBySerial: successfully deleted event %0x and all indexes", ev.ID)
	return nil
}

// deleteKeysByPrefix deletes all keys starting with the given prefix from an object store
func (w *W) deleteKeysByPrefix(store *idb.ObjectStore, prefix []byte) {
	cursorReq, err := store.OpenCursor(idb.CursorNext)
	if err != nil {
		return
	}

	var keysToDelete [][]byte
	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) {
			keysToDelete = append(keysToDelete, keyBytes)
		}

		return cursor.Continue()
	})

	// Delete collected keys
	for _, key := range keysToDelete {
		keyJS := bytesToSafeValue(key)
		store.Delete(keyJS)
	}
}

// DeleteExpired scans for events with expiration timestamps that have passed and deletes them.
func (w *W) DeleteExpired() {
	now := time.Now().Unix()

	// Open read transaction to find expired events
	tx, err := w.db.Transaction(idb.TransactionReadOnly, string(indexes.ExpirationPrefix))
	if err != nil {
		w.Logger.Warnf("DeleteExpired: failed to start transaction: %v", err)
		return
	}

	objStore, err := tx.ObjectStore(string(indexes.ExpirationPrefix))
	if err != nil {
		w.Logger.Warnf("DeleteExpired: failed to get expiration store: %v", err)
		return
	}

	var expiredSerials types.Uint40s

	cursorReq, err := objStore.OpenCursor(idb.CursorNext)
	if err != nil {
		w.Logger.Warnf("DeleteExpired: failed to open cursor: %v", err)
		return
	}

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

		keyBytes := safeValueToBytes(keyVal)
		if len(keyBytes) < 8 { // exp prefix (3) + expiration (variable) + serial (5)
			return cursor.Continue()
		}

		// Parse expiration key: exp|expiration_timestamp|serial
		exp, ser := indexes.ExpirationVars()
		buf := bytes.NewBuffer(keyBytes)
		if err := indexes.ExpirationDec(exp, ser).UnmarshalRead(buf); err != nil {
			return cursor.Continue()
		}

		if int64(exp.Get()) > now {
			// Not expired yet
			return cursor.Continue()
		}

		expiredSerials = append(expiredSerials, ser)
		return cursor.Continue()
	})

	// Delete expired events
	for _, ser := range expiredSerials {
		ev, fetchErr := w.FetchEventBySerial(ser)
		if fetchErr != nil || ev == nil {
			continue
		}
		if err := w.DeleteEventBySerial(context.Background(), ser, ev); err != nil {
			w.Logger.Warnf("DeleteExpired: failed to delete expired event: %v", err)
		}
	}
}

// ProcessDelete processes a kind 5 deletion event, deleting referenced events.
func (w *W) ProcessDelete(ev *event.E, admins [][]byte) (err error) {
	eTags := ev.Tags.GetAll([]byte("e"))
	aTags := ev.Tags.GetAll([]byte("a"))
	kTags := ev.Tags.GetAll([]byte("k"))

	// Process e-tags: delete specific events by ID
	for _, eTag := range eTags {
		if eTag.Len() < 2 {
			continue
		}
		// Use ValueHex() to handle both binary and hex storage formats
		eventIdHex := eTag.ValueHex()
		if len(eventIdHex) != 64 { // hex encoded event ID
			continue
		}
		// Decode hex event ID
		var eid []byte
		if eid, err = hexenc.DecAppend(nil, eventIdHex); chk.E(err) {
			continue
		}
		// Fetch the event to verify ownership
		var ser *types.Uint40
		if ser, err = w.GetSerialById(eid); chk.E(err) || ser == nil {
			continue
		}
		var targetEv *event.E
		if targetEv, err = w.FetchEventBySerial(ser); chk.E(err) || targetEv == nil {
			continue
		}
		// Only allow users to delete their own events
		if !utils.FastEqual(targetEv.Pubkey, ev.Pubkey) {
			continue
		}
		// Delete the event
		if err = w.DeleteEvent(context.Background(), eid); chk.E(err) {
			w.Logger.Warnf("failed to delete event %x via e-tag: %v", eid, err)
			continue
		}
		w.Logger.Debugf("deleted event %x via e-tag deletion", eid)
	}

	// Process a-tags: delete addressable events by kind:pubkey:d-tag
	for _, aTag := range aTags {
		if aTag.Len() < 2 {
			continue
		}
		// Parse the 'a' tag value: kind:pubkey:d-tag (for parameterized) or kind:pubkey (for regular)
		split := bytes.Split(aTag.Value(), []byte{':'})
		if len(split) < 2 {
			continue
		}
		// Parse the kind
		kindStr := string(split[0])
		kindInt, parseErr := strconv.Atoi(kindStr)
		if parseErr != nil {
			continue
		}
		kk := kind.New(uint16(kindInt))
		// Parse the pubkey
		var pk []byte
		if pk, err = hexenc.DecAppend(nil, split[1]); chk.E(err) {
			continue
		}
		// Only allow users to delete their own events
		if !utils.FastEqual(pk, ev.Pubkey) {
			continue
		}

		// Build filter for events to delete
		delFilter := &filter.F{
			Authors: tag.NewFromBytesSlice(pk),
			Kinds:   kind.NewS(kk),
		}

		// For parameterized replaceable events, add d-tag filter
		if kind.IsParameterizedReplaceable(kk.K) && len(split) >= 3 {
			dValue := split[2]
			delFilter.Tags = tag.NewS(tag.NewFromAny([]byte("d"), dValue))
		}

		// Find matching events
		var idxs []database.Range
		if idxs, err = database.GetIndexesFromFilter(delFilter); chk.E(err) {
			continue
		}
		var sers types.Uint40s
		for _, idx := range idxs {
			var s types.Uint40s
			if s, err = w.GetSerialsByRange(idx); chk.E(err) {
				continue
			}
			sers = append(sers, s...)
		}

		// Delete events older than the deletion event
		if len(sers) > 0 {
			var idPkTss []*store.IdPkTs
			var tmp []*store.IdPkTs
			if tmp, err = w.GetFullIdPubkeyBySerials(sers); chk.E(err) {
				continue
			}
			idPkTss = append(idPkTss, tmp...)
			// Sort by timestamp
			sort.Slice(idPkTss, func(i, j int) bool {
				return idPkTss[i].Ts > idPkTss[j].Ts
			})
			for _, v := range idPkTss {
				if v.Ts < ev.CreatedAt {
					if err = w.DeleteEvent(context.Background(), v.Id); chk.E(err) {
						w.Logger.Warnf("failed to delete event %x via a-tag: %v", v.Id, err)
						continue
					}
					w.Logger.Debugf("deleted event %x via a-tag deletion", v.Id)
				}
			}
		}
	}

	// If there are no e or a tags, delete all replaceable events of the kinds
	// specified by the k tags for the pubkey of the delete event.
	if len(eTags) == 0 && len(aTags) == 0 {
		// Parse the kind tags
		var kinds []*kind.K
		for _, k := range kTags {
			kv := k.Value()
			iv := ints.New(0)
			if _, err = iv.Unmarshal(kv); chk.E(err) {
				continue
			}
			kinds = append(kinds, kind.New(iv.N))
		}

		var idxs []database.Range
		if idxs, err = database.GetIndexesFromFilter(
			&filter.F{
				Authors: tag.NewFromBytesSlice(ev.Pubkey),
				Kinds:   kind.NewS(kinds...),
			},
		); chk.E(err) {
			return
		}

		var sers types.Uint40s
		for _, idx := range idxs {
			var s types.Uint40s
			if s, err = w.GetSerialsByRange(idx); chk.E(err) {
				return
			}
			sers = append(sers, s...)
		}

		if len(sers) > 0 {
			var idPkTss []*store.IdPkTs
			var tmp []*store.IdPkTs
			if tmp, err = w.GetFullIdPubkeyBySerials(sers); chk.E(err) {
				return
			}
			idPkTss = append(idPkTss, tmp...)
			// Sort by timestamp
			sort.Slice(idPkTss, func(i, j int) bool {
				return idPkTss[i].Ts > idPkTss[j].Ts
			})
			for _, v := range idPkTss {
				if v.Ts < ev.CreatedAt {
					if err = w.DeleteEvent(context.Background(), v.Id); chk.E(err) {
						continue
					}
				}
			}
		}
	}
	return
}

// CheckForDeleted checks if the event has been deleted, and returns an error with
// prefix "blocked:" if it is. This function also allows designating admin
// pubkeys that may also delete the event.
func (w *W) CheckForDeleted(ev *event.E, admins [][]byte) (err error) {
	keys := append([][]byte{ev.Pubkey}, admins...)
	authors := tag.NewFromBytesSlice(keys...)

	// If the event is addressable, check for a deletion event with the same
	// kind/pubkey/dtag
	if kind.IsParameterizedReplaceable(ev.Kind) {
		var idxs []database.Range
		// Construct an a-tag
		t := ev.Tags.GetFirst([]byte("d"))
		var dTagValue []byte
		if t != nil {
			dTagValue = t.Value()
		}
		a := atag.T{
			Kind:   kind.New(ev.Kind),
			Pubkey: ev.Pubkey,
			DTag:   dTagValue,
		}
		at := a.Marshal(nil)
		if idxs, err = database.GetIndexesFromFilter(
			&filter.F{
				Authors: authors,
				Kinds:   kind.NewS(kind.Deletion),
				Tags:    tag.NewS(tag.NewFromAny("#a", at)),
			},
		); chk.E(err) {
			return
		}

		var sers types.Uint40s
		for _, idx := range idxs {
			var s types.Uint40s
			if s, err = w.GetSerialsByRange(idx); chk.E(err) {
				return
			}
			sers = append(sers, s...)
		}

		if len(sers) > 0 {
			var idPkTss []*store.IdPkTs
			var tmp []*store.IdPkTs
			if tmp, err = w.GetFullIdPubkeyBySerials(sers); chk.E(err) {
				return
			}
			idPkTss = append(idPkTss, tmp...)
			// Find the newest deletion timestamp
			maxTs := idPkTss[0].Ts
			for i := 1; i < len(idPkTss); i++ {
				if idPkTss[i].Ts > maxTs {
					maxTs = idPkTss[i].Ts
				}
			}
			if ev.CreatedAt < maxTs {
				err = errorf.E(
					"blocked: %0x was deleted by address %s because it is older than the delete: event: %d delete: %d",
					ev.ID, at, ev.CreatedAt, maxTs,
				)
				return
			}
			return
		}
		return
	}

	// If the event is replaceable, check if there is a deletion event newer
	// than the event
	if kind.IsReplaceable(ev.Kind) {
		var idxs []database.Range
		if idxs, err = database.GetIndexesFromFilter(
			&filter.F{
				Authors: tag.NewFromBytesSlice(ev.Pubkey),
				Kinds:   kind.NewS(kind.Deletion),
				Tags: tag.NewS(
					tag.NewFromAny("#k", fmt.Sprint(ev.Kind)),
				),
			},
		); chk.E(err) {
			return
		}

		var sers types.Uint40s
		for _, idx := range idxs {
			var s types.Uint40s
			if s, err = w.GetSerialsByRange(idx); chk.E(err) {
				return
			}
			sers = append(sers, s...)
		}

		if len(sers) > 0 {
			var idPkTss []*store.IdPkTs
			var tmp []*store.IdPkTs
			if tmp, err = w.GetFullIdPubkeyBySerials(sers); chk.E(err) {
				return
			}
			idPkTss = append(idPkTss, tmp...)
			// Find the newest deletion
			maxTs := idPkTss[0].Ts
			maxId := idPkTss[0].Id
			for i := 1; i < len(idPkTss); i++ {
				if idPkTss[i].Ts > maxTs {
					maxTs = idPkTss[i].Ts
					maxId = idPkTss[i].Id
				}
			}
			if ev.CreatedAt < maxTs {
				err = fmt.Errorf(
					"blocked: %0x was deleted: the event is older than the delete event %0x: event: %d delete: %d",
					ev.ID, maxId, ev.CreatedAt, maxTs,
				)
				return
			}
		}

		// This type of delete can also use an a tag to specify kind and author
		idxs = nil
		a := atag.T{
			Kind:   kind.New(ev.Kind),
			Pubkey: ev.Pubkey,
		}
		at := a.Marshal(nil)
		if idxs, err = database.GetIndexesFromFilter(
			&filter.F{
				Authors: authors,
				Kinds:   kind.NewS(kind.Deletion),
				Tags:    tag.NewS(tag.NewFromAny("#a", at)),
			},
		); chk.E(err) {
			return
		}

		sers = nil
		for _, idx := range idxs {
			var s types.Uint40s
			if s, err = w.GetSerialsByRange(idx); chk.E(err) {
				return
			}
			sers = append(sers, s...)
		}

		if len(sers) > 0 {
			var idPkTss []*store.IdPkTs
			var tmp []*store.IdPkTs
			if tmp, err = w.GetFullIdPubkeyBySerials(sers); chk.E(err) {
				return
			}
			idPkTss = append(idPkTss, tmp...)
			// Find the newest deletion
			maxTs := idPkTss[0].Ts
			for i := 1; i < len(idPkTss); i++ {
				if idPkTss[i].Ts > maxTs {
					maxTs = idPkTss[i].Ts
				}
			}
			if ev.CreatedAt < maxTs {
				err = errorf.E(
					"blocked: %0x was deleted by address %s because it is older than the delete: event: %d delete: %d",
					ev.ID, at, ev.CreatedAt, maxTs,
				)
				return
			}
			return
		}
		return
	}

	// Otherwise check for a delete by event id
	var idxs []database.Range
	if idxs, err = database.GetIndexesFromFilter(
		&filter.F{
			Authors: authors,
			Kinds:   kind.NewS(kind.Deletion),
			Tags: tag.NewS(
				tag.NewFromAny("e", hexenc.Enc(ev.ID)),
			),
		},
	); chk.E(err) {
		return
	}

	for _, idx := range idxs {
		var s types.Uint40s
		if s, err = w.GetSerialsByRange(idx); chk.E(err) {
			return
		}
		if len(s) > 0 {
			// Any e-tag deletion found means the exact event was deleted
			err = errorf.E("blocked: %0x has been deleted", ev.ID)
			return
		}
	}

	return
}
