package unread

import (
	"bytes"
	"fmt"
	"strings"
	"time"

	"github.com/tidbcloud/AgentTeam/internal/store/model"
	unreadservice "github.com/tidbcloud/AgentTeam/internal/unread"
)

const taskSnapshotShadowFormatV1 int64 = 1

func isTaskSnapshot(snapshot unreadservice.Snapshot) bool {
	return snapshot.ScopeType == unreadservice.ScopeTypeTask ||
		strings.HasPrefix(strings.TrimSpace(snapshot.ScopeID), "tsk_")
}

func encodeTaskSnapshotShadow(snapshot unreadservice.Snapshot) (int64, string, error) {
	if !isTaskSnapshot(snapshot) {
		return 0, "", nil
	}
	shadow := model.TaskSnapshotShadowV1JSON{
		ScopeType:                  string(unreadservice.ScopeTypeTask),
		MessageUnreadCount:         snapshot.MessageUnreadCount,
		ActivityUnreadCount:        snapshot.ActivityUnreadCount,
		ManualUnread:               snapshot.ManualUnread,
		LastMessageReadSeq:         snapshot.LastMessageReadSeq,
		LastActivityReadSeq:        snapshot.LastActivityReadSeq,
		EffectiveFromSeq:           snapshot.EffectiveFromSeq,
		LatestMessageAccountedSeq:  snapshot.LatestMessageAccountedSeq,
		LatestActivityAccountedSeq: snapshot.LatestActivityAccountedSeq,
		NotificationState:          snapshot.NotificationState,
		MembershipVisibility:       snapshot.MembershipVisibility,
		ScopeMessageGeneration:     snapshot.ScopeMessageGeneration,
		ScopeActivityGeneration:    snapshot.ScopeActivityGeneration,
		MembershipGeneration:       snapshot.MembershipGeneration,
		NotificationGeneration:     snapshot.NotificationGeneration,
		ManualUnreadGeneration:     snapshot.ManualUnreadGeneration,
		RewindGeneration:           snapshot.RewindGeneration,
		// TiDB stores DATETIME(6). Canonical UTC/microsecond JSON keeps the
		// contract migration's strict timestamp validation and reconciliation
		// lossless for every payload emitted by this writer.
		UpdatedAt: snapshot.UpdatedAt.UTC().Truncate(time.Microsecond),
	}
	if err := validateTaskSnapshotShadow(shadow); err != nil {
		return 0, "", err
	}
	payload, err := model.MarshalTaskSnapshotShadowV1(shadow)
	if err != nil {
		return 0, "", err
	}
	return taskSnapshotShadowFormatV1, string(payload), nil
}

