package app

import (
	"context"
	"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/attachment"
	"github.com/tidbcloud/AgentTeam/internal/store/message"
	"github.com/tidbcloud/AgentTeam/internal/store/privateagent"
)

// deleteAttachmentDerivative delegates to the attachment store's thumbnail
// cleanup, used as the DeleteAttachmentDerivatives callback for message stores.
func (store *Store) deleteAttachmentDerivative(ctx context.Context, workspaceID string, attachmentID string) {
	store.attachmentStore().DeleteAttachmentThumbnail(ctx, workspaceID, attachmentID)
}

func (store *Store) messageStore() *message.Store {
	if store.messages != nil {
		return store.messages
	}
	return store.newMessageStore()
}

func (store *Store) newMessageStore() *message.Store {
	deleteDerivatives := store.deleteAttachmentDerivative
	if store.options.DeleteAttachmentDerivatives != nil {
		deleteDerivatives = store.options.DeleteAttachmentDerivatives
	}
	return message.New(store.db, store.archiveStorage, store.hotMessageRetentionDays, message.Options{
		LoadAttachmentsForMessages:             store.loadAttachmentsForMessages,
		DeleteAttachmentDerivatives:            deleteDerivatives,
		AttachServiceAuthorProfilePresentation: store.AttachServiceAuthorProfilePresentation,
		EnsureReplyThreadScopeProjection:       store.ensureReplyThreadScopeProjection,
		WriteScopeParticipantReadState: func(ctx context.Context, execer coredb.QueryExecer, memberID string, scopeID string, seq int64, globalSeq int64) error {
			return writeScopeParticipantReadStateWithExecer(ctx, execer, memberID, scopeID, seq, globalSeq)
		},
	})
}

func (store *Store) privateAgentProjectionStore() *privateagent.Store {
	return privateagent.New(store.db, store.PrivateAgentProjectionOptions())
}

func (store *Store) AttachmentOptions() attachment.Options {
	resolveUploadScopeForUser := func(ctx context.Context, queryer coredb.Queryer, userID string, workspaceID string, scopeID string, channelAction access.HumanAction, dmAction access.HumanAction, threadAction access.HumanAction) (string, string, error) {
		if memberID, resolvedScopeID, found, err := resolveServiceAccountAttachmentUploadScopeForUser(ctx, queryer, userID, workspaceID, scopeID); err != nil {
			return "", "", err
		} else if found {
			return memberID, resolvedScopeID, nil
		}
		membership, target, err := store.resolveAuthorizedHumanMessageTargetWithQueryer(ctx, queryer, userID, workspaceID, scopeID, channelAction, dmAction, threadAction)
		if err != nil {
			return "", "", err
		}
		return membership.Member.ID, appChannelIDForTarget(target), nil
	}

	return attachment.Options{
		ResolveUploadScopeForUser: func(ctx context.Context, queryer coredb.Queryer, userID string, workspaceID string, scopeID string) (string, string, error) {
			return resolveUploadScopeForUser(ctx, queryer, userID, workspaceID, scopeID, channelPostAction, dmPostAction, threadPostAction)
		},
		ResolveUploadScopeForMember: func(ctx context.Context, queryer coredb.QueryRower, memberID string, workspaceID string, scopeID string) (string, error) {
			return resolveAgentAttachmentUploadScopeForMember(ctx, queryer, workspaceID, memberID, scopeID)
		},
		AuthorizeAttachmentMessageReadForUser: func(ctx context.Context, queryer coredb.QueryExecer, userID string, workspaceID string, messageID string) error {
			return store.authorizeMessageSidecarReadForUser(ctx, queryer, userID, workspaceID, messageID, message.SidecarKindAttachment)
		},
		AuthorizeAttachmentMessageReadForMember: func(ctx context.Context, queryer coredb.QueryExecer, memberID string, workspaceID string, messageID string) error {
			return store.authorizeMessageSidecarReadForMember(ctx, queryer, memberID, workspaceID, messageID, message.SidecarKindAttachment)
		},
		ThreadIDForChannelAccess: func(ctx context.Context, queryer coredb.QueryRower, memberID string, workspaceID string, channelID string) (string, error) {
			threadID, err := message.ThreadIDForChannelAccess(ctx, queryer, memberID, workspaceID, channelID)
			if err != nil {
				return "", mapMessagingError(err)
			}
			return threadID, nil
		},
		LoadHumanMemberID: func(ctx context.Context, queryer coredb.QueryRower, userID string, workspaceID string) (string, error) {
			membership, err := store.loadAuthorizedHumanWorkspaceMember(ctx, queryer, userID, workspaceID, workspaceAccessAction)
			if err != nil {
				return "", err
			}
			return membership.Member.ID, nil
		},
		MediaPublicBaseURL: store.mediaPublicBaseURL,
		MediaObjectPrefix:  store.mediaObjectPrefix,
	}
}

