package server

import (
	"bytes"
	_ "embed"
	"io"
	"os"
	"path"
	"sort"
	"time"

	"github.com/google/uuid"
	"github.com/rs/zerolog/log"
	"go.buzzert.net/kordophone-mock/data"
	"go.buzzert.net/kordophone-mock/model"
	"go.buzzert.net/kordophone-mock/resources"
)

const VERSION string = "KordophoneMock-2.6"

var ATTACHMENT_CONVO_DISP_NAME string = "Attachments"

const (
	AUTH_USERNAME = "test"
	AUTH_PASSWORD = "test"
)

type Server struct {
	version         string
	conversations   []model.Conversation
	authTokens      []model.AuthToken
	attachmentStore model.AttachmentStore
	messageStore    map[string][]model.Message
	updateItems     map[int]model.UpdateItem
	updateChannels  []chan []model.UpdateItem
	updateItemSeq   int
}

type MessagesQuery struct {
	ConversationGUID string
	BeforeDate       *model.Date
	AfterGUID        *string
	BeforeGUID       *string
	Limit            *int
}

type AuthError struct {
	message string
}

func (e *AuthError) Error() string {
	return e.message
}

type DatabaseError struct {
	message string
}

func (e *DatabaseError) Error() string {
	return e.message
}

func NewServer() *Server {
	attachmentStorePath := path.Join(os.TempDir(), "kpmock", "attachments")

	return &Server{
		version:         VERSION,
		conversations:   []model.Conversation{},
		authTokens:      []model.AuthToken{},
		attachmentStore: model.NewAttachmentStore(attachmentStorePath),
		messageStore:    make(map[string][]model.Message),
		updateItems:     make(map[int]model.UpdateItem),
		updateChannels:  []chan []model.UpdateItem{},
		updateItemSeq:   0,
	}
}

func (s *Server) Version() string {
	return s.version
}

func (s *Server) Conversations() []model.Conversation {
	return s.conversations
}

func (s *Server) SortedConversations() []model.Conversation {
	conversations := s.Conversations()
	sort.Slice(conversations, func(i, j int) bool {
		return conversations[i].Date.After(conversations[j].Date)
	})

	return conversations
}

func (s *Server) ConversationForGUID(guid string) (*model.Conversation, error) {
	var conversation *model.Conversation = nil
	for i := range s.conversations {
		c := &s.conversations[i]
		if c.Guid == guid {
			conversation = c
			break
		}
	}

	if conversation != nil {
		return conversation, nil
	}

	return nil, &DatabaseError{message: "Conversation not found"}
}

func (s *Server) AddConversation(c model.Conversation) {
	s.conversations = append(s.conversations, c)
}

func (s *Server) PopulateWithTestData() {
	numConversations := 100
	cs := make([]model.Conversation, numConversations)
	for i := 0; i < numConversations; i++ {
		cs[i] = data.GenerateRandomConversation()

		// Generate messages
		convo := &cs[i]
		var lastMessage model.Message
		for i := 0; i < 100; i++ {
			message := data.GenerateRandomMessage(convo.Participants)
			s.AppendMessageToConversation(convo, message)

			if lastMessage.Date.Before(message.Date) {
				lastMessage = message
			}
		}

		// Update last message
		convo.LastMessage = lastMessage
		convo.LastMessagePreview = lastMessage.Text
	}

	// Also add an "attachment" conversation
	attachmentConversation := model.Conversation{
		Participants: []string{"Attachments"},
		DisplayName:  &ATTACHMENT_CONVO_DISP_NAME,
		UnreadCount:  0,
		Guid:         uuid.New().String(),
		Date:         model.Date(time.Now()),
	}

	cs = append(cs, attachmentConversation)

	reader := bytes.NewReader(resources.TestAttachmentData)
	attachmentGUID, err := s.attachmentStore.StoreAttachment("test.jpg", reader)
	if err != nil {
		log.Fatal().Msgf("Error storing test attachment: %s", err)
	} else {
		attachmentMessage := data.GenerateAttachmentMessage(attachmentConversation.Participants, *attachmentGUID)
		attachmentMessage.Date = model.Date(time.Now())
		s.AppendMessageToConversation(&attachmentConversation, attachmentMessage)
	}

	s.conversations = cs
}

func (s *Server) Authenticate(username string, password string) (*model.AuthToken, error) {
	if username != AUTH_USERNAME || password != AUTH_PASSWORD {
		return nil, &AuthError{"Invalid username or password"}
	}

	token, err := model.NewAuthToken(username)
	if err != nil {
		return nil, err
	}

	// Register for future auth
	s.registerAuthToken(token)

	return token, nil
}

func (s *Server) CheckBearerToken(token string) bool {
	return s.authenticateToken(token)
}

