package spindle

import (
	"sync"
	"sync/atomic"
	"time"

	"github.com/bluesky-social/indigo/atproto/syntax"
)

const lockStripes = 8192

// every did hashes to one of these mutexes, and dids sharing one is fine as long as holders only touch the db
type stripedLocks [lockStripes]sync.Mutex

func fnv32a(s string) uint32 {
	h := uint32(2166136261)
	for i := 0; i < len(s); i++ {
		h ^= uint32(s[i])
		h *= 16777619
	}
	return h
}

func stripeOf(did syntax.DID) uint32 {
	return fnv32a(string(did)) % lockStripes
}

type heldLock struct{ mu *sync.Mutex }

func (h heldLock) Unlock() { h.mu.Unlock() }

func (t *stripedLocks) lock(did syntax.DID) heldLock {
	mu := &t[stripeOf(did)]
	mu.Lock()
	return heldLock{mu}
}

// don't hold this over a knot or git call, other dids on the stripe wait too
func (s *Spindle) lockRepo(repoDid syntax.DID) heldLock {
	return s.repoLocks.lock(repoDid)
}

// seeded from the newest turn in case the clock went back across a restart
type turnClock struct {
	once sync.Once
	last atomic.Int64
}

// a turn is one refresh of a repo, numbered when it starts. its knot read can go stale,
// so the write only lands if no newer turn wrote first
type repoTurn struct {
	s   *Spindle
	did syntax.DID
	seq int64
}

func (s *Spindle) beginTurn(did syntax.DID) *repoTurn {
	s.turns.once.Do(func() {
		newest, err := s.db.NewestRepoTurn()
		if err != nil {
			s.l.Error("reading the newest repo turn, falling back to the clock", "err", err)
		}
		s.turns.last.Store(newest)
	})
	for {
		last := s.turns.last.Load()
		seq := max(time.Now().UnixNano(), last+1)
		if s.turns.last.CompareAndSwap(last, seq) {
			return &repoTurn{s: s, did: did, seq: seq}
		}
	}
}

// applies even past a later turn, otherwise an older refresh could undo a wipe
func (r *repoTurn) force(fn func() error) error {
	defer r.s.lockRepo(r.did).Unlock()
	if _, err := r.s.db.AdvanceRepoTurn(r.did, r.seq, time.Now()); err != nil {
		return err
	}
	return fn()
}

func (r *repoTurn) apply(fn func() error) (applied bool, err error) {
	defer r.s.lockRepo(r.did).Unlock()
	applied, err = r.s.db.AdvanceRepoTurn(r.did, r.seq, time.Now())
	if err != nil || !applied {
		return false, err
	}
	return true, fn()
}
