package message

import (
	"context"
	"strings"
	"time"

	"github.com/tidbcloud/AgentTeam/internal/archive"
	"github.com/tidbcloud/AgentTeam/internal/coredb"
	"github.com/tidbcloud/AgentTeam/internal/store/attachment"
)

// Store owns message history/search/query persistence. It deliberately receives
// cross-domain behavior through narrow callbacks so internal/store/message never
// imports the root store facade.
type Store struct {
	db                      coredb.DB
	archiveStorage          archive.Storage
	hotMessageRetentionDays int
	options                 Options
}

type TxStore struct {
	tx                      coredb.Tx
	archiveStorage          archive.Storage
	hotMessageRetentionDays int
	options                 Options
}

type Options struct {
	LoadAttachmentsForMessages             func(ctx context.Context, workspaceID string, messageIDs []string) (map[string][]attachment.AppAttachment, error)
	AttachServiceAuthorProfilePresentation func(ctx context.Context, workspaceID string, messages []AppMessage) error
	EnsureReplyThreadScopeProjection       func(ctx context.Context, execer coredb.QueryExecer, threadChannelID string) (string, error)
	WriteScopeParticipantReadState         func(ctx context.Context, execer coredb.QueryExecer, memberID string, scopeID string, seq int64, globalSeq int64) error
	// DeleteAttachmentDerivatives deletes thumbnail derivative objects from
	// object storage. Called post-commit (best-effort) when messages are
	// rewound/recalled and their attachments are scrubbed. Wired by the root
	// store to attachment.Store.DeleteAttachmentThumbnail.
	DeleteAttachmentDerivatives func(ctx context.Context, workspaceID string, attachmentID string)
}

func New(db coredb.DB, archiveStorage archive.Storage, hotMessageRetentionDays int, options Options) *Store {
	return &Store{
		db:                      db,
		archiveStorage:          archiveStorage,
		hotMessageRetentionDays: hotMessageRetentionDays,
		options:                 options,
	}
}

// CleanupAttachmentDerivatives runs post-commit to best-effort delete thumbnail
// derivative objects for attachments that were scrubbed by recall/rewind.
// Failures are logged; derivatives are regenerable cache so missing cleanup
// is corrected by the OSS lifecycle policy or regeneration on next access.
func (store *Store) CleanupAttachmentDerivatives(ctx context.Context, derivatives []DeletedAttachmentDerivative) {
	if store == nil || store.options.DeleteAttachmentDerivatives == nil {
		return
	}
	for _, d := range derivatives {
		store.options.DeleteAttachmentDerivatives(ctx, d.WorkspaceID, d.AttachmentID)
	}
}

func (store *Store) InTx(tx coredb.Tx) TxStore {
	if store == nil {
		return TxStore{tx: tx}
	}
	return TxStore{
		tx:                      tx,
		archiveStorage:          store.archiveStorage,
		hotMessageRetentionDays: store.hotMessageRetentionDays,
		options:                 store.options,
	}
}

// HotMessageRetentionCutoff returns the oldest timestamp that remains in TiDB
// hot storage. Public history APIs and archive workers share this calculation so
// the read and archive windows cannot drift.
func HotMessageRetentionCutoff(hotMessageRetentionDays int, now time.Time) time.Time {
	return now.UTC().Add(-time.Duration(hotMessageRetentionDays) * 24 * time.Hour)
}

func (txs TxStore) hotMessageRetentionCutoff(now time.Time) time.Time {
	return HotMessageRetentionCutoff(txs.hotMessageRetentionDays, now)
}

func (store *Store) loadAttachmentsForMessages(ctx context.Context, workspaceID string, messageIDs []string) (map[string][]attachment.AppAttachment, error) {
	if store.options.LoadAttachmentsForMessages == nil {
		return map[string][]attachment.AppAttachment{}, nil
	}
	return store.options.LoadAttachmentsForMessages(ctx, workspaceID, messageIDs)
}

func (txs TxStore) loadAttachmentsForMessages(ctx context.Context, workspaceID string, messageIDs []string) (map[string][]attachment.AppAttachment, error) {
	if txs.options.LoadAttachmentsForMessages == nil {
		return map[string][]attachment.AppAttachment{}, nil
	}
	return txs.options.LoadAttachmentsForMessages(ctx, workspaceID, messageIDs)
}

func (store *Store) attachServiceAuthorProfilePresentation(ctx context.Context, workspaceID string, messages []AppMessage) error {
	if store.options.AttachServiceAuthorProfilePresentation == nil {
		return nil
	}
	return store.options.AttachServiceAuthorProfilePresentation(ctx, workspaceID, messages)
}

func (txs TxStore) attachServiceAuthorProfilePresentation(ctx context.Context, workspaceID string, messages []AppMessage) error {
	if txs.options.AttachServiceAuthorProfilePresentation == nil {
		return nil
	}
	return txs.options.AttachServiceAuthorProfilePresentation(ctx, workspaceID, messages)
}

func (store *Store) ensureReplyThreadScopeProjection(ctx context.Context, execer coredb.QueryExecer, threadChannelID string) (string, error) {
	if store.options.EnsureReplyThreadScopeProjection == nil {
		return strings.TrimSpace(threadChannelID), nil
	}
	return store.options.EnsureReplyThreadScopeProjection(ctx, execer, threadChannelID)
}

func (txs TxStore) ensureReplyThreadScopeProjection(ctx context.Context, threadChannelID string) (string, error) {
	if txs.options.EnsureReplyThreadScopeProjection == nil {
		return strings.TrimSpace(threadChannelID), nil
	}
	return txs.options.EnsureReplyThreadScopeProjection(ctx, txs.tx, threadChannelID)
}

func (store *Store) writeScopeParticipantReadState(ctx context.Context, execer coredb.QueryExecer, memberID string, scopeID string, seq int64, globalSeq int64) error {
	if store.options.WriteScopeParticipantReadState == nil {
		return nil
	}
	return store.options.WriteScopeParticipantReadState(ctx, execer, memberID, scopeID, seq, globalSeq)
}

func (txs TxStore) writeScopeParticipantReadState(ctx context.Context, memberID string, scopeID string, seq int64, globalSeq int64) error {
	if txs.options.WriteScopeParticipantReadState == nil {
		return nil
	}
	return txs.options.WriteScopeParticipantReadState(ctx, txs.tx, memberID, scopeID, seq, globalSeq)
}

func CollectAppMessageIDs(messages []AppMessage) []string {
	if len(messages) == 0 {
		return nil
	}
	ids := make([]string, 0, len(messages))
	for _, message := range messages {
		if trimmed := strings.TrimSpace(message.ID); trimmed != "" {
			ids = append(ids, trimmed)
		}
	}
	return ids
}