func (s *Server) PerformMessageQuery(query *MessagesQuery) []model.Message {
	messages := s.messageStore[query.ConversationGUID]

	// Sort
	sort.Slice(messages, func(i int, j int) bool {
		return messages[i].Date.Before(messages[j].Date)
	})

	// Apply before/after filters
	// The following code assumes the messages are sorted by date ascending
	if query.BeforeGUID != nil {
		beforeGUID := *query.BeforeGUID
		for i := range messages {
			if messages[i].Guid == beforeGUID {
				messages = messages[:i]
				break
			}
		}
	} else if query.AfterGUID != nil {
		afterGUID := *query.AfterGUID
		for i := range messages {
			if messages[i].Guid == afterGUID {
				messages = messages[i+1:]
				break
			}
		}
	} else if query.BeforeDate != nil {
		beforeDate := *query.BeforeDate
		for i := range messages {
			if messages[i].Date.Before(beforeDate) {
				messages = messages[:i]
				break
			}
		}
	}

	// Limit
	if query.Limit != nil {
		limit := *query.Limit
		if len(messages) > limit {
			messages = messages[len(messages)-limit:]
		}
	}

	return messages
}

func (s *Server) MessagesForConversation(conversation *model.Conversation) []model.Message {
	return s.PerformMessageQuery(&MessagesQuery{
		ConversationGUID: conversation.Guid,
	})
}

func (s *Server) AppendMessageToConversation(conversation *model.Conversation, message model.Message) {
	s.messageStore[conversation.Guid] = append(s.messageStore[conversation.Guid], message)
}

func (s *Server) SendMessage(conversation *model.Conversation, message model.Message) {
	s.AppendMessageToConversation(conversation, message)

	// Update Conversation
	ourConversation, _ := s.ConversationForGUID(conversation.Guid)
	ourConversation.LastMessagePreview = message.Text
	ourConversation.Date = message.Date

	log.Info().EmbedObject(message).Msgf("Sent message to conversation %s", conversation.Guid)

	// Enqueue Update
	s.EnqueueUpdateItem(model.UpdateItem{
		Conversation: ourConversation,
		Message:      nil, // not what I would do today, but this is what the server does
	})
}

func (s *Server) ReceiveMessage(conversation *model.Conversation, message model.Message) {
	s.AppendMessageToConversation(conversation, message)

	// Update conversation
	ourConversation, _ := s.ConversationForGUID(conversation.Guid)
	ourConversation.LastMessagePreview = message.Text
	ourConversation.Date = message.Date
	ourConversation.UnreadCount += 1

	// Enqueue Update
	s.EnqueueUpdateItem(model.UpdateItem{
		Conversation: ourConversation,
		Message:      &message,
	})

	log.Info().EmbedObject(message).Msgf("Received message from conversation %s", conversation.Guid)
}

func (s *Server) EnqueueUpdateItem(item model.UpdateItem) {
	log.Info().EmbedObject(&item).Msg("Enqueuing update item")

	s.updateItemSeq += 1
	item.MessageSequenceNumber = s.updateItemSeq

	s.updateItems[s.updateItemSeq] = item

	// Publish to channel
	for i := range s.updateChannels {
		s.updateChannels[i] <- []model.UpdateItem{item}
	}
}

func (s *Server) FetchUpdates(since int) []model.UpdateItem {
	items := []model.UpdateItem{}
	for i := since; i <= s.updateItemSeq; i++ {
		if val, ok := s.updateItems[i]; ok {
			items = append(items, val)
		}
	}

	return items
}

func (s *Server) FetchUpdatesBlocking(since int) []model.UpdateItem {
	if since < 0 || since >= s.updateItemSeq {
		// Wait for updates
		log.Info().Msgf("Waiting for updates since %d", since)
		updateChannel := make(chan []model.UpdateItem)
		s.updateChannels = append(s.updateChannels, updateChannel)
		items := <-updateChannel

		// Remove channel
		for i := range s.updateChannels {
			if s.updateChannels[i] == updateChannel {
				s.updateChannels = append(s.updateChannels[:i], s.updateChannels[i+1:]...)
				break
			}
		}

		return items
	} else {
		return s.FetchUpdates(since)
	}
}

func (s *Server) MarkConversationAsRead(conversation *model.Conversation) {
	conversation.UnreadCount = 0

	// enqueue update
	s.EnqueueUpdateItem(model.UpdateItem{
		Conversation: conversation,
	})
}

func (s *Server) FetchAttachment(guid string) (io.Reader, error) {
	return s.attachmentStore.FetchAttachment(guid)
}

func (s *Server) UploadAttachment(filename string, reader io.Reader) (*string, error) {
	return s.attachmentStore.StoreAttachment(filename, reader)
}

func (s *Server) DeleteAttachment(guid string) error {
	return s.attachmentStore.DeleteAttachment(guid)
}

// Private

func (s *Server) registerAuthToken(token *model.AuthToken) {
	s.authTokens = append(s.authTokens, *token)
}

func (s *Server) authenticateToken(token string) bool {
	for _, t := range s.authTokens {
		if t.SignedToken == token {
			return true
		}
	}

	return false
}
