package main

import (
	"context"
	"crypto/rand"
	"encoding/hex"
	"encoding/json"
	"time"
	"unicode/utf8"
)

// ---- Helpers ----

func randHex(n int) string {
	b := make([]byte, n)
	rand.Read(b)
	return hex.EncodeToString(b)
}

func msNow() int64 { return time.Now().UnixMilli() }

// voteThreshold returns the number of votes required to trigger pending_removal
// for a non-#1 entry. = min(3, ceil(n/2)), minimum 1.
func voteThreshold(n int) int {
	half := (n + 1) / 2
	if half > 3 {
		return 3
	}
	if half < 1 {
		return 1
	}
	return half
}

func sanitize(s string, maxRunes int) string {
	runes := []rune(s)
	if len(runes) > maxRunes {
		runes = runes[:maxRunes]
	}
	out := make([]rune, 0, len(runes))
	for _, r := range runes {
		if r != 0 && utf8.ValidRune(r) {
			out = append(out, r)
		}
	}
	return string(out)
}

// ---- Data model ----

type Entry struct {
	ID        string
	Number    int
	Name      string
	JoinedAt  int64 // Unix ms
	Status    string // "active" | "pending_removal"
	PendingBy []int
	PendingAt *int64 // Unix ms
	token     string
	cancelFn  context.CancelFunc
}

// pub returns the public (broadcast-safe) view of an entry.
type entryPub struct {
	ID        string  `json:"id"`
	Number    int     `json:"number"`
	Name      string  `json:"name,omitempty"`
	JoinedAt  int64   `json:"joinedAt"`
	Status    string  `json:"status"`
	PendingBy []int   `json:"pendingBy,omitempty"`
	PendingAt *int64  `json:"pendingAt,omitempty"`
}

func (e *Entry) pub() entryPub {
	return entryPub{
		ID:        e.ID,
		Number:    e.Number,
		Name:      e.Name,
		JoinedAt:  e.JoinedAt,
		Status:    e.Status,
		PendingBy: e.PendingBy,
		PendingAt: e.PendingAt,
	}
}

// ---- Queue state ----

type QueueState struct {
	Name           string    // display name, set at creation
	Entries        []*Entry
	Counter        int
	CompletedSlots []float64 // seconds; only self-removals, not voted-out entries
}

// stateJSON serialises the full public state for broadcast.
func (q *QueueState) stateJSON() []byte {
	views := make([]entryPub, len(q.Entries))
	for i, e := range q.Entries {
		views[i] = e.pub()
	}
	b, _ := json.Marshal(map[string]interface{}{
		"type":        "state",
		"name":        q.Name,
		"entries":     views,
		"counter":     q.Counter,
		"avgDuration": q.avgDuration(),
	})
	return b
}

// avgDuration returns the average slot duration in seconds, or nil if <3 samples.
func (q *QueueState) avgDuration() interface{} {
	if len(q.CompletedSlots) < 3 {
		return nil
	}
	var sum float64
	for _, d := range q.CompletedSlots {
		sum += d
	}
	return sum / float64(len(q.CompletedSlots))
}

func (q *QueueState) findByToken(token string) *Entry {
	for _, e := range q.Entries {
		if e.token == token {
			return e
		}
	}
	return nil
}

func (q *QueueState) findByID(id string) *Entry {
	for _, e := range q.Entries {
		if e.ID == id {
			return e
		}
	}
	return nil
}

func (q *QueueState) removeEntry(id string) {
	for i, e := range q.Entries {
		if e.ID == id {
			q.Entries = append(q.Entries[:i], q.Entries[i+1:]...)
			return
		}
	}
}

// findVoteTarget returns the Number of the entry the given voter is targeting,
// or 0 if they have no active vote.
func (q *QueueState) findVoteTarget(voterNumber int) int {
	for _, e := range q.Entries {
		for _, n := range e.PendingBy {
			if n == voterNumber {
				return e.Number
			}
		}
	}
	return 0
}

// ---- Mutations ----

// join creates a new entry and returns it with its secret token.
func (q *QueueState) join(name string) (*Entry, string) {
	name = sanitize(name, 50)
	token := randHex(16)
	q.Counter++
	e := &Entry{
		ID:       randHex(8),
		Number:   q.Counter,
		Name:     name,
		JoinedAt: msNow(),
		Status:   "active",
		token:    token,
	}
	q.Entries = append(q.Entries, e)
	return e, token
}

