package app

import (
	"context"
	"crypto/sha256"
	"encoding/hex"
	"fmt"
	"strings"
	"time"

	"github.com/tidbcloud/AgentTeam/internal/coredb"
	"github.com/tidbcloud/AgentTeam/internal/store/app/installation"
	"github.com/tidbcloud/AgentTeam/internal/store/app/registry"
	"github.com/tidbcloud/AgentTeam/internal/store/serviceactor"
)

// AppPermissionsView is the read-only, secret-free projection of one external
// App's effective permission scopes. AllowedScopes is the immutable ceiling
// pinned by the manifest; ActiveScopes is the currently granted (narrowed)
// subset; PolicyDigest is the optimistic-concurrency token for PATCH.
type AppPermissionsView struct {
	AllowedScopes []string
	ActiveScopes  []string
	PolicyDigest  string
}

// AppPermissionsUpdateResult is the durable outcome of one App permission scope
// narrowing.
type AppPermissionsUpdateResult struct {
	ActiveScopes []string
	PolicyDigest string
}

// ListAppPermissionsForUser returns the effective App permission scopes for one
// active external App installation. GET shares PATCH's manager/admin authority
// and returns existence-hiding 404 for non-managers, so a member holding the
// installation ID cannot read the full policy of an App turned private.
func (store *Store) ListAppPermissionsForUser(
	ctx context.Context,
	userID string,
	workspaceID string,
	installationID string,
) (AppPermissionsView, error) {
	workspaceMember, summary, err := store.loadManagedWorkspaceApp(ctx, userID, workspaceID, installationID)
	if err != nil {
		return AppPermissionsView{}, err
	}
	// GET shares PATCH's manager/admin authority and returns existence-hiding
	// 404 for everyone else. The ordinary-member projection hides private Apps
	// and their grants; exposing the full policy to any member holding the
	// installation ID would leak an App turned private. Manager/admin-only read
	// also keeps this surface consistent with the manager-gated AppDetail UI.
	if err := store.authorizeInstallationManagementFromSummary(
		ctx, store.db, workspaceID, workspaceMember, summary,
	); err != nil {
		return AppPermissionsView{}, err
	}
	allowedScopes, err := pinnedAppManifestScopes(summary.Installation)
	if err != nil {
		return AppPermissionsView{}, err
	}
	activeScopes, err := store.serviceActorStore().ListActiveGrantScopes(ctx, summary.Installation.ID)
	if err != nil {
		return AppPermissionsView{}, err
	}
	return AppPermissionsView{
		AllowedScopes: allowedScopes,
		ActiveScopes:  activeScopes,
		PolicyDigest:  computeScopePolicyDigest(activeScopes),
	}, nil
}

// UpdateAppPermissionsForUser narrows one active external App's effective
// permission scopes to a validated subset of its pinned manifest capability
// scopes. Only an active App manager or workspace administrator may mutate.
func (store *Store) UpdateAppPermissionsForUser(
	ctx context.Context,
	userID string,
	workspaceID string,
	installationID string,
	expectedPolicyDigest string,
	requestedScopes []string,
	now time.Time,
) (AppPermissionsUpdateResult, error) {
	workspaceMember, summary, err := store.loadManagedWorkspaceApp(ctx, userID, workspaceID, installationID)
	if err != nil {
		return AppPermissionsUpdateResult{}, err
	}
	if err := store.authorizeInstallationManagementFromSummary(
		ctx, store.db, workspaceID, workspaceMember, summary,
	); err != nil {
		return AppPermissionsUpdateResult{}, err
	}
	if now.IsZero() {
		now = time.Now().UTC()
	} else {
		now = now.UTC()
	}

	result := AppPermissionsUpdateResult{}
	if err := coredb.WithTx(ctx, store.db, func(tx coredb.Tx) error {
		return applyAppPermissionsUpdateInTx(ctx, store, tx, appPermissionsMutation{
			UserID:                userID,
			WorkspaceID:           workspaceID,
			InstallationID:        installationID,
			ExpectedActorMemberID: workspaceMember.Member.ID,
			ExpectedSummary:       summary,
			ExpectedPolicyDigest:  expectedPolicyDigest,
			RequestedScopes:       requestedScopes,
			Now:                   now,
			Result:                &result,
		})
	}); err != nil {
		return AppPermissionsUpdateResult{}, err
	}
	return result, nil
}

type appPermissionsMutation struct {
	UserID                string
	WorkspaceID           string
	InstallationID        string
	ExpectedActorMemberID string
	ExpectedSummary       serviceactor.InstallationSummary
	ExpectedPolicyDigest  string
	RequestedScopes       []string
	Now                   time.Time
	Result                *AppPermissionsUpdateResult
}

