package arn

import (
	"errors"
	"fmt"
	"os"
	"path"
	"sync"

	"github.com/aerogo/nano"
	"github.com/akyoto/color"
)

// GroupID represents a group ID.
type GroupID = ID

// Group represents a group of users.
type Group struct {
	Name        string         `json:"name" editable:"true"`
	Tagline     string         `json:"tagline" editable:"true"`
	Image       Image          `json:"image"`
	Description string         `json:"description" editable:"true" type:"textarea"`
	Rules       string         `json:"rules" editable:"true" type:"textarea"`
	Tags        []string       `json:"tags" editable:"true"`
	Members     []*GroupMember `json:"members"`
	Neighbors   []GroupID      `json:"neighbors"`
	// Applications []UserApplication `json:"applications"`

	// Mixins
	hasID
	hasPosts
	hasCreator
	hasEditor
	hasDraft

	// Mutex
	membersMutex sync.Mutex

	// Moved this boolean field to the bottom because the structure consumes less bytes that way
	Restricted bool `json:"restricted" editable:"true" tooltip:"Restricted groups can only be joined with the founder's permission."`
}

// Link returns the URI to the group page.
func (group *Group) Link() string {
	return "/group/" + group.ID
}

// TitleByUser returns the preferred title for the given user.
func (group *Group) TitleByUser(user *User) string {
	if group.Name == "" {
		return "untitled"
	}

	return group.Name
}

// String is the default text representation of the group.
func (group *Group) String() string {
	return group.TitleByUser(nil)
}

// FindMember returns the group member by user ID, if available.
func (group *Group) FindMember(userID UserID) *GroupMember {
	group.membersMutex.Lock()
	defer group.membersMutex.Unlock()

	for _, member := range group.Members {
		if member.UserID == userID {
			return member
		}
	}

	return nil
}

// HasMember returns true if the user is a member of the group.
func (group *Group) HasMember(userID UserID) bool {
	return group.FindMember(userID) != nil
}

// Users returns a slice of all users in the group.
func (group *Group) Users() []*User {
	group.membersMutex.Lock()
	defer group.membersMutex.Unlock()
	users := make([]*User, len(group.Members))

	for index, member := range group.Members {
		users[index] = member.User()
	}

	return users
}

// TypeName returns the type name.
func (group *Group) TypeName() string {
	return "Group"
}

// Self returns the object itself.
func (group *Group) Self() Loggable {
	return group
}

// Publish ...
func (group *Group) Publish() error {
	if len(group.Name) < 2 {
		return errors.New("Name too short: Should be at least 2 characters")
	}

	if len(group.Name) > 35 {
		return errors.New("Name too long: Should not be more than 35 characters")
	}

	if len(group.Tagline) < 4 {
		return errors.New("Tagline too short: Should be at least 4 characters")
	}

	if len(group.Tagline) > 60 {
		return errors.New("Tagline too long: Should not be more than 60 characters")
	}

	if len(group.Description) < 10 {
		return errors.New("Your group needs a description (at least 10 characters)")
	}

	if len(group.Tags) < 1 {
		return errors.New("At least one tag is required")
	}

	if !group.HasImage() {
		return errors.New("Group image required")
	}

	return publish(group)
}

// Unpublish ...
func (group *Group) Unpublish() error {
	return unpublish(group)
}

// Join makes the given user join the group.
func (group *Group) Join(user *User) error {
	// Check if the user is already a member
	member := group.FindMember(user.ID)

	if member != nil {
		return errors.New("Already a member of this group")
	}

	// Add user to the members list
	group.membersMutex.Lock()

	group.Members = append(group.Members, &GroupMember{
		UserID: user.ID,
		Joined: DateTimeUTC(),
	})

	group.membersMutex.Unlock()

	// Trigger notifications
	group.OnJoin(user)
	return nil
}

