package app

import (
	"context"
	"database/sql"
	"errors"
	"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/model"
	"github.com/tidbcloud/AgentTeam/internal/store/serviceactor"
	"github.com/tidbcloud/AgentTeam/internal/store/storeerr"
)

const defaultAppRunnerCredentialName = "App runner stream token"

type AppRunnerCredentialCreateParams struct {
	Name      string
	ExpiresAt *time.Time
	Metadata  map[string]any
}

type AppRunnerCredentialView struct {
	ID                    string
	WorkspaceID           string
	ServiceDefinitionID   string
	ServiceInstallationID string
	ServiceMemberID       string
	CredentialKind        string
	TransportKind         string
	Name                  string
	TokenHint             string
	Status                string
	CreatedByMemberID     string
	CreatedAt             time.Time
	UpdatedAt             time.Time
	LastUsedAt            *time.Time
	DisabledAt            *time.Time
	ExpiresAt             *time.Time
	Metadata              map[string]any
}

type AppRunnerCredentialTokenView struct {
	Credential AppRunnerCredentialView
	Token      string
}

func (store *Store) ListAppRunnerCredentialsForUser(ctx context.Context, userID string, workspaceID string, installationID string) ([]AppRunnerCredentialView, error) {
	workspaceMember, err := store.requireWorkspaceAccess(ctx, userID, workspaceID)
	if err != nil {
		return nil, err
	}
	summary, err := store.requireAppRunnerInstallation(ctx, workspaceID, installationID)
	if err != nil {
		return nil, err
	}
	if err := store.authorizeInstallationManagementFromSummary(ctx, store.db, workspaceID, workspaceMember, summary); err != nil {
		return nil, err
	}
	rows, err := store.db.Query(ctx, `
SELECT id, workspace_id, service_definition_id, service_installation_id, service_member_id,
       service_kind, credential_kind, transport_kind, name, token_hash, token_hint,
       status, created_by_member_id, created_at, updated_at, last_used_at,
       disabled_at, expires_at, metadata
FROM service_runtime_credentials
WHERE workspace_id = $1
  AND service_definition_id = $2
  AND service_installation_id = $3
  AND service_member_id = $4
  AND service_kind = $5
  AND credential_kind = $6
  AND transport_kind = $7
  AND status = $8
ORDER BY created_at DESC, id DESC`,
		strings.TrimSpace(workspaceID),
		strings.TrimSpace(summary.Definition.ID),
		strings.TrimSpace(summary.Installation.ID),
		strings.TrimSpace(summary.Member.ID),
		serviceactor.KindApp,
		serviceactor.RuntimeCredentialKindRunnerStreamToken,
		serviceactor.RuntimeTransportKindRunnerStream,
		serviceactor.RuntimeCredentialStatusActive,
	)
	if err != nil {
		return nil, fmt.Errorf("list App runner credentials: %w", err)
	}
	defer rows.Close()
	credentials := []AppRunnerCredentialView{}
	for rows.Next() {
		credential, err := scanAppRunnerRuntimeCredential(rows)
		if err != nil {
			return nil, err
		}
		credentials = append(credentials, appRunnerCredentialView(credential))
	}
	if err := rows.Err(); err != nil {
		return nil, fmt.Errorf("iterate App runner credentials: %w", err)
	}
	return credentials, nil
}

