package zooid

import (
	"context"
	"iter"
	"log"
	"net/http"
	"slices"

	"fiatjaf.com/nostr"
	"fiatjaf.com/nostr/khatru"
	"github.com/gosimple/slug"
)

type Instance struct {
	Relay      *khatru.Relay
	Config     *Config
	Events     *EventStore
	Blossom    *BlossomStore
	Management *ManagementStore
	Groups     *GroupStore
	Push       *PushManager
}

func MakeInstance(filename string) (*Instance, error) {
	config, err := LoadConfig(filename)
	if err != nil {
		return nil, err
	}

	relay := khatru.NewRelay()

	events := &EventStore{
		Relay:  relay,
		Config: config,
		Schema: &Schema{
			Name: slug.Make(config.Schema),
		},
	}

	blossom := &BlossomStore{
		Config: config,
		Events: events,
	}

	management := &ManagementStore{
		Config: config,
		Events: events,
	}

	groups := &GroupStore{
		Config:     config,
		Events:     events,
		Management: management,
	}

	push := &PushManager{
		Config:     config,
		Events:     events,
		Management: management,
		Groups:     groups,
	}

	instance := &Instance{
		Relay:      relay,
		Config:     config,
		Events:     events,
		Blossom:    blossom,
		Management: management,
		Groups:     groups,
		Push:       push,
	}

	// NIP 11 info

	self := config.GetSelf()
	owner := config.GetOwner()

	instance.Relay.Negentropy = true
	instance.Relay.Info.Name = config.Info.Name
	instance.Relay.Info.Icon = config.Info.Icon
	instance.Relay.Info.Self = &self
	instance.Relay.Info.PubKey = &owner
	instance.Relay.Info.Description = config.Info.Description
	instance.Relay.Info.Software = "https://github.com/coracle-social/zooid"
	instance.Relay.Info.Version = "v0.1.0"
	instance.Relay.Info.SupportedNIPs = append(instance.Relay.Info.SupportedNIPs, "43")

	// Handlers

	instance.Relay.OnConnect = instance.OnConnect
	instance.Relay.PreventBroadcast = instance.PreventBroadcast
	instance.Relay.StoreEvent = instance.StoreEvent
	instance.Relay.ReplaceEvent = instance.ReplaceEvent
	instance.Relay.DeleteEvent = instance.DeleteEvent
	instance.Relay.OnRequest = instance.OnRequest
	instance.Relay.QueryStored = instance.QueryStored
	instance.Relay.OnEvent = instance.OnEvent
	instance.Relay.OnEventSaved = instance.OnEventSaved
	instance.Relay.OnEphemeralEvent = instance.OnEphemeralEvent

	// Expiration

	instance.Relay.StartExpirationManager(instance.Relay.QueryStored, instance.Relay.DeleteEvent)

	// HTTP request handling

	router := instance.Relay.Router()

	router.HandleFunc("/{$}", func(w http.ResponseWriter, r *http.Request) {
		http.ServeFile(w, r, "templates/index.html")
	})

	router.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static"))))

	// Initialize the database

	if err := instance.Events.Init(); err != nil {
		log.Fatal("Failed to initialize event store: ", err)
	}

	// Enable extra functionality

	if config.Blossom.Enabled {
		instance.Blossom.Enable(instance)
	}

	if config.Management.Enabled {
		instance.Management.Enable(instance)
	}

	if config.Groups.Enabled {
		instance.Groups.Enable(instance)
	}

	if config.Push.Enabled {
		instance.Push.Enable(instance)
	}

	// Update managed membership/admin lists

	instance.Management.AllowPubkey(config.GetSelf())
	instance.Management.AllowPubkey(config.GetOwner())

	for _, role := range config.Roles {
		for _, hex := range role.Pubkeys {
			if pubkey, err := nostr.PubKeyFromHex(hex); err == nil {
				instance.Management.AllowPubkey(pubkey)
			}
		}
	}

	return instance, nil
}

func (instance *Instance) Cleanup() {
	instance.Relay.DisableExpirationManager()
	instance.Events.Close()
}

// Utility methods

func (instance *Instance) StripSignature(pubkey nostr.PubKey, event nostr.Event) nostr.Event {
	if instance.Config.Policy.StripSignatures && !instance.Config.CanManage(pubkey) {
		var zeroSig [64]byte
		event.Sig = zeroSig
	}

	return event
}

func (instance *Instance) AllowRecipientEvent(event nostr.Event) bool {
	// For zap receipts and gift wraps, authorize the recipient instead of the author.
	// For everything else, make sure the authenticated user is the same as the event author
	recipientAuthKinds := []nostr.Kind{
		nostr.KindZap,
		nostr.KindGiftWrap,
	}

	if slices.Contains(recipientAuthKinds, event.Kind) {
		recipientTag := event.Tags.Find("p")

		if recipientTag != nil {
			pubkey, err := nostr.PubKeyFromHex(recipientTag[1])

			if err == nil && instance.Management.IsMember(pubkey) {
				return true
			}
		}
	}

	return false
}

func (instance *Instance) GenerateInviteEvent(pubkey nostr.PubKey) nostr.Event {
	filter := nostr.Filter{
		Kinds: []nostr.Kind{RELAY_INVITE},
		Tags: nostr.TagMap{
			"#p": []string{pubkey.Hex()},
		},
	}

	for event := range instance.Events.QueryEvents(filter, 1) {
		return event
	}

	event := nostr.Event{
		Kind:      RELAY_INVITE,
		CreatedAt: nostr.Now(),
		Tags: nostr.Tags{
			[]string{"claim", RandomString(8)},
			[]string{"p", pubkey.Hex()},
		},
	}

	if err := instance.Events.SignAndStoreEvent(&event, false); err != nil {
		log.Printf("Failed to sign invite event: %v", err)
	}

	return event
}