func resolveServiceAccountAttachmentUploadScopeForUser(ctx context.Context, queryer coredb.QueryRower, userID string, workspaceID string, scopeID string) (string, string, bool, error) {
	userID = strings.TrimSpace(userID)
	workspaceID = strings.TrimSpace(workspaceID)
	scopeID = strings.TrimSpace(scopeID)
	if userID == "" || workspaceID == "" || scopeID == "" {
		return "", "", false, nil
	}
	row := queryer.QueryRow(ctx, `
SELECT service_account.author_member_id
FROM scopes s
JOIN members m
  ON m.workspace_id = s.workspace_id
 AND m.user_id = $1
 AND m.type = 'human'
 AND m.status = 'active'
JOIN scope_memberships sm
  ON sm.scope_id = s.id
 AND sm.member_id = m.id
 AND sm.removed_at IS NULL
 AND sm.archived_at IS NULL
 AND sm.effective_to_seq IS NULL
 AND sm.visibility = 'public'
JOIN service_accounts service_account
  ON service_account.workspace_id = s.workspace_id
 AND service_account.message_scope_id = s.id
 AND service_account.management_kind = 'workspace'
 AND service_account.audience_state = 'ready'
JOIN service_installations service_installation
  ON service_installation.id = service_account.service_installation_id
 AND service_installation.workspace_id = service_account.workspace_id
 AND service_installation.service_kind = 'service_account'
 AND service_installation.status = 'active'
JOIN service_members service_member
  ON service_member.id = service_account.service_member_id
 AND service_member.workspace_id = service_account.workspace_id
 AND service_member.service_definition_id = 'svcdef_service_account'
 AND service_member.service_installation_id = service_installation.id
 AND service_member.member_id = service_account.author_member_id
 AND service_member.service_kind = 'service_account'
 AND service_member.status = 'active'
JOIN members durable_author
  ON durable_author.id = service_account.author_member_id
 AND durable_author.workspace_id = service_account.workspace_id
 AND durable_author.type = 'service'
 AND durable_author.status = 'active'
JOIN service_grants service_grant
  ON service_grant.workspace_id = service_account.workspace_id
 AND service_grant.service_definition_id = 'svcdef_service_account'
 AND service_grant.service_installation_id = service_installation.id
 AND service_grant.service_kind = 'service_account'
 AND service_grant.scope = 'message.write'
 AND service_grant.revoked_at IS NULL
JOIN service_target_bindings service_binding
  ON service_binding.workspace_id = service_account.workspace_id
 AND service_binding.service_definition_id = 'svcdef_service_account'
 AND service_binding.service_installation_id = service_installation.id
 AND service_binding.service_member_id = service_account.service_member_id
 AND service_binding.service_kind = 'service_account'
 AND service_binding.target_kind = 'service_account'
 AND service_binding.target_ref = CONCAT('service-account:', service_account.id)
 AND service_binding.delivery_scope_id = s.id
 AND service_binding.permission_check_scope_id = s.id
 AND service_binding.authority_subject_member_id = service_account.author_member_id
 AND service_binding.status = 'active'
 AND service_binding.removed_at IS NULL
LEFT JOIN service_account_managers service_account_manager
  ON service_account_manager.workspace_id = service_account.workspace_id
 AND service_account_manager.service_account_id = service_account.id
 AND service_account_manager.member_id = m.id
 AND service_account_manager.status = 'active'
 AND service_account_manager.ended_at IS NULL
WHERE s.workspace_id = $2
  AND s.id = $3
  AND s.type = 'service_account'
  AND s.status = 'active'
  AND s.archived_at IS NULL
  AND service_account.management_kind = 'workspace'
  AND (
    service_account_manager.id IS NOT NULL
    OR m.workspace_role IN ('owner', 'admin')
  )
LIMIT 1
`, userID, workspaceID, scopeID)
	var durableAuthorMemberID string
	if err := row.Scan(&durableAuthorMemberID); err != nil {
		if coredb.IsNotFound(err) {
			return "", "", false, nil
		}
		return "", "", false, fmt.Errorf("resolve service-account attachment upload scope: %w", err)
	}
	return strings.TrimSpace(durableAuthorMemberID), scopeID, true, nil
}

