package extensionflow

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

	"github.com/tidbcloud/AgentTeam/internal/coredb"
	"github.com/tidbcloud/AgentTeam/internal/extension/registry"
	"github.com/tidbcloud/AgentTeam/internal/store/extension"
	"github.com/tidbcloud/AgentTeam/internal/store/model"
	"github.com/tidbcloud/AgentTeam/internal/store/serviceactor"
)

// effectiveExtension is one server-internal App-hosted selection after
// intersecting App authorization, connection state, capability policy, and the
// immutable Registry pin. Host identities never enter the flattened Catalog.
type effectiveExtension struct {
	Selection            serviceactor.InstallationExtension
	Definition           registry.RegisteredExtension
	Actions              []string
	AllowDestructive     bool
	Connections          []extension.AppHostedConnectionIdentity
	AuthorizedByMemberID string
}

type appAuthorizationDecision struct {
	Authorization serviceactor.PrincipalAuthorization
	Allowed       bool
	Scopes        []string
}

// projectAppHostedCatalogEntries is the single App-hosted Catalog projection.
// It intentionally does not read or project any standalone Extension Service
// Definition, Installation, Connection, or Authorization identity.
func (service *Service) projectAppHostedCatalogEntries(
	ctx context.Context,
	tx coredb.Tx,
	extTx *extension.TxStore,
	workspaceID string,
	agentID string,
) ([]catalogEntry, bool, error) {
	effectiveExtensions, err := service.resolveAppHostedExtensions(
		ctx,
		tx,
		extTx,
		workspaceID,
		agentID,
	)
	if err != nil {
		return nil, false, err
	}
	entries, err := catalogEntriesForEffectiveExtensions(effectiveExtensions)
	if err != nil {
		return nil, false, err
	}
	return entries, len(entries) > 0, nil
}

func (service *Service) projectAppHostedCatalogEntriesFromSelections(
	ctx context.Context,
	tx coredb.Tx,
	workspaceID string,
	selections []extension.AppHostedExtensionSelection,
	decisionsByInstallation map[string]appAuthorizationDecision,
) ([]catalogEntry, bool, error) {
	effectiveExtensions, err := service.resolveAppHostedExtensionsFromSelections(
		ctx,
		tx,
		workspaceID,
		selections,
		decisionsByInstallation,
	)
	if err != nil {
		return nil, false, err
	}
	entries, err := catalogEntriesForEffectiveExtensions(effectiveExtensions)
	if err != nil {
		return nil, false, err
	}
	return entries, len(entries) > 0, nil
}

func catalogEntriesForEffectiveExtensions(
	effectiveExtensions []effectiveExtension,
) ([]catalogEntry, error) {
	entries := make([]catalogEntry, 0, len(effectiveExtensions))
	for _, effective := range effectiveExtensions {
		entry, err := catalogEntryFromAuthorization(
			effective.Selection.ExtensionName,
			effective.Definition.Definition,
			effective.Actions,
		)
		if err != nil {
			return nil, fmt.Errorf(
				"App-hosted Extension selection %q Catalog projection: %w",
				effective.Selection.ID,
				err,
			)
		}
		entries = append(entries, entry)
	}
	return entries, nil
}

// resolveAppHostedExtensions is the single authoritative App-hosted
// projection. Catalog reads flatten its result; invocation keeps the
// server-internal selection and connection identities needed to resolve the
// host App without exposing them to Desktop.
func (service *Service) resolveAppHostedExtensions(
	ctx context.Context,
	tx coredb.Tx,
	extTx *extension.TxStore,
	workspaceID string,
	agentID string,
) ([]effectiveExtension, error) {
	decisions, err := extTx.ListAgentAppAuthorizationDecisions(ctx, workspaceID, agentID)
	if err != nil {
		return nil, err
	}
	return service.resolveAppHostedExtensionsFromDecisions(
		ctx,
		tx,
		extTx,
		workspaceID,
		agentID,
		decisions,
	)
}

// resolveAppHostedExtensionsFromDecisions projects the effective extensions
// from caller-acquired authorization decisions. Invocation resolution passes
// non-locking snapshot decisions read before the catalog row lock (the
// catalog-change hook locks decisions first and then the catalog row;
// locking the same decision rows from the invocation side would deadlock
// with authorization lifecycle updates).
func (service *Service) resolveAppHostedExtensionsFromDecisions(
	ctx context.Context,
	tx coredb.Tx,
	extTx *extension.TxStore,
	workspaceID string,
	agentID string,
	decisions []serviceactor.PrincipalAuthorization,
) ([]effectiveExtension, error) {
	if service == nil || service.registry == nil {
		return nil, fmt.Errorf("extension registry is not configured")
	}
	decisionsByInstallation, err := validateAppAuthorizationDecisions(
		workspaceID,
		agentID,
		decisions,
	)
	if err != nil {
		return nil, err
	}
	selections, err := extTx.ListActiveAppHostedExtensionSelections(ctx, workspaceID)
	if err != nil {
		return nil, err
	}
	return service.resolveAppHostedExtensionsFromSelections(
		ctx,
		tx,
		workspaceID,
		selections,
		decisionsByInstallation,
	)
}

