package app

import (
	"context"
	"encoding/json"
	"fmt"
	"strings"
	"time"

	"github.com/tidbcloud/AgentTeam/internal/coredb"
	"github.com/tidbcloud/AgentTeam/internal/store/message"
	"github.com/tidbcloud/AgentTeam/internal/store/model"
	"github.com/tidbcloud/AgentTeam/internal/store/serviceactor"
	"github.com/tidbcloud/AgentTeam/internal/store/storeerr"
)

const appNotificationActionActorLimit = 60

const (
	appNotificationMaxActionInputBytes  = 32 * 1024
	appNotificationMaxActionInputDepth  = 20
	appNotificationMaxActionInputValues = 1000
)

// InvokeAppNotificationActionForUser authenticates user membership and invokes
// one card action as the resulting Human workspace member.
func (store *Store) InvokeAppNotificationActionForUser(ctx context.Context, userID, workspaceID string, command AppNotificationActionCommand) (AppNotificationActionResult, error) {
	membership, err := store.loadAuthorizedHumanWorkspaceMember(ctx, store.db, userID, workspaceID, workspaceAccessAction)
	if err != nil {
		return AppNotificationActionResult{}, err
	}
	command.WorkspaceID = workspaceID
	command.ActorMemberID = membership.Member.ID
	return store.InvokeAppNotificationAction(ctx, command)
}

