package privateagent

import (
	"context"
	"fmt"
	"strings"
	"time"

	"github.com/tidbcloud/AgentTeam/internal/archive"
	"github.com/tidbcloud/AgentTeam/internal/core/domain"
	"github.com/tidbcloud/AgentTeam/internal/core/routing"
	"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/storeerr"
)

var (
	ErrUnauthorized = storeerr.ErrUnauthorized
	ErrForbidden    = storeerr.ErrForbidden
	ErrNotFound     = storeerr.ErrNotFound
	ErrConflict     = storeerr.ErrConflict
)

func badRequest(message string) error { return storeerr.BadRequest(message) }
func forbidden(message string) error  { return storeerr.Forbidden(message) }
func conflict(message string) error   { return storeerr.Conflict(message) }

var (
	channelReadAction = struct{}{}
	dmReadAction      = struct{}{}
	threadReadAction  = struct{}{}
)

func rollback(tx coredb.Tx, ctx context.Context) { _ = tx.Rollback(ctx) }

func normalizeNonEmptyStrings(values []string) []string {
	if len(values) == 0 {
		return nil
	}
	normalized := make([]string, 0, len(values))
	seen := make(map[string]struct{}, len(values))
	for _, value := range values {
		trimmed := strings.TrimSpace(value)
		if trimmed == "" {
			continue
		}
		if _, ok := seen[trimmed]; ok {
			continue
		}
		seen[trimmed] = struct{}{}
		normalized = append(normalized, trimmed)
	}
	return normalized
}

// Store owns private-agent projection, wake-state, and parent-surface write persistence.
// Cross-domain behavior is supplied as narrow callbacks so this package does not
// import the root store facade.
type Store struct {
	db      coredb.DB
	options Options
}

type Options struct {
	RequireChannelInvite                func(context.Context, string, string, string) (access.HumanChannelAccess, error)
	LoadAuthorizedHumanChannelAccess    func(context.Context, string, string, string) (access.HumanChannelAccess, error)
	ResolveAuthorizedHumanMessageTarget func(context.Context, coredb.Queryer, string, string, string) (access.HumanWorkspaceMember, message.ThreadAccess, error)
	LoadHumanWorkspaceMember            func(context.Context, coredb.Queryer, string, string) (access.HumanWorkspaceMember, error)

	ArchiveStorage          archive.Storage
	HotMessageRetentionDays int

	ClaimAttachmentsForMessage               func(context.Context, coredb.Tx, string, string, string, string, []string) ([]attachment.AppAttachment, error)
	CopyMessageAttachments                   func(context.Context, coredb.Tx, string, string, string, string, []string) ([]attachment.AppAttachment, error)
	LoadPublicMemberNamesByMemberID          func(context.Context, coredb.Queryer, string, []string) (map[string]string, error)
	SyncReplyThreadSubscriptions             func(context.Context, coredb.Tx, string, domain.MemberType, string, string, []string, []string, int64, int64, time.Time) error
	EnsureReplyThreadParentAuthorParticipant func(context.Context, coredb.Tx, string, string, time.Time) error
}

func New(db coredb.DB, options Options) *Store {
	return &Store{db: db, options: options}
}

func (s *Store) Validate() error { return s.options.Validate() }

func (o Options) Validate() error {
	var missing []string
	if o.RequireChannelInvite == nil {
		missing = append(missing, "RequireChannelInvite")
	}
	if o.LoadAuthorizedHumanChannelAccess == nil {
		missing = append(missing, "LoadAuthorizedHumanChannelAccess")
	}
	if o.ResolveAuthorizedHumanMessageTarget == nil {
		missing = append(missing, "ResolveAuthorizedHumanMessageTarget")
	}
	if o.LoadHumanWorkspaceMember == nil {
		missing = append(missing, "LoadHumanWorkspaceMember")
	}
	if o.ClaimAttachmentsForMessage == nil {
		missing = append(missing, "ClaimAttachmentsForMessage")
	}
	if o.LoadPublicMemberNamesByMemberID == nil {
		missing = append(missing, "LoadPublicMemberNamesByMemberID")
	}
	if o.SyncReplyThreadSubscriptions == nil {
		missing = append(missing, "SyncReplyThreadSubscriptions")
	}
	if o.EnsureReplyThreadParentAuthorParticipant == nil {
		missing = append(missing, "EnsureReplyThreadParentAuthorParticipant")
	}
	if len(missing) > 0 {
		return fmt.Errorf("private agent projection store: missing dependencies: %s", strings.Join(missing, ", "))
	}
	return nil
}

