package app

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

	"github.com/tidbcloud/AgentTeam/internal/core/domain"
	"github.com/tidbcloud/AgentTeam/internal/coredb"
	"github.com/tidbcloud/AgentTeam/internal/store/access"
	"github.com/tidbcloud/AgentTeam/internal/store/coordinator"
	"github.com/tidbcloud/AgentTeam/internal/store/message"
	"github.com/tidbcloud/AgentTeam/internal/store/model"
	"github.com/tidbcloud/AgentTeam/internal/store/serviceaccount"
)

const (
	serviceAccountRecallAuditType = "service_account.message.recalled"
	serviceAccountRecallBasis     = "service_account_manager"
	workspaceRoleRecallBasis      = "workspace_role"
	platformAccountRecallBasis    = "server_owner"
)

type serviceAccountRecallAuthority struct {
	AccountID                string
	PlatformServiceAccountID string
	PrincipalID              string
	AuthorMemberID           string
	Name                     string
	ManagementKind           string
	ManagerAssignmentID      string
	ManagerVersion           int64
	WorkspaceRole            string
}

func (store *Store) RecallMessageForUser(ctx context.Context, user model.User, workspaceID string, channelID string, messageID string, _ message.HumanMessageRecallOptions) (message.AppMessageRecallResult, error) {
	trimmedMessageID := strings.TrimSpace(messageID)
	if trimmedMessageID == "" {
		return message.AppMessageRecallResult{}, badRequest("messageId is required")
	}

	var result message.AppMessageRecallResult
	var viewerMemberID string
	if err := coordinator.New(store.db).Run(ctx, "recall app message workflow", func(tx coredb.Tx) error {
		if ok, err := isActiveTaskScope(ctx, tx, workspaceID, channelID); err != nil {
			return err
		} else if ok {
			return badRequest("message recall is not supported for task scopes")
		}

		membership, target, serviceAccountTarget, err := loadAuthorizedHumanServiceAccountMessageTarget(ctx, tx, user.ID, workspaceID, channelID, channelReadAction)
		if err != nil {
			return err
		}
		if !serviceAccountTarget {
			membership, target, err = store.resolveAuthorizedHumanMessageTargetWithQueryer(ctx, tx, user.ID, workspaceID, channelID, channelPostAction, dmPostAction, threadPostAction)
			if err != nil {
				return err
			}
		}
		viewerMemberID = strings.TrimSpace(membership.Member.ID)
		scopeID := message.ScopeIDForThreadAccess(target)
		if scopeID == "" {
			return badRequest("scope is required")
		}

		recalled, err := store.recallScopeMessageForMemberTx(ctx, tx, user, workspaceID, target, scopeID, trimmedMessageID, viewerMemberID, strings.TrimSpace(membership.Member.DisplayName))
		if err != nil {
			return err
		}
		result = recalled
		return nil
	}); err != nil {
		return message.AppMessageRecallResult{}, err
	}
	// Post-commit best-effort derivative cleanup.
	store.messageStore().CleanupAttachmentDerivatives(ctx, result.DeletedAttachmentDerivatives)
	if result.Message.ID == "" {
		return message.AppMessageRecallResult{}, notFound("message not found")
	}
	messages := []message.AppMessage{result.Message}
	if err := store.attachAppMessageReadDecorations(ctx, workspaceID, viewerMemberID, messages); err != nil {
		return message.AppMessageRecallResult{}, err
	}
	result.Message = messages[0]
	return result, nil
}