func (store *Store) authorizeMessageSidecarReadForUser(ctx context.Context, queryer coredb.QueryExecer, userID string, workspaceID string, messageID string, kind message.SidecarKind) error {
	membership, err := store.loadAuthorizedHumanWorkspaceMember(ctx, queryer, userID, workspaceID, workspaceAccessAction)
	if err != nil {
		return err
	}
	return store.authorizeMessageSidecarReadForMember(ctx, queryer, membership.Member.ID, workspaceID, messageID, kind)
}

func (store *Store) authorizeMessageSidecarReadForMember(ctx context.Context, queryer coredb.QueryExecer, memberID string, workspaceID string, messageID string, kind message.SidecarKind) error {
	viewerType, err := loadMessageSidecarViewerTypeForMember(ctx, queryer, workspaceID, memberID)
	if err != nil {
		return err
	}
	decision, err := message.NewScopeAPIServiceForQueryer(queryer).AuthorizeSidecarRead(ctx, message.SidecarReadRequest{
		WorkspaceID:    strings.TrimSpace(workspaceID),
		MessageID:      strings.TrimSpace(messageID),
		ViewerMemberID: strings.TrimSpace(memberID),
		ViewerType:     viewerType,
		Kind:           kind,
	})
	if err != nil {
		return mapMessagingError(err)
	}
	if !decision.Allowed {
		return ErrForbidden
	}
	return nil
}

func loadMessageSidecarViewerTypeForMember(ctx context.Context, queryer coredb.QueryRower, workspaceID string, memberID string) (string, error) {
	row := queryer.QueryRow(ctx, `
SELECT type
FROM members
WHERE workspace_id = $1
  AND id = $2
  AND status = 'active'
LIMIT 1
`, strings.TrimSpace(workspaceID), strings.TrimSpace(memberID))
	var memberType string
	if err := row.Scan(&memberType); err != nil {
		if coredb.IsNotFound(err) {
			return "", ErrForbidden
		}
		return "", fmt.Errorf("load message sidecar viewer type: %w", err)
	}
	switch strings.TrimSpace(memberType) {
	case string(domain.MemberTypeHuman):
		return message.ScopeActorHuman, nil
	case string(domain.MemberTypeAgent):
		return message.ScopeActorAgent, nil
	default:
		return "", ErrForbidden
	}
}

func resolveAgentTaskUploadScopeForMember(ctx context.Context, queryer coredb.QueryRower, workspaceID string, memberID string, taskScopeID string) (bool, error) {
	// Authorize through the single canonical Task->board Channel resolver: the
	// server derives the unique channel from the active Task/board authority
	// (tasks.board_id -> task_boards), then verifies the same-workspace active
	// Agent+Machine+Member holds active membership on that channel scope. The
	// client-supplied taskScope is only a location/consistency hint, never the
	// authorization fact.
	if _, err := access.AuthorizeActiveAgentTaskBoardChannel(ctx, queryer, workspaceID, memberID, taskScopeID); err != nil {
		if errors.Is(err, access.ErrForbidden) || errors.Is(err, access.ErrNotFound) {
			return false, nil
		}
		return false, err
	}
	return true, nil
}