func mergeStoredTaskSnapshot(
	regular unreadservice.Snapshot,
	formatVersion int64,
	payload []byte,
) (unreadservice.Snapshot, error) {
	switch {
	case formatVersion == 0 && len(bytes.TrimSpace(payload)) == 0:
		return regular, nil
	case formatVersion == 0:
		return unreadservice.Snapshot{}, fmt.Errorf("Task unread snapshot shadow has payload without a format version")
	case formatVersion != taskSnapshotShadowFormatV1:
		return unreadservice.Snapshot{}, fmt.Errorf("unsupported Task unread snapshot shadow format %d", formatVersion)
	case len(bytes.TrimSpace(payload)) == 0:
		return unreadservice.Snapshot{}, fmt.Errorf("Task unread snapshot shadow format %d has no payload", formatVersion)
	}
	if regular.ScopeType != "" && regular.ScopeType != unreadservice.ScopeTypeTask {
		return unreadservice.Snapshot{}, fmt.Errorf(
			"Task unread snapshot shadow is attached to non-Task scope %q",
			regular.ScopeID,
		)
	}

	shadow, err := model.UnmarshalTaskSnapshotShadowV1(payload)
	if err != nil {
		return unreadservice.Snapshot{}, err
	}
	if err := validateTaskSnapshotShadow(shadow); err != nil {
		return unreadservice.Snapshot{}, err
	}

	protected := unreadservice.Snapshot{
		WorkspaceID:                regular.WorkspaceID,
		MemberID:                   regular.MemberID,
		ScopeID:                    regular.ScopeID,
		ScopeType:                  unreadservice.ScopeTypeTask,
		MessageUnreadCount:         shadow.MessageUnreadCount,
		ActivityUnreadCount:        shadow.ActivityUnreadCount,
		ManualUnread:               shadow.ManualUnread,
		LastMessageReadSeq:         shadow.LastMessageReadSeq,
		LastActivityReadSeq:        shadow.LastActivityReadSeq,
		EffectiveFromSeq:           shadow.EffectiveFromSeq,
		LatestMessageAccountedSeq:  shadow.LatestMessageAccountedSeq,
		LatestActivityAccountedSeq: shadow.LatestActivityAccountedSeq,
		NotificationState:          shadow.NotificationState,
		MembershipVisibility:       shadow.MembershipVisibility,
		ScopeMessageGeneration:     shadow.ScopeMessageGeneration,
		ScopeActivityGeneration:    shadow.ScopeActivityGeneration,
		MembershipGeneration:       shadow.MembershipGeneration,
		NotificationGeneration:     shadow.NotificationGeneration,
		ManualUnreadGeneration:     shadow.ManualUnreadGeneration,
		RewindGeneration:           shadow.RewindGeneration,
		UpdatedAt:                  shadow.UpdatedAt,
		ServingState:               regular.ServingState,
	}
	return overlayTaskSnapshotRegularControls(protected, regular)
}

// overlayTaskSnapshotRegularControls keeps the shadow's opaque Message and
// Activity aggregates authoritative. A legacy aggregate with a larger frontier
// does not prove it contains the targeted-v2 event: mixed owners can account
// disjoint seqs independently. Only monotonic control state and a cursor that
// fully covers the opaque shadow domain are safe to overlay. Partial cursor or
// effective-from intersections fail closed instead of fabricating a count.
func overlayTaskSnapshotRegularControls(
	protected unreadservice.Snapshot,
	regular unreadservice.Snapshot,
) (unreadservice.Snapshot, error) {
	merged := protected
	merged.ScopeType = unreadservice.ScopeTypeTask
	if regular.LastMessageReadSeq > protected.LastMessageReadSeq {
		if regular.LastMessageReadSeq < protected.LatestMessageAccountedSeq {
			return unreadservice.Snapshot{}, fmt.Errorf(
				"legacy Task message read cursor %d partially intersects protected frontier %d",
				regular.LastMessageReadSeq,
				protected.LatestMessageAccountedSeq,
			)
		}
		merged.LastMessageReadSeq = regular.LastMessageReadSeq
		merged.MessageUnreadCount = 0
		merged.LatestMessageAccountedSeq = maxSnapshotInt64(
			merged.LatestMessageAccountedSeq,
			regular.LastMessageReadSeq,
		)
	}
	if regular.LastActivityReadSeq > protected.LastActivityReadSeq {
		if regular.LastActivityReadSeq < protected.LatestActivityAccountedSeq {
			return unreadservice.Snapshot{}, fmt.Errorf(
				"legacy Task activity read cursor %d partially intersects protected frontier %d",
				regular.LastActivityReadSeq,
				protected.LatestActivityAccountedSeq,
			)
		}
		merged.LastActivityReadSeq = regular.LastActivityReadSeq
		merged.ActivityUnreadCount = 0
		merged.LatestActivityAccountedSeq = maxSnapshotInt64(
			merged.LatestActivityAccountedSeq,
			regular.LastActivityReadSeq,
		)
	}

	if regular.MembershipGeneration > protected.MembershipGeneration {
		if regular.EffectiveFromSeq > protected.EffectiveFromSeq {
			if regular.EffectiveFromSeq <= protected.LatestMessageAccountedSeq {
				return unreadservice.Snapshot{}, fmt.Errorf(
					"legacy Task effective-from seq %d partially intersects protected frontier %d",
					regular.EffectiveFromSeq,
					protected.LatestMessageAccountedSeq,
				)
			}
			merged.EffectiveFromSeq = regular.EffectiveFromSeq
			merged.MessageUnreadCount = 0
		}
		merged.MembershipVisibility = regular.MembershipVisibility
		merged.MembershipGeneration = regular.MembershipGeneration
	}
	if regular.NotificationGeneration > protected.NotificationGeneration {
		merged.NotificationState = regular.NotificationState
		merged.NotificationGeneration = regular.NotificationGeneration
	}
	if regular.ManualUnreadGeneration > protected.ManualUnreadGeneration {
		merged.ManualUnread = regular.ManualUnread
		merged.ManualUnreadGeneration = regular.ManualUnreadGeneration
	}
	// Scope Message / Activity generation and rewind generation describe the
	// same opaque event domains as the protected counts/frontiers. A larger
	// legacy value cannot prove that it contains the targeted-v2 event, and
	// carrying it forward would make an earlier pending v2 barrier impossible
	// to commit. Keep all three shadow-authoritative.
	if regular.UpdatedAt.After(protected.UpdatedAt) {
		merged.UpdatedAt = regular.UpdatedAt
	}
	if regular.ServingState != "" {
		merged.ServingState = regular.ServingState
	}
	return merged, nil
}

