package runner

import (
	"context"
	"encoding/json"
	"fmt"
	"sort"

	"github.com/tidbcloud/AgentTeam/internal/extension/registry"
)

// RESTHandler executes one provider Action against the provider client. The
// handler receives the raw validated input JSON, the resolved credentials (nil
// for AuthNone providers), and the provider client bound to the fixed origin.
// Handlers return the Action output as a single JSON value.
type RESTHandler func(
	ctx context.Context,
	client *RESTClient,
	credentials Credentials,
	input json.RawMessage,
) (json.RawMessage, error)

// RESTExecutor is the shared Builtin executor for token-authenticated REST
// providers: it resolves the App connection credential, dispatches to the
// per-Action handler table, and classifies errors onto the closed invocation
// set.
type RESTExecutor struct {
	domain      string
	auth        AuthSpec
	client      *RESTClient
	template    *RESTTemplate
	credentials CredentialSource
	handlers    map[string]RESTHandler
}

// NewRESTExecutor binds one provider identity to its handler table. The
// client's AuthSpec and the executor's must agree; the executor adopts the
// client's to keep a single source.
func NewRESTExecutor(
	domain string,
	client *RESTClient,
	credentials CredentialSource,
	handlers map[string]RESTHandler,
) *RESTExecutor {
	auth := AuthSpec{}
	if client != nil {
		auth = client.auth
	}
	return &RESTExecutor{
		domain:      domain,
		auth:        auth,
		client:      client,
		credentials: credentials,
		handlers:    handlers,
	}
}

// NewConnectionScopedRESTExecutor binds one provider identity whose API
// origin is resolved per invocation from the connection credential named by
// template.BaseURLField. It fails closed on a recipe that could never resolve
// a client. Handlers receive the connection-resolved client, so every
// fixed-origin guarantee applies unchanged.
func NewConnectionScopedRESTExecutor(
	domain string,
	template RESTTemplate,
	credentials CredentialSource,
	handlers map[string]RESTHandler,
) (*RESTExecutor, error) {
	if err := validateTemplate(template); err != nil {
		return nil, fmt.Errorf("%s: %w", domain, err)
	}
	return &RESTExecutor{
		domain:      domain,
		auth:        template.Auth,
		template:    &template,
		credentials: credentials,
		handlers:    handlers,
	}, nil
}

// Strategy labels the execution strategy in registry terms.
func (executor *RESTExecutor) Strategy() string {
	return "http"
}

// ActionNames returns the sorted dispatch table keys.
func (executor *RESTExecutor) ActionNames() []string {
	names := make([]string, 0, len(executor.handlers))
	for name := range executor.handlers {
		names = append(names, name)
	}
	sort.Strings(names)
	return names
}

// Invoke adapts the Builtin envelope: credential resolution, dispatch, and
// classified error propagation.
func (executor *RESTExecutor) Invoke(
	ctx context.Context,
	invocation registry.BuiltinInvocation,
) (registry.BuiltinResult, error) {
	if executor == nil || (executor.client == nil && executor.template == nil) {
		return registry.BuiltinResult{}, fmt.Errorf(
			"provider REST executor is not configured")
	}
	handler, ok := executor.handlers[invocation.Action]
	if !ok {
		return registry.BuiltinResult{}, Errorf(
			ErrorNotFound, "action is not served by this provider", nil)
	}
	if len(invocation.Input) > 0 && !json.Valid(invocation.Input) {
		return registry.BuiltinResult{}, Errorf(
			ErrorValidation, "action input was not valid JSON", nil)
	}
	credentials := Credentials(nil)
	if executor.auth.Kind != AuthNone {
		if executor.credentials == nil {
			return registry.BuiltinResult{}, InvocationError(CredentialsUnavailable(nil))
		}
		resolved, err := executor.credentials.ResolveProviderCredentials(
			ctx, invocation.WorkspaceID, invocation.ServiceInstallationID)
		if err != nil {
			// Map the seam failure onto the closed set HERE so the extension
			// service receives provider_auth_unavailable, not a generic
			// upstream wrap, and the persistence cause never leaks. A
			// cancellation/deadline does not map (InvocationError returns
			// nil); it MUST propagate as the original error so the invocation
			// is never recorded as a cancelled success.
			mapped := InvocationError(CredentialsUnavailable(err))
			if mapped == nil {
				return registry.BuiltinResult{}, err
			}
			return registry.BuiltinResult{}, mapped
		}
		credentials = resolved
	}
	client := executor.client
	if executor.template != nil {
		resolved, err := BuildConnectionClient(*executor.template, credentials)
		if err != nil {
			return registry.BuiltinResult{}, err
		}
		client = resolved
	}
	output, err := handler(ctx, client, credentials, invocation.Input)
	if err != nil {
		if mapped := InvocationError(err); mapped != nil {
			return registry.BuiltinResult{}, mapped
		}
		return registry.BuiltinResult{}, err
	}
	if len(output) == 0 {
		output = json.RawMessage("null")
	}
	return registry.BuiltinResult{Data: output}, nil
}