// Leave makes the given user leave the group.
func (group *Group) Leave(user *User) error {
	group.membersMutex.Lock()
	defer group.membersMutex.Unlock()

	for index, member := range group.Members {
		if member.UserID == user.ID {
			if member.UserID == group.CreatedBy {
				return errors.New("The founder can not leave the group, please contact a staff member")
			}

			group.Members = append(group.Members[:index], group.Members[index+1:]...)
			return nil
		}
	}

	return nil
}

// OnJoin sends notifications to the creator.
func (group *Group) OnJoin(user *User) {
	go func() {
		group.Creator().SendNotification(&PushNotification{
			Title:   fmt.Sprintf(`%s joined your group!`, user.Nick),
			Message: fmt.Sprintf(`%s has joined your group "%s"`, user.Nick, group.Name),
			Icon:    "https:" + user.AvatarLink("large"),
			Link:    "https://notify.moe" + group.Link() + "/members",
			Type:    NotificationTypeGroupJoin,
		})
	}()
}

// SendNotification sends a notification to all group members except for the excluded user ID.
func (group *Group) SendNotification(notification *PushNotification, excludeUserID UserID) {
	for _, user := range group.Users() {
		if user.ID == excludeUserID {
			continue
		}

		user.SendNotification(notification)
	}
}

// AverageColor returns the average color of the image.
func (group *Group) AverageColor() string {
	color := group.Image.AverageColor

	if color.Hue == 0 && color.Saturation == 0 && color.Lightness == 0 {
		return ""
	}

	return color.String()
}

// ImageLink returns a link to the group image.
func (group *Group) ImageLink(size string) string {
	if !group.HasImage() {
		return fmt.Sprintf("//%s/images/elements/no-group-image.svg", MediaHost)
	}

	extension := ".jpg"

	if size == "original" {
		extension = group.Image.Extension
	}

	return fmt.Sprintf("//%s/images/groups/%s/%s%s?%v", MediaHost, size, group.ID, extension, group.Image.LastModified)
}

// DeleteImages deletes all images for the group.
func (group *Group) DeleteImages() {
	if group.Image.Extension == "" {
		return
	}

	// Original
	err := os.Remove(path.Join(Root, "images/groups/original/", group.ID+group.Image.Extension))

	if err != nil {
		// Don't return the error.
		// It's too late to stop the process at this point.
		// Instead, log the error.
		color.Red(err.Error())
	}

	// Small
	os.Remove(path.Join(Root, "images/groups/small/", group.ID+".jpg"))
	os.Remove(path.Join(Root, "images/groups/small/", group.ID+"@2.jpg"))
	os.Remove(path.Join(Root, "images/groups/small/", group.ID+".webp"))
	os.Remove(path.Join(Root, "images/groups/small/", group.ID+"@2.webp"))

	// Large
	os.Remove(path.Join(Root, "images/groups/large/", group.ID+".jpg"))
	os.Remove(path.Join(Root, "images/groups/large/", group.ID+"@2.jpg"))
	os.Remove(path.Join(Root, "images/groups/large/", group.ID+".webp"))
	os.Remove(path.Join(Root, "images/groups/large/", group.ID+"@2.webp"))
}

// GetGroup ...
func GetGroup(id GroupID) (*Group, error) {
	obj, err := DB.Get("Group", id)

	if err != nil {
		return nil, err
	}

	return obj.(*Group), nil
}

// StreamGroups returns a stream of all groups.
func StreamGroups() <-chan *Group {
	channel := make(chan *Group, nano.ChannelBufferSize)

	go func() {
		for obj := range DB.All("Group") {
			channel <- obj.(*Group)
		}

		close(channel)
	}()

	return channel
}

// AllGroups returns a slice of all groups.
func AllGroups() []*Group {
	all := make([]*Group, 0, DB.Collection("Group").Count())
	stream := StreamGroups()

	for obj := range stream {
		all = append(all, obj)
	}

	return all
}

// FilterGroups filters all groups by a custom function.
func FilterGroups(filter func(*Group) bool) []*Group {
	var filtered []*Group

	for obj := range DB.All("Group") {
		realObject := obj.(*Group)

		if filter(realObject) {
			filtered = append(filtered, realObject)
		}
	}

	return filtered
}