package agentrouter

import (
	"errors"

	"connectrpc.com/connect"
	"google.golang.org/genproto/googleapis/rpc/errdetails"
)

// Error is the typed error returned by every SDK call when the server
// responds with a non-OK status. Use errors.As to extract:
//
//	var fe *agentrouter.Error
//	if errors.As(err, &fe) {
//	    if fe.Code == agentrouter.CodePermissionDenied { ... }
//	}
type Error struct {
	// Code is the canonical Connect error code.
	Code Code

	// Reason is the agentrouter-canonical machine-readable reason from the
	// `reason` field of the google.rpc.ErrorInfo detail the platform attaches
	// to every error (e.g. MODEL_NOT_FOUND, CROSS_TENANT_DENIED). It mirrors
	// the `reason` member of the RFC 7807 Problem envelope. Empty when the
	// error carries no ErrorInfo detail.
	Reason string

	// Message is the server-supplied human-readable error.
	Message string

	// underlying is the original Connect error.
	underlying error
}

// errorInfoTypeName is the fully-qualified proto name of google.rpc.ErrorInfo,
// which Connect uses as the detail's Type().
const errorInfoTypeName = "google.rpc.ErrorInfo"

// reasonFromConnect extracts the google.rpc.ErrorInfo reason from a Connect
// error's details. It returns "" when no ErrorInfo detail is present or the
// detail cannot be decoded -- a missing reason must never mask the error.
func reasonFromConnect(ce *connect.Error) string {
	for _, d := range ce.Details() {
		if d.Type() != errorInfoTypeName {
			continue
		}
		v, err := d.Value()
		if err != nil {
			continue
		}
		if info, ok := v.(*errdetails.ErrorInfo); ok {
			return info.GetReason()
		}
	}
	return ""
}

// Code is a typed alias for the Connect canonical error codes.
type Code = connect.Code

// Re-exported codes for ergonomic comparison.
const (
	CodeUnauthenticated    = connect.CodeUnauthenticated
	CodePermissionDenied   = connect.CodePermissionDenied
	CodeInvalidArgument    = connect.CodeInvalidArgument
	CodeNotFound           = connect.CodeNotFound
	CodeAlreadyExists      = connect.CodeAlreadyExists
	CodeFailedPrecondition = connect.CodeFailedPrecondition
	CodeAborted            = connect.CodeAborted
	CodeUnavailable        = connect.CodeUnavailable
	CodeDeadlineExceeded   = connect.CodeDeadlineExceeded
	CodeInternal           = connect.CodeInternal
)

// Error implements the error interface.
func (e *Error) Error() string {
	if e.Message != "" {
		return e.Message
	}
	if e.underlying != nil {
		return e.underlying.Error()
	}
	return e.Code.String()
}

// Unwrap returns the underlying Connect error so errors.As / errors.Is keep
// working through the SDK layer.
func (e *Error) Unwrap() error { return e.underlying }

// wrap converts a Connect error into a typed *Error. Non-Connect errors
// pass through unchanged.
func wrap(err error) error {
	if err == nil {
		return nil
	}
	var ce *connect.Error
	if !errors.As(err, &ce) {
		return err
	}
	return &Error{
		Code:       ce.Code(),
		Reason:     reasonFromConnect(ce),
		Message:    ce.Message(),
		underlying: err,
	}
}
