package runner

import (
	"context"
	"errors"
	"fmt"
	"net/http"

	"github.com/tidbcloud/AgentTeam/internal/service/extensionflow"
)

// Error is the stable provider-execution error classification shared by
// Builtin REST executors. The message is Agent-visible and must never carry an
// upstream URL, header, or credential-bearing cause.
type Error struct {
	Code    ErrorCode
	Message string
	Cause   error
}

// ErrorCode is the closed classification consumed by executors and the
// connection test API.
type ErrorCode string

const (
	ErrorValidation              ErrorCode = "validation"
	ErrorPermissionDenied        ErrorCode = "permission_denied"
	ErrorConnectionUnavailable   ErrorCode = "connection_unavailable"
	ErrorProviderAuthUnavailable ErrorCode = "provider_auth_unavailable"
	ErrorNotFound                ErrorCode = "not_found"
	ErrorRateLimited             ErrorCode = "rate_limited"
	ErrorUpstreamFailure         ErrorCode = "upstream_failure"
)

func (err *Error) Error() string {
	if err == nil {
		return ""
	}
	return err.Message
}

func (err *Error) Unwrap() error {
	if err == nil {
		return nil
	}
	return err.Cause
}

// Errorf builds a classified runtime error.
func Errorf(code ErrorCode, message string, cause error) error {
	return &Error{Code: code, Message: message, Cause: cause}
}

// CodeOf returns the stable runtime error code or the empty value.
func CodeOf(err error) ErrorCode {
	var typed *Error
	if errors.As(err, &typed) && typed != nil {
		return typed.Code
	}
	return ""
}

// ErrCredentialsUnavailable marks an expected local App connection failure
// (missing, ambiguous, or undecryptable credentials). The rendered error stays
// stable and never exposes the persistence cause.
var ErrCredentialsUnavailable = errors.New("provider connection credentials are unavailable")

type credentialsUnavailableError struct{ cause error }

func (err credentialsUnavailableError) Error() string {
	message := ErrCredentialsUnavailable.Error()
	return message
}

func (err credentialsUnavailableError) Unwrap() []error {
	return []error{ErrCredentialsUnavailable, err.cause}
}

// CredentialsUnavailable wraps an expected local connection failure.
func CredentialsUnavailable(cause error) error {
	if cause == nil {
		cause = ErrCredentialsUnavailable
	}
	return credentialsUnavailableError{cause: cause}
}

// InvocationError maps a runtime error onto the closed Agent-visible
// InvocationError set. Unclassified errors return nil so the extension service
// applies its default upstream_failure wrap.
func InvocationError(err error) error {
	if err == nil {
		return nil
	}
	if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
		return nil
	}
	var invocation *extensionflow.InvocationError
	if errors.As(err, &invocation) {
		return err
	}
	// The credential seam fails closed BEFORE any handler runs, so its
	// sentinel must map onto the closed set here: a missing or undecryptable
	// connection is provider_auth_unavailable, never a generic upstream
	// failure, and the persistence cause stays out of the Agent-visible
	// message.
	if errors.Is(err, ErrCredentialsUnavailable) {
		return &extensionflow.InvocationError{
			Code:    extensionflow.InvocationProviderAuthUnavailable,
			Message: "provider connection is unavailable or unauthorized",
		}
	}
	var code extensionflow.InvocationErrorCode
	var message string
	switch CodeOf(err) {
	case ErrorValidation:
		code, message = extensionflow.InvocationValidation, "provider rejected the request input"
	case ErrorPermissionDenied:
		code, message = extensionflow.InvocationPermissionDenied, "provider denied permission to perform the request"
	case ErrorConnectionUnavailable, ErrorProviderAuthUnavailable:
		code, message = extensionflow.InvocationProviderAuthUnavailable, "provider connection is unavailable or unauthorized"
	case ErrorNotFound:
		code, message = extensionflow.InvocationNotFound, "provider resource was not found"
	case ErrorRateLimited:
		code, message = extensionflow.InvocationRateLimited, "provider rate limited the request"
	case ErrorUpstreamFailure:
		code, message = extensionflow.InvocationUpstreamFailure, "provider request failed"
	default:
		return nil
	}
	return &extensionflow.InvocationError{Code: code, Message: message}
}

// classifyHTTPStatus maps one upstream response status onto the runtime error
// classification.
func classifyHTTPStatus(status int) ErrorCode {
	switch {
	case status == http.StatusUnauthorized || status == http.StatusForbidden:
		return ErrorProviderAuthUnavailable
	case status == http.StatusNotFound:
		return ErrorNotFound
	case status == http.StatusTooManyRequests:
		return ErrorRateLimited
	case status >= 400 && status < 500:
		return ErrorValidation
	default:
		return ErrorUpstreamFailure
	}
}

// UpstreamStatusCodeOf extracts the HTTP status of a classified upstream
// rejection, so providers can branch partial-failure handling on the wire
// status without seeing headers or the body.
func UpstreamStatusCodeOf(err error) (int, bool) {
	var statusErr *httpStatusError
	if !errors.As(err, &statusErr) || statusErr == nil {
		return 0, false
	}
	return statusErr.status, true
}

// httpStatusError carries the upstream status for classification.
type httpStatusError struct {
	status int
	body   []byte
}

func (err *httpStatusError) Error() string {
	message := fmt.Sprintf("provider responded with HTTP %d", err.status)
	return message
}
