// Copyright 2024 The OPA Authors.  All rights reserved.
// Use of this source code is governed by an Apache2
// license that can be found in the LICENSE file.

package debug

import (
	"github.com/open-policy-agent/opa/v1/rego"
	"github.com/open-policy-agent/opa/v1/topdown"
)

type stack interface {
	topdown.QueryTracer
	Current() (int, *topdown.Event)
	Event(i int) *topdown.Event
	Next() (int, *topdown.Event)
	Result() rego.ResultSet
	Close() error
}

type debugTracer struct {
	history   []*topdown.Event
	eventChan chan *topdown.Event
	waitChan  chan bool
	enabled   bool
	resultSet rego.ResultSet
}

func newDebugTracer() *debugTracer {
	return &debugTracer{
		eventChan: make(chan *topdown.Event),
		waitChan:  make(chan bool),
		enabled:   true,
	}
}

func (dt *debugTracer) Enabled() bool {
	return dt.enabled
}

func (dt *debugTracer) TraceEvent(e topdown.Event) {
	if !dt.enabled {
		return
	}

	defer func() {
		if recover() != nil {
			dt.enabled = false
		}
	}()

	dt.eventChan <- &e
	// Block until the consumer wants another event, so that evaluation isn't "ahead"
	<-dt.waitChan
}

func (*debugTracer) Config() topdown.TraceConfig {
	return topdown.TraceConfig{
		PlugLocalVars: true,
	}
}

func (dt *debugTracer) Current() (int, *topdown.Event) {
	stackLength := len(dt.history)
	if stackLength > 0 {
		return stackLength - 1, dt.history[len(dt.history)-1]
	}
	return -1, nil
}

func (dt *debugTracer) Event(i int) *topdown.Event {
	if i >= 0 && i < len(dt.history) {
		return dt.history[i]
	}
	return nil
}

func (dt *debugTracer) Next() (int, *topdown.Event) {
	if !dt.enabled {
		return -1, nil
	}

	if len(dt.history) > 0 {
		// We don't need to unblock the producer for the first event.
		dt.waitChan <- true
	}

	e, ok := <-dt.eventChan
	if ok {
		dt.history = append(dt.history, e)
		return len(dt.history) - 1, e
	}
	return len(dt.history) - 1, nil
}

func (dt *debugTracer) Result() rego.ResultSet {
	return dt.resultSet
}

func (dt *debugTracer) Close() error {
	if !dt.enabled {
		return nil
	}

	dt.enabled = false
	close(dt.eventChan)
	close(dt.waitChan)
	return nil
}