func (store *Store) recallScopeMessageForMemberTx(ctx context.Context, tx coredb.Tx, user model.User, workspaceID string, target message.ThreadAccess, scopeID string, trimmedMessageID string, viewerMemberID string, viewerMemberDisplayName string) (message.AppMessageRecallResult, error) {
	repos := message.NewScopeRepositories(tx)
	record, err := repos.Messages.LoadByID(ctx, workspaceID, scopeID, trimmedMessageID)
	if err != nil {
		return message.AppMessageRecallResult{}, err
	}
	if strings.TrimSpace(record.MessageType) != message.MessageTypeServiceAccountPost {
		readCheck, err := message.NewScopeAPIServiceForQueryer(tx).CanReadMessage(ctx, message.ScopeAPIReadCheckRequest{
			WorkspaceID:    workspaceID,
			MessageID:      trimmedMessageID,
			ViewerMemberID: viewerMemberID,
			ViewerType:     message.ScopeActorHuman,
			RequestKind:    message.ScopeVisibilityRequestHistory,
		})
		if err != nil {
			return message.AppMessageRecallResult{}, err
		}
		if !readCheck.Allowed {
			return message.AppMessageRecallResult{}, notFound("message not found")
		}
	}
	var recallAuthority *serviceAccountRecallAuthority
	var authorProfile message.AuthorProfile
	var placeholder string
	if strings.TrimSpace(record.MessageType) == message.MessageTypeServiceAccountPost {
		authority, err := loadServiceAccountRecallAuthority(ctx, tx, user.ID, workspaceID, scopeID, viewerMemberID)
		if err != nil {
			return message.AppMessageRecallResult{}, err
		}
		if strings.TrimSpace(record.AuthorType) != string(message.ScopeActorService) || strings.TrimSpace(record.AuthorMemberID) != authority.AuthorMemberID {
			return message.AppMessageRecallResult{}, forbidden("service-account article author does not match the account")
		}
		recallAuthority = &authority
		authorProfile = message.AuthorProfile{
			ActorType:   message.AuthorActorTypeService,
			PrincipalID: authority.PrincipalID,
			MemberID:    authority.AuthorMemberID,
			TargetID:    "service-account:" + authority.AccountID,
			DisplayName: authority.Name,
			ServiceKind: message.AuthorServiceKindServiceAccount,
			ServiceSlug: authority.AccountID,
		}
		placeholder = authority.Name + " 撤回了一篇文章"
	} else {
		if strings.TrimSpace(record.AuthorType) != "human" || strings.TrimSpace(record.AuthorMemberID) != viewerMemberID {
			return message.AppMessageRecallResult{}, forbidden("only the original author can recall this message")
		}
		if !isRecallableUserMessageType(record.MessageType) {
			return message.AppMessageRecallResult{}, badRequest("only ordinary human messages can be recalled")
		}
		authorProfile = humanAuthorProfileFromUser(user, viewerMemberID, viewerMemberDisplayName)
		placeholder = recallPlaceholderForUser(user, viewerMemberID, viewerMemberDisplayName)
	}
	decodedMetadata, err := message.DecodeMessageMetadata(record.Metadata)
	if err != nil {
		return message.AppMessageRecallResult{}, err
	}
	if message.MessageMetadataIsRecalled(decodedMetadata) {
		return message.AppMessageRecallResult{
			Message:                appMessageFromEditedRecord(appChannelIDForTarget(target), target, authorProfile, record, decodedMetadata),
			Changed:                false,
			IsLatestVisibleMessage: false,
		}, nil
	}

	recalledAt := time.Now().UTC()
	publicRecalledByMemberID := viewerMemberID
	if recallAuthority != nil {
		// The public tombstone is authored by the service account. The Human
		// manager remains available only in the immutable audit record.
		publicRecalledByMemberID = recallAuthority.AuthorMemberID
	}
	recallMetadata := message.BuildMessageRecallMetadata(publicRecalledByMemberID, recalledAt.Format(time.RFC3339Nano), placeholder)
	encodedMetadata, normalizedMetadata, err := message.EncodeMessageMetadata(recallMetadata)
	if err != nil {
		return message.AppMessageRecallResult{}, err
	}
	recallRequest := message.ScopeRecallMessageRequest{
		WorkspaceID:        workspaceID,
		ScopeID:            scopeID,
		MessageID:          trimmedMessageID,
		RecalledByMemberID: publicRecalledByMemberID,
		PlaceholderContent: placeholder,
		Metadata:           encodedMetadata,
		RecalledAt:         recalledAt,
	}
	if recallAuthority != nil {
		recallRequest.ExpectedAuthorType = message.ScopeActorService
		recallRequest.ExpectedAuthorID = recallAuthority.AuthorMemberID
		recallRequest.ExpectedMessageType = message.MessageTypeServiceAccountPost
	}
	recalled, err := message.RecallScopeMessageTx(ctx, tx, recallRequest)
	if err != nil {
		if errors.Is(err, message.ErrNotFound) {
			return message.AppMessageRecallResult{}, notFound("message not found")
		}
		return message.AppMessageRecallResult{}, err
	}
	if err := scrubDurableIMRecallProjections(ctx, tx, workspaceID, scopeID, trimmedMessageID, publicRecalledByMemberID, recalledAt, placeholder); err != nil {
		return message.AppMessageRecallResult{}, err
	}
	if recallAuthority != nil {
		if err := store.insertServiceAccountRecallAudit(ctx, tx, workspaceID, viewerMemberID, trimmedMessageID, recalledAt, *recallAuthority); err != nil {
			return message.AppMessageRecallResult{}, err
		}
	}
	return message.AppMessageRecallResult{
		Message:                      appMessageFromEditedRecord(appChannelIDForTarget(target), target, authorProfile, recalled.Message, normalizedMetadata),
		Changed:                      true,
		IsLatestVisibleMessage:       recalled.IsLatestVisibleMessage,
		DeletedAttachmentDerivatives: recalled.DeletedAttachmentDerivatives,
	}, nil
}

