package activity

import (
	"context"
	"encoding/base64"
	"errors"
	"fmt"
	"strings"
	"time"

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

var ErrInvalidCursor = errors.New("invalid activity cursor")

// Store owns persistence for resource-domain activity events, latest-commit
// projections, and projector checkpoints. It is distinct from AgentTeam server task,
// message, and timeline activity stores.
type Store struct{ db *gorm.DB }

func New(db *gorm.DB) *Store               { return &Store{db: db} }
func (s *Store) WithDB(db *gorm.DB) *Store { return &Store{db: db} }
func (s *Store) DB() *gorm.DB {
	if s == nil {
		return nil
	}
	return s.db
}

func (s *Store) ensureDB() error {
	if s == nil || s.db == nil {
		return fmt.Errorf("activity store is not configured")
	}
	return nil
}

// ListOptions defines a bounded resource-activity query. WorkspaceID and
// AssetType select the resource domain, while VisibleAssetIDs and the optional
// tombstone actor constraint enforce the caller-provided visibility scope.
// ListOptions is a store query contract, not an API request DTO.
type ListOptions struct {
	WorkspaceID         string
	AssetType           string
	VisibleAssetIDs     []string
	TombstoneActorID    string
	TombstoneEventTypes []string
	AssetID             string
	AssetIDs            []string
	EventType           string
	ActorID             string
	Cursor              string
	Limit               int
	Since               *time.Time
	Until               *time.Time
}

// Page is one store result page of persisted resource activity. Callers must
// convert its events to service or API representations before transport.
type Page struct {
	Items      []ResourceActivityEvent
	NextCursor string
}

const (
	defaultLimit = 50
	maxLimit     = 100
)

// UpsertEvent writes an activity event. When SourceEventID is present the write
// is idempotent by (workspace, source, source_event_id) and returns the existing
// row on replay.
func (s *Store) UpsertEvent(ctx context.Context, event ResourceActivityEvent) (ResourceActivityEvent, error) {
	if err := s.ensureDB(); err != nil {
		return ResourceActivityEvent{}, err
	}
	var err error
	event, err = normalizeEvent(event)
	if err != nil {
		return ResourceActivityEvent{}, err
	}
	if strings.TrimSpace(event.SourceEventIDValue()) == "" {
		if event.EventType != EventTypeCommit {
			if err := s.db.WithContext(ctx).Create(&event).Error; err != nil {
				return ResourceActivityEvent{}, fmt.Errorf("create activity event: %w", err)
			}
			return event, nil
		}
		if err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
			if err := tx.Create(&event).Error; err != nil {
				return err
			}
			return upsertLatestCommit(tx, event)
		}); err != nil {
			return ResourceActivityEvent{}, fmt.Errorf("create activity event: %w", err)
		}
		return event, nil
	}
	if event.EventType != EventTypeCommit {
		if err := s.db.WithContext(ctx).Clauses(clause.OnConflict{
			Columns:   []clause.Column{{Name: "workspace_id"}, {Name: "source"}, {Name: "source_event_id"}},
			DoNothing: true,
		}).Create(&event).Error; err != nil {
			return ResourceActivityEvent{}, fmt.Errorf("upsert activity event: %w", err)
		}
		return s.GetBySourceEventID(ctx, event.WorkspaceID, event.Source, event.SourceEventIDValue())
	}
	if err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		result := tx.Clauses(clause.OnConflict{
			Columns:   []clause.Column{{Name: "workspace_id"}, {Name: "source"}, {Name: "source_event_id"}},
			DoNothing: true,
		}).Create(&event)
		if result.Error != nil {
			return result.Error
		}
		if result.RowsAffected == 0 {
			return nil
		}
		return upsertLatestCommit(tx, event)
	}); err != nil {
		return ResourceActivityEvent{}, fmt.Errorf("upsert activity event: %w", err)
	}
	loaded, err := s.GetBySourceEventID(ctx, event.WorkspaceID, event.Source, event.SourceEventIDValue())
	if err != nil {
		return ResourceActivityEvent{}, err
	}
	return loaded, nil
}
func upsertLatestCommit(tx *gorm.DB, event ResourceActivityEvent) error {
	latest := ResourceActivityLatestCommit{
		WorkspaceID: event.WorkspaceID,
		AssetType:   event.AssetType,
		AssetID:     event.AssetID,
		OccurredAt:  event.OccurredAt,
		EventID:     event.ID,
	}
	shouldAdvance := "occurred_at < ? OR (occurred_at = ? AND event_id < ?)"
	return tx.Clauses(clause.OnConflict{
		Columns: []clause.Column{{Name: "workspace_id"}, {Name: "asset_type"}, {Name: "asset_id"}},
		DoUpdates: clause.Set{
			{Column: clause.Column{Name: "event_id"}, Value: gorm.Expr("CASE WHEN "+shouldAdvance+" THEN ? ELSE event_id END", event.OccurredAt, event.OccurredAt, event.ID, event.ID)},
			{Column: clause.Column{Name: "occurred_at"}, Value: gorm.Expr("CASE WHEN "+shouldAdvance+" THEN ? ELSE occurred_at END", event.OccurredAt, event.OccurredAt, event.ID, event.OccurredAt)},
		},
	}).Create(&latest).Error
}