// InvokeAppNotificationAction atomically validates current Human authority,
// creates the invocation receipt, and reserves reliable Service Event Delivery.
func (store *Store) InvokeAppNotificationAction(ctx context.Context, command AppNotificationActionCommand) (AppNotificationActionResult, error) {
	now := time.Now().UTC()
	command.WorkspaceID = strings.TrimSpace(command.WorkspaceID)
	command.MessageID = strings.TrimSpace(command.MessageID)
	command.ActionKey = strings.TrimSpace(command.ActionKey)
	command.ActorMemberID = strings.TrimSpace(command.ActorMemberID)
	command.ClientActionID = strings.TrimSpace(command.ClientActionID)
	command.CorrelationID = strings.TrimSpace(command.CorrelationID)
	if command.WorkspaceID == "" || command.MessageID == "" || command.ActionKey == "" || command.ActorMemberID == "" || command.ClientActionID == "" || command.CardRevision <= 0 {
		return AppNotificationActionResult{}, badRequest("App notification action workspace, message, action, actor, card revision, and client action id are required")
	}
	if len(command.ClientActionID) > 128 {
		return AppNotificationActionResult{}, badRequest("App notification client action id is too long")
	}
	if len(command.WorkspaceID) > 255 || len(command.MessageID) > 255 || len(command.ActionKey) > 128 || len(command.ActorMemberID) > 255 || len(command.CorrelationID) > 255 {
		return AppNotificationActionResult{}, badRequest("App notification action identifier is too long")
	}
	if command.Input == nil {
		command.Input = map[string]any{}
	}
	inputJSON, err := appNotificationDeterministicJSON(command.Input)
	if err != nil {
		return AppNotificationActionResult{}, err
	}
	if len(inputJSON) > appNotificationMaxActionInputBytes {
		return AppNotificationActionResult{}, badRequest("App notification action input is too large")
	}
	valueCount := 0
	if err := validateAppNotificationActionInputComplexity(command.Input, 0, &valueCount); err != nil {
		return AppNotificationActionResult{}, err
	}
	inputDigest, err := appNotificationDigest(command.Input)
	if err != nil {
		return AppNotificationActionResult{}, err
	}
	requestDigest, err := appNotificationDigest(struct {
		ActionKey string         `json:"actionKey"`
		Input     map[string]any `json:"input"`
	}{command.ActionKey, command.Input})
	if err != nil {
		return AppNotificationActionResult{}, err
	}
	if command.CorrelationID == "" {
		command.CorrelationID = deterministicAppNotificationActionID(command.MessageID, command.CardRevision, command.ActorMemberID, command.ClientActionID)
	}
	actionID := deterministicAppNotificationActionID(command.MessageID, command.CardRevision, command.ActorMemberID, command.ClientActionID)

	var result AppNotificationActionResult
	err = coredb.WithTx(ctx, store.db, func(tx coredb.Tx) error {
		decision, err := message.NewScopeAPIServiceForQueryer(tx).AuthorizeSidecarRead(ctx, message.SidecarReadRequest{
			WorkspaceID: command.WorkspaceID, MessageID: command.MessageID, ViewerMemberID: command.ActorMemberID,
			ViewerType: message.ScopeActorHuman, Kind: message.SidecarKindAppNotificationCard,
		})
		if err != nil {
			return err
		}
		if !decision.Allowed {
			return ErrNotFound
		}
		// This is an optimistic fast path. Do not lock a missing client key here:
		// the notification row below is the ordering fence, and a pessimistic
		// missing-key lock can serialize identical requests before both can reach
		// that fence. The post-fence lookup is the authoritative concurrency read.
		if existing, found, err := store.FindAppNotificationActionInTx(ctx, tx, command.MessageID, command.CardRevision, command.ActorMemberID, command.ClientActionID, false); err != nil {
			return err
		} else if found {
			if existing.RequestDigest != requestDigest || existing.InputDigest != inputDigest || existing.ActionKey != command.ActionKey {
				return conflict("App notification action id was already used with another action or input")
			}
			notification, err := store.LoadAppNotificationInTx(ctx, tx, command.WorkspaceID, existing.AppNotificationID, false)
			if err != nil {
				return err
			}
			if notification.MessageID != command.MessageID || notification.ScopeID != decision.ScopeID {
				return ErrNotFound
			}
			result = appNotificationActionResult(existing, true, notification.Revision)
			return nil
		}
		cardSnapshot, err := store.LoadAppNotificationCardInTx(ctx, tx, command.MessageID, false)
		if err != nil {
			return err
		}
		notification, err := store.LoadAppNotificationInTx(ctx, tx, command.WorkspaceID, cardSnapshot.AppNotificationID, true)
		if err != nil {
			return err
		}
		// The notification row is the serialization fence for accepting card
		// actions. A concurrent request can miss the durable action receipt
		// before either request acquires this lock; re-read the receipt after the
		// lock so an identical loser observes the committed winner instead of
		// continuing into admission/reservation and reporting a conflict.
		if existing, found, err := store.FindAppNotificationActionInTx(ctx, tx, command.MessageID, command.CardRevision, command.ActorMemberID, command.ClientActionID, true); err != nil {
			return err
		} else if found {
			if existing.RequestDigest != requestDigest || existing.InputDigest != inputDigest || existing.ActionKey != command.ActionKey {
				return conflict("App notification action id was already used with another action or input")
			}
			if existing.AppNotificationID != notification.ID || notification.MessageID != command.MessageID || notification.ScopeID != decision.ScopeID {
				return ErrNotFound
			}
			result = appNotificationActionResult(existing, true, notification.Revision)
			return nil
		}
		card, err := store.LoadAppNotificationCardInTx(ctx, tx, command.MessageID, true)
		if err != nil {
			return err
		}
		if card.AppNotificationID != notification.ID {
			return conflict("App notification card changed concurrently")
		}
		if notification.MessageID != command.MessageID || notification.ScopeID != decision.ScopeID || notification.Status != AppNotificationStatusActive || card.State != AppNotificationCardStateActive {
			return conflict("App notification card is no longer active")
		}
		if card.CardRevision != command.CardRevision {
			return conflict("App notification card revision changed")
		}
		if (notification.ExpiresAt != nil && !notification.ExpiresAt.After(now)) || (card.ExpiresAt != nil && !card.ExpiresAt.After(now)) {
			return conflict("App notification card is expired")
		}
		action, err := loadAppNotificationCardAction(card.ActionSchemaJSON, command.ActionKey)
		if err != nil {
			return err
		}
		if action.Type != "callback" {
			return badRequest("open-url App notification actions are handled by the client")
		}
		if action.ExpiresAt != nil && !action.ExpiresAt.After(now) {
			return conflict("App notification action is expired")
		}
		if err := validateAppNotificationActionInput(action.InputSchema, command.Input); err != nil {
			return err
		}
		if err := store.authorizeAppNotificationActionActorInTx(ctx, tx, notification, action, command, now); err != nil {
			return err
		}
		if err := requireAppInstallationScopes(ctx, tx, notification.ServiceInstallationID, []string{AppRuntimeScopeMessageWrite, AppNotificationScopeWrite, AppNotificationActionScopeRead}); err != nil {
			return err
		}
		if err := requireCurrentAppNotificationBinding(ctx, tx, notification); err != nil {
			return err
		}
		if err := enforceAppNotificationActionAdmission(ctx, tx, command, now); err != nil {
			return err
		}
		subscription, err := store.loadActiveAppNotificationActionSubscriptionInTx(ctx, tx, notification)
		if err != nil {
			return err
		}
		actorProfile, err := loadAppNotificationHumanActorProfile(ctx, tx, command.WorkspaceID, command.ActorMemberID)
		if err != nil {
			return err
		}
		payload, err := appNotificationActionEnvelope(notification, card, actionID, command, actorProfile)
		if err != nil {
			return err
		}
		delivery, _, err := store.serviceActorStore().InTx(tx).ReserveEventDelivery(ctx, serviceactor.EventDeliveryReserve{
			WorkspaceID: notification.WorkspaceID, EventSubscriptionID: subscription.ID,
			EventType: appNotificationEventAction, EventAction: command.ActionKey,
			EventID:      actionID,
			EventKey:     strings.Join([]string{notification.ID, fmt.Sprintf("%d", command.CardRevision), command.ActionKey, command.ActorMemberID, command.ClientActionID}, ":"),
			EventPayload: payload, ExternalResourceType: appNotificationExternalResource,
			ExternalResourceKey: notification.ID, ExternalResourceRef: notification.ID,
			IdempotencyKey: "app-notification-action:" + actionID, Now: now,
		})
		if err != nil {
			return err
		}
		if err := store.InsertAppNotificationActionInTx(ctx, tx, AppNotificationActionRecord{
			ID: actionID, WorkspaceID: notification.WorkspaceID, AppNotificationID: notification.ID,
			MessageID: notification.MessageID, CardRevision: command.CardRevision, ActionKey: command.ActionKey,
			ActorMemberID: command.ActorMemberID, ClientActionID: command.ClientActionID,
			RequestDigest: requestDigest, InputDigest: inputDigest, InputJSON: inputJSON,
			ServiceEventDeliveryID: delivery.ID, State: AppNotificationActionStateAccepted,
			CorrelationID: command.CorrelationID, AcceptedAt: now, CreatedAt: now, UpdatedAt: now,
		}); err != nil {
			if existing, found, loadErr := store.FindAppNotificationActionInTx(ctx, tx, command.MessageID, command.CardRevision, command.ActorMemberID, command.ClientActionID, true); loadErr == nil && found {
				if existing.RequestDigest != requestDigest || existing.InputDigest != inputDigest || existing.ActionKey != command.ActionKey {
					return conflict("App notification action id was already used with another action or input")
				}
				result = appNotificationActionResult(existing, true, notification.Revision)
				return nil
			}
			return err
		}
		result = AppNotificationActionResult{
			Accepted: true, ActionInvocationID: actionID, ServiceEventDeliveryID: delivery.ID,
			NotificationID: notification.ID, MessageID: notification.MessageID,
			CardRevision: card.CardRevision, NotificationRevision: notification.Revision,
			State: AppNotificationActionStateAccepted, CorrelationID: command.CorrelationID,
		}
		return nil
	})
	if err != nil {
		return AppNotificationActionResult{}, err
	}
	return result, nil
}

