package activity

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

	"github.com/tidbcloud/AgentTeam/internal/core/identifiers"
	"gorm.io/gorm"
	"gorm.io/gorm/clause"
)

// ProjectorCheckpointIdentity identifies one resource repository event stream.
// The resource identity and repository role form the durable checkpoint key;
// asset fields describe the activity projection target and may change as the
// resource is reconciled.
type ProjectorCheckpointIdentity struct {
	WorkspaceID  string
	ResourceType string
	ResourceID   string
	AssetType    string
	AssetID      string
	RepoRole     string
	RepoFullName string
	EventKind    string
}

// ProjectorCheckpointUpdate carries the latest source cursor, opaque source
// state, polling status, and diagnostics written after one projection attempt.
// Success and error methods apply their own status and error-reset semantics.
type ProjectorCheckpointUpdate struct {
	LastSeenAt        *time.Time
	LastSourceEventID string
	StateJSON         string
	Status            string
	LastError         string
	LastErrorAt       *time.Time
	LastPolledAt      time.Time
}

func (s *Store) GetProjectorCheckpoint(ctx context.Context, identity ProjectorCheckpointIdentity) (ResourceActivityProjectorCheckpoint, error) {
	if err := s.ensureDB(); err != nil {
		return ResourceActivityProjectorCheckpoint{}, err
	}
	identity = normalizeProjectorCheckpointIdentity(identity)
	var checkpoint ResourceActivityProjectorCheckpoint
	err := s.db.WithContext(ctx).
		Where("workspace_id = ? AND resource_type = ? AND resource_id = ? AND repo_role = ? AND event_kind = ?", identity.WorkspaceID, identity.ResourceType, identity.ResourceID, identity.RepoRole, identity.EventKind).
		First(&checkpoint).Error
	if err != nil {
		return ResourceActivityProjectorCheckpoint{}, err
	}
	return checkpoint, nil
}

func (s *Store) MarkProjectorCheckpointSuccess(ctx context.Context, identity ProjectorCheckpointIdentity, update ProjectorCheckpointUpdate) (ResourceActivityProjectorCheckpoint, error) {
	if update.Status == "" {
		update.Status = ProjectorStatusOK
	}
	update.LastError = ""
	update.LastErrorAt = nil
	return s.upsertProjectorCheckpoint(ctx, identity, update, true)
}

func (s *Store) MarkProjectorCheckpointError(ctx context.Context, identity ProjectorCheckpointIdentity, update ProjectorCheckpointUpdate) (ResourceActivityProjectorCheckpoint, error) {
	if update.Status == "" {
		update.Status = ProjectorStatusError
	}
	if update.LastPolledAt.IsZero() {
		update.LastPolledAt = time.Now().UTC()
	}
	if update.LastErrorAt == nil {
		errorAt := update.LastPolledAt
		update.LastErrorAt = &errorAt
	}
	if existing, err := s.GetProjectorCheckpoint(ctx, identity); err == nil {
		if update.LastSeenAt == nil {
			update.LastSeenAt = existing.LastSeenAt
		}
		if strings.TrimSpace(update.LastSourceEventID) == "" {
			update.LastSourceEventID = existing.LastSourceEventID
		}
		if strings.TrimSpace(update.StateJSON) == "" {
			update.StateJSON = existing.StateJSON
		}
	} else if !errors.Is(err, gorm.ErrRecordNotFound) {
		return ResourceActivityProjectorCheckpoint{}, err
	}
	return s.upsertProjectorCheckpoint(ctx, identity, update, false)
}