func (service *Service) resolveAppHostedExtensionsFromSelections(
	ctx context.Context,
	tx coredb.Tx,
	workspaceID string,
	selections []extension.AppHostedExtensionSelection,
	decisionsByInstallation map[string]appAuthorizationDecision,
) ([]effectiveExtension, error) {
	if service == nil || service.registry == nil {
		return nil, fmt.Errorf("extension registry is not configured")
	}
	effectiveExtensions := make([]effectiveExtension, 0, len(selections))
	seenNames := make(map[string]string, len(selections))
	for _, projected := range selections {
		effective, ok, err := service.resolveEffectiveExtension(
			ctx,
			tx,
			workspaceID,
			projected,
			decisionsByInstallation,
		)
		if err != nil {
			return nil, err
		}
		if !ok {
			continue
		}
		if previous, duplicate := seenNames[effective.Selection.ExtensionName]; duplicate {
			return nil, fmt.Errorf(
				"App-hosted Extension selections %q and %q share flattened name %q",
				previous,
				effective.Selection.ID,
				effective.Selection.ExtensionName,
			)
		}
		seenNames[effective.Selection.ExtensionName] = effective.Selection.ID
		effectiveExtensions = append(effectiveExtensions, effective)
	}
	return effectiveExtensions, nil
}

func validateAppAuthorizationDecisions(
	workspaceID string,
	agentID string,
	authorizations []serviceactor.PrincipalAuthorization,
) (map[string]appAuthorizationDecision, error) {
	decisions := make(map[string]appAuthorizationDecision, len(authorizations))
	for _, authorization := range authorizations {
		if authorization.ID == "" ||
			authorization.WorkspaceID != workspaceID ||
			authorization.ServiceDefinitionID == "" ||
			authorization.ServiceInstallationID == "" ||
			authorization.ServiceKind != serviceactor.KindApp ||
			authorization.PrincipalKind != extension.PrincipalKindAgent ||
			authorization.PrincipalRef != agentID {
			return nil, fmt.Errorf(
				"App authorization %q identity does not match the Catalog request",
				authorization.ID,
			)
		}
		if previous, duplicate := decisions[authorization.ServiceInstallationID]; duplicate {
			return nil, fmt.Errorf(
				"multiple App authorization decisions %q and %q exist for installation %q",
				previous.Authorization.ID,
				authorization.ID,
				authorization.ServiceInstallationID,
			)
		}

		allowed := false
		switch {
		case authorization.Status == serviceactor.StatusActive &&
			authorization.DisabledAt == nil &&
			authorization.RemovedAt == nil:
			allowed = true
		case authorization.Status == serviceactor.StatusDisabled &&
			authorization.DisabledAt != nil:
			allowed = false
		case authorization.RemovedAt != nil:
			allowed = false
		default:
			return nil, fmt.Errorf(
				"App authorization %q has inconsistent lifecycle state",
				authorization.ID,
			)
		}
		scopes, err := decodePrincipalAuthorizationScopes(
			authorization.ScopesJSON,
		)
		if err != nil {
			return nil, fmt.Errorf(
				"App authorization %q has invalid scopes: %w",
				authorization.ID,
				err,
			)
		}
		decisions[authorization.ServiceInstallationID] = appAuthorizationDecision{
			Authorization: authorization,
			Allowed:       allowed,
			Scopes:        scopes,
		}
	}
	return decisions, nil
}