func (store *Store) CreateAppRunnerCredentialForUser(ctx context.Context, userID string, workspaceID string, installationID string, params AppRunnerCredentialCreateParams) (AppRunnerCredentialTokenView, error) {
	workspaceMember, err := store.requireWorkspaceAccess(ctx, userID, workspaceID)
	if err != nil {
		return AppRunnerCredentialTokenView{}, err
	}
	summary, err := store.requireAppRunnerInstallation(ctx, workspaceID, installationID)
	if err != nil {
		return AppRunnerCredentialTokenView{}, err
	}
	if err := store.authorizeInstallationManagementFromSummary(ctx, store.db, workspaceID, workspaceMember, summary); err != nil {
		return AppRunnerCredentialTokenView{}, err
	}
	metadata, err := appRunnerCredentialMetadata(params.Metadata)
	if err != nil {
		return AppRunnerCredentialTokenView{}, err
	}
	now := time.Now().UTC()
	var created serviceactor.RuntimeCredentialToken
	if err := coredb.WithTx(ctx, store.db, func(tx coredb.Tx) error {
		if err := installation.LockAppInstallationMutationScope(ctx, tx, summary.Definition.ID); err != nil {
			return err
		}
		currentWorkspaceMember, err := store.revalidateInstallationManagementForUpdate(
			ctx,
			tx,
			userID,
			workspaceID,
			workspaceMember.Member.ID,
			installationID,
			summary.Definition.ID,
		)
		if err != nil {
			return err
		}
		txServiceActor := store.serviceActorStore().InTx(tx)
		currentInstallation, err := txServiceActor.LoadInstallationForUpdate(ctx, summary.Installation.ID)
		if err != nil {
			return err
		}
		if !currentAppRuntimeInstallationMatchesSummary(currentInstallation, summary, workspaceID) {
			return conflict("App installation changed before runner credential creation.")
		}
		created, err = txServiceActor.CreateRuntimeCredentialToken(ctx, serviceactor.RuntimeCredentialTokenCreate{
			RuntimeCredentialCreate: serviceactor.RuntimeCredentialCreate{
				WorkspaceID:           strings.TrimSpace(workspaceID),
				ServiceDefinitionID:   currentInstallation.ServiceDefinitionID,
				ServiceInstallationID: currentInstallation.ID,
				ServiceMemberID:       summary.Member.ID,
				ServiceKind:           serviceactor.KindApp,
				CredentialKind:        serviceactor.RuntimeCredentialKindRunnerStreamToken,
				TransportKind:         serviceactor.RuntimeTransportKindRunnerStream,
				Name:                  appRunnerCredentialName(params.Name),
				Status:                serviceactor.RuntimeCredentialStatusActive,
				CreatedByMemberID:     currentWorkspaceMember.Member.ID,
				Now:                   now,
				ExpiresAt:             params.ExpiresAt,
				Metadata:              metadata,
			},
		})
		return err
	}); err != nil {
		return AppRunnerCredentialTokenView{}, err
	}
	return AppRunnerCredentialTokenView{Credential: appRunnerCredentialView(created.Credential), Token: created.Token}, nil
}

func (store *Store) DisableAppRunnerCredentialForUser(ctx context.Context, userID string, workspaceID string, credentialID string) (AppRunnerCredentialView, error) {
	workspaceMember, err := store.requireWorkspaceAccess(ctx, userID, workspaceID)
	if err != nil {
		return AppRunnerCredentialView{}, err
	}
	current, err := store.loadAppRunnerCredentialForUserMutation(ctx, workspaceID, credentialID)
	if err != nil {
		return AppRunnerCredentialView{}, err
	}
	summary, err := store.requireAppRunnerInstallation(ctx, workspaceID, current.ServiceInstallationID)
	if err != nil {
		return AppRunnerCredentialView{}, err
	}
	if err := store.authorizeInstallationManagementFromSummary(ctx, store.db, workspaceID, workspaceMember, summary); err != nil {
		return AppRunnerCredentialView{}, err
	}
	var disabled serviceactor.RuntimeCredential
	now := time.Now().UTC()
	if err := coredb.WithTx(ctx, store.db, func(tx coredb.Tx) error {
		if err := installation.LockAppInstallationMutationScope(ctx, tx, summary.Definition.ID); err != nil {
			return err
		}
		if _, err := store.revalidateInstallationManagementForUpdate(
			ctx,
			tx,
			userID,
			workspaceID,
			workspaceMember.Member.ID,
			current.ServiceInstallationID,
			summary.Definition.ID,
		); err != nil {
			return err
		}
		txServiceActor := store.serviceActorStore().InTx(tx)
		currentInstallation, err := txServiceActor.LoadInstallationForUpdate(ctx, summary.Installation.ID)
		if err != nil {
			return err
		}
		if !currentAppRuntimeInstallationMatchesSummary(currentInstallation, summary, workspaceID) {
			return conflict("App installation changed before runner credential disable.")
		}
		disabled, err = txServiceActor.DisableRuntimeCredential(ctx, strings.TrimSpace(workspaceID), strings.TrimSpace(credentialID), now)
		if err != nil {
			return err
		}
		_, err = txServiceActor.SetRuntimeSessionsStatusByCredential(ctx, strings.TrimSpace(workspaceID), strings.TrimSpace(credentialID), serviceactor.RuntimeSessionStatusDisabled, now)
		return err
	}); err != nil {
		return AppRunnerCredentialView{}, err
	}
	return appRunnerCredentialView(disabled), nil
}

func currentAppRuntimeInstallationMatchesSummary(current serviceactor.Installation, summary serviceactor.InstallationSummary, workspaceID string) bool {
	return strings.TrimSpace(current.ID) == strings.TrimSpace(summary.Installation.ID) &&
		strings.TrimSpace(current.WorkspaceID) == strings.TrimSpace(workspaceID) &&
		strings.TrimSpace(current.ServiceDefinitionID) == strings.TrimSpace(summary.Definition.ID) &&
		strings.TrimSpace(current.ServiceKind) == serviceactor.KindApp &&
		strings.TrimSpace(current.Status) == serviceactor.StatusActive
}