// selfRemove removes the entry owned by token, recording the slot duration.
func (q *QueueState) selfRemove(token string) bool {
	e := q.findByToken(token)
	if e == nil {
		return false
	}
	dur := float64(msNow()-e.JoinedAt) / 1000.0
	q.CompletedSlots = append(q.CompletedSlots, dur)
	if e.cancelFn != nil {
		e.cancelFn()
	}
	removedNum := e.Number
	q.removeEntry(e.ID)
	q.cleanupVotes(removedNum)
	return true
}

// startPending transitions an entry into pending_removal and launches the 60s timer.
// When the timer fires it sends the entry ID to timerC.
func (q *QueueState) startPending(e *Entry, timerC chan<- string) {
	now := msNow()
	e.Status = "pending_removal"
	e.PendingAt = &now
	ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
	e.cancelFn = cancel
	id := e.ID
	go func() {
		<-ctx.Done()
		cancel()
		if ctx.Err() == context.DeadlineExceeded {
			select {
			case timerC <- id:
			default:
			}
		}
	}()
}

// markDone marks the #1 entry as pending_removal on behalf of byNumber.
// Returns false if the queue shifted or entry is already pending.
func (q *QueueState) markDone(byNumber int, entryID string, timerC chan<- string) bool {
	if len(q.Entries) == 0 {
		return false
	}
	target := q.Entries[0]
	if target.ID != entryID {
		return false // queue shifted since the client clicked
	}
	if target.Status == "pending_removal" {
		return false
	}
	target.PendingBy = []int{byNumber}
	q.startPending(target, timerC)
	return true
}

// voteRemove adds a vote against a non-#1 entry.
// Returns "ok", "already_voting", or "noop".
func (q *QueueState) voteRemove(voterNumber int, targetID string, timerC chan<- string) string {
	target := q.findByID(targetID)
	if target == nil {
		return "noop"
	}
	// Cannot vote on own entry
	if target.Number == voterNumber {
		return "noop"
	}
	// Cannot vote on #1 (use markDone instead)
	if len(q.Entries) > 0 && q.Entries[0].ID == targetID {
		return "noop"
	}
	// One active vote per user across the whole queue
	if existing := q.findVoteTarget(voterNumber); existing != 0 {
		return "already_voting"
	}
	target.PendingBy = append(target.PendingBy, voterNumber)
	if len(target.PendingBy) >= voteThreshold(len(q.Entries)) {
		q.startPending(target, timerC)
	}
	return "ok"
}

// restore cancels pending_removal for the entry owned by token.
func (q *QueueState) restore(token string) bool {
	e := q.findByToken(token)
	if e == nil || e.Status != "pending_removal" {
		return false
	}
	if e.cancelFn != nil {
		e.cancelFn()
		e.cancelFn = nil
	}
	e.Status = "active"
	e.PendingAt = nil
	e.PendingBy = nil
	return true
}

// timerRemove is called when the 60s timer fires for an entry.
func (q *QueueState) timerRemove(entryID string) bool {
	e := q.findByID(entryID)
	if e == nil || e.Status != "pending_removal" {
		return false // already restored
	}
	removedNum := e.Number
	q.removeEntry(entryID)
	q.cleanupVotes(removedNum)
	return true
}

// cleanupVotes removes a departed entry's number from all PendingBy lists.
// For entries in pending_removal with remaining votes below threshold, restores them.
// Called AFTER the entry has been removed from q.Entries so threshold uses updated count.
func (q *QueueState) cleanupVotes(removedNumber int) {
	thresh := voteThreshold(len(q.Entries))
	for _, e := range q.Entries {
		for i, n := range e.PendingBy {
			if n == removedNumber {
				e.PendingBy = append(e.PendingBy[:i], e.PendingBy[i+1:]...)
				// Restore only if votes remain but are now below threshold (Case B).
				// If PendingBy is empty after removal (Case A marker left), let the
				// 60s timer run its course — the action was already taken.
				if e.Status == "pending_removal" &&
					len(e.PendingBy) > 0 &&
					len(e.PendingBy) < thresh {
					if e.cancelFn != nil {
						e.cancelFn()
						e.cancelFn = nil
					}
					e.Status = "active"
					e.PendingAt = nil
					e.PendingBy = nil
				}
				break
			}
		}
	}
}