func loadServiceAccountRecallAuthority(ctx context.Context, tx coredb.Tx, userID string, workspaceID string, scopeID string, viewerMemberID string) (serviceAccountRecallAuthority, error) {
	var authority serviceAccountRecallAuthority
	err := tx.QueryRow(ctx, `
SELECT account.id, COALESCE(account.platform_service_account_id, ''),
       account.principal_id, account.author_member_id, account.name,
       account.management_kind, COALESCE(manager.id, ''),
       COALESCE(manager.version, 0), COALESCE(viewer_member.workspace_role, '')
FROM service_accounts account
JOIN scopes scope
  ON scope.workspace_id = account.workspace_id
 AND scope.id = account.message_scope_id
 AND scope.type = 'service_account'
 AND scope.status = 'active'
 AND scope.archived_at IS NULL
JOIN members viewer_member
  ON viewer_member.workspace_id = account.workspace_id
 AND viewer_member.id = $3
 AND viewer_member.user_id = $4
 AND viewer_member.type = 'human'
 AND viewer_member.status = 'active'
JOIN users viewer_user ON viewer_user.id = viewer_member.user_id
LEFT JOIN service_account_managers manager
  ON manager.workspace_id = account.workspace_id
 AND manager.service_account_id = account.id
 AND manager.member_id = viewer_member.id
 AND manager.status = 'active'
 AND manager.ended_at IS NULL
WHERE account.workspace_id = $1
  AND account.message_scope_id = $2
  AND account.audience_state = 'ready'
  AND (
    (account.management_kind = 'workspace'
      AND (manager.id IS NOT NULL OR viewer_member.workspace_role IN ('owner', 'admin')))
    OR (account.management_kind = 'platform' AND viewer_user.is_admin = TRUE)
  )
LIMIT 1
FOR UPDATE
`, strings.TrimSpace(workspaceID), strings.TrimSpace(scopeID), strings.TrimSpace(viewerMemberID), strings.TrimSpace(userID)).Scan(
		&authority.AccountID,
		&authority.PlatformServiceAccountID,
		&authority.PrincipalID,
		&authority.AuthorMemberID,
		&authority.Name,
		&authority.ManagementKind,
		&authority.ManagerAssignmentID,
		&authority.ManagerVersion,
		&authority.WorkspaceRole,
	)
	if err != nil {
		if coredb.IsNotFound(err) {
			return serviceAccountRecallAuthority{}, forbidden("service-account moderation authority is required to recall this article")
		}
		return serviceAccountRecallAuthority{}, fmt.Errorf("resolve service-account recall authority: %w", err)
	}
	return authority, nil
}

func (store *Store) insertServiceAccountRecallAudit(ctx context.Context, tx coredb.Tx, workspaceID string, viewerMemberID string, messageID string, recalledAt time.Time, authority serviceAccountRecallAuthority) error {
	basisKind := serviceAccountRecallBasis
	basisRef := authority.ManagerAssignmentID
	if authority.ManagementKind == serviceaccount.ManagementKindWorkspace &&
		access.HasWorkspaceAdminRole(domain.WorkspaceRole(authority.WorkspaceRole)) {
		basisKind = workspaceRoleRecallBasis
		basisRef = authority.WorkspaceRole
	} else if authority.ManagementKind == serviceaccount.ManagementKindPlatform {
		basisKind = platformAccountRecallBasis
		basisRef = authority.PlatformServiceAccountID
	}
	metadata, err := json.Marshal(map[string]any{
		"managementKind":     authority.ManagementKind,
		"managerVersion":     authority.ManagerVersion,
		"recalledAt":         recalledAt.UTC().Format(time.RFC3339Nano),
		"recalledByMemberId": viewerMemberID,
	})
	if err != nil {
		return fmt.Errorf("encode service-account recall audit metadata: %w", err)
	}
	digest := sha256.Sum256([]byte("service-account-message-recall-audit:" + strings.TrimSpace(messageID)))
	return serviceaccount.New(store.db).InTx(tx).InsertAuditEvent(ctx, serviceaccount.AuditEvent{
		ID:                       "saaudit_" + hex.EncodeToString(digest[:16]),
		WorkspaceID:              strings.TrimSpace(workspaceID),
		ServiceAccountID:         authority.AccountID,
		PlatformServiceAccountID: authority.PlatformServiceAccountID,
		EventType:                serviceAccountRecallAuditType,
		SubjectType:              "message",
		SubjectID:                strings.TrimSpace(messageID),
		SubjectVersion:           1,
		ActorType:                message.AuthorActorTypeService,
		ActorID:                  authority.PrincipalID,
		CausedByActorType:        message.AuthorActorTypeHuman,
		CausedByActorID:          strings.TrimSpace(viewerMemberID),
		ExecutionActorType:       message.AuthorActorTypeHuman,
		ExecutionActorID:         strings.TrimSpace(viewerMemberID),
		PermissionBasisKind:      basisKind,
		PermissionBasisRef:       basisRef,
		CorrelationID:            strings.TrimSpace(messageID),
		Metadata:                 metadata,
		CreatedAt:                recalledAt.UTC(),
	})
}

