package proto_socket

import (
	"runtime"
	"sync"

	"google.golang.org/protobuf/proto"

	"git.toki-labs.com/toki/proto-socket/go/packets"
)

// InboundFrame is a raw inbound frame queued for gateway preprocessing.
//
// Seq is an internal monotonically increasing sequence assigned by the receive
// side before submitting to the gateway. It is used only to restore input order
// after parallel decoding and never touches the wire format or any public
// protocol field.
type InboundFrame struct {
	Seq   int64
	Bytes []byte
}

// DecodedFrame is the result of decoding a PacketBase envelope inside a gateway.
//
// It carries the same envelope fields that Communicator.OnReceivedData expects,
// plus the originating Seq so results can be reordered before dispatch.
type DecodedFrame struct {
	Seq           int64
	TypeName      string
	Data          []byte
	IncomingNonce int32
	ResponseNonce int32
}

// InboundGateway performs only pure raw-bytes work (PacketBase envelope decode /
// lightweight preprocessing) off the receive coordinator. It must NOT own
// stateful dispatch, the pending-request map, listeners, or the outbound write
// queue — those stay on the receive coordinator. Decoded results are delivered
// to a sink in input Seq order so the coordinator can dispatch deterministically.
type InboundGateway interface {
	// Submit hands a raw frame to the gateway. Fire-and-forget; the decoded
	// result is later delivered to the sink in Seq order.
	Submit(frame InboundFrame)
	// Close releases gateway resources and stops delivering results.
	Close()
}

// decodeFunc decodes a raw frame into a PacketBase envelope. Pure: no shared
// state, safe to run concurrently across workers.
type decodeFunc func([]byte) (*packets.PacketBase, error)

func decodePacketBase(raw []byte) (*packets.PacketBase, error) {
	base := &packets.PacketBase{}
	if err := proto.Unmarshal(raw, base); err != nil {
		return nil, err
	}
	return base, nil
}

// workerResult is a per-frame decode outcome carried through the reorder buffer.
// err frames still advance the sequence so a single bad frame cannot stall the
// reorder window.
type workerResult struct {
	seq   int64
	frame DecodedFrame
	err   error
}

// frameReorderBuffer restores input order from results that may arrive out of
// order. Sequences are expected to be contiguous starting at 1. A result is
// buffered until every earlier sequence has been released, then it and any
// now-contiguous successors are returned in order. It is not safe for concurrent
// use; the WorkerGateway confines it to the single collector goroutine.
type frameReorderBuffer struct {
	nextSeq int64
	pending map[int64]workerResult
}

func newFrameReorderBuffer() *frameReorderBuffer {
	return &frameReorderBuffer{nextSeq: 1, pending: make(map[int64]workerResult)}
}

// release buffers res and returns the results now releasable in seq order.
func (b *frameReorderBuffer) release(res workerResult) []workerResult {
	if res.seq < b.nextSeq {
		// Already released (or duplicate); ignore to avoid emitting twice.
		return nil
	}
	b.pending[res.seq] = res
	var ready []workerResult
	for {
		r, ok := b.pending[b.nextSeq]
		if !ok {
			break
		}
		ready = append(ready, r)
		delete(b.pending, b.nextSeq)
		b.nextSeq++
	}
	return ready
}

// WorkerGateway is a goroutine worker-pool gateway. A bounded jobs channel feeds
// N workers that decode PacketBase envelopes in parallel; a single collector
// goroutine reorders the results by Seq and delivers them to the sink in input
// order. Because the collector is the only goroutine that calls the sink, sink
// invocations are serialized and ordered, keeping stateful dispatch on the
// receive coordinator.
type WorkerGateway struct {
	jobs          chan InboundFrame
	results       chan workerResult
	done          chan struct{}
	collectorDone chan struct{}
	wg            sync.WaitGroup
	closeOnce     sync.Once
	parse         decodeFunc
	sink          func(DecodedFrame)
	onError       func(error)
}

// NewWorkerGateway creates a worker-pool gateway that decodes PacketBase frames
// and delivers them to sink in Seq order. A frame that fails to decode is not
// sent to sink; instead onError is invoked with the decode error in Seq order so
// the transport can apply its existing parse-error disconnect semantics. The
// gateway itself never tears down the transport — onError keeps that decision on
// the caller. onError may be nil to ignore decode errors.
//
// workers <= 0 defaults to NumCPU and queueSize <= 0 defaults to 64. The bounded
// jobs channel provides backpressure: Submit blocks once it is full until a
// worker frees a slot.
func NewWorkerGateway(workers, queueSize int, sink func(DecodedFrame), onError func(error)) *WorkerGateway {
	return newWorkerGateway(workers, queueSize, decodePacketBase, sink, onError)
}

func newWorkerGateway(workers, queueSize int, parse decodeFunc, sink func(DecodedFrame), onError func(error)) *WorkerGateway {
	if workers <= 0 {
		workers = runtime.NumCPU()
	}
	if queueSize <= 0 {
		queueSize = 64
	}
	g := &WorkerGateway{
		jobs:          make(chan InboundFrame, queueSize),
		results:       make(chan workerResult, queueSize),
		done:          make(chan struct{}),
		collectorDone: make(chan struct{}),
		parse:         parse,
		sink:          sink,
		onError:       onError,
	}
	for i := 0; i < workers; i++ {
		g.wg.Add(1)
		go g.worker()
	}
	go g.collector()
	return g
}

func (g *WorkerGateway) worker() {
	defer g.wg.Done()
	for {
		select {
		case frame := <-g.jobs:
			res := workerResult{seq: frame.Seq}
			base, err := g.parse(frame.Bytes)
			if err != nil {
				res.err = err
			} else {
				res.frame = DecodedFrame{
					Seq:           frame.Seq,
					TypeName:      base.GetTypeName(),
					Data:          base.GetData(),
					IncomingNonce: base.GetNonce(),
					ResponseNonce: base.GetResponseNonce(),
				}
			}
			select {
			case g.results <- res:
			case <-g.done:
				return
			}
		case <-g.done:
			return
		}
	}
}

func (g *WorkerGateway) collector() {
	defer close(g.collectorDone)
	reorder := newFrameReorderBuffer()
	for {
		select {
		case res := <-g.results:
			for _, ready := range reorder.release(res) {
				if ready.err != nil {
					// Report the decode error in Seq order and keep going so a
					// single bad frame does not stall later frames. The caller's
					// onError decides whether to tear down the transport.
					if g.onError != nil {
						g.onError(ready.err)
					}
					continue
				}
				g.sink(ready.frame)
			}
		case <-g.done:
			return
		}
	}
}

// Submit hands a raw frame to the worker pool. It blocks while the bounded jobs
// channel is full (backpressure) and returns immediately once the gateway is
// closed.
func (g *WorkerGateway) Submit(frame InboundFrame) {
	select {
	case g.jobs <- frame:
	case <-g.done:
	}
}

// Close signals all workers and the collector to stop. It is idempotent and
// does not block on in-flight sink calls.
func (g *WorkerGateway) Close() {
	g.closeOnce.Do(func() {
		close(g.done)
	})
}