func (store *Store) loadHumanWorkspaceMember(ctx context.Context, queryer coredb.Queryer, userID string, workspaceID string) (access.HumanWorkspaceMember, error) {
	if store == nil || store.options.LoadHumanWorkspaceMember == nil {
		return access.HumanWorkspaceMember{}, fmt.Errorf("private agent projection human workspace member dependency is not configured")
	}
	return store.options.LoadHumanWorkspaceMember(ctx, queryer, userID, workspaceID)
}

func (store *Store) resolveAuthorizedHumanMessageTarget(ctx context.Context, userID string, workspaceID string, channelID string, _ any, _ any, _ any) (access.HumanWorkspaceMember, message.ThreadAccess, error) {
	return store.resolveAuthorizedHumanMessageTargetWithQueryer(ctx, store.db, userID, workspaceID, channelID, nil, nil, nil)
}

func (store *Store) resolveAuthorizedHumanMessageTargetWithQueryer(ctx context.Context, queryer coredb.Queryer, userID string, workspaceID string, channelID string, _ any, _ any, _ any) (access.HumanWorkspaceMember, message.ThreadAccess, error) {
	if store == nil || store.options.ResolveAuthorizedHumanMessageTarget == nil {
		return access.HumanWorkspaceMember{}, message.ThreadAccess{}, fmt.Errorf("private agent projection message-target dependency is not configured")
	}
	return store.options.ResolveAuthorizedHumanMessageTarget(ctx, queryer, userID, workspaceID, channelID)
}

func (store *Store) requireChannelInvite(ctx context.Context, userID string, workspaceID string, channelID string) (access.HumanChannelAccess, error) {
	if store == nil || store.options.RequireChannelInvite == nil {
		return access.HumanChannelAccess{}, fmt.Errorf("private agent projection channel invite dependency is not configured")
	}
	return store.options.RequireChannelInvite(ctx, userID, workspaceID, channelID)
}

func (store *Store) loadAuthorizedHumanChannelAccess(ctx context.Context, _ coredb.Queryer, userID string, workspaceID string, channelID string, _ any) (access.HumanChannelAccess, error) {
	if store == nil || store.options.LoadAuthorizedHumanChannelAccess == nil {
		return access.HumanChannelAccess{}, fmt.Errorf("private agent projection channel read dependency is not configured")
	}
	return store.options.LoadAuthorizedHumanChannelAccess(ctx, userID, workspaceID, channelID)
}

func (store *Store) claimAttachmentsForMessage(ctx context.Context, tx coredb.Tx, workspaceID string, scopeID string, authorMemberID string, messageID string, attachmentIDs []string) ([]attachment.AppAttachment, error) {
	if store == nil || store.options.ClaimAttachmentsForMessage == nil {
		return nil, fmt.Errorf("private agent projection attachment claim dependency is not configured")
	}
	return store.options.ClaimAttachmentsForMessage(ctx, tx, workspaceID, scopeID, authorMemberID, messageID, attachmentIDs)
}

func (store *Store) copyMessageAttachments(ctx context.Context, tx coredb.Tx, workspaceID string, scopeID string, authorMemberID string, messageID string, sourceMessageIDs []string) ([]attachment.AppAttachment, error) {
	if store == nil || store.options.CopyMessageAttachments == nil {
		return nil, fmt.Errorf("private agent projection attachment copy dependency is not configured")
	}
	return store.options.CopyMessageAttachments(ctx, tx, workspaceID, scopeID, authorMemberID, messageID, sourceMessageIDs)
}

func (store *Store) loadPublicMemberNamesByMemberID(ctx context.Context, queryer coredb.Queryer, workspaceID string, memberIDs []string) (map[string]string, error) {
	if store == nil || store.options.LoadPublicMemberNamesByMemberID == nil {
		return nil, fmt.Errorf("private agent projection member-name dependency is not configured")
	}
	return store.options.LoadPublicMemberNamesByMemberID(ctx, queryer, workspaceID, memberIDs)
}