// LoadCurrentAppRunnerCredential reloads and authorizes a credential before a
// runner heartbeat, claim, or offline mutation.
func (store *Store) LoadCurrentAppRunnerCredential(ctx context.Context, workspaceID string, credentialID string) (serviceactor.RuntimeCredential, error) {
	if strings.TrimSpace(workspaceID) == "" || strings.TrimSpace(credentialID) == "" {
		return serviceactor.RuntimeCredential{}, ErrUnauthorized
	}
	credential, err := store.serviceActorStore().LoadRuntimeCredential(ctx, workspaceID, credentialID)
	if err != nil {
		if errors.Is(err, serviceactor.ErrNotFound) || errors.Is(err, storeerr.ErrNotFound) {
			return serviceactor.RuntimeCredential{}, ErrUnauthorized
		}
		return serviceactor.RuntimeCredential{}, err
	}
	return store.ValidateAppRunnerCredential(ctx, credential)
}

// ValidateAppRunnerCredential enforces current App installation, grant, and
// runner-stream authority for an authenticated runtime credential.
func (store *Store) ValidateAppRunnerCredential(ctx context.Context, credential serviceactor.RuntimeCredential) (serviceactor.RuntimeCredential, error) {
	if strings.TrimSpace(credential.ID) == "" || strings.TrimSpace(credential.WorkspaceID) == "" || strings.TrimSpace(credential.ServiceInstallationID) == "" {
		return serviceactor.RuntimeCredential{}, ErrUnauthorized
	}
	if strings.TrimSpace(credential.ServiceKind) != serviceactor.KindApp || strings.TrimSpace(credential.CredentialKind) != serviceactor.RuntimeCredentialKindRunnerStreamToken || strings.TrimSpace(credential.TransportKind) != serviceactor.RuntimeTransportKindRunnerStream {
		return serviceactor.RuntimeCredential{}, ErrUnauthorized
	}
	if strings.TrimSpace(credential.Status) != serviceactor.RuntimeCredentialStatusActive {
		return serviceactor.RuntimeCredential{}, ErrUnauthorized
	}
	if credential.ExpiresAt != nil && !credential.ExpiresAt.After(time.Now().UTC()) {
		return serviceactor.RuntimeCredential{}, ErrUnauthorized
	}
	summary, err := store.requireAppRunnerInstallation(ctx, credential.WorkspaceID, credential.ServiceInstallationID)
	if err != nil {
		return serviceactor.RuntimeCredential{}, err
	}
	if strings.TrimSpace(summary.Definition.ID) != strings.TrimSpace(credential.ServiceDefinitionID) || strings.TrimSpace(summary.Member.ID) != strings.TrimSpace(credential.ServiceMemberID) {
		return serviceactor.RuntimeCredential{}, ErrUnauthorized
	}
	return credential, nil
}

func (store *Store) authenticateAppRunnerCredential(ctx context.Context, bearerToken string) (serviceactor.RuntimeCredential, error) {
	credential, err := store.serviceActorStore().AuthenticateRuntimeCredential(ctx, strings.TrimSpace(bearerToken))
	if err != nil {
		if errors.Is(err, serviceactor.ErrNotFound) || errors.Is(err, serviceactor.ErrBadRequest) {
			return serviceactor.RuntimeCredential{}, ErrUnauthorized
		}
		return serviceactor.RuntimeCredential{}, err
	}
	return store.ValidateAppRunnerCredential(ctx, credential)
}

func (store *Store) requireActiveAppRuntimeInstallation(ctx context.Context, workspaceID string, installationID string, requiredScopes []string) (serviceactor.InstallationSummary, error) {
	summary, err := store.serviceActorStore().LoadInstallationSummaryByKind(ctx, strings.TrimSpace(workspaceID), strings.TrimSpace(installationID), serviceactor.KindApp)
	if err != nil {
		return serviceactor.InstallationSummary{}, err
	}
	if strings.TrimSpace(summary.Installation.Status) != serviceactor.StatusActive || strings.TrimSpace(summary.Member.Status) != serviceactor.StatusActive {
		return serviceactor.InstallationSummary{}, badRequest("App installation is not active")
	}
	if _, err := requirePinnedRuntimeCapability(summary.Installation); err != nil {
		return serviceactor.InstallationSummary{}, err
	}
	if err := requireAppInstallationScopes(ctx, store.db, summary.Installation.ID, appRuntimeRequiredScopes(requiredScopes...)); err != nil {
		return serviceactor.InstallationSummary{}, err
	}
	return summary, nil
}