func (s *Store) GetBySourceEventID(ctx context.Context, workspaceID, source, sourceEventID string) (ResourceActivityEvent, error) {
	if err := s.ensureDB(); err != nil {
		return ResourceActivityEvent{}, err
	}
	var event ResourceActivityEvent
	err := s.db.WithContext(ctx).
		Where("workspace_id = ? AND source = ? AND source_event_id = ?", strings.TrimSpace(workspaceID), normalizeSource(source), strings.TrimSpace(sourceEventID)).
		First(&event).Error
	if err != nil {
		return ResourceActivityEvent{}, fmt.Errorf("get activity event by source event id: %w", err)
	}
	return event, nil
}

func (s *Store) ListEvents(ctx context.Context, opts ListOptions) (Page, error) {
	if err := s.ensureDB(); err != nil {
		return Page{}, err
	}
	workspaceID := strings.TrimSpace(opts.WorkspaceID)
	assetType := strings.TrimSpace(opts.AssetType)
	if workspaceID == "" || assetType == "" {
		return Page{}, fmt.Errorf("workspaceID and assetType are required")
	}
	visibleIDs := uniqueStrings(opts.VisibleAssetIDs)
	tombstoneActorID := strings.TrimSpace(opts.TombstoneActorID)
	tombstoneEventTypes := uniqueStrings(opts.TombstoneEventTypes)
	hasTombstoneVisibility := tombstoneActorID != "" && len(tombstoneEventTypes) > 0
	if len(visibleIDs) == 0 && !hasTombstoneVisibility {
		return Page{Items: []ResourceActivityEvent{}}, nil
	}
	limit := opts.Limit
	if limit <= 0 {
		limit = defaultLimit
	}
	if limit > maxLimit {
		limit = maxLimit
	}
	q := s.db.WithContext(ctx).Model(&ResourceActivityEvent{}).
		Where("workspace_id = ? AND asset_type = ?", workspaceID, assetType)
	visibilityConditions := make([]string, 0, 2)
	visibilityArgs := make([]any, 0, 3)
	if len(visibleIDs) > 0 {
		visibilityConditions = append(visibilityConditions, "asset_id IN ?")
		visibilityArgs = append(visibilityArgs, visibleIDs)
	}
	if hasTombstoneVisibility {
		visibilityConditions = append(visibilityConditions, "(actor_id = ? AND event_type IN ?)")
		visibilityArgs = append(visibilityArgs, tombstoneActorID, tombstoneEventTypes)
	}
	q = q.Where("("+strings.Join(visibilityConditions, " OR ")+")", visibilityArgs...)
	if assetID := strings.TrimSpace(opts.AssetID); assetID != "" {
		q = q.Where("asset_id = ?", assetID)
	} else if assetIDs := uniqueStrings(opts.AssetIDs); len(assetIDs) > 0 {
		q = q.Where("asset_id IN ?", assetIDs)
	}
	if eventType := strings.TrimSpace(opts.EventType); eventType != "" {
		q = q.Where("event_type = ?", eventType)
	}
	if actorID := strings.TrimSpace(opts.ActorID); actorID != "" {
		q = q.Where("actor_id = ?", actorID)
	}
	if opts.Since != nil && !opts.Since.IsZero() {
		q = q.Where("occurred_at >= ?", opts.Since.UTC())
	}
	if opts.Until != nil && !opts.Until.IsZero() {
		q = q.Where("occurred_at <= ?", opts.Until.UTC())
	}
	if cursor := strings.TrimSpace(opts.Cursor); cursor != "" {
		decoded, err := DecodeCursor(cursor)
		if err != nil {
			return Page{}, err
		}
		q = q.Where("occurred_at < ? OR (occurred_at = ? AND id < ?)", decoded.OccurredAt, decoded.OccurredAt, decoded.ID)
	}
	var rows []ResourceActivityEvent
	if err := q.Order("occurred_at DESC").Order("id DESC").Limit(limit + 1).Find(&rows).Error; err != nil {
		return Page{}, fmt.Errorf("list activity events: %w", err)
	}
	page := Page{Items: rows}
	if len(rows) > limit {
		page.Items = rows[:limit]
		last := page.Items[len(page.Items)-1]
		cursor, err := EncodeCursor(last.OccurredAt, last.ID)
		if err != nil {
			return Page{}, err
		}
		page.NextCursor = cursor
	}
	return page, nil
}

