package eventbus

import (
	"context"
	"database/sql"
	"fmt"
	"sync"
	"time"

	"github.com/tidbcloud/account/commons/database"

	"github.com/tidbcloud/tiinf/errs"
	"github.com/tidbcloud/tiinf/transport/tieventbus"
	"gorm.io/gorm"
)

type EventHandleFuc func(event *Event) error

type Event struct {
	EventID   string
	EventType string
	EventData interface{}
}

type EventBusHandler interface {
	// start run event bus handler
	Run() error

	PublishEvent(eventType string, event *Event) error

	PublishEventWithTransaction(eventType string, event *Event, gTx *gorm.DB) error
	// event data should only be a struct not pointer, and can be cloned by golang := operator
	RegisterEventHandleFunc(eventType string, group string, eventDataStruct interface{}, eventFunc EventHandleFuc) error
}

func Get() EventBusHandler {
	return global
}

var global EventBusHandler = &EventBusHandlerImpl{}

// for test only
func Set(eventBusHandler EventBusHandler) {
	global = eventBusHandler
}

type EventBusHandlerImpl struct {
	Impl tieventbus.Eventbus
	Once sync.Once
}

func (impl *EventBusHandlerImpl) getImpl() (tieventbus.Eventbus, error) {
	var err error = nil
	impl.Once.Do(func() {
		var sqlDB *sql.DB
		sqlDB, err = database.NewProvider().WriteDB(context.Background()).DB()
		if err != nil {
			err = errs.NewInternalMQ(fmt.Sprintf("eventbus get impl: %s", err.Error()), err)
			return
		}
		var res tieventbus.Eventbus
		res, err = tieventbus.NewEventbus(tieventbus.EventbusConfig{
			AppName:    "account",
			DB:         sqlDB,
			TxPubTable: "account_tieventbus",
			MQTimeout:  time.Second * 5,
		})
		if err != nil {
			err = errs.NewInternalMQ(fmt.Sprintf("eventbus get impl: %s", err.Error()), err)
			return
		}
		impl.Impl = res
	})
	if err != nil {
		return nil, err
	}
	return impl.Impl, nil
}

func (impl *EventBusHandlerImpl) PublishEvent(eventType string, event *Event) error {
	tieventbusImpl, err := impl.getImpl()
	if err != nil {
		return err
	}
	err = tieventbusImpl.Publish(&tieventbus.Event{
		ID:   event.EventID,
		Type: eventType,
		Data: event.EventData,
	})
	if err != nil {
		err = errs.NewInternalMQ(fmt.Sprintf("eventbus publish: %s", err.Error()), err)
		return err
	}
	return nil
}

func (impl *EventBusHandlerImpl) PublishEventWithTransaction(eventType string, event *Event, gTx *gorm.DB) error {
	tieventbusImpl, err := impl.getImpl()
	if err != nil {
		return err
	}
	tx := gTx.Statement.ConnPool.(*sql.Tx)

	err = tieventbusImpl.PublishWithDBTx(tx, &tieventbus.Event{
		ID:   event.EventID,
		Type: eventType,
		Data: event.EventData,
	})
	if err != nil {
		err = errs.NewInternalMQ(fmt.Sprintf("eventbus publish: %s", err.Error()), err)
		return err
	}
	return nil
}

func (impl *EventBusHandlerImpl) Run() error {
	tieventbusImpl, err := impl.getImpl()
	if err != nil {
		return err
	}
	return tieventbusImpl.Run()
}

func (impl *EventBusHandlerImpl) RegisterEventHandleFunc(eventType string, group string, eventDataStruct interface{}, eventFunc EventHandleFuc) error {
	tieventbusImpl, err := impl.getImpl()
	if err != nil {
		return err
	}

	if group == "" {
		group = "default"
	}

	tieventbusImpl.RegisterHandler(tieventbus.HandlerConfig{
		EventType: eventType,
		Group:     group,
		HandleFunc: func(event *tieventbus.Event) error {
			return eventFunc(&Event{
				EventID:   event.ID,
				EventType: event.Type,
				EventData: event.Data,
			})
		},
		DataStruct: eventDataStruct,
	})
	return nil
}