func isRecallableUserMessageType(messageType string) bool {
	switch strings.TrimSpace(messageType) {
	case "", message.MessageTypeSticker:
		return true
	default:
		return false
	}
}

func recallPlaceholderForUser(user model.User, memberID string, memberDisplayName string) string {
	name := firstNonEmpty(memberDisplayName, user.DisplayName, user.Name, memberID)
	if name == "" {
		name = "用户"
	}
	return name + " 撤回了一条消息"
}

func scrubDurableIMRecallProjections(ctx context.Context, tx coredb.Tx, workspaceID string, scopeID string, messageID string, recalledByMemberID string, recalledAt time.Time, placeholder string) error {
	recallMetadataSQL := `
JSON_OBJECT(
  'messageRecall',
  JSON_OBJECT(
    'status', $7,
    'recalledByMemberId', $4,
    'recalledAt', $5,
    'placeholder', $6
  )
)`
	if _, err := tx.Exec(ctx, `
UPDATE im_stream_events
SET payload_json = JSON_SET(
  JSON_REMOVE(
    payload_json,
    '$.mention',
    '$.mentionRecipients',
    '$.message.resolvedMentions',
    '$.message.attachments',
    '$.message.reactions',
    '$.message.source',
    '$.message.deliverySummaryPreview'
  ),
  '$.content', $6,
  '$.metadata', `+recallMetadataSQL+`,
  '$.attachments', JSON_ARRAY(),
  '$.reactions', JSON_ARRAY(),
  '$.message.content', $6,
  '$.message.metadata', `+recallMetadataSQL+`,
  '$.message.attachments', JSON_ARRAY(),
  '$.message.reactions', JSON_ARRAY()
)
WHERE workspace_id = $1
  AND scope_id = $2
  AND entity_id = $3
  AND type IN ('message.created', 'message.updated')
`, strings.TrimSpace(workspaceID), strings.TrimSpace(scopeID), strings.TrimSpace(messageID), strings.TrimSpace(recalledByMemberID), recalledAt.UTC().Format(time.RFC3339Nano), strings.TrimSpace(placeholder), message.MessageLifecycleStatusRecalled); err != nil {
		return err
	}
	if _, err := tx.Exec(ctx, `
UPDATE im_conversation_index
SET projection_json = JSON_SET(
  JSON_REMOVE(
    projection_json,
    '$.resolvedMentions',
    '$.attachments',
    '$.reactions',
    '$.source',
    '$.deliverySummaryPreview',
    '$.lastMessage.resolvedMentions',
    '$.lastMessage.attachments',
    '$.lastMessage.reactions',
    '$.lastMessage.source',
    '$.lastMessage.deliverySummaryPreview'
  ),
  '$.content', $6,
  '$.metadata', `+recallMetadataSQL+`,
  '$.attachments', JSON_ARRAY(),
  '$.reactions', JSON_ARRAY(),
  '$.lastMessage.content', $6,
  '$.lastMessage.metadata', `+recallMetadataSQL+`,
  '$.lastMessage.attachments', JSON_ARRAY(),
  '$.lastMessage.reactions', JSON_ARRAY()
)
WHERE workspace_id = $1
  AND scope_id = $2
  AND JSON_UNQUOTE(JSON_EXTRACT(projection_json, '$.lastMessage.id')) = $3
`, strings.TrimSpace(workspaceID), strings.TrimSpace(scopeID), strings.TrimSpace(messageID), strings.TrimSpace(recalledByMemberID), recalledAt.UTC().Format(time.RFC3339Nano), strings.TrimSpace(placeholder), message.MessageLifecycleStatusRecalled); err != nil {
		return err
	}
	return nil
}
