package reconcile

import (
	"errors"
	"sync"
)

// ErrQueueClosed is returned for a job submitted after Close.
var ErrQueueClosed = errors.New("reconcile: queue closed")

// Queue is the per-guest serializer (doc 03 §10): the single choke point ALL
// mutation sources funnel through (reconcile now; one-shot jobs and the local API
// later). Jobs for the SAME vmid run strictly one-at-a-time in submit order;
// independent vmids proceed in parallel. This is what keeps Proxmox from ever seeing
// concurrent conflicting ops on one guest while letting a multi-guest host converge
// concurrently.
//
// Each vmid gets a "lane": a goroutine draining an unbounded FIFO of jobs. Submit
// never blocks the caller (the FIFO is unbounded) and returns a channel that delivers
// that job's error when it runs — so a caller can await a specific job while others
// proceed.
type Queue struct {
	mu     sync.Mutex
	lanes  map[int]*lane
	closed bool
}

// NewQueue builds an empty queue. Lanes are created lazily on first Submit per vmid
// and live for the queue's lifetime (a host has a bounded guest set); Close stops them.
func NewQueue() *Queue {
	return &Queue{lanes: make(map[int]*lane)}
}

// Submit enqueues fn on vmid's lane and returns a buffered channel that receives
// fn's return value (or ErrQueueClosed) exactly once. Submission preserves per-lane
// FIFO order; different lanes run concurrently. The result channel is buffered, so the
// lane never blocks delivering a result even if the caller abandons it.
func (q *Queue) Submit(vmid int, fn func() error) <-chan error {
	res := make(chan error, 1)

	q.mu.Lock()
	if q.closed {
		q.mu.Unlock()
		res <- ErrQueueClosed
		return res
	}
	l := q.lanes[vmid]
	if l == nil {
		l = newLane()
		q.lanes[vmid] = l
		go l.run()
	}
	q.mu.Unlock()

	l.enqueue(&laneTask{fn: fn, res: res})
	return res
}

// Close stops accepting new jobs and signals every lane to drain its pending jobs and
// exit. Already-queued jobs still run (graceful drain). Idempotent.
func (q *Queue) Close() {
	q.mu.Lock()
	if q.closed {
		q.mu.Unlock()
		return
	}
	q.closed = true
	lanes := make([]*lane, 0, len(q.lanes))
	for _, l := range q.lanes {
		lanes = append(lanes, l)
	}
	q.mu.Unlock()

	for _, l := range lanes {
		l.close()
	}
}

type laneTask struct {
	fn  func() error
	res chan error
}

// lane is one vmid's serial worker: a goroutine draining an unbounded FIFO guarded by
// a cond var. cond-var (not a buffered channel) gives unbounded, non-blocking,
// order-preserving submission without an arbitrary capacity.
type lane struct {
	mu      sync.Mutex
	cond    *sync.Cond
	pending []*laneTask
	closed  bool
}

func newLane() *lane {
	l := &lane{}
	l.cond = sync.NewCond(&l.mu)
	return l
}

func (l *lane) enqueue(t *laneTask) {
	l.mu.Lock()
	if l.closed {
		l.mu.Unlock()
		t.res <- ErrQueueClosed
		return
	}
	l.pending = append(l.pending, t)
	l.cond.Signal()
	l.mu.Unlock()
}

func (l *lane) close() {
	l.mu.Lock()
	l.closed = true
	l.cond.Broadcast()
	l.mu.Unlock()
}

// run drains the FIFO one job at a time. On close it finishes any already-queued jobs
// (so nothing submitted-before-close is silently dropped) and then exits.
func (l *lane) run() {
	for {
		l.mu.Lock()
		for len(l.pending) == 0 && !l.closed {
			l.cond.Wait()
		}
		if len(l.pending) == 0 { // closed and drained
			l.mu.Unlock()
			return
		}
		t := l.pending[0]
		l.pending = l.pending[1:]
		l.mu.Unlock()

		t.res <- t.fn()
	}
}