func (store *Store) requireAppRunnerInstallation(ctx context.Context, workspaceID string, installationID string) (serviceactor.InstallationSummary, error) {
	summary, err := store.requireActiveAppRuntimeInstallation(ctx, workspaceID, installationID, nil)
	if err != nil {
		return serviceactor.InstallationSummary{}, err
	}
	if err := store.requireAppRuntimeGrantEligibility(ctx, summary.Installation); err != nil {
		return serviceactor.InstallationSummary{}, err
	}
	enabled, err := pinnedAppTransportEnabled(summary.Installation, serviceactor.RuntimeTransportKindRunnerStream)
	if err != nil {
		return serviceactor.InstallationSummary{}, err
	}
	if !enabled {
		return serviceactor.InstallationSummary{}, badRequest("App runner stream transport is not enabled")
	}
	return summary, nil
}

func appRuntimeRequiredScopes(requiredScopes ...string) []string {
	seen := map[string]bool{}
	scopes := make([]string, 0, len(requiredScopes))
	for _, scope := range requiredScopes {
		scope = strings.TrimSpace(scope)
		if scope == "" || seen[scope] {
			continue
		}
		seen[scope] = true
		scopes = append(scopes, scope)
	}
	return scopes
}

func (store *Store) requireAppRuntimeGrantEligibility(ctx context.Context, installation serviceactor.Installation) error {
	capability, err := requirePinnedRuntimeCapability(installation)
	if err != nil {
		return err
	}
	scopes, err := store.serviceActorStore().ListActiveGrantScopes(ctx, strings.TrimSpace(installation.ID))
	if err != nil {
		return err
	}
	if len(appRuntimeSubscriptionKinds(capability, scopes)) > 0 || appRuntimeNotificationWriterEligible(capability, scopes) {
		return nil
	}
	return forbidden("App installation has no enabled runtime grant")
}

// AppRunnerClaimSubscriptionKinds returns the grant-authorized delivery kinds
// for one active runner installation.
func (store *Store) AppRunnerClaimSubscriptionKinds(ctx context.Context, workspaceID string, installationID string) ([]string, error) {
	summary, err := store.requireActiveAppRuntimeInstallation(ctx, workspaceID, installationID, nil)
	if err != nil {
		return nil, err
	}
	capability, err := requirePinnedRuntimeCapability(summary.Installation)
	if err != nil {
		return nil, err
	}
	scopes, err := store.serviceActorStore().ListActiveGrantScopes(ctx, strings.TrimSpace(installationID))
	if err != nil {
		return nil, err
	}
	return appRuntimeSubscriptionKinds(capability, scopes), nil
}

func appRuntimeSubscriptionKinds(capability registry.StoredAppRuntimeCapability, scopes []string) []string {
	granted := make(map[string]struct{}, len(scopes))
	for _, scope := range scopes {
		granted[strings.TrimSpace(scope)] = struct{}{}
	}
	has := func(scope string) bool {
		_, ok := granted[scope]
		return ok
	}
	kinds := make([]string, 0, 2)
	if pinnedRuntimeCapabilityDeclaresEvent(capability, registry.AppEventMessageMention) && has(AppMentionScopeRead) && has(AppRuntimeScopeMessageWrite) {
		kinds = append(kinds, serviceactor.SubscriptionKindAppMention)
	}
	if pinnedRuntimeCapabilityDeclaresEvent(capability, registry.AppEventNotificationAction) && has(AppNotificationActionScopeRead) {
		kinds = append(kinds, serviceactor.SubscriptionKindAppNotificationAction)
	}
	return kinds
}

func appRuntimeNotificationWriterEligible(capability registry.StoredAppRuntimeCapability, scopes []string) bool {
	if !capability.Notifications.Supported {
		return false
	}
	granted := make(map[string]struct{}, len(scopes))
	for _, scope := range scopes {
		granted[strings.TrimSpace(scope)] = struct{}{}
	}
	_, writeNotifications := granted[AppNotificationScopeWrite]
	_, writeMessages := granted[AppRuntimeScopeMessageWrite]
	return writeNotifications && writeMessages
}

