package private

import (
	"sort"
	"sync"

	"github.com/diamondburned/arikawa/v2/discord"
	"github.com/diamondburned/arikawa/v2/gateway"
	"github.com/diamondburned/cchat"
	"github.com/diamondburned/cchat-discord/internal/discord/channel"
	"github.com/diamondburned/cchat-discord/internal/discord/private/hub"
	"github.com/diamondburned/cchat-discord/internal/discord/state"
	"github.com/diamondburned/cchat/text"
	"github.com/diamondburned/cchat/utils/empty"
	"github.com/pkg/errors"
)

// I don't think the cchat specs said anything about sharing a cchat.Server, so
// we might need to do this. Nevertheless, it seems overkill.
type containerSet struct {
	mut sync.Mutex
	set map[cchat.ServersContainer]struct{}
}

func newContainerSet() *containerSet {
	return &containerSet{
		set: map[cchat.ServersContainer]struct{}{},
	}
}

func (cset *containerSet) Register(container cchat.ServersContainer) {
	cset.mut.Lock()
	cset.set[container] = struct{}{}
	cset.mut.Unlock()
}

// prependServer wraps around Server to always prepend this wrapped server on
// top of the servers container.
type prependServer struct{ cchat.Server }

var _ cchat.ServerUpdate = (*prependServer)(nil)

// PreviousID returns the appropriate parameters to prepend this server.
func (ps prependServer) PreviousID() (cchat.ID, bool) {
	// Return the private container's ID so this server goes right after it.
	return "!!!private-container!!!", false
}

func (cset *containerSet) AddChannel(s *state.Instance, ch *discord.Channel) {
	c, err := channel.New(s, *ch)
	if err != nil {
		return
	}

	replace := prependServer{Server: c}

	cset.mut.Lock()

	for container := range cset.set {
		container.UpdateServer(replace)
	}

	cset.mut.Unlock()
}

type Private struct {
	empty.Server
	state      *state.Instance
	hub        *hub.Server
	containers *containerSet
}

func New(s *state.Instance) (cchat.Server, error) {
	containers := newContainerSet()

	hubServer, err := hub.New(s, containers)
	if err != nil {
		return nil, errors.Wrap(err, "failed to make hub server")
	}

	return Private{
		state:      s,
		hub:        hubServer,
		containers: containers,
	}, nil
}

func (priv Private) ID() cchat.ID {
	// Not even a number, so no chance of colliding with snowflakes.
	return "!!!private-container!!!"
}

func (priv Private) Name() text.Rich {
	return text.Plain("Private Channels")
}

func (priv Private) AsLister() cchat.Lister { return priv }

type activeChannel struct {
	*discord.Channel
	*gateway.ReadState // used for sorting
}

func (active activeChannel) LastMessageID() discord.MessageID {
	if active.ReadState == nil {
		return active.Channel.LastMessageID
	}
	if active.ReadState.LastMessageID > active.Channel.LastMessageID {
		return active.ReadState.LastMessageID
	}
	if active.Channel.LastMessageID.IsValid() {
		return active.Channel.LastMessageID
	}
	// Whatever.
	return discord.MessageID(active.Channel.ID)
}

func (priv Private) Servers(container cchat.ServersContainer) error {
	activeIDs := priv.hub.ActiveChannelIDs()

	channels := make([]activeChannel, 0, len(activeIDs))

	for _, id := range activeIDs {
		c, err := priv.state.Channel(id)
		if err != nil {
			return errors.Wrap(err, "failed to get private channel")
		}

		channels = append(channels, activeChannel{
			Channel:   c,
			ReadState: priv.state.ReadState.FindLast(id),
		})
	}

	// Sort so that channels with the largest last message ID (and therefore the
	// latest message) will be on top.
	sort.Slice(channels, func(i, j int) bool {
		return channels[i].LastMessageID() > channels[j].LastMessageID()
	})

	servers := make([]cchat.Server, len(channels)+1)
	servers[0] = priv.hub

	for i, ch := range channels {
		c, err := channel.New(priv.state, *ch.Channel)
		if err != nil {
			return errors.Wrap(err, "failed to create server for private channel")
		}

		servers[i+1] = c
	}

	container.SetServers(servers)
	priv.containers.Register(container)
	return nil
}