// Copyright 2017 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 topdown

import (
	"errors"
	"strconv"

	"github.com/open-policy-agent/opa/v1/ast"
	"github.com/open-policy-agent/opa/v1/util"
)

var cancelErr = &Error{Code: CancelErr}

// Halt is a special error type that built-in function implementations return to indicate
// that policy evaluation should stop immediately.
type Halt struct {
	Err error
}

func (h Halt) Error() string {
	return h.Err.Error()
}

func (h Halt) Unwrap() error { return h.Err }

// Error is the error type returned by the Eval and Query functions when
// an evaluation error occurs.
type Error struct {
	Code     string        `json:"code"`
	Message  string        `json:"message"`
	Location *ast.Location `json:"location,omitempty"`

	// StackTrace is the stack of queries being evaluated when the error occurred.
	// Only populated when enabled (see Query.WithStackTraces), and left out of
	// Error() so enabling it doesn't change the messages callers display.
	StackTrace StackTrace `json:"stack_trace,omitempty"`

	err error `json:"-"`
}

const (
	// InternalErr represents an unknown evaluation error.
	InternalErr string = "eval_internal_error"

	// CancelErr indicates the evaluation process was cancelled.
	CancelErr string = "eval_cancel_error"

	// ConflictErr indicates a conflict was encountered during evaluation. For
	// instance, a conflict occurs if a rule produces multiple, differing values
	// for the same key in an object. Conflict errors indicate the policy does
	// not account for the data loaded into the policy engine.
	ConflictErr string = "eval_conflict_error"

	// TypeErr indicates evaluation stopped because an expression was applied to
	// a value of an inappropriate type.
	TypeErr string = "eval_type_error"

	// BuiltinErr indicates a built-in function received a semantically invalid
	// input or encountered some kind of runtime error, e.g., connection
	// timeout, connection refused, etc.
	BuiltinErr string = "eval_builtin_error"

	// WithMergeErr indicates that the real and replacement data could not be merged.
	WithMergeErr string = "eval_with_merge_error"
)

// IsError returns true if the err is an Error.
func IsError(err error) bool {
	_, ok := errors.AsType[*Error](err)
	return ok
}

// IsCancel returns true if err was caused by cancellation.
func IsCancel(err error) bool {
	return errors.Is(err, cancelErr)
}

// Is allows matching topdown errors using errors.Is (see IsCancel).
func (e *Error) Is(target error) bool {
	if t, ok := errors.AsType[*Error](target); ok {
		return (t.Code == "" || e.Code == t.Code) &&
			(t.Message == "" || e.Message == t.Message) &&
			(t.Location == nil || t.Location.Equal(e.Location))
	}
	return false
}

func (e *Error) Error() string {
	buf, _ := e.AppendText(make([]byte, 0, e.StringLength()))
	return util.ByteSliceToString(buf)
}

func (e *Error) AppendText(buf []byte) ([]byte, error) {
	if e.Location != nil {
		buf, _ := e.Location.AppendText(buf)
		buf = append(append(buf, ": "...), e.Code...)
		buf = append(append(buf, ": "...), e.Message...)
		return buf, nil
	}

	return append(append(append(buf, e.Code...), ": "...), e.Message...), nil
}

func (e *Error) StringLength() int {
	l := len(e.Code) + 2 + len(e.Message)
	if e.Location != nil {
		l += e.Location.StringLength() + 2
	}
	return l
}

func (e *Error) Wrap(err error) *Error {
	e.err = err
	return e
}

func (e *Error) Unwrap() error {
	return e.err
}

func functionConflictErr(loc *ast.Location) error {
	return &Error{
		Code:     ConflictErr,
		Location: loc,
		Message:  "functions must not produce multiple outputs for same inputs",
	}
}

func completeDocConflictErr(loc *ast.Location) error {
	return &Error{
		Code:     ConflictErr,
		Location: loc,
		Message:  "complete rules must not produce multiple outputs",
	}
}

func objectDocKeyConflictErr(loc *ast.Location) error {
	return &Error{
		Code:     ConflictErr,
		Location: loc,
		Message:  "object keys must be unique",
	}
}

func unsupportedBuiltinErr(loc *ast.Location, name string) error {
	return &Error{
		Code:     InternalErr,
		Location: loc,
		Message:  "unsupported built-in: " + name,
	}
}

func mergeConflictErr(loc *ast.Location) error {
	return &Error{
		Code:     WithMergeErr,
		Location: loc,
		Message:  "real and replacement data could not be merged",
	}
}

// unevaluatedOperandErr is returned when a built-in function would have been
// called with an operand that requires evaluation, which indicates a bug in OPA
// rather than in the policy being evaluated.
func unevaluatedOperandErr(loc *ast.Location, name string, pos int, operand *ast.Term) error {
	return &Error{
		Code:     InternalErr,
		Location: loc,
		Message: "built-in function " + name + " called with operand " + strconv.Itoa(pos) +
			" that requires evaluation: " + operand.String(),
	}
}

func internalErr(loc *ast.Location, msg string) error {
	return &Error{
		Code:     InternalErr,
		Location: loc,
		Message:  msg,
	}
}