// PrivateAgentProjectionOptions exposes app-owned cross-domain callbacks needed by the
// private-agent projection store without requiring callers to depend on a root facade.
func (store *Store) PrivateAgentProjectionOptions() privateagent.Options {
	return privateagent.Options{
		RequireChannelInvite: func(ctx context.Context, userID string, workspaceID string, channelID string) (access.HumanChannelAccess, error) {
			return store.requireChannelInvite(ctx, userID, workspaceID, channelID)
		},
		LoadAuthorizedHumanChannelAccess: func(ctx context.Context, userID string, workspaceID string, channelID string) (access.HumanChannelAccess, error) {
			return store.loadAuthorizedHumanChannelAccess(ctx, store.db, userID, workspaceID, channelID, channelReadAction)
		},
		ResolveAuthorizedHumanMessageTarget: func(ctx context.Context, queryer coredb.Queryer, userID string, workspaceID string, channelID string) (access.HumanWorkspaceMember, message.ThreadAccess, error) {
			return store.resolveAuthorizedHumanMessageTargetWithQueryer(ctx, queryer, userID, workspaceID, channelID, channelReadAction, dmReadAction, threadReadAction)
		},
		LoadHumanWorkspaceMember: func(ctx context.Context, queryer coredb.Queryer, userID string, workspaceID string) (access.HumanWorkspaceMember, error) {
			return access.LoadHumanWorkspaceMember(ctx, queryer, userID, workspaceID)
		},
		ArchiveStorage:          store.archiveStorage,
		HotMessageRetentionDays: store.hotMessageRetentionDays,
		ClaimAttachmentsForMessage: func(ctx context.Context, tx coredb.Tx, workspaceID string, scopeID string, authorMemberID string, messageID string, attachmentIDs []string) ([]attachment.AppAttachment, error) {
			return store.claimAttachmentsForMessage(ctx, tx, workspaceID, scopeID, authorMemberID, messageID, attachmentIDs)
		},
		CopyMessageAttachments: func(ctx context.Context, tx coredb.Tx, workspaceID string, scopeID string, authorMemberID string, messageID string, sourceMessageIDs []string) ([]attachment.AppAttachment, error) {
			return store.attachmentStore().InTx(tx).CopyMessageAttachments(ctx, workspaceID, scopeID, authorMemberID, messageID, sourceMessageIDs)
		},
		LoadPublicMemberNamesByMemberID: func(ctx context.Context, queryer coredb.Queryer, workspaceID string, memberIDs []string) (map[string]string, error) {
			return loadPublicMemberNamesByMemberID(ctx, queryer, workspaceID, memberIDs)
		},
		SyncReplyThreadSubscriptions: func(ctx context.Context, tx coredb.Tx, authorMemberID string, authorType domain.MemberType, threadChannelID string, parentMessageID string, mentionedMemberIDs []string, agentMentionMemberIDs []string, currentMessageSeq int64, currentMessageGlobalSeq int64, subscribedAt time.Time) error {
			return store.syncReplyThreadSubscriptions(ctx, tx, authorMemberID, authorType, threadChannelID, parentMessageID, mentionedMemberIDs, agentMentionMemberIDs, currentMessageSeq, currentMessageGlobalSeq, subscribedAt)
		},
		EnsureReplyThreadParentAuthorParticipant: func(ctx context.Context, tx coredb.Tx, threadChannelID string, parentMessageID string, createdAt time.Time) error {
			return store.messageStore().InTx(tx).EnsureReplyThreadParentAuthorParticipant(ctx, threadChannelID, parentMessageID, createdAt)
		},
	}
}

func writeScopeParticipantReadStateWithExecer(ctx context.Context, execer coredb.QueryExecer, memberID string, scopeID string, lastReadSeq int64, lastReadGlobalSeq int64) error {
	updateTag, err := execer.Exec(ctx, `
UPDATE scope_memberships
SET unread_generation = CASE
        WHEN GREATEST(last_read_seq, $3) <> last_read_seq OR GREATEST(last_read_global_seq, $4) <> last_read_global_seq THEN unread_generation + 1
        ELSE unread_generation
    END,
    last_read_seq = GREATEST(last_read_seq, $3),
    last_read_global_seq = GREATEST(last_read_global_seq, $4),
    updated_at = $5
WHERE member_id = $1
  AND scope_id = $2
  AND removed_at IS NULL
  AND effective_to_seq IS NULL
`, strings.TrimSpace(memberID), strings.TrimSpace(scopeID), lastReadSeq, lastReadGlobalSeq, time.Now().UTC())
	if err != nil {
		return fmt.Errorf("update scope membership read state: %w", err)
	}
	if updateTag.RowsAffected() == 0 {
		return nil
	}
	return nil
}
