package eventbus

import (
	"context"
	"time"

	"lingniu-vehicle-ingest/go/vehicle-gateway/internal/envelope"
)

type RetryConfig struct {
	Attempts       int
	Backoff        time.Duration
	AttemptTimeout time.Duration
}

type RetryingSink struct {
	delegate Sink
	cfg      RetryConfig
}

func NewRetryingSink(delegate Sink, cfg RetryConfig) *RetryingSink {
	if delegate == nil {
		panic("retry delegate sink must not be nil")
	}
	if cfg.Attempts <= 0 {
		cfg.Attempts = 1
	}
	return &RetryingSink{delegate: delegate, cfg: cfg}
}

func (s *RetryingSink) PublishRaw(ctx context.Context, env envelope.FrameEnvelope) error {
	return s.withRetry(ctx, func(attemptCtx context.Context) error {
		return s.delegate.PublishRaw(attemptCtx, env)
	})
}

func (s *RetryingSink) PublishUnified(ctx context.Context, env envelope.FrameEnvelope) error {
	return s.withRetry(ctx, func(attemptCtx context.Context) error {
		return s.delegate.PublishUnified(attemptCtx, env)
	})
}

func (s *RetryingSink) PublishFields(ctx context.Context, env envelope.FrameEnvelope) error {
	return s.withRetry(ctx, func(attemptCtx context.Context) error {
		return s.delegate.PublishFields(attemptCtx, env)
	})
}

func (s *RetryingSink) PublishRecords(ctx context.Context, records []durableRecord) error {
	if publisher, ok := s.delegate.(recordPublishingSink); ok {
		return s.withRetry(ctx, func(attemptCtx context.Context) error {
			return publisher.PublishRecords(attemptCtx, records)
		})
	}
	for _, record := range records {
		switch record.Kind {
		case "raw":
			if err := s.PublishRaw(ctx, record.Envelope); err != nil {
				return err
			}
		case "unified":
			if err := s.PublishUnified(ctx, record.Envelope); err != nil {
				return err
			}
		case "fields":
			if err := s.PublishFields(ctx, record.Envelope); err != nil {
				return err
			}
		default:
			return errUnknownRecordKind(record.Kind)
		}
	}
	return nil
}

func (s *RetryingSink) Close() error {
	return s.delegate.Close()
}

func (s *RetryingSink) withRetry(ctx context.Context, publish func(context.Context) error) error {
	var lastErr error
	for attempt := 1; attempt <= s.cfg.Attempts; attempt++ {
		attemptCtx := ctx
		cancel := func() {}
		if s.cfg.AttemptTimeout > 0 {
			attemptCtx, cancel = context.WithTimeout(ctx, s.cfg.AttemptTimeout)
		}
		err := publish(attemptCtx)
		cancel()
		if err == nil {
			return nil
		}
		lastErr = err
		if ctx.Err() != nil {
			return ctx.Err()
		}
		if attempt == s.cfg.Attempts {
			break
		}
		if s.cfg.Backoff > 0 {
			timer := time.NewTimer(s.cfg.Backoff)
			select {
			case <-ctx.Done():
				if !timer.Stop() {
					<-timer.C
				}
				return ctx.Err()
			case <-timer.C:
			}
		}
	}
	return lastErr
}