func (s *Store) upsertProjectorCheckpoint(ctx context.Context, identity ProjectorCheckpointIdentity, update ProjectorCheckpointUpdate, success bool) (ResourceActivityProjectorCheckpoint, error) {
	if err := s.ensureDB(); err != nil {
		return ResourceActivityProjectorCheckpoint{}, err
	}
	identity = normalizeProjectorCheckpointIdentity(identity)
	if identity.WorkspaceID == "" || identity.ResourceType == "" || identity.ResourceID == "" || identity.RepoRole == "" || identity.EventKind == "" {
		return ResourceActivityProjectorCheckpoint{}, fmt.Errorf("activity projector checkpoint identity is required")
	}
	now := update.LastPolledAt
	if now.IsZero() {
		now = time.Now().UTC()
	}
	now = now.UTC()
	stateJSON := strings.TrimSpace(update.StateJSON)
	if stateJSON == "" {
		stateJSON = "{}"
	}
	status := strings.TrimSpace(update.Status)
	if status == "" {
		status = ProjectorStatusOK
	}
	checkpointID, err := ProjectorCheckpointID()
	if err != nil {
		return ResourceActivityProjectorCheckpoint{}, err
	}
	cp := ResourceActivityProjectorCheckpoint{
		ID:                checkpointID,
		WorkspaceID:       identity.WorkspaceID,
		ResourceType:      identity.ResourceType,
		ResourceID:        identity.ResourceID,
		AssetType:         identity.AssetType,
		AssetID:           identity.AssetID,
		RepoRole:          identity.RepoRole,
		RepoFullName:      identity.RepoFullName,
		EventKind:         identity.EventKind,
		LastSeenAt:        normalizeOptionalTime(update.LastSeenAt),
		LastSourceEventID: strings.TrimSpace(update.LastSourceEventID),
		StateJSON:         stateJSON,
		Status:            status,
		LastError:         strings.TrimSpace(update.LastError),
		LastErrorAt:       normalizeOptionalTime(update.LastErrorAt),
		LastPolledAt:      &now,
		CreatedAt:         now,
		UpdatedAt:         now,
	}
	if !success {
		cp.ConsecutiveErrors = 1
	}
	assignments := map[string]any{
		"asset_type":           cp.AssetType,
		"asset_id":             cp.AssetID,
		"repo_full_name":       cp.RepoFullName,
		"last_seen_at":         cp.LastSeenAt,
		"last_source_event_id": cp.LastSourceEventID,
		"state_json":           cp.StateJSON,
		"status":               cp.Status,
		"last_error":           cp.LastError,
		"last_error_at":        cp.LastErrorAt,
		"last_polled_at":       cp.LastPolledAt,
		"updated_at":           cp.UpdatedAt,
		"consecutive_errors":   gorm.Expr("consecutive_errors + 1"),
	}
	if success {
		assignments["consecutive_errors"] = 0
	}
	if err := s.db.WithContext(ctx).Clauses(clause.OnConflict{
		Columns: []clause.Column{
			{Name: "workspace_id"},
			{Name: "resource_type"},
			{Name: "resource_id"},
			{Name: "repo_role"},
			{Name: "event_kind"},
		},
		DoUpdates: clause.Assignments(assignments),
	}).Create(&cp).Error; err != nil {
		return ResourceActivityProjectorCheckpoint{}, fmt.Errorf("upsert activity projector checkpoint: %w", err)
	}
	loaded, err := s.GetProjectorCheckpoint(ctx, identity)
	if err != nil {
		return ResourceActivityProjectorCheckpoint{}, fmt.Errorf("get activity projector checkpoint: %w", err)
	}
	return loaded, nil
}

func normalizeProjectorCheckpointIdentity(identity ProjectorCheckpointIdentity) ProjectorCheckpointIdentity {
	identity.WorkspaceID = strings.TrimSpace(identity.WorkspaceID)
	identity.ResourceType = strings.TrimSpace(identity.ResourceType)
	identity.ResourceID = strings.TrimSpace(identity.ResourceID)
	identity.AssetType = strings.TrimSpace(identity.AssetType)
	identity.AssetID = strings.TrimSpace(identity.AssetID)
	identity.RepoRole = strings.TrimSpace(identity.RepoRole)
	identity.RepoFullName = strings.TrimSpace(identity.RepoFullName)
	identity.EventKind = strings.TrimSpace(identity.EventKind)
	return identity
}

func normalizeOptionalTime(value *time.Time) *time.Time {
	if value == nil || value.IsZero() {
		return nil
	}
	normalized := value.UTC()
	return &normalized
}

func ProjectorCheckpointID() (string, error) {
	id, err := identifiers.NewEntityID("rac")
	if err != nil {
		return "", fmt.Errorf("generate activity projector checkpoint id: %w", err)
	}
	return id, nil
}