// Handlers

func (instance *Instance) OnConnect(ctx context.Context) {
	khatru.RequestAuth(ctx)
}

func (instance *Instance) PreventBroadcast(ws *khatru.WebSocket, filter nostr.Filter, event nostr.Event) bool {
	return IsWriteOnlyEvent(event)
}

func (instance *Instance) StoreEvent(ctx context.Context, event nostr.Event) error {
	return instance.Events.StoreEvent(event)
}

func (instance *Instance) ReplaceEvent(ctx context.Context, event nostr.Event) error {
	return instance.Events.ReplaceEvent(event)
}

func (instance *Instance) DeleteEvent(ctx context.Context, id nostr.ID) error {
	return instance.Events.DeleteEvent(id)
}

// Requests

func (instance *Instance) OnRequest(ctx context.Context, filter nostr.Filter) (reject bool, msg string) {
	pubkey, ok := khatru.GetAuthed(ctx)

	if !ok {
		return true, "auth-required: authentication is required for access"
	}

	if !instance.Management.IsMember(pubkey) {
		return true, "restricted: you are not a member of this relay"
	}

	return false, ""
}

func (instance *Instance) QueryStored(ctx context.Context, filter nostr.Filter) iter.Seq[nostr.Event] {
	return func(yield func(nostr.Event) bool) {
		if khatru.IsInternalCall(ctx) {
			for event := range instance.Events.QueryEvents(filter, 0) {
				if !yield(event) {
					return
				}
			}
		} else {
			pubkey, _ := khatru.GetAuthed(ctx)
			generated := make([]nostr.Event, 0)

			if slices.Contains(filter.Kinds, RELAY_INVITE) && instance.Config.CanInvite(pubkey) {
				generated = append(generated, instance.GenerateInviteEvent(pubkey))
			}

			for _, event := range generated {
				if !filter.Matches(event) {
					continue
				}

				if !yield(instance.StripSignature(pubkey, event)) {
					return
				}
			}

			for event := range instance.Events.QueryEvents(filter, 1000) {
				if !IsReadableEvent(event) {
					continue
				}

				if event.Kind == PUSH_SUBSCRIPTION && event.PubKey != pubkey {
					continue
				}

				if instance.Groups.IsGroupEvent(event) && !instance.Groups.CanRead(pubkey, event) {
					continue
				}

				if !yield(instance.StripSignature(pubkey, event)) {
					return
				}
			}
		}
	}
}

// Event publishing

func (instance *Instance) OnEvent(ctx context.Context, event nostr.Event) (reject bool, msg string) {
	if instance.AllowRecipientEvent(event) {
		return false, ""
	}

	pubkey, isAuthenticated := khatru.GetAuthed(ctx)

	if !isAuthenticated {
		return true, "auth-required: authentication is required for access"
	} else if pubkey != event.PubKey {
		return true, "restricted: you cannot publish events on behalf of others"
	}

	if event.Kind == RELAY_JOIN {
		return instance.Management.ValidateJoinRequest(event)
	}

	if event.Kind == PUSH_SUBSCRIPTION {
		return instance.Push.ValidatePushSubscription(event)
	}

	if !instance.Management.IsMember(pubkey) {
		return true, "restricted: you are not a member of this relay"
	}

	if IsInternalEvent(event) {
		return true, "invalid: this event's kind is not accepted"
	}

	if IsReadOnlyEvent(event) {
		return true, "invalid: this event's kind is not accepted"
	}

	if instance.Groups.IsGroupEvent(event) {
		if err := instance.Groups.CheckWrite(event); err != "" {
			return true, err
		}
	}

	if instance.Management.EventIsBanned(event.ID) {
		return true, "restricted: this event has been banned from this relay"
	}

	return false, ""
}

func (instance *Instance) OnEventSaved(ctx context.Context, event nostr.Event) {
	h := GetGroupIDFromEvent(event)

	if event.Kind == nostr.KindSimpleGroupJoinRequest && instance.Config.Groups.AutoJoin {
		instance.Groups.AddMember(h, event.PubKey)
		instance.Groups.UpdateMembersList(h)
	}

	if event.Kind == nostr.KindSimpleGroupLeaveRequest {
		instance.Groups.RemoveMember(h, event.PubKey)
		instance.Groups.UpdateMembersList(h)
	}

	if event.Kind == nostr.KindSimpleGroupPutUser {
		instance.Groups.UpdateMembersList(h)
	}

	if event.Kind == nostr.KindSimpleGroupRemoveUser {
		instance.Groups.UpdateMembersList(h)
	}

	if event.Kind == nostr.KindSimpleGroupCreateGroup {
		instance.Groups.UpdateMetadata(event)
		instance.Groups.UpdateAdminsList(h)
	}

	if event.Kind == nostr.KindSimpleGroupEditMetadata {
		instance.Groups.UpdateMetadata(event)
		instance.Groups.UpdateAdminsList(h)
	}

	if event.Kind == nostr.KindSimpleGroupDeleteGroup {
		instance.Groups.DeleteGroup(h)
	}

	if instance.Config.Push.Enabled && !IsWriteOnlyEvent(event) {
		instance.Push.HandleEvent(event)
	}
}

func (instance *Instance) OnEphemeralEvent(ctx context.Context, event nostr.Event) {
	if event.Kind == RELAY_JOIN {
		instance.Management.AddMember(event.PubKey)
	}

	if event.Kind == RELAY_LEAVE {
		instance.Management.RemoveMember(event.PubKey)
	}

	if instance.Config.Push.Enabled && !IsWriteOnlyEvent(event) {
		instance.Push.HandleEvent(event)
	}
}