func validateAppNotificationActionInputComplexity(value any, depth int, valueCount *int) error {
	if depth > appNotificationMaxActionInputDepth {
		return badRequest("App notification action input is too deeply nested")
	}
	(*valueCount)++
	if *valueCount > appNotificationMaxActionInputValues {
		return badRequest("App notification action input has too many values")
	}
	switch typed := value.(type) {
	case map[string]any:
		for key, child := range typed {
			if strings.TrimSpace(key) == "" || len(key) > 255 {
				return badRequest("App notification action input contains an invalid key")
			}
			if err := validateAppNotificationActionInputComplexity(child, depth+1, valueCount); err != nil {
				return err
			}
		}
	case []any:
		for _, child := range typed {
			if err := validateAppNotificationActionInputComplexity(child, depth+1, valueCount); err != nil {
				return err
			}
		}
	}
	return nil
}

func loadAppNotificationCardAction(payload []byte, actionKey string) (appNotificationCardAction, error) {
	var schema struct {
		Actions []appNotificationCardAction `json:"actions"`
	}
	if err := json.Unmarshal(payload, &schema); err != nil {
		return appNotificationCardAction{}, fmt.Errorf("decode App notification action schema: %w", err)
	}
	action, found := appNotificationActionByKey(appNotificationCardDocument{Actions: schema.Actions}, actionKey)
	if !found {
		return appNotificationCardAction{}, ErrNotFound
	}
	return action, nil
}

