package main

import (
	"context"
	"encoding/json"
	"fmt"
	"math/rand"
	"net/url"
	"time"

	"github.com/nbd-wtf/go-nostr"
	"github.com/nbd-wtf/go-nostr/nip05"
	"github.com/nbd-wtf/go-nostr/nip19"
	"github.com/nbd-wtf/go-nostr/sdk"
	"golang.org/x/exp/slices"
)

var (
	pool   = nostr.NewSimplePool(context.Background())
	serial int

	always = []string{
		"wss://relay.nostr.band",
	}
	everything = []string{
		"wss://nostr-pub.wellorder.net",
		"wss://relay.damus.io",
		"wss://relay.nostr.bg",
		"wss://nostr.wine",
		"wss://nos.lol",
		"wss://nostr.mom",
		"wss://atlas.nostr.land",
		"wss://relay.snort.social",
		"wss://offchain.pub",
		"wss://relay.primal.net",
		"wss://public.relaying.io",
	}
	profiles = []string{
		"wss://purplepag.es",
	}

	trustedPubKeys = []string{
		"7bdef7be22dd8e59f4600e044aa53a1cf975a9dc7d27df5833bc77db784a5805", // dtonon
		"3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d", // fiatjaf
		"97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322", // hodlbod
		"ee11a5dff40c19a555f41fe42b48f00e618c91225622ae37b6c2bb67b76c4e49", // Michael Dilger
	}

	excludedRelays = []string{
		"wss://filter.nostr.wine", // paid
	}
)

type CachedEvent struct {
	Event  *nostr.Event `json:"e"`
	Relays []string     `json:"r"`
}

func getRelay() string {
	if serial == 0 {
		serial = rand.Intn(len(everything))
	}
	serial = (serial + 1) % len(everything)
	return everything[serial]
}

func getEvent(ctx context.Context, code string, relayHints []string) (*nostr.Event, []string, error) {
	if b, ok := cache.Get(code); ok {
		v := CachedEvent{}
		err := json.Unmarshal(b, &v)
		return v.Event, v.Relays, err
	}

	withRelays := false

	if len(relayHints) > 0 {
		withRelays = true
	}

	prefix, data, err := nip19.Decode(code)
	if err != nil {
		pp, _ := nip05.QueryIdentifier(ctx, code)
		if pp == nil {
			return nil, nil, fmt.Errorf("failed to decode %w", err)
		}
		data = *pp
	}

	var author string

	var filter nostr.Filter
	relays := make([]string, 0, 25)
	relays = append(relays, relayHints...)
	relays = append(relays, always...)

	switch v := data.(type) {
	case nostr.ProfilePointer:
		author = v.PublicKey
		filter.Authors = []string{v.PublicKey}
		filter.Kinds = []int{0}
		relays = append(relays, profiles...)
		relays = append(relays, v.Relays...)
		withRelays = true
	case nostr.EventPointer:
		author = v.Author
		filter.IDs = []string{v.ID}
		relays = append(relays, getRelay())
		relays = append(relays, getRelay())
		relays = append(relays, v.Relays...)
		withRelays = true
	case nostr.EntityPointer:
		author = v.PublicKey
		filter.Authors = []string{v.PublicKey}
		filter.Tags = nostr.TagMap{
			"d": []string{v.Identifier},
		}
		if v.Kind != 0 {
			filter.Kinds = append(filter.Kinds, v.Kind)
		}
		relays = append(relays, getRelay())
		relays = append(relays, getRelay())
		relays = append(relays, v.Relays...)
		withRelays = true
	case string:
		if prefix == "note" {
			filter.IDs = []string{v}
			relays = append(relays, getRelay())
			relays = append(relays, getRelay())
			relays = append(relays, getRelay())
		} else if prefix == "npub" {
			author = v
			filter.Authors = []string{v}
			filter.Kinds = []int{0}
			relays = append(relays, profiles...)
		}
	}

	if author != "" {
		// fetch relays for author
		authorRelays := relaysForPubkey(ctx, author, relays...)
		if len(authorRelays) > 5 {
			authorRelays = authorRelays[:5]
		}
		relays = append(relays, authorRelays...)
	}

	for len(relays) < 5 {
		relays = append(relays, getRelay())
	}

	relays = unique(relays)
	ctx, cancel := context.WithTimeout(ctx, time.Second*8)
	defer cancel()

	// actually fetch the event here
	var result *nostr.Event
	var successRelays []string = nil
	if withRelays {
		successRelays = make([]string, 0, len(relays))
		countdown := 7.5
		go func() {
			for {
				time.Sleep(500 * time.Millisecond)
				if countdown <= 0 {
					cancel()
					break
				}
				countdown -= 0.5
			}
		}()

		for ie := range pool.SubManyEoseNonUnique(ctx, relays, nostr.Filters{filter}) {
			if pu, err := url.Parse(ie.Relay.URL); err == nil {
				successRelays = append(successRelays, pu.Host+pu.RawPath)
			}
			result = ie.Event
			countdown = min(countdown, 1)
		}
	} else {
		ie := pool.QuerySingle(ctx, relays, filter)
		if ie != nil {
			result = ie.Event
		}
	}

	if result == nil {
		return nil, nil, fmt.Errorf("couldn't find this %s", prefix)
	}

	cache.SetJSONWithTTL(code, CachedEvent{Event: result, Relays: successRelays}, time.Hour*24*7)
	return result, successRelays, nil
}

