// Copyright 2016-2023, Pulumi Corporation.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package result

import (
	"errors"
	"fmt"
	"io"

	"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
)

type bailError struct {
	err error
}

// A BailError represents an expected error or a graceful failure -- that is
// something which is not a bug but a normal (albeit unhappy-path) part of the
// program's execution. A BailError implements the Error interface but will
// prefix its error string with "BAIL: ", which if ever seen in user-facing
// messages indicates that a check for bailing was missed. It also does *not*
// implement Unwrap. To ascertain whether an error is a BailError, use the
// IsBail function.
func BailError(err error) error {
	contract.Requiref(err != nil, "err", "must not be nil")

	return &bailError{err: err}
}

func (b *bailError) Error() string {
	return fmt.Sprintf("BAIL: %v", b.err)
}

// BailErrorf is a helper for BailError(fmt.Errorf(...)).
func BailErrorf(format string, args ...interface{}) error {
	return BailError(fmt.Errorf(format, args...))
}

// FprintBailf writes a formatted string to the given writer and returns a BailError with the same message.
func FprintBailf(w io.Writer, msg string, args ...any) error {
	msg = fmt.Sprintf(msg, args...)
	fmt.Fprintln(w, msg)
	return BailError(errors.New(msg))
}

// IsBail returns true if any error in the given error's tree is a BailError.
func IsBail(err error) bool {
	if err == nil {
		return false
	}

	var bail *bailError
	ok := errors.As(err, &bail)
	return ok
}

// MergeBails accepts a set of errors and returns a single error that is the
// result of merging them according to the following criteria:
//
//   - If all the errors are nil, MergeBails returns nil.
//   - If any of the errors is *not* a BailError, MergeBails returns a single
//     error whose message is the concatenation of the messages of all the
//     errors which are not bails (that is, if any error is unexpected, MergeBails
//     will propagate it).
//   - In the remaining case that all errors are either nil or BailErrors, MergeBails
//     will return a single BailError whose message is the concatenation of the
//     messages of all the BailErrors.
func MergeBails(errs ...error) error {
	allNil := true
	joinableErrs := []error{}
	for _, err := range errs {
		if err == nil {
			continue
		}

		allNil = false
		if IsBail(err) {
			continue
		}

		joinableErrs = append(joinableErrs, err)
	}

	if allNil {
		return nil
	}

	if len(joinableErrs) == 0 {
		return BailError(errors.Join(errs...))
	}

	return errors.Join(joinableErrs...)
}