// Package extensionflow projects App-hosted Extension selections into a
// flattened Agent Catalog and executes authorized Actions through one
// authoritative runtime boundary. Extension is an App capability, not a
// Service Kind: host App identity remains server-internal while Desktop sees
// only Extension names and Actions.
package extensionflow

import (
	"context"
	"fmt"
	"net/url"
	"time"

	"github.com/tidbcloud/AgentTeam/internal/coredb"
	"github.com/tidbcloud/AgentTeam/internal/extension/binding"
	"github.com/tidbcloud/AgentTeam/internal/extension/egress"
	"github.com/tidbcloud/AgentTeam/internal/extension/registry"
	"github.com/tidbcloud/AgentTeam/internal/store/extension"
	"golang.org/x/sync/singleflight"
)

// Service owns Extension Catalog projection, delivery, invocation resolution,
// execution, and audit over the shared App Service Actor substrate.
type Service struct {
	db             coredb.DB
	store          *extension.Store
	registry       *registry.Registry
	credentialKey  []byte
	egressFactory  InvocationEgressFactory
	oauthRefresher OAuthRefresher
	oauthFlights   singleflight.Group
	now            func() time.Time
	// invocationAuditTimeout is an unexported, per-Service test seam. A zero
	// value always resolves to the fixed production default; only focused
	// contention tests override it so host load cannot hide lock-order bugs.
	invocationAuditTimeout time.Duration
	// decisionsSnapshotHook is a test-only seam invoked in invocation and
	// reauthorization resolution immediately after the authorization-decision
	// snapshot is read and before the catalog row is locked. Tests use it to
	// interleave a concurrent authorization/catalog mutation and prove the
	// stale snapshot fails the catalog fence. It is nil in production, must
	// not touch the database, and must not block on locks.
	decisionsSnapshotHook func()
}

// InvocationEgress executes one bound request with one already-resolved
// provider credential. *egress.Client satisfies it.
type InvocationEgress interface {
	Execute(
		context.Context,
		binding.BoundRequest,
		egress.Credential,
	) (egress.Result, error)
}

// InvocationEgressFactory creates a fixed-origin guarded client from an
// immutable External ActionPlan.
type InvocationEgressFactory func(
	registry.ActionPlan,
) (InvocationEgress, error)

// Options carries optional Service collaborators.
type Options struct {
	Now            func() time.Time
	OAuthRefresher OAuthRefresher

	// EgressFactory is a narrow test/embedding seam. Nil selects the guarded
	// production client.
	EgressFactory InvocationEgressFactory
}

// New constructs the App-hosted Extension runtime service.
func New(
	db coredb.DB,
	extensionRegistry *registry.Registry,
	credentialKey []byte,
	options Options,
) *Service {
	now := options.Now
	if now == nil {
		now = func() time.Time { return time.Now().UTC() }
	}
	factory := options.EgressFactory
	if factory == nil {
		factory = func(plan registry.ActionPlan) (InvocationEgress, error) {
			origin, err := url.Parse(plan.Endpoint.BaseURL)
			if err != nil {
				return nil, fmt.Errorf(
					"parse extension endpoint: %w",
					err,
				)
			}
			return egress.NewClient(egress.Config{
				Origin: origin,
			})
		}
	}
	return &Service{
		db:             db,
		store:          extension.New(db),
		registry:       extensionRegistry,
		credentialKey:  cloneKey(credentialKey),
		egressFactory:  factory,
		oauthRefresher: options.OAuthRefresher,
		now:            now,
	}
}

func (service *Service) validate() error {
	if service == nil || service.db == nil || service.store == nil {
		return fmt.Errorf("extension runtime service is not configured")
	}
	if service.egressFactory == nil {
		return fmt.Errorf(
			"extension invocation egress factory is not configured",
		)
	}
	return nil
}

func (service *Service) requireRegistry() error {
	if service.registry == nil {
		return fmt.Errorf("extension registry is not configured")
	}
	return nil
}