func (service *Service) resolveEffectiveExtension(
	ctx context.Context,
	tx coredb.Tx,
	workspaceID string,
	projected extension.AppHostedExtensionSelection,
	decisions map[string]appAuthorizationDecision,
) (effectiveExtension, bool, error) {
	selection := projected.Selection
	if err := validateAppHostedSelection(workspaceID, projected); err != nil {
		return effectiveExtension{}, false, err
	}
	decision, explicit, grantedScopes, toolAllowlist,
		executionModeExplicitOnly, available, err :=
		resolveAppExecutionGrant(projected, decisions)
	if err != nil || !available {
		return effectiveExtension{}, false, err
	}
	registered, available, err := service.resolveRegisteredAppExtension(
		workspaceID,
		projected,
	)
	if err != nil || !available {
		return effectiveExtension{}, false, err
	}
	actions := allowedDefinitionActionNames(
		registered.Definition.Actions,
		grantedScopes,
	)
	// The durable mcp.tools allowlist is authoritative only under the
	// capability's explicit-only execution contract: legacy default-allow
	// capabilities carry snake_case tool labels from the pre-Extension
	// surface and must keep no-op semantics (the flattened surface is
	// selected by capability_key='extensions'), even under an explicit
	// agent grant. For explicit-only Apps the kebab-case tools list is the
	// durable action contract: removing a tool from the App's execution
	// capability removes the action without touching scopes, and an EMPTY
	// list revokes every action — the intersection is unconditional, so an
	// empty allowlist can never fall back to the scope-derived set.
	if executionModeExplicitOnly {
		actions = intersectActionAllowlist(actions, toolAllowlist)
	}
	if len(actions) == 0 {
		return effectiveExtension{}, false, nil
	}
	actions, allowDestructive, available, err := service.applyAppHostPolicy(
		ctx,
		tx,
		workspaceID,
		projected,
		registered,
		explicit,
		grantedScopes,
		actions,
	)
	if err != nil || !available {
		return effectiveExtension{}, false, err
	}
	return effectiveExtension{
		Selection:        selection,
		Definition:       registered,
		Actions:          actions,
		AllowDestructive: allowDestructive,
		Connections: append(
			[]extension.AppHostedConnectionIdentity(nil),
			projected.Connections...,
		),
		AuthorizedByMemberID: decision.Authorization.
			AuthorizedByMemberID,
	}, true, nil
}

func resolveAppExecutionGrant(
	projected extension.AppHostedExtensionSelection,
	decisions map[string]appAuthorizationDecision,
) (appAuthorizationDecision, bool, []string, []string, bool, bool, error) {
	selection := projected.Selection
	executionCapability := model.AppExecutionCapability{
		AuthorizationMode: model.AppAuthorizationModeDefaultAllow,
	}
	if projected.ExecutionCapabilityEnabled {
		var err error
		executionCapability, err = model.DecodeAppExecutionCapability(
			projected.ExecutionCapabilityConfig,
		)
		if err != nil {
			return appAuthorizationDecision{}, false, nil, nil, false, false,
				fmt.Errorf(
					"App-hosted Extension selection %q has corrupt execution policy: %w",
					selection.ID,
					err,
				)
		}
	}
	decision, explicit := decisions[selection.ServiceInstallationID]
	if explicit {
		if decision.Authorization.ServiceDefinitionID !=
			selection.ServiceDefinitionID {
			return appAuthorizationDecision{}, false, nil, nil, false, false,
				fmt.Errorf(
					"App authorization %q does not match selection %q host",
					decision.Authorization.ID,
					selection.ID,
				)
		}
		if !decision.Allowed {
			return decision, true, nil, nil, false, false, nil
		}
	} else if executionCapability.AuthorizationMode ==
		model.AppAuthorizationModeExplicitOnly {
		return appAuthorizationDecision{}, false, nil, nil, false, false, nil
	} else if projected.InstallationOwnershipKind ==
		serviceactor.OwnershipKindPersonal {
		// Personal installations fail closed: without an explicit Agent grant
		// the Agent catalog never exposes the owner's capabilities. Legacy
		// workspace installations keep their default-allow compatibility.
		return appAuthorizationDecision{}, false, nil, nil, false, false, nil
	}
	grantedScopes := executionCapability.Scopes
	if explicit {
		grantedScopes = decision.Scopes
	}
	executionModeExplicitOnly := executionCapability.AuthorizationMode ==
		model.AppAuthorizationModeExplicitOnly
	return decision, explicit, grantedScopes, executionCapability.Tools,
		executionModeExplicitOnly, true, nil
}

// intersectActionAllowlist keeps only the actions the durable capability
// allowlist still declares.
func intersectActionAllowlist(
	actions []string,
	allowlist []string,
) []string {
	allowed := make(map[string]struct{}, len(allowlist))
	for _, tool := range allowlist {
		allowed[tool] = struct{}{}
	}
	filtered := make([]string, 0, len(actions))
	for _, action := range actions {
		if _, ok := allowed[action]; ok {
			filtered = append(filtered, action)
		}
	}
	return filtered
}

