package services

import (
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"errors"
	"os"
	"sort"
	"strings"
	"time"

	"go.etcd.io/bbolt"
)

var reactionsBucket = []byte("file_reactions")

type ReactionService struct {
	db *bbolt.DB
}

type FileReaction struct {
	BoxID       string    `json:"boxId"`
	FileID      string    `json:"fileId"`
	EmojiID     string    `json:"emojiId"`
	VisitorHash string    `json:"visitorHash"`
	CreatedAt   time.Time `json:"createdAt"`
}

type ReactionSummary struct {
	EmojiID string `json:"emojiId"`
	Count   int    `json:"count"`
}

func NewReactionService(db *bbolt.DB) (*ReactionService, error) {
	if err := db.Update(func(tx *bbolt.Tx) error {
		_, err := tx.CreateBucketIfNotExists(reactionsBucket)
		return err
	}); err != nil {
		return nil, err
	}
	return &ReactionService{db: db}, nil
}

func (s *ReactionService) Add(boxID, fileID, visitorID, emojiID string) ([]ReactionSummary, error) {
	boxID = strings.TrimSpace(boxID)
	fileID = strings.TrimSpace(fileID)
	visitorHash := reactionVisitorHash(visitorID)
	emojiID = strings.TrimSpace(emojiID)
	if boxID == "" || fileID == "" || visitorHash == "" || emojiID == "" {
		return nil, errors.New("missing reaction data")
	}

	reaction := FileReaction{
		BoxID:       boxID,
		FileID:      fileID,
		EmojiID:     emojiID,
		VisitorHash: visitorHash,
		CreatedAt:   time.Now().UTC(),
	}
	data, err := json.Marshal(reaction)
	if err != nil {
		return nil, err
	}

	key := reactionKey(boxID, fileID, visitorHash)
	if err := s.db.Update(func(tx *bbolt.Tx) error {
		bucket := tx.Bucket(reactionsBucket)
		if bucket.Get([]byte(key)) != nil {
			return os.ErrExist
		}
		return bucket.Put([]byte(key), data)
	}); err != nil {
		return nil, err
	}
	return s.SummaryForFile(boxID, fileID)
}

func (s *ReactionService) SummaryForBox(boxID, visitorID string) (map[string][]ReactionSummary, map[string]bool, error) {
	visitorHash := reactionVisitorHash(visitorID)
	summaries := make(map[string]map[string]int)
	viewerReacted := make(map[string]bool)
	err := s.db.View(func(tx *bbolt.Tx) error {
		bucket := tx.Bucket(reactionsBucket)
		if bucket == nil {
			return nil
		}
		return bucket.ForEach(func(_, data []byte) error {
			var reaction FileReaction
			if err := json.Unmarshal(data, &reaction); err != nil {
				return err
			}
			if reaction.BoxID != boxID {
				return nil
			}
			if summaries[reaction.FileID] == nil {
				summaries[reaction.FileID] = make(map[string]int)
			}
			summaries[reaction.FileID][reaction.EmojiID]++
			if visitorHash != "" && reaction.VisitorHash == visitorHash {
				viewerReacted[reaction.FileID] = true
			}
			return nil
		})
	})
	if err != nil {
		return nil, nil, err
	}

	result := make(map[string][]ReactionSummary, len(summaries))
	for fileID, counts := range summaries {
		result[fileID] = reactionCountsToSummaries(counts)
	}
	return result, viewerReacted, nil
}

func (s *ReactionService) SummaryForFile(boxID, fileID string) ([]ReactionSummary, error) {
	counts := make(map[string]int)
	err := s.db.View(func(tx *bbolt.Tx) error {
		bucket := tx.Bucket(reactionsBucket)
		if bucket == nil {
			return nil
		}
		return bucket.ForEach(func(_, data []byte) error {
			var reaction FileReaction
			if err := json.Unmarshal(data, &reaction); err != nil {
				return err
			}
			if reaction.BoxID == boxID && reaction.FileID == fileID {
				counts[reaction.EmojiID]++
			}
			return nil
		})
	})
	if err != nil {
		return nil, err
	}
	return reactionCountsToSummaries(counts), nil
}

func reactionCountsToSummaries(counts map[string]int) []ReactionSummary {
	summaries := make([]ReactionSummary, 0, len(counts))
	for emojiID, count := range counts {
		summaries = append(summaries, ReactionSummary{EmojiID: emojiID, Count: count})
	}
	sort.Slice(summaries, func(i, j int) bool {
		if summaries[i].Count == summaries[j].Count {
			return summaries[i].EmojiID < summaries[j].EmojiID
		}
		return summaries[i].Count > summaries[j].Count
	})
	return summaries
}

func reactionKey(boxID, fileID, visitorHash string) string {
	return boxID + "\x00" + fileID + "\x00" + visitorHash
}

func reactionVisitorHash(visitorID string) string {
	visitorID = strings.TrimSpace(visitorID)
	if visitorID == "" {
		return ""
	}
	sum := sha256.Sum256([]byte(visitorID))
	return hex.EncodeToString(sum[:])
}