// establishTaskSnapshotBarrier creates the first protected Task shadow from a
// targeted-v2 completion candidate. A pre-existing format-0 row is only an
// unprotected V1 hint: its opaque aggregate/frontier/generation/rewind cannot
// fence the first v2 barrier. Safe generation-owned controls and full-cover
// cursors still overlay through the same rules used after a shadow exists.
func establishTaskSnapshotBarrier(
	legacy unreadservice.Snapshot,
	candidate unreadservice.Snapshot,
) (unreadservice.Snapshot, error) {
	return overlayTaskSnapshotRegularControls(candidate, legacy)
}

// advanceTaskSnapshotBarrier validates that the new completion candidate
// descends from the currently protected aggregate. Aggregate snapshots cannot
// union disjoint mixed-owner events, so an ambiguous partial read/effective
// range is deferred and an uncleared candidate may not reduce the protected
// count. An unchanged count at an advanced frontier remains valid: the event
// can be suppressed, already read, or idempotently retried. This does not claim
// to recover a disjoint legacy V1 event during the rolling crash window.
//
// This is a rolling old/new fence under AgentTeam's existing single workspace-owner
// contract; it is deliberately not a multi-owner CRDT.
func advanceTaskSnapshotBarrier(
	protected unreadservice.Snapshot,
	candidate unreadservice.Snapshot,
) (unreadservice.Snapshot, error) {
	if candidate.ScopeMessageGeneration < protected.ScopeMessageGeneration ||
		candidate.RewindGeneration < protected.RewindGeneration ||
		candidate.LatestMessageAccountedSeq < protected.LatestMessageAccountedSeq ||
		candidate.LastMessageReadSeq < protected.LastMessageReadSeq ||
		candidate.EffectiveFromSeq < protected.EffectiveFromSeq {
		return unreadservice.Snapshot{}, fmt.Errorf("Task message barrier candidate predates protected snapshot")
	}
	if candidate.ScopeActivityGeneration < protected.ScopeActivityGeneration ||
		candidate.LatestActivityAccountedSeq < protected.LatestActivityAccountedSeq ||
		candidate.LastActivityReadSeq < protected.LastActivityReadSeq {
		return unreadservice.Snapshot{}, fmt.Errorf("Task activity barrier candidate predates protected snapshot")
	}

	messageFullyCleared := false
	if candidate.LastMessageReadSeq > protected.LastMessageReadSeq {
		if candidate.LastMessageReadSeq < protected.LatestMessageAccountedSeq {
			return unreadservice.Snapshot{}, fmt.Errorf(
				"Task message barrier read cursor %d partially intersects protected frontier %d",
				candidate.LastMessageReadSeq,
				protected.LatestMessageAccountedSeq,
			)
		}
		messageFullyCleared = true
	}
	if candidate.EffectiveFromSeq > protected.EffectiveFromSeq {
		if candidate.EffectiveFromSeq <= protected.LatestMessageAccountedSeq {
			return unreadservice.Snapshot{}, fmt.Errorf(
				"Task message barrier effective-from seq %d partially intersects protected frontier %d",
				candidate.EffectiveFromSeq,
				protected.LatestMessageAccountedSeq,
			)
		}
		messageFullyCleared = true
	}
	if !messageFullyCleared {
		if candidate.MessageUnreadCount < protected.MessageUnreadCount {
			return unreadservice.Snapshot{}, fmt.Errorf("Task message barrier candidate loses protected unread")
		}
	}

	activityFullyCleared := false
	if candidate.LastActivityReadSeq > protected.LastActivityReadSeq {
		if candidate.LastActivityReadSeq < protected.LatestActivityAccountedSeq {
			return unreadservice.Snapshot{}, fmt.Errorf(
				"Task activity barrier read cursor %d partially intersects protected frontier %d",
				candidate.LastActivityReadSeq,
				protected.LatestActivityAccountedSeq,
			)
		}
		activityFullyCleared = true
	}
	if !activityFullyCleared {
		if candidate.ActivityUnreadCount < protected.ActivityUnreadCount {
			return unreadservice.Snapshot{}, fmt.Errorf("Task activity barrier candidate loses protected unread")
		}
	}

	advanced := candidate
	advanced.ScopeType = unreadservice.ScopeTypeTask
	if protected.MembershipGeneration > candidate.MembershipGeneration {
		advanced.MembershipVisibility = protected.MembershipVisibility
		advanced.MembershipGeneration = protected.MembershipGeneration
	}
	if protected.NotificationGeneration > candidate.NotificationGeneration {
		advanced.NotificationState = protected.NotificationState
		advanced.NotificationGeneration = protected.NotificationGeneration
	}
	if protected.ManualUnreadGeneration > candidate.ManualUnreadGeneration {
		advanced.ManualUnread = protected.ManualUnread
		advanced.ManualUnreadGeneration = protected.ManualUnreadGeneration
	}
	return advanced, nil
}

