//go:build !(js && wasm)

package database

import (
	"context"
	"errors"
	"fmt"
	"strings"

	"github.com/dgraph-io/badger/v4"
	"lol.mleku.dev/chk"
	"lol.mleku.dev/log"
	"next.orly.dev/pkg/database/bufpool"
	"next.orly.dev/pkg/database/indexes"
	"next.orly.dev/pkg/database/indexes/types"
	"next.orly.dev/pkg/mode"
	"git.mleku.dev/mleku/nostr/encoders/event"
	"git.mleku.dev/mleku/nostr/encoders/filter"
	"git.mleku.dev/mleku/nostr/encoders/hex"
	"git.mleku.dev/mleku/nostr/encoders/kind"
	"git.mleku.dev/mleku/nostr/encoders/tag"
)

var (
	// ErrOlderThanExisting is returned when a candidate event is older than an existing replaceable/addressable event.
	ErrOlderThanExisting = errors.New("older than existing event")
	// ErrMissingDTag is returned when a parameterized replaceable event lacks the required 'd' tag.
	ErrMissingDTag = errors.New("event is missing a d tag identifier")
)

func (d *D) GetSerialsFromFilter(f *filter.F) (
	sers types.Uint40s, err error,
) {
	// Try p-tag graph optimization first
	if CanUsePTagGraph(f) {
		log.D.F("GetSerialsFromFilter: trying p-tag graph optimization")
		if sers, err = d.QueryPTagGraph(f); err == nil && len(sers) >= 0 {
			log.D.F("GetSerialsFromFilter: p-tag graph optimization returned %d serials", len(sers))
			return
		}
		// Fall through to traditional indexes on error
		log.D.F("GetSerialsFromFilter: p-tag graph optimization failed, falling back to traditional indexes: %v", err)
		err = nil
	}

	var idxs []Range
	if idxs, err = GetIndexesFromFilter(f); chk.E(err) {
		return
	}
	// Pre-allocate slice with estimated capacity to reduce reallocations
	sers = make(
		types.Uint40s, 0, len(idxs)*100,
	) // Estimate 100 serials per index
	for _, idx := range idxs {
		var s types.Uint40s
		if s, err = d.GetSerialsByRange(idx); chk.E(err) {
			continue
		}
		sers = append(sers, s...)
	}
	return
}

// WouldReplaceEvent checks if the provided event would replace existing events
// based on Nostr's replaceable or parameterized replaceable semantics. It
// returns true if the candidate is newer-or-equal than existing events.
// If an existing event is newer, it returns (false, nil, ErrOlderThanExisting).
// If no conflicts exist, it returns (false, nil, nil).
func (d *D) WouldReplaceEvent(ev *event.E) (bool, types.Uint40s, error) {
	// Only relevant for replaceable or parameterized replaceable kinds
	if !(kind.IsReplaceable(ev.Kind) || kind.IsParameterizedReplaceable(ev.Kind)) {
		return false, nil, nil
	}

	var f *filter.F
	if kind.IsReplaceable(ev.Kind) {
		f = &filter.F{
			Authors: tag.NewFromBytesSlice(ev.Pubkey),
			Kinds:   kind.NewS(kind.New(ev.Kind)),
		}
	} else {
		// parameterized replaceable requires 'd' tag
		dTag := ev.Tags.GetFirst([]byte("d"))
		if dTag == nil {
			return false, nil, ErrMissingDTag
		}
		f = &filter.F{
			Authors: tag.NewFromBytesSlice(ev.Pubkey),
			Kinds:   kind.NewS(kind.New(ev.Kind)),
			Tags: tag.NewS(
				tag.NewFromAny("d", dTag.Value()),
			),
		}
	}

	sers, err := d.GetSerialsFromFilter(f)
	if chk.E(err) {
		return false, nil, err
	}
	if len(sers) == 0 {
		return false, nil, nil
	}

	// Determine if any existing event is newer than the candidate
	shouldReplace := true
	for _, s := range sers {
		oldEv, ferr := d.FetchEventBySerial(s)
		if chk.E(ferr) {
			continue
		}
		if ev.CreatedAt < oldEv.CreatedAt {
			shouldReplace = false
			break
		}
	}
	if shouldReplace {
		return true, nil, nil
	}
	return false, nil, ErrOlderThanExisting
}