func (service *Service) resolveRegisteredAppExtension(
	workspaceID string,
	projected extension.AppHostedExtensionSelection,
) (registry.RegisteredExtension, bool, error) {
	selection := projected.Selection
	registered, ok := service.registry.LookupByDigest(selection.DefinitionDigest)
	if !ok {
		return registry.RegisteredExtension{}, false,
			newCatalogReconcileBlockedError(
				CatalogReconcileErrorUnregisteredDigest,
				"App-hosted Extension selection %q references unregistered digest %q",
				selection.ID,
				selection.DefinitionDigest,
			)
	}
	if registered.Definition.Domain != selection.DefinitionDomain ||
		registered.Definition.Version != selection.DefinitionVersion ||
		registered.Definition.Digest != selection.DefinitionDigest {
		return registry.RegisteredExtension{}, false, fmt.Errorf(
			"App-hosted Extension selection %q Registry identity does not match its pin",
			selection.ID,
		)
	}
	switch registered.ConnectionPolicy {
	case registry.ConnectionRequired:
		if len(projected.Connections) == 0 {
			return registry.RegisteredExtension{}, false, nil
		}
		for _, connection := range projected.Connections {
			if connection.ID == "" ||
				connection.WorkspaceID != workspaceID ||
				connection.ServiceDefinitionID != selection.ServiceDefinitionID ||
				connection.ServiceInstallationID != selection.ServiceInstallationID ||
				connection.ServiceKind != serviceactor.KindApp ||
				connection.Status != serviceactor.StatusActive {
				return registry.RegisteredExtension{}, false, fmt.Errorf(
					"App-hosted Extension selection %q has an inconsistent App connection",
					selection.ID,
				)
			}
		}
	case registry.ConnectionNone:
		if len(projected.Connections) != 0 {
			return registry.RegisteredExtension{}, false, fmt.Errorf(
				"connectionless App-hosted Extension selection %q unexpectedly has %d active App connection(s)",
				selection.ID,
				len(projected.Connections),
			)
		}
	default:
		return registry.RegisteredExtension{}, false, fmt.Errorf(
			"App-hosted Extension selection %q has unsupported connection policy %d",
			selection.ID,
			registered.ConnectionPolicy,
		)
	}
	return registered, true, nil
}

func (service *Service) applyAppHostPolicy(
	ctx context.Context,
	tx coredb.Tx,
	workspaceID string,
	projected extension.AppHostedExtensionSelection,
	registered registry.RegisteredExtension,
	explicit bool,
	grantedScopes []string,
	actions []string,
) ([]string, bool, bool, error) {
	_ = ctx
	_ = tx
	// Provider-owned catalog facets travel with the Extension composite
	// through bootstrap registration and are keyed by definition digest.
	// Without one, the scope-derived action set stays authoritative.
	policy, found := service.registry.CatalogPolicyFor(
		registered.Definition.Digest,
	)
	if !found {
		return actions, false, true, nil
	}
	selection := projected.Selection
	connections := make(
		[]registry.CatalogConnection,
		0,
		len(projected.Connections),
	)
	for _, connection := range projected.Connections {
		connections = append(
			connections,
			registry.CatalogConnection{
				ID:                      connection.ID,
				WorkspaceID:             connection.WorkspaceID,
				ServiceDefinitionID:     connection.ServiceDefinitionID,
				ServiceInstallationID:   connection.ServiceInstallationID,
				ServiceKind:             connection.ServiceKind,
				Status:                  connection.Status,
				ExternalSubjectRef:      connection.ExternalSubjectRef,
				CurrentCredentialDigest: connection.CurrentCredentialDigest,
				Config: append(
					[]byte(nil),
					connection.Config...,
				),
			},
		)
	}
	filtered, available, err := policy.FilterActions(
		registry.CatalogPolicyInput{
			WorkspaceID:           workspaceID,
			ServiceDefinitionID:   selection.ServiceDefinitionID,
			ServiceInstallationID: selection.ServiceInstallationID,
			DefinitionDomain:      selection.DefinitionDomain,
			DefinitionVersion:     int(selection.DefinitionVersion),
			DefinitionDigest:      selection.DefinitionDigest,
			Connections:           connections,
			GrantedScopes:         append([]string(nil), grantedScopes...),
			ExplicitAuthorization: explicit,
			Actions:               append([]string(nil), actions...),
		},
	)
	if err != nil {
		return nil, false, false, fmt.Errorf(
			"App-hosted Extension selection %q policy: %w",
			selection.ID,
			err,
		)
	}
	if !available {
		return nil, false, false, nil
	}
	// The provider facet may only NARROW the action set: its output is
	// intersected back against the scope/tools-filtered input, so a buggy
	// or hostile policy cannot re-advertise an action the durable authority
	// already revoked. An emptied result is unavailable, and duplicates or
	// blanks in the policy output can never survive the intersection.
	narrowed := intersectActionAllowlist(actions, filtered)
	if len(narrowed) == 0 {
		return nil, false, false, nil
	}
	return narrowed, false, true, nil
}