func validateTaskSnapshotShadow(shadow model.TaskSnapshotShadowV1JSON) error {
	if shadow.ScopeType != string(unreadservice.ScopeTypeTask) {
		return fmt.Errorf(
			"Task unread snapshot shadow scopeType = %q, want %q",
			shadow.ScopeType,
			unreadservice.ScopeTypeTask,
		)
	}
	values := []struct {
		name  string
		value int64
	}{
		{name: "messageUnreadCount", value: shadow.MessageUnreadCount},
		{name: "activityUnreadCount", value: shadow.ActivityUnreadCount},
		{name: "lastMessageReadSeq", value: shadow.LastMessageReadSeq},
		{name: "lastActivityReadSeq", value: shadow.LastActivityReadSeq},
		{name: "effectiveFromSeq", value: shadow.EffectiveFromSeq},
		{name: "latestMessageAccountedSeq", value: shadow.LatestMessageAccountedSeq},
		{name: "latestActivityAccountedSeq", value: shadow.LatestActivityAccountedSeq},
		{name: "scopeMessageGeneration", value: shadow.ScopeMessageGeneration},
		{name: "scopeActivityGeneration", value: shadow.ScopeActivityGeneration},
		{name: "membershipGeneration", value: shadow.MembershipGeneration},
		{name: "notificationGeneration", value: shadow.NotificationGeneration},
		{name: "manualUnreadGeneration", value: shadow.ManualUnreadGeneration},
		{name: "rewindGeneration", value: shadow.RewindGeneration},
	}
	for _, value := range values {
		if value.value < 0 {
			return fmt.Errorf("Task unread snapshot shadow %s must be non-negative", value.name)
		}
	}
	if shadow.UpdatedAt.IsZero() {
		return fmt.Errorf("Task unread snapshot shadow updatedAt is required")
	}
	return nil
}

func maxSnapshotInt64(left, right int64) int64 {
	if left > right {
		return left
	}
	return right
}