func applyAppPermissionsUpdateInTx(
	ctx context.Context,
	store *Store,
	tx coredb.Tx,
	mutation appPermissionsMutation,
) error {
	summary := mutation.ExpectedSummary
	if err := installation.LockAppInstallationMutationScope(ctx, tx, summary.Definition.ID); err != nil {
		return err
	}
	currentActor, err := store.revalidateInstallationManagementForUpdate(
		ctx,
		tx,
		mutation.UserID,
		mutation.WorkspaceID,
		mutation.ExpectedActorMemberID,
		summary.Installation.ID,
		summary.Definition.ID,
	)
	if err != nil {
		return err
	}
	attribution, attributed, err := loadAppManagementAttribution(
		ctx, tx, mutation.WorkspaceID, summary.Definition.ID, false,
	)
	if err != nil {
		return err
	}
	if !attributed {
		return ErrNotFound
	}
	_ = attribution

	txStore := store.serviceActorStore().InTx(tx)
	currentInstallation, err := txStore.LoadInstallationForUpdate(ctx, strings.TrimSpace(mutation.InstallationID))
	if err != nil {
		return err
	}
	if !currentAppRuntimeInstallationMatchesSummary(currentInstallation, summary, mutation.WorkspaceID) {
		return conflict("App installation changed. Retry the permission update.")
	}
	capability, err := requirePinnedRuntimeCapability(currentInstallation)
	if err != nil {
		return err
	}
	allowedScopes, err := pinnedAppManifestScopes(currentInstallation)
	if err != nil {
		return err
	}

	requestedScopes := registry.SortedUniqueScopes(mutation.RequestedScopes)
	if err := validateRequestedAppScopes(requestedScopes, allowedScopes, capability); err != nil {
		return err
	}

	currentMember, err := txStore.LoadMemberByInstallation(ctx, currentInstallation.ID)
	if err != nil {
		return err
	}
	if strings.TrimSpace(currentMember.WorkspaceID) != strings.TrimSpace(mutation.WorkspaceID) ||
		strings.TrimSpace(currentMember.ServiceDefinitionID) != strings.TrimSpace(summary.Definition.ID) ||
		strings.TrimSpace(currentMember.ServiceInstallationID) != strings.TrimSpace(currentInstallation.ID) ||
		strings.TrimSpace(currentMember.MemberID) != strings.TrimSpace(summary.Member.MemberID) ||
		strings.TrimSpace(currentMember.ServiceKind) != serviceactor.KindApp ||
		strings.TrimSpace(currentMember.Status) != serviceactor.StatusActive {
		return conflict("App service member changed. Retry the permission update.")
	}

	// Optimistic concurrency: recompute the current active-scope digest under the
	// grant lock and reject if the client's expectation is stale.
	currentActiveScopes, err := txStore.ListActiveGrantScopes(ctx, currentInstallation.ID)
	if err != nil {
		return err
	}
	currentDigest := computeScopePolicyDigest(currentActiveScopes)
	if currentDigest != strings.TrimSpace(mutation.ExpectedPolicyDigest) {
		return conflict("App permission scopes changed. Retry the update.")
	}

	// Atomic canonical grant sync. The service grant table reconciles to the
	// exact requested subset or the whole transaction rolls back.
	if err := txStore.SyncGrantScopes(ctx, currentInstallation, currentActor.Member.ID, requestedScopes, mutation.Now); err != nil {
		return err
	}

	// Narrowing must close the authorization agentteam: every active principal
	// authorization is trimmed to the new active scope set (and disabled when
	// nothing remains), so tokens, runtime checks, and the Agent catalog can
	// never project a scope the installation no longer holds.
	trimNarrowed, err := store.serviceActorStore().InTx(tx).TrimInstallationPrincipalAuthorizationScopes(
		ctx,
		mutation.WorkspaceID,
		currentInstallation.ID,
		requestedScopes,
		mutation.Now,
	)
	if err != nil {
		return err
	}

	finalActiveScopes, err := txStore.ListActiveGrantScopes(ctx, currentInstallation.ID)
	if err != nil {
		return err
	}
	mutation.Result.ActiveScopes = finalActiveScopes
	mutation.Result.PolicyDigest = computeScopePolicyDigest(finalActiveScopes)
	if trimNarrowed {
		// At least one Agent authorization changed: refresh flattened
		// catalogs inside the same transaction.
		return store.RunWorkspaceAppCatalogReconcileHook(ctx, tx, mutation.WorkspaceID, mutation.Now)
	}
	return nil
}

// validateRequestedAppScopes enforces the anti-escalation contract: requested
// scopes must be a subset of the full pinned manifest scopes (top-level
// generic + capabilities.runtime.scopes) — the same set granted at install.
func validateRequestedAppScopes(requestedScopes []string, allowedScopes []string, capability registry.StoredAppRuntimeCapability) error {
	allowedSet := make(map[string]struct{}, len(allowedScopes))
	for _, scope := range allowedScopes {
		allowedSet[scope] = struct{}{}
	}
	for _, scope := range requestedScopes {
		if _, ok := allowedSet[scope]; !ok {
			return badRequest(fmt.Sprintf("requested App scope %q is not allowed by the pinned capability", scope))
		}
	}
	return registry.ValidateAppScopeSubset(capability, requestedScopes)
}

// computeScopePolicyDigest returns the sha256 hex of the sorted unique scope
// set, joined by newlines. It is the optimistic-concurrency token for App
// permission PATCH commands.
func computeScopePolicyDigest(scopes []string) string {
	normalized := registry.SortedUniqueScopes(scopes)
	digest := sha256.Sum256([]byte(strings.Join(normalized, "\n")))
	return hex.EncodeToString(digest[:])
}