func (store *Store) loadAppRunnerCredentialForUserMutation(ctx context.Context, workspaceID string, credentialID string) (serviceactor.RuntimeCredential, error) {
	credential, err := store.serviceActorStore().LoadRuntimeCredential(ctx, strings.TrimSpace(workspaceID), strings.TrimSpace(credentialID))
	if err != nil {
		return serviceactor.RuntimeCredential{}, err
	}
	if strings.TrimSpace(credential.ServiceKind) != serviceactor.KindApp || strings.TrimSpace(credential.CredentialKind) != serviceactor.RuntimeCredentialKindRunnerStreamToken || strings.TrimSpace(credential.TransportKind) != serviceactor.RuntimeTransportKindRunnerStream {
		return serviceactor.RuntimeCredential{}, storeerr.ErrNotFound
	}
	if _, err := store.requireAppRunnerInstallation(ctx, workspaceID, credential.ServiceInstallationID); err != nil {
		return serviceactor.RuntimeCredential{}, err
	}
	return credential, nil
}

func scanAppRunnerRuntimeCredential(row coredb.Row) (serviceactor.RuntimeCredential, error) {
	var credential serviceactor.RuntimeCredential
	var lastUsedAt, disabledAt, expiresAt sql.NullTime
	var metadata []byte
	if err := row.Scan(
		&credential.ID,
		&credential.WorkspaceID,
		&credential.ServiceDefinitionID,
		&credential.ServiceInstallationID,
		&credential.ServiceMemberID,
		&credential.ServiceKind,
		&credential.CredentialKind,
		&credential.TransportKind,
		&credential.Name,
		&credential.TokenHash,
		&credential.TokenHint,
		&credential.Status,
		&credential.CreatedByMemberID,
		&credential.CreatedAt,
		&credential.UpdatedAt,
		&lastUsedAt,
		&disabledAt,
		&expiresAt,
		&metadata,
	); err != nil {
		return serviceactor.RuntimeCredential{}, fmt.Errorf("scan App runner credential: %w", err)
	}
	credential.ID = strings.TrimSpace(credential.ID)
	credential.WorkspaceID = strings.TrimSpace(credential.WorkspaceID)
	credential.ServiceDefinitionID = strings.TrimSpace(credential.ServiceDefinitionID)
	credential.ServiceInstallationID = strings.TrimSpace(credential.ServiceInstallationID)
	credential.ServiceMemberID = strings.TrimSpace(credential.ServiceMemberID)
	credential.ServiceKind = strings.TrimSpace(credential.ServiceKind)
	credential.CredentialKind = strings.TrimSpace(credential.CredentialKind)
	credential.TransportKind = strings.TrimSpace(credential.TransportKind)
	credential.Status = strings.TrimSpace(credential.Status)
	credential.LastUsedAt = ptrFromSQLTime(lastUsedAt)
	credential.DisabledAt = ptrFromSQLTime(disabledAt)
	credential.ExpiresAt = ptrFromSQLTime(expiresAt)
	credential.Metadata = metadata
	return credential, nil
}

func appRunnerCredentialName(name string) string {
	trimmed := strings.TrimSpace(name)
	if trimmed == "" {
		return defaultAppRunnerCredentialName
	}
	return trimmed
}

func appRunnerCredentialMetadata(input map[string]any) ([]byte, error) {
	metadata := firstNonNilMap(input)
	metadata["transport"] = serviceactor.RuntimeTransportKindRunnerStream
	metadata["capability"] = serviceactor.CapabilityApp
	return model.MarshalJSONObject("app runner credential metadata", metadata)
}

func firstNonNilMap(input map[string]any) map[string]any {
	if input == nil {
		return map[string]any{}
	}
	out := make(map[string]any, len(input))
	for key, value := range input {
		out[strings.TrimSpace(key)] = value
	}
	return out
}

func appRunnerCredentialView(credential serviceactor.RuntimeCredential) AppRunnerCredentialView {
	return AppRunnerCredentialView{
		ID:                    credential.ID,
		WorkspaceID:           credential.WorkspaceID,
		ServiceDefinitionID:   credential.ServiceDefinitionID,
		ServiceInstallationID: credential.ServiceInstallationID,
		ServiceMemberID:       credential.ServiceMemberID,
		CredentialKind:        credential.CredentialKind,
		TransportKind:         credential.TransportKind,
		Name:                  credential.Name,
		TokenHint:             credential.TokenHint,
		Status:                credential.Status,
		CreatedByMemberID:     credential.CreatedByMemberID,
		CreatedAt:             credential.CreatedAt,
		UpdatedAt:             credential.UpdatedAt,
		LastUsedAt:            credential.LastUsedAt,
		DisabledAt:            credential.DisabledAt,
		ExpiresAt:             credential.ExpiresAt,
		Metadata:              decodeJSONObjectMap(credential.Metadata),
	}
}