// Cursor is the decoded ordering state carried by an opaque resource-activity
// pagination token. Occurred and ID preserve the token fields; OccurredAt is
// derived for the database predicate and is never serialized directly. Cursor
// is neither an API DTO nor a persistence model and therefore carries no JSON
// tags.
type Cursor struct {
	OccurredAt time.Time
	Occurred   string
	ID         string
}

func EncodeCursor(occurredAt time.Time, id string) (string, error) {
	payload := Cursor{Occurred: occurredAt.UTC().Format(time.RFC3339Nano), ID: strings.TrimSpace(id)}
	encodedOccurred, err := model.MarshalJSONObject("resource activity cursor occurredAt", payload.Occurred)
	if err != nil {
		return "", fmt.Errorf("encode activity cursor: %w", err)
	}
	encodedID, err := model.MarshalJSONObject("resource activity cursor id", payload.ID)
	if err != nil {
		return "", fmt.Errorf("encode activity cursor: %w", err)
	}
	bytes := []byte(`{"occurredAt":` + string(encodedOccurred) + `,"id":` + string(encodedID) + `}`)
	return base64.RawURLEncoding.EncodeToString(bytes), nil
}

func DecodeCursor(raw string) (Cursor, error) {
	bytes, err := base64.RawURLEncoding.DecodeString(strings.TrimSpace(raw))
	if err != nil {
		return Cursor{}, fmt.Errorf("%w: decode", ErrInvalidCursor)
	}
	var object map[string]any
	if err := model.UnmarshalJSONObject("resource activity cursor", bytes, &object); err != nil {
		return Cursor{}, fmt.Errorf("%w: json", ErrInvalidCursor)
	}
	occurredText, occurredValid := cursorStringField(object, "occurredAt")
	idText, idValid := cursorStringField(object, "id")
	if !occurredValid || !idValid {
		return Cursor{}, fmt.Errorf("%w: json", ErrInvalidCursor)
	}
	payload := Cursor{Occurred: occurredText, ID: idText}
	occurred, err := time.Parse(time.RFC3339Nano, strings.TrimSpace(payload.Occurred))
	if err != nil || strings.TrimSpace(payload.ID) == "" {
		return Cursor{}, fmt.Errorf("%w: fields", ErrInvalidCursor)
	}
	payload.OccurredAt = occurred.UTC()
	payload.ID = strings.TrimSpace(payload.ID)
	return payload, nil
}

func cursorStringField(object map[string]any, key string) (string, bool) {
	value, ok := object[key]
	if !ok || value == nil {
		return "", true
	}
	text, ok := value.(string)
	return text, ok
}

func normalizeEvent(event ResourceActivityEvent) (ResourceActivityEvent, error) {
	event.ID = strings.TrimSpace(event.ID)
	if event.ID == "" {
		id, err := newEventID()
		if err != nil {
			return ResourceActivityEvent{}, err
		}
		event.ID = id
	}
	event.WorkspaceID = strings.TrimSpace(event.WorkspaceID)
	event.AssetType = strings.TrimSpace(event.AssetType)
	event.AssetID = strings.TrimSpace(event.AssetID)
	event.RepoFullName = strings.TrimSpace(event.RepoFullName)
	event.EventType = strings.TrimSpace(event.EventType)
	event.ActorType = strings.TrimSpace(event.ActorType)
	event.ActorID = strings.TrimSpace(event.ActorID)
	event.ActorName = strings.TrimSpace(event.ActorName)
	event.SummaryKey = strings.TrimSpace(event.SummaryKey)
	if strings.TrimSpace(event.DetailsJSON) == "" {
		event.DetailsJSON = "{}"
	}
	event.TargetKind = strings.TrimSpace(event.TargetKind)
	event.TargetID = strings.TrimSpace(event.TargetID)
	event.TargetURL = strings.TrimSpace(event.TargetURL)
	event.Source = normalizeSource(event.Source)
	if event.SourceEventID != nil {
		trimmed := strings.TrimSpace(*event.SourceEventID)
		if trimmed == "" {
			event.SourceEventID = nil
		} else {
			event.SourceEventID = &trimmed
		}
	}
	if event.OccurredAt.IsZero() {
		event.OccurredAt = time.Now().UTC()
	} else {
		event.OccurredAt = event.OccurredAt.UTC()
	}
	if event.CreatedAt.IsZero() {
		event.CreatedAt = time.Now().UTC()
	} else {
		event.CreatedAt = event.CreatedAt.UTC()
	}
	return event, nil
}

func normalizeSource(source string) string {
	if trimmed := strings.TrimSpace(source); trimmed != "" {
		return trimmed
	}
	return SourceControl
}

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

func newEventID() (string, error) {
	id, err := identifiers.NewEntityID("act")
	if err != nil {
		return "", fmt.Errorf("generate activity event id: %w", err)
	}
	return id, nil
}

func (event ResourceActivityEvent) SourceEventIDValue() string {
	if event.SourceEventID == nil {
		return ""
	}
	return strings.TrimSpace(*event.SourceEventID)
}