func (store *Store) authorizeAppNotificationActionActorInTx(ctx context.Context, queryer coredb.QueryRower, notification AppNotificationRecord, action appNotificationCardAction, command AppNotificationActionCommand, now time.Time) error {
	if notification.TargetKind == AppNotificationTargetHumanDM {
		if notification.RecipientMemberID == nil || strings.TrimSpace(*notification.RecipientMemberID) != command.ActorMemberID {
			return forbidden("App notification action actor is not the Human recipient")
		}
		basis := appNotificationRecipientBasisFromRecord(notification)
		resolver, ok := store.appNotificationRecipientResolver(basis.Kind)
		if !ok {
			return forbidden("App notification action recipient resolver is not registered")
		}
		if err := resolver.AuthorizeAppNotificationAction(ctx, queryer, AppNotificationActionAuthorizationRequest{
			WorkspaceID: notification.WorkspaceID, Basis: basis,
			BusinessObject: appNotificationBusinessObjectFromRecord(notification), ActorMemberID: command.ActorMemberID,
			ActionKey: command.ActionKey, CardRevision: command.CardRevision, Input: command.Input, Now: now,
		}); err != nil {
			return err
		}
	}
	if len(action.ActorPolicy) == 0 {
		return nil
	}
	var policy struct {
		Kind      string   `json:"kind"`
		MemberIDs []string `json:"memberIds"`
	}
	if err := json.Unmarshal(action.ActorPolicy, &policy); err != nil {
		return badRequest("App notification action actor policy is invalid")
	}
	switch strings.TrimSpace(policy.Kind) {
	case "", "readable_human":
		return nil
	case "members":
		if !stringSliceContains(policy.MemberIDs, command.ActorMemberID) {
			return forbidden("App notification action actor policy denied the Human")
		}
		return nil
	default:
		return forbidden("App notification action actor policy is not supported")
	}
}

func requireCurrentAppNotificationBinding(ctx context.Context, queryer coredb.QueryRower, notification AppNotificationRecord) error {
	var marker int
	if err := queryer.QueryRow(ctx, `
SELECT 1 FROM service_target_bindings
WHERE id = $1 AND workspace_id = $2 AND service_installation_id = $3
  AND service_member_id = $4 AND delivery_scope_id = $5
  AND status = 'active' AND removed_at IS NULL
LIMIT 1
`, notification.ServiceTargetBindingID, notification.WorkspaceID, notification.ServiceInstallationID, notification.ServiceMemberID, notification.ScopeID).Scan(&marker); err != nil {
		if coredb.IsNotFound(err) {
			return forbidden("App notification target binding is no longer active")
		}
		return fmt.Errorf("load App notification action target binding: %w", err)
	}
	return nil
}

func (store *Store) loadActiveAppNotificationActionSubscriptionInTx(ctx context.Context, tx coredb.Tx, notification AppNotificationRecord) (serviceactor.EventSubscription, error) {
	subscriptions, err := store.serviceActorStore().InTx(tx).ListEventSubscriptionsForUpdate(ctx, serviceactor.EventSubscriptionListQuery{
		WorkspaceID: notification.WorkspaceID, ServiceInstallationID: notification.ServiceInstallationID,
		SubscriptionKind:    serviceactor.SubscriptionKindAppNotificationAction,
		ExternalResourceKey: notification.ID, Status: serviceactor.EventSubscriptionStatusActive, Limit: 10,
	})
	if err != nil {
		return serviceactor.EventSubscription{}, err
	}
	for _, subscription := range subscriptions {
		if subscription.ServiceMemberID == notification.ServiceMemberID && subscription.ServiceTargetBindingID == notification.ServiceTargetBindingID && subscription.RemovedAt == nil {
			return subscription, nil
		}
	}
	return serviceactor.EventSubscription{}, forbidden("App notification action subscription is not active")
}

func enforceAppNotificationActionAdmission(ctx context.Context, queryer coredb.QueryRower, command AppNotificationActionCommand, now time.Time) error {
	var count int
	if err := queryer.QueryRow(ctx, `
SELECT COUNT(*) FROM service_notification_actions
WHERE workspace_id = $1 AND message_id = $2 AND actor_member_id = $3 AND action_key = $4
  AND created_at >= $5
`, command.WorkspaceID, command.MessageID, command.ActorMemberID, command.ActionKey, now.Add(-time.Minute)).Scan(&count); err != nil {
		return fmt.Errorf("count App notification action admission: %w", err)
	}
	if count >= appNotificationActionActorLimit {
		return storeerr.Conflict("app_notification_action_rate_limited")
	}
	return nil
}