func allowedDefinitionActionNames(
	actions []registry.DefinitionAction,
	grantedScopes []string,
) []string {
	allowed := make([]string, 0, len(actions))
	for _, action := range actions {
		if !scopeSetCoversAll(
			grantedScopes,
			action.RequiredScopes,
		) {
			continue
		}
		allowed = append(allowed, action.Name)
	}
	return allowed
}

func decodePrincipalAuthorizationScopes(payload []byte) ([]string, error) {
	if len(payload) == 0 {
		return nil, nil
	}
	var scopes []string
	if err := json.Unmarshal(payload, &scopes); err != nil {
		return nil, err
	}
	seen := make(map[string]struct{}, len(scopes))
	normalized := make([]string, 0, len(scopes))
	for _, scope := range scopes {
		if scope == "" {
			return nil, fmt.Errorf("scope is blank")
		}
		if _, duplicate := seen[scope]; duplicate {
			continue
		}
		seen[scope] = struct{}{}
		normalized = append(normalized, scope)
	}
	sort.Strings(normalized)
	return normalized, nil
}

func scopeSetCoversAll(granted []string, required []string) bool {
	if len(required) == 0 {
		return true
	}
	set := make(map[string]struct{}, len(granted))
	for _, scope := range granted {
		set[scope] = struct{}{}
	}
	for _, scope := range required {
		if _, ok := set[scope]; !ok {
			return false
		}
	}
	return true
}

func validateAppHostedSelection(
	workspaceID string,
	projected extension.AppHostedExtensionSelection,
) error {
	selection := projected.Selection
	if selection.ID == "" ||
		selection.WorkspaceID != workspaceID ||
		selection.ServiceDefinitionID == "" ||
		selection.ServiceInstallationID == "" ||
		selection.ExtensionName == "" ||
		len(selection.ExtensionName) > 255 ||
		!validKebabExtensionName(selection.ExtensionName) ||
		selection.DefinitionDomain == "" ||
		selection.DefinitionVersion <= 0 ||
		selection.DefinitionDigest == "" ||
		selection.Status != serviceactor.StatusActive ||
		selection.DisabledReason != "" ||
		selection.DisabledAt != nil {
		return fmt.Errorf(
			"App-hosted Extension selection %q has an invalid active identity",
			selection.ID,
		)
	}
	if projected.DefinitionID != selection.ServiceDefinitionID ||
		projected.DefinitionKind != serviceactor.KindApp ||
		projected.DefinitionStatus != serviceactor.StatusActive ||
		projected.InstallationID != selection.ServiceInstallationID ||
		projected.InstallationWorkspaceID != workspaceID ||
		projected.InstallationDefinitionID != selection.ServiceDefinitionID ||
		projected.InstallationKind != serviceactor.KindApp ||
		projected.InstallationStatus != serviceactor.StatusActive {
		return fmt.Errorf(
			"App-hosted Extension selection %q does not match its active App host",
			selection.ID,
		)
	}
	if projected.CapabilityDefinitionID != selection.ServiceDefinitionID ||
		projected.CapabilityKind != serviceactor.KindApp ||
		projected.CapabilityKey != serviceactor.CapabilityExtensions ||
		!projected.CapabilityEnabled {
		return fmt.Errorf(
			"App-hosted Extension selection %q does not match an enabled App Extension capability",
			selection.ID,
		)
	}

	capability, err := model.DecodeExtensionCapabilityConfig(projected.CapabilityConfig)
	if err != nil {
		return fmt.Errorf(
			"App-hosted Extension selection %q has corrupt capability policy: %w",
			selection.ID,
			err,
		)
	}
	capabilityEntry, found := extensionCapabilityForDomain(capability, selection.DefinitionDomain)
	if !found ||
		capabilityEntry.Version != selection.DefinitionVersion ||
		capabilityEntry.DefinitionDigest != selection.DefinitionDigest {
		return fmt.Errorf(
			"App-hosted Extension selection %q pin does not match its App capability",
			selection.ID,
		)
	}
	return nil
}

func extensionCapabilityForDomain(
	capability model.ExtensionCapabilityConfig,
	domain string,
) (model.ExtensionCapabilityEntry, bool) {
	for _, entry := range capability.Extensions {
		if entry.Domain == domain {
			return entry, true
		}
	}
	return model.ExtensionCapabilityEntry{}, false
}