func authorLastNotes(ctx context.Context, pubkey string, relays []string, isSitemap bool) []*nostr.Event {
	key := ""
	limit := 100
	if isSitemap {
		key = "lns:" + pubkey
		limit = 50000
	} else {
		key = "ln:" + pubkey
	}

	lastNotes := make([]*nostr.Event, 0, limit)
	if ok := cache.GetJSON(key, &lastNotes); ok {
		return lastNotes
	}

	ctx, cancel := context.WithTimeout(ctx, time.Second*4)
	defer cancel()

	relays = append(relays, getRelay())
	relays = append(relays, getRelay())
	relays = unique(relays)

	ch := pool.SubManyEose(ctx, relays, nostr.Filters{
		{
			Kinds:   []int{nostr.KindTextNote},
			Authors: []string{pubkey},
			Limit:   limit,
		},
	})

	for {
		select {
		case ie, more := <-ch:
			if !more {
				goto end
			}
			lastNotes = append(lastNotes, ie.Event)
		case <-ctx.Done():
			goto end
		}
	}

end:
	slices.SortFunc(lastNotes, func(a, b *nostr.Event) bool { return a.CreatedAt > b.CreatedAt })
	if len(lastNotes) > 0 {
		cache.SetJSONWithTTL(key, lastNotes, time.Hour*24)
	}
	return lastNotes
}

func relayLastNotes(ctx context.Context, relayUrl string, isSitemap bool) []*nostr.Event {
	key := ""
	limit := 1000
	if isSitemap {
		key = "rlns:" + nostr.NormalizeURL(relayUrl)
		limit = 5000
	} else {
		key = "rln:" + nostr.NormalizeURL(relayUrl)
	}

	lastNotes := make([]*nostr.Event, 0, limit)
	if ok := cache.GetJSON(key, &lastNotes); ok {
		return lastNotes
	}

	ctx, cancel := context.WithTimeout(ctx, time.Second*4)
	defer cancel()

	if relay, err := pool.EnsureRelay(relayUrl); err == nil {
		lastNotes, _ = relay.QuerySync(ctx, nostr.Filter{
			Kinds: []int{1},
			Limit: limit,
		})
	}

	slices.SortFunc(lastNotes, func(a, b *nostr.Event) bool { return a.CreatedAt > b.CreatedAt })
	if len(lastNotes) > 0 {
		cache.SetJSONWithTTL(key, lastNotes, time.Hour*24)
	}
	return lastNotes
}

func relaysForPubkey(ctx context.Context, pubkey string, extraRelays ...string) []string {
	pubkeyRelays := make([]string, 0, 12)
	if ok := cache.GetJSON("io:"+pubkey, &pubkeyRelays); !ok {
		ctx, cancel := context.WithTimeout(ctx, time.Millisecond*1500)
		for _, relay := range sdk.FetchRelaysForPubkey(ctx, pool, pubkey, extraRelays...) {
			if relay.Outbox {
				pubkeyRelays = append(pubkeyRelays, relay.URL)
			}
		}
		cancel()
		if len(pubkeyRelays) > 0 {
			cache.SetJSONWithTTL("io:"+pubkey, pubkeyRelays, time.Hour*24*7)
		}
	}
	pubkeyRelays = unique(pubkeyRelays)
	return pubkeyRelays
}

func contactsForPubkey(ctx context.Context, pubkey string, extraRelays ...string) []string {
	pubkeyContacts := make([]string, 0, 100)
	relays := make([]string, 0, 12)
	if ok := cache.GetJSON("cc:"+pubkey, &pubkeyContacts); !ok {
		log.Debug().Msgf("searching contacts for %s", pubkey)
		ctx, cancel := context.WithTimeout(ctx, time.Second*3)

		pubkeyRelays := relaysForPubkey(ctx, pubkey, relays...)
		relays = append(relays, pubkeyRelays...)
		relays = append(relays, always...)
		relays = append(relays, profiles...)

		ch := pool.SubManyEose(ctx, relays, nostr.Filters{
			{
				Kinds:   []int{3},
				Authors: []string{pubkey},
				Limit:   2,
			},
		})

		for {
			select {
			case evt, more := <-ch:
				if !more {
					goto end
				}
				for _, tag := range evt.Tags {
					if tag[0] == "p" {
						pubkeyContacts = append(pubkeyContacts, tag[1])
					}
				}
			case <-ctx.Done():
				goto end
			}
		}

	end:
		cancel()
		if len(pubkeyContacts) > 0 {
			cache.SetJSONWithTTL("cc:"+pubkey, pubkeyContacts, time.Hour*6)
		}
	}
	pubkeyContacts = unique(pubkeyContacts)
	return pubkeyContacts
}