type appNotificationHumanActorProfile struct {
	MemberID    string `json:"memberId"`
	UserID      string `json:"userId"`
	DisplayName string `json:"displayName"`
	AvatarURL   string `json:"avatarUrl,omitempty"`
}

func loadAppNotificationHumanActorProfile(ctx context.Context, queryer coredb.QueryRower, workspaceID, memberID string) (appNotificationHumanActorProfile, error) {
	var profile appNotificationHumanActorProfile
	if err := queryer.QueryRow(ctx, `
SELECT member.id, COALESCE(member.user_id, ''),
       COALESCE(NULLIF(member.display_name, ''), NULLIF(user.display_name, ''), NULLIF(user.name, ''), member.id),
       COALESCE(user.avatar_url, '')
FROM members member
LEFT JOIN users user ON user.id = member.user_id
WHERE member.workspace_id = $1 AND member.id = $2
  AND member.type = 'human' AND member.status = 'active'
LIMIT 1
`, strings.TrimSpace(workspaceID), strings.TrimSpace(memberID)).Scan(&profile.MemberID, &profile.UserID, &profile.DisplayName, &profile.AvatarURL); err != nil {
		if coredb.IsNotFound(err) {
			return appNotificationHumanActorProfile{}, forbidden("App notification action Human is not active")
		}
		return appNotificationHumanActorProfile{}, fmt.Errorf("load App notification action actor profile: %w", err)
	}
	return profile, nil
}

func appNotificationActionEnvelope(notification AppNotificationRecord, card AppNotificationCardRecord, actionID string, command AppNotificationActionCommand, actor appNotificationHumanActorProfile) ([]byte, error) {
	payload := map[string]any{
		"eventType": appNotificationEventAction,
		"app": map[string]any{
			"serviceDefinitionId":   notification.ServiceDefinitionID,
			"serviceInstallationId": notification.ServiceInstallationID,
			"serviceMemberId":       notification.ServiceMemberID,
		},
		"notification": map[string]any{
			"id": notification.ID, "revision": notification.Revision,
			"messageId": notification.MessageID, "scopeId": notification.ScopeID,
			"businessObject": appNotificationBusinessObjectFromRecord(notification),
		},
		"action": map[string]any{
			"invocationId": actionID, "actionKey": command.ActionKey, "input": command.Input,
			"cardRevision": card.CardRevision, "clientActionId": command.ClientActionID,
		},
		"actor":         actor,
		"correlationId": command.CorrelationID,
	}
	return model.MarshalJSONObject("App notification action event", payload)
}

func appNotificationActionResult(record AppNotificationActionRecord, already bool, notificationRevision int64) AppNotificationActionResult {
	return AppNotificationActionResult{
		Accepted: true, AlreadyAccepted: already, ActionInvocationID: record.ID,
		ServiceEventDeliveryID: record.ServiceEventDeliveryID, NotificationID: record.AppNotificationID,
		MessageID: record.MessageID, CardRevision: record.CardRevision,
		NotificationRevision: notificationRevision, State: record.State, CorrelationID: record.CorrelationID,
	}
}

func (store *Store) completeAppNotificationActionForDeliveryInTx(ctx context.Context, tx coredb.Tx, delivery serviceactor.EventDelivery, nextState, resultCode string, result map[string]any, now time.Time) error {
	action, err := store.LoadAppNotificationActionInTx(ctx, tx, delivery.WorkspaceID, delivery.EventID, true)
	if err != nil {
		return err
	}
	if action.ServiceEventDeliveryID != delivery.ID || action.AppNotificationID != delivery.ExternalResourceKey {
		return conflict("App notification action delivery does not match its invocation")
	}
	if action.State == nextState && action.CompletedAt != nil {
		return nil
	}
	resultDigest := ""
	if len(result) > 0 {
		resultDigest, err = appNotificationDigest(result)
		if err != nil {
			return err
		}
	}
	for _, expected := range []string{AppNotificationActionStateDelivered, AppNotificationActionStateAccepted} {
		changed, err := store.CompleteAppNotificationActionInTx(ctx, tx, delivery.WorkspaceID, action.ID, expected, nextState, resultCode, resultDigest, now, now)
		if err != nil {
			return err
		}
		if changed {
			return nil
		}
	}
	return conflict("App notification action invocation changed concurrently")
}
