package mock

import (
	"context"
	"strconv"
	"sync"

	eventbus "code.justin.tv/eventbus/client"
	"code.justin.tv/eventbus/client/encryption"
	"code.justin.tv/eventbus/client/internal"
	"code.justin.tv/eventbus/client/internal/wire"
	"code.justin.tv/eventbus/client/publisher"
)

// Publisher is a mock publisher that can be used for asserting calls
type Publisher struct {
	PublishCalls []PublishCall
	EncryptCalls []EncryptCall

	// PublishHandler, if set, can be used to make a mock implementation do something.
	PublishHandler func(*Publisher, context.Context, eventbus.Message) error

	// EncryptHandler, if set, can be used to make calls to encrypt authorized fields do something.
	EncryptHandler func(*Publisher, map[string]string, string) error

	mu sync.Mutex
}

func (p *Publisher) Publish(ctx context.Context, message eventbus.Message) error {
	m := message.(internal.Message)

	call := PublishCall{
		Context:   ctx,
		Message:   message,
		EventName: m.EventBusName(),
	}

	p.mu.Lock()
	p.PublishCalls = append(p.PublishCalls, call)
	p.mu.Unlock()

	var err error
	if handler := p.PublishHandler; handler != nil {
		err = handler(p, ctx, message)
	}
	return err
}

func (p *Publisher) Environment() string {
	return "mock"
}

func (p *Publisher) Encrypter() encryption.Encrypter {
	return p
}

func (p *Publisher) Close() error {
	return nil
}

func (p *Publisher) EncryptString(authCtx map[string]string, plaintext string) ([]byte, error) {
	p.mu.Lock()
	p.EncryptCalls = append(p.EncryptCalls, EncryptCall{
		Plaintext:            plaintext,
		AuthorizationContext: authCtx,
	})
	p.mu.Unlock()

	var err error
	if handler := p.EncryptHandler; handler != nil {
		err = handler(p, authCtx, plaintext)
	}

	return []byte(plaintext), err
}

func (p *Publisher) EncryptFloat32(authCtx map[string]string, float32ToEnc float32) ([]byte, error) {
	plaintext := strconv.FormatFloat(float64(float32ToEnc), 'f', -1, 32)
	p.mu.Lock()
	p.EncryptCalls = append(p.EncryptCalls, EncryptCall{
		Plaintext:            plaintext,
		AuthorizationContext: authCtx,
	})
	p.mu.Unlock()

	var err error
	if handler := p.EncryptHandler; handler != nil {
		err = handler(p, authCtx, plaintext)
	}

	return []byte(plaintext), err
}

func (p *Publisher) EncryptFloat64(authCtx map[string]string, float64ToEnc float64) ([]byte, error) {
	plaintext := strconv.FormatFloat(float64ToEnc, 'f', -1, 64)
	p.mu.Lock()
	p.EncryptCalls = append(p.EncryptCalls, EncryptCall{
		Plaintext:            plaintext,
		AuthorizationContext: authCtx,
	})
	p.mu.Unlock()

	var err error
	if handler := p.EncryptHandler; handler != nil {
		err = handler(p, authCtx, plaintext)
	}

	return []byte(plaintext), err
}

func (p *Publisher) EncryptInt32(authCtx map[string]string, int32ToEnc int32) ([]byte, error) {
	plaintext := strconv.Itoa(int(int32ToEnc))
	p.mu.Lock()
	p.EncryptCalls = append(p.EncryptCalls, EncryptCall{
		Plaintext:            plaintext,
		AuthorizationContext: authCtx,
	})
	p.mu.Unlock()

	var err error
	if handler := p.EncryptHandler; handler != nil {
		err = handler(p, authCtx, plaintext)
	}

	return []byte(plaintext), err
}

func (p *Publisher) EncryptInt64(authCtx map[string]string, int64ToEnc int64) ([]byte, error) {
	plaintext := strconv.Itoa(int(int64ToEnc))
	p.mu.Lock()
	p.EncryptCalls = append(p.EncryptCalls, EncryptCall{
		Plaintext:            plaintext,
		AuthorizationContext: authCtx,
	})
	p.mu.Unlock()

	var err error
	if handler := p.EncryptHandler; handler != nil {
		err = handler(p, authCtx, plaintext)
	}

	return []byte(plaintext), err
}

type PublishCall struct {
	Context context.Context
	Message eventbus.Message

	EventName string // The event name of the Message object
}

type EncryptCall struct {
	Plaintext            string
	AuthorizationContext map[string]string
}

// DispatchPublisher gives a publisher that sends directly into a given dispatcher.
//
// The dispatcher is called synchronously within the Publish call, and any
// errors incurred by the dispatcher will be returned from the call to Publish.
func DispatchPublisher(dispatcher eventbus.Dispatcher) *Publisher {
	return &Publisher{
		PublishHandler: func(p *Publisher, ctx context.Context, m eventbus.Message) error {
			return forwardDispatch(dispatcher, ctx, m)
		},
	}
}

func forwardDispatch(dispatcher eventbus.Dispatcher, ctx context.Context, message eventbus.Message) error {
	buf, err := wire.DefaultsEncode(message, string(publisher.EnvLocal))
	if err != nil {
		return err
	}
	return dispatcher.Dispatch(ctx, buf)
}
