package rewind

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

	"github.com/tidbcloud/AgentTeam/internal/core/rewindplan"
	"github.com/tidbcloud/AgentTeam/internal/coredb"
	"github.com/tidbcloud/AgentTeam/internal/store/message"
)

// LockRewindScopeBoundaries locks every explicit scope suffix boundary in the plan.
func LockRewindScopeBoundaries(ctx context.Context, tx coredb.Tx, plan Plan) error {
	if err := plan.ValidateSuffixBoundaries(); err != nil {
		return err
	}
	scopeIDs := plan.RewindScopeIDs()
	if len(scopeIDs) == 0 {
		return nil
	}
	sort.Strings(scopeIDs)

	requestedScopesCTE, args, _, err := coredb.BuildStringValuesCTE("requested_scopes", "scope_id", scopeIDs, 1)
	if err != nil {
		return fmt.Errorf("build rewind scope lock query: %w", err)
	}
	query := fmt.Sprintf(`
WITH %s
SELECT 1
FROM scopes s
JOIN requested_scopes rs ON rs.scope_id = s.id
ORDER BY s.id
FOR UPDATE
`, requestedScopesCTE)
	if _, err := tx.Exec(ctx, query, args...); err != nil {
		return fmt.Errorf("lock rewind scope boundaries: %w", err)
	}
	return nil
}

// LockThreads is the historical name kept for compatibility with older tests and
// callers; rewind now locks scope suffix boundaries, not legacy thread rows.
func LockThreads(ctx context.Context, tx coredb.Tx, plan Plan) error {
	return LockRewindScopeBoundaries(ctx, tx, plan)
}

func InsertOperationAgents(ctx context.Context, execer interface {
	Exec(context.Context, string, ...any) (coredb.Result, error)
}, operationID string, agentIDs []string) error {
	trimmedOperationID := strings.TrimSpace(operationID)
	normalizedAgentIDs := rewindplan.NormalizeNonEmptyStrings(agentIDs)
	if trimmedOperationID == "" || len(normalizedAgentIDs) == 0 {
		return nil
	}

	operationIDs := make([]string, len(normalizedAgentIDs))
	for i := range normalizedAgentIDs {
		operationIDs[i] = trimmedOperationID
	}

	unionSelect, args, _, err := coredb.BuildStringPairsUnionSelect("operation_id", "agent_id", operationIDs, normalizedAgentIDs, 1)
	if err != nil {
		return fmt.Errorf("build rewind operation agents select: %w", err)
	}

	query := fmt.Sprintf(`
INSERT INTO rewind_operation_agents (operation_id, agent_id)
SELECT operation_id, agent_id
FROM (
%s
) AS operation_agents
	`, unionSelect)
	if _, err := execer.Exec(ctx, query, args...); err != nil {
		return fmt.Errorf("insert rewind operation agents: %w", err)
	}
	return nil
}

func SetOperationStatus(ctx context.Context, execer interface {
	Exec(context.Context, string, ...any) (coredb.Result, error)
}, operationID string, status rewindplan.OperationStatus, reason string) error {
	if !status.Valid() {
		return fmt.Errorf("update rewind operation status: unsupported status %q", status)
	}
	_, err := execer.Exec(ctx, `
UPDATE rewind_operations
SET status = $2,
    failure_reason = $3,
    updated_at = NOW()
WHERE id = $1
`, strings.TrimSpace(operationID), status.String(), strings.TrimSpace(reason))
	if err != nil {
		return fmt.Errorf("update rewind operation status: %w", err)
	}
	return nil
}

// RewindCascadeReplyThread tombstones all visible messages in a tail reply
// thread and archives the child scope so the parent rewind removes the thread
// entry without relying on transcript hard-delete.
func RewindCascadeReplyThread(ctx context.Context, tx coredb.Tx, workspaceID string, crt CascadeReplyThread) error {
	normalizedMessageIDs := rewindplan.NormalizeNonEmptyStrings(crt.MessageIDs)
	if len(normalizedMessageIDs) > 0 {
		if _, err := message.RewindScopeSuffixTx(ctx, tx, message.ScopeRewindSuffixRequest{
			WorkspaceID:       strings.TrimSpace(workspaceID),
			ScopeID:           strings.TrimSpace(crt.CascadeScopeID()),
			FromSeq:           1,
			RewoundAt:         time.Now().UTC(),
			SuppressionReason: message.ScopeCascadeReplyThreadSuppressionReason,
		}); err != nil {
			return fmt.Errorf("rewind cascade reply thread scope facts: %w", err)
		}
	}

	if _, err := tx.Exec(ctx, `
UPDATE scopes
SET archived_at = COALESCE(archived_at, NOW()),
    updated_at = NOW()
WHERE id = $1
`, strings.TrimSpace(crt.CascadeScopeID())); err != nil {
		return fmt.Errorf("archive cascade reply thread scope: %w", err)
	}

	return nil
}

// DeleteCascadeReplyThread physically removes a reply thread as part of channel
// or DM deletion. Rewind must use RewindCascadeReplyThread so conversation
// rewind keeps transcript rows as rewound tombstones.
func DeleteCascadeReplyThread(ctx context.Context, tx coredb.Tx, workspaceID string, crt CascadeReplyThread) error {
	workspaceID = strings.TrimSpace(workspaceID)
	scopeID := strings.TrimSpace(crt.CascadeScopeID())
	normalizedMessageIDs := rewindplan.NormalizeNonEmptyStrings(crt.MessageIDs)
	if len(normalizedMessageIDs) > 0 {
		if _, err := message.RewindScopeSuffixTx(ctx, tx, message.ScopeRewindSuffixRequest{
			WorkspaceID:       workspaceID,
			ScopeID:           scopeID,
			FromSeq:           1,
			RewoundAt:         time.Now().UTC(),
			SuppressionReason: message.ScopeCascadeReplyThreadSuppressionReason,
		}); err != nil {
			return fmt.Errorf("prepare cascade reply thread scope facts for deletion: %w", err)
		}
	}

	// The Inbox index intentionally has no scope foreign key because it must
	// survive source deletion long enough for ordinary hide actions. A physical
	// reply-thread delete is authoritative, though, so remove its exact
	// projections in the same transaction instead of leaving answer content in
	// the Inbox after the source scope disappears.
	if _, err := tx.Exec(ctx, `
DELETE FROM im_conversation_index
WHERE workspace_id = $1 AND scope_id = $2
`, workspaceID, scopeID); err != nil {
		return fmt.Errorf("delete cascade reply thread conversation index: %w", err)
	}

	for _, statement := range []string{
		`DELETE FROM scope_notification_settings WHERE scope_id = $1`,
		`DELETE FROM scope_memberships WHERE scope_id = $1`,
		`DELETE FROM scope_private_grants WHERE scope_id = $1`,
		`DELETE FROM scope_message_search_state WHERE scope_id = $1`,
		`DELETE FROM scope_channel_profiles WHERE scope_id = $1`,
		`DELETE FROM scope_messages WHERE scope_id = $1`,
		`DELETE FROM scopes WHERE id = $1`,
	} {
		if _, err := tx.Exec(ctx, statement, scopeID); err != nil {
			return fmt.Errorf("delete cascade reply thread scope row: %w", err)
		}
	}

	return nil
}