func (store *Store) syncReplyThreadSubscriptions(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 {
	if store == nil || store.options.SyncReplyThreadSubscriptions == nil {
		return fmt.Errorf("private agent projection reply-thread subscription dependency is not configured")
	}
	return store.options.SyncReplyThreadSubscriptions(ctx, tx, authorMemberID, authorType, threadChannelID, parentMessageID, mentionedMemberIDs, agentMentionMemberIDs, currentMessageSeq, currentMessageGlobalSeq, subscribedAt)
}

func (store *Store) ensureReplyThreadParentAuthorParticipant(ctx context.Context, tx coredb.Tx, threadChannelID string, parentMessageID string, createdAt time.Time) error {
	if store == nil || store.options.EnsureReplyThreadParentAuthorParticipant == nil {
		return fmt.Errorf("private agent projection reply-thread parent participant dependency is not configured")
	}
	return store.options.EnsureReplyThreadParentAuthorParticipant(ctx, tx, threadChannelID, parentMessageID, createdAt)
}

type commandTagExecer interface {
	Exec(context.Context, string, ...any) (coredb.Result, error)
}

func encodeMessageMetadata(metadata map[string]any) ([]byte, map[string]any, error) {
	return message.EncodeMessageMetadata(metadata)
}
func decodeMessageMetadata(raw []byte) (map[string]any, error) {
	return message.DecodeMessageMetadata(raw)
}
func copyMessageMetadata(metadata map[string]any) map[string]any {
	return message.CopyMessageMetadata(metadata)
}
func normalizeAppMessageType(messageType string) string {
	return message.NormalizeAppMessageType(messageType)
}
func mergeAgentRecipients(recipients ...[]message.AgentRecipient) []message.AgentRecipient {
	return message.MergeAgentRecipients(recipients...)
}
func filterAgentIDsByScopeParticipants(ctx context.Context, tx coredb.Tx, scopeID string, agentIDs []string) ([]string, error) {
	return message.FilterAgentIDsByScopeMembers(ctx, tx, scopeID, agentIDs)
}
func humanMentionRecipientMemberIDs(recipients []message.HumanMentionRecipient) []string {
	return message.HumanMentionRecipientMemberIDs(recipients)
}

func agentRecipientMemberIDs(recipients []message.AgentRecipient) []string {
	seen := make(map[string]struct{}, len(recipients))
	memberIDs := make([]string, 0, len(recipients))
	for _, recipient := range recipients {
		memberID := strings.TrimSpace(recipient.AgentMemberID)
		if memberID == "" {
			continue
		}
		if _, ok := seen[memberID]; ok {
			continue
		}
		seen[memberID] = struct{}{}
		memberIDs = append(memberIDs, memberID)
	}
	return memberIDs
}
func messageMentionContextForTarget(target message.ThreadAccess) message.MentionContext {
	return message.MentionContextForTarget(target)
}
func resolveReplyThreadParticipantID(ctx context.Context, tx coredb.Tx, authorMemberID string, authorType domain.MemberType) (string, error) {
	return message.ResolveReplyThreadParticipantID(ctx, tx, authorMemberID, authorType)
}
func bumpReplyThreadSummary(ctx context.Context, tx coredb.Tx, threadChannelID string, parentMessageID string, lastReplyAt time.Time, participantID string) (message.ReplyThreadUpdated, error) {
	return message.BumpReplyThreadSummary(ctx, tx, threadChannelID, parentMessageID, lastReplyAt, participantID)
}
func addReplyThreadParticipantIDs(ctx context.Context, tx coredb.Tx, threadChannelID string, parentMessageID string, participantIDs []string) error {
	return message.AddReplyThreadParticipantIDs(ctx, tx, threadChannelID, parentMessageID, participantIDs)
}
func publicMemberNameForMemberID(memberID string, publicNames map[string]string) string {
	if name := strings.TrimSpace(publicNames[strings.TrimSpace(memberID)]); name != "" {
		return name
	}
	return strings.TrimSpace(memberID)
}
func mentionRoutingContent(messageType string, content string) string {
	return routing.MentionRoutingContent(messageType, content)
}

func rollbackSavepoint(ctx context.Context, tx coredb.Tx, savepointName string) error {
	_, err := tx.Exec(ctx, "ROLLBACK TO SAVEPOINT "+savepointName)
	return err
}

func releaseSavepoint(ctx context.Context, tx coredb.Tx, savepointName string) error {
	_, err := tx.Exec(ctx, "RELEASE SAVEPOINT "+savepointName)
	return err
}