// SaveEvent saves an event to the database, generating all the necessary indexes.
func (d *D) SaveEvent(c context.Context, ev *event.E) (
	replaced bool, err error,
) {
	if ev == nil {
		err = errors.New("nil event")
		return
	}

	// Reject ephemeral events (kinds 20000-29999) - they should never be stored
	if ev.Kind >= 20000 && ev.Kind <= 29999 {
		err = errors.New("blocked: ephemeral events should not be stored")
		return
	}

	// Validate kind 3 (follow list) events have at least one p tag
	// This prevents storing malformed follow lists that may come from buggy relays
	if ev.Kind == 3 {
		hasPTag := false
		tagCount := 0
		if ev.Tags != nil {
			tagCount = ev.Tags.Len()
			for _, tag := range *ev.Tags {
				if tag != nil && tag.Len() >= 2 {
					key := tag.Key()
					if len(key) == 1 && key[0] == 'p' {
						hasPTag = true
						break
					}
				}
			}
		}
		if !hasPTag {
			log.W.F("SaveEvent: rejecting kind 3 event without p tags from pubkey %x (total tags: %d, event ID: %x)",
				ev.Pubkey, tagCount, ev.ID)
			err = errors.New("blocked: kind 3 follow list events must have at least one p tag")
			return
		}
	}

	// check if the event already exists
	var ser *types.Uint40
	if ser, err = d.GetSerialById(ev.ID); err == nil && ser != nil {
		err = errors.New("blocked: event already exists: " + hex.Enc(ev.ID[:]))
		return
	}

	// If the error is "id not found", we can proceed with saving the event
	if err != nil && strings.Contains(err.Error(), "id not found in database") {
		// Reset error since this is expected for new events
		err = nil
	} else if err != nil {
		// For any other error, return it
		// log.E.F("error checking if event exists: %s", err)
		return
	}

	// Check if the event has been deleted before allowing resubmission
	// Skip deletion check when ACL is "none" (open relay mode)
	if !mode.IsOpen() {
		if err = d.CheckForDeleted(ev, nil); err != nil {
			// log.I.F(
			// 	"SaveEvent: rejecting resubmission of deleted event ID=%s: %v",
			// 	hex.Enc(ev.ID), err,
			// )
			err = fmt.Errorf("blocked: %s", err.Error())
			return
		}
	}
	// check for replacement - only validate, don't delete old events
	if kind.IsReplaceable(ev.Kind) || kind.IsParameterizedReplaceable(ev.Kind) {
		var werr error
		if replaced, _, werr = d.WouldReplaceEvent(ev); werr != nil {
			if errors.Is(werr, ErrOlderThanExisting) {
				if kind.IsReplaceable(ev.Kind) {
					err = errors.New("blocked: event is older than existing replaceable event")
				} else {
					err = errors.New("blocked: event is older than existing addressable event")
				}
				return
			}
			if errors.Is(werr, ErrMissingDTag) {
				// keep behavior consistent with previous implementation
				err = ErrMissingDTag
				return
			}
			// any other error
			return
		}
		// Note: replaced flag is kept for compatibility but old events are no longer deleted
	}
	// Get the next sequence number for the event
	var serial uint64
	if serial, err = d.seq.Next(); chk.E(err) {
		return
	}
	// Generate all indexes for the event
	var idxs [][]byte
	if idxs, err = GetIndexesForEvent(ev, serial); chk.E(err) {
		return
	}

	// Collect all pubkeys for graph: author + p-tags
	// Store with direction indicator: author (0) vs p-tag (1)
	type pubkeyWithDirection struct {
		serial    *types.Uint40
		isAuthor  bool
	}
	pubkeysForGraph := make(map[string]pubkeyWithDirection)

	// Add author pubkey
	var authorSerial *types.Uint40
	if authorSerial, err = d.GetOrCreatePubkeySerial(ev.Pubkey); chk.E(err) {
		return
	}
	pubkeysForGraph[hex.Enc(ev.Pubkey)] = pubkeyWithDirection{
		serial:   authorSerial,
		isAuthor: true,
	}

	// Extract p-tag pubkeys using GetAll
	pTags := ev.Tags.GetAll([]byte("p"))
	for _, pTag := range pTags {
		if pTag.Len() >= 2 {
			// Get pubkey from p-tag, handling both binary and hex storage formats
			// ValueHex() returns hex regardless of internal storage format
			var ptagPubkey []byte
			if ptagPubkey, err = hex.Dec(string(pTag.ValueHex())); err == nil && len(ptagPubkey) == 32 {
				pkHex := hex.Enc(ptagPubkey)
				// Skip if already added as author
				if _, exists := pubkeysForGraph[pkHex]; !exists {
					var ptagSerial *types.Uint40
					if ptagSerial, err = d.GetOrCreatePubkeySerial(ptagPubkey); chk.E(err) {
						return
					}
					pubkeysForGraph[pkHex] = pubkeyWithDirection{
						serial:   ptagSerial,
						isAuthor: false,
					}
				}
			}
		}
	}
	// log.T.F(
	// 	"SaveEvent: generated %d indexes for event %x (kind %d)", len(idxs),
	// 	ev.ID, ev.Kind,
	// )

	// Create serial resolver for compact encoding
	resolver := NewDatabaseSerialResolver(d, d.serialCache)

	// Serialize event in compact format using serial references
	// This dramatically reduces storage by replacing 32-byte IDs/pubkeys with 5-byte serials
	compactData, compactErr := MarshalCompactEvent(ev, resolver)

	// Calculate legacy size for comparison (for metrics tracking)
	// We marshal to get accurate size comparison
	legacyBuf := bufpool.GetMedium()
	defer bufpool.PutMedium(legacyBuf)
	ev.MarshalBinary(legacyBuf)
	legacySize := legacyBuf.Len()

	if compactErr != nil {
		// Fall back to legacy format if compact encoding fails
		log.W.F("SaveEvent: compact encoding failed, using legacy format: %v", compactErr)
		compactData = bufpool.CopyBytes(legacyBuf)
	} else {
		// Track storage savings
		TrackCompactSaving(legacySize, len(compactData))
		log.T.F("SaveEvent: compact %d bytes vs legacy %d bytes (saved %d bytes, %.1f%%)",
			len(compactData), legacySize, legacySize-len(compactData),
			float64(legacySize-len(compactData))/float64(legacySize)*100.0)
	}

	// Start a transaction to save the event and all its indexes
	err = d.Update(
		func(txn *badger.Txn) (err error) {
			// Pre-allocate key buffer to avoid allocations in loop
			ser := new(types.Uint40)
			if err = ser.Set(serial); chk.E(err) {
				return
			}

			// Save each index
			for _, key := range idxs {
				if err = txn.Set(key, nil); chk.E(err) {
					return
				}
			}

			// Store the SerialEventId mapping (serial -> full 32-byte event ID)
			// This is required for reconstructing compact events
			if err = d.StoreEventIdSerial(txn, serial, ev.ID); chk.E(err) {
				return
			}

			// Cache the event ID mapping
			d.serialCache.CacheEventId(serial, ev.ID)

			// Store compact event with cmp prefix
			// Format: cmp|serial|compact_event_data
			// This is the only storage format - legacy evt/sev/aev/rev prefixes
			// are handled by migration and no longer written for new events
			cmpKeyBuf := bufpool.GetSmall()
			if err = indexes.CompactEventEnc(ser).MarshalWrite(cmpKeyBuf); chk.E(err) {
				bufpool.PutSmall(cmpKeyBuf)
				return
			}
			if err = txn.Set(bufpool.CopyBytes(cmpKeyBuf), compactData); chk.E(err) {
				bufpool.PutSmall(cmpKeyBuf)
				return
			}
			bufpool.PutSmall(cmpKeyBuf)

			// Create graph edges between event and all related pubkeys
			// This creates bidirectional edges: event->pubkey and pubkey->event
			// Include the event kind and direction for efficient graph queries
			eventKind := new(types.Uint16)
			eventKind.Set(ev.Kind)

			// Reuse a single buffer for graph edge keys (reset between uses)
			graphKeyBuf := bufpool.GetSmall()
			defer bufpool.PutSmall(graphKeyBuf)

			for _, pkInfo := range pubkeysForGraph {
				// Determine direction for forward edge (event -> pubkey perspective)
				directionForward := new(types.Letter)
				// Determine direction for reverse edge (pubkey -> event perspective)
				directionReverse := new(types.Letter)

				if pkInfo.isAuthor {
					// Event author relationship
					directionForward.Set(types.EdgeDirectionAuthor)  // 0: author
					directionReverse.Set(types.EdgeDirectionAuthor)  // 0: is author of event
				} else {
					// P-tag relationship
					directionForward.Set(types.EdgeDirectionPTagOut) // 1: event references pubkey (outbound)
					directionReverse.Set(types.EdgeDirectionPTagIn)  // 2: pubkey is referenced (inbound)
				}

				// Create event -> pubkey edge (with kind and direction)
				graphKeyBuf.Reset()
				if err = indexes.EventPubkeyGraphEnc(ser, pkInfo.serial, eventKind, directionForward).MarshalWrite(graphKeyBuf); chk.E(err) {
					return
				}
				if err = txn.Set(bufpool.CopyBytes(graphKeyBuf), nil); chk.E(err) {
					return
				}

				// Create pubkey -> event edge (reverse, with kind and direction for filtering)
				graphKeyBuf.Reset()
				if err = indexes.PubkeyEventGraphEnc(pkInfo.serial, eventKind, directionReverse, ser).MarshalWrite(graphKeyBuf); chk.E(err) {
					return
				}
				if err = txn.Set(bufpool.CopyBytes(graphKeyBuf), nil); chk.E(err) {
					return
				}
			}

			// Create event-to-event graph edges for e-tags
			// This enables thread traversal and finding replies/reactions to events
			eTags := ev.Tags.GetAll([]byte("e"))
			for _, eTag := range eTags {
				if eTag.Len() >= 2 {
					// Get event ID from e-tag, handling both binary and hex storage formats
					var targetEventID []byte
					if targetEventID, err = hex.Dec(string(eTag.ValueHex())); err != nil || len(targetEventID) != 32 {
						continue
					}

					// Look up the target event's serial (if it exists in our database)
					var targetSerial *types.Uint40
					if targetSerial, err = d.GetSerialById(targetEventID); err != nil {
						// Target event not in our database - skip edge creation
						// This is normal for replies to events we don't have
						err = nil
						continue
					}

					// Create forward edge: source event -> target event (outbound e-tag)
					directionOut := new(types.Letter)
					directionOut.Set(types.EdgeDirectionETagOut)
					graphKeyBuf.Reset()
					if err = indexes.EventEventGraphEnc(ser, targetSerial, eventKind, directionOut).MarshalWrite(graphKeyBuf); chk.E(err) {
						return
					}
					if err = txn.Set(bufpool.CopyBytes(graphKeyBuf), nil); chk.E(err) {
						return
					}

					// Create reverse edge: target event -> source event (inbound e-tag)
					directionIn := new(types.Letter)
					directionIn.Set(types.EdgeDirectionETagIn)
					graphKeyBuf.Reset()
					if err = indexes.GraphEventEventEnc(targetSerial, eventKind, directionIn, ser).MarshalWrite(graphKeyBuf); chk.E(err) {
						return
					}
					if err = txn.Set(bufpool.CopyBytes(graphKeyBuf), nil); chk.E(err) {
						return
					}
				}
			}

			return
		},
	)
	if err != nil {
		return
	}

	// Process deletion events to actually delete the referenced events
	if ev.Kind == kind.Deletion.K {
		if err = d.ProcessDelete(ev, nil); chk.E(err) {
			log.W.F("failed to process deletion for event %x: %v", ev.ID, err)
			// Don't return error - the deletion event was saved successfully
			err = nil
		}
	}

	// Invalidate query cache since a new event was stored
	// This ensures subsequent queries will see the new event
	if d.queryCache != nil {
		d.queryCache.Invalidate()
		// log.T.F("SaveEvent: invalidated query cache")
	}

	return
}
