package query

import (
	"context"
	"time"

	"github.com/zitadel/zitadel/internal/api/authz"
	"github.com/zitadel/zitadel/internal/api/call"
	"github.com/zitadel/zitadel/internal/eventstore"
	"github.com/zitadel/zitadel/internal/telemetry/tracing"
)

type Event struct {
	Editor       *EventEditor
	Aggregate    *eventstore.Aggregate
	Sequence     uint64
	CreationDate time.Time
	Type         string
	Payload      []byte
}

type EventEditor struct {
	ID                string
	DisplayName       string
	Service           string
	PreferedLoginName string
	AvatarKey         string
}

type eventsReducer struct {
	ctx     context.Context
	q       *Queries
	events  []*Event
	editors map[string]*EventEditor
}

func (r *eventsReducer) AppendEvents(events ...eventstore.Event) {
	r.events = append(r.events, r.convertEvents(r.ctx, events)...)
}

func (r *eventsReducer) Reduce() error { return nil }

func (q *Queries) SearchEvents(ctx context.Context, query *eventstore.SearchQueryBuilder) (_ []*Event, err error) {
	ctx, span := tracing.NewSpan(ctx)
	defer func() { span.EndWithError(err) }()
	auditLogRetention := q.defaultAuditLogRetention
	if instanceAuditLogRetention := authz.GetInstance(ctx).AuditLogRetention(); instanceAuditLogRetention != nil {
		auditLogRetention = *instanceAuditLogRetention
	}
	if auditLogRetention != 0 {
		query = filterAuditLogRetention(ctx, auditLogRetention, query)
	}
	reducer := &eventsReducer{ctx: ctx, q: q, editors: make(map[string]*EventEditor, query.GetLimit())}
	if err = q.eventstore.FilterToReducer(ctx, query, reducer); err != nil {
		return nil, err
	}
	return reducer.events, nil
}

func filterAuditLogRetention(ctx context.Context, auditLogRetention time.Duration, builder *eventstore.SearchQueryBuilder) *eventstore.SearchQueryBuilder {
	callTime := call.FromContext(ctx)
	if callTime.IsZero() {
		callTime = time.Now()
	}
	oldestAllowed := callTime.Add(-auditLogRetention)
	// The audit log retention time should overwrite the creation date after query only if it is older
	// For example API calls should still be able to restrict the creation date after to a more recent date
	if builder.GetCreationDateAfter().Before(oldestAllowed) {
		return builder.CreationDateAfter(oldestAllowed)
	}
	return builder
}

func (q *Queries) SearchEventTypes(ctx context.Context) []string {
	return q.eventstore.EventTypes()
}

func (q *Queries) SearchAggregateTypes(ctx context.Context) []string {
	return q.eventstore.AggregateTypes()
}

func (er *eventsReducer) convertEvents(ctx context.Context, events []eventstore.Event) []*Event {
	result := make([]*Event, len(events))
	for i, event := range events {
		result[i] = er.convertEvent(ctx, event)
	}
	return result
}

func (er *eventsReducer) convertEvent(ctx context.Context, event eventstore.Event) *Event {
	editor, ok := er.editors[event.Creator()]
	if !ok {
		editor = er.q.editorUserByID(ctx, event.Creator())
		er.editors[event.Creator()] = editor
	}

	return &Event{
		Editor: &EventEditor{
			ID:                event.Creator(),
			Service:           "zitadel",
			DisplayName:       editor.DisplayName,
			PreferedLoginName: editor.PreferedLoginName,
			AvatarKey:         editor.AvatarKey,
		},
		Aggregate:    event.Aggregate(),
		Sequence:     event.Sequence(),
		CreationDate: event.CreatedAt(),
		Type:         string(event.Type()),
		Payload:      event.DataAsBytes(),
	}
}

func (q *Queries) editorUserByID(ctx context.Context, userID string) *EventEditor {
	user, err := q.GetUserByID(ctx, false, userID)
	if err != nil {
		return &EventEditor{ID: userID}
	}

	if user.Human != nil {
		return &EventEditor{
			ID:                user.ID,
			DisplayName:       user.Human.DisplayName,
			PreferedLoginName: user.PreferredLoginName,
			AvatarKey:         user.Human.AvatarKey,
		}
	} else if user.Machine != nil {
		return &EventEditor{
			ID:                user.ID,
			DisplayName:       user.Machine.Name,
			PreferedLoginName: user.PreferredLoginName,
		}
	}
	return &EventEditor{ID: userID}
}