package service

import (
	"context"
	"os"

	"gitlab.com/uafrica/go-utils/audit"
	"gitlab.com/uafrica/go-utils/errors"
	"gitlab.com/uafrica/go-utils/logger"
	"gitlab.com/uafrica/go-utils/string_utils"
)

type Service interface {
	logger.Logger
	IErrorReporter
	Producer
	audit.Auditor
	WithStarter(name string, starter IStarter) Service
	WithProducer(producer Producer) Service
	WithAuditor(auditor audit.Auditor) Service
	WithErrorReporter(reporter IErrorReporter) Service
	NewContext(base context.Context, requestID string, values map[string]interface{}) (Context, error)
}

func New() Service {
	env := os.Getenv("ENVIRONMENT") //todo: support config loading for local dev and env for lambda in prod
	if env == "" {
		env = "dev"
	}
	return service{
		Producer:       nil,
		Logger:         logger.New().WithFields(map[string]interface{}{"env": env}),
		IErrorReporter: DoNotReportErrors{},
		Auditor:        audit.None(),
		env:            env,
		starters:       map[string]IStarter{},
	}
}

type service struct {
	logger.Logger //for logging outside of context
	Producer      //for sending async events
	IErrorReporter
	audit.Auditor
	env      string
	starters map[string]IStarter
}

func (s service) Env() string {
	return s.env
}

//adds a starter function to call in each new context
//they will be called in the sequence they were added (before api/cron/queue checks)
//and they do not have details about the event
//if starter returns error, processing fails
//if starter succeeds, and return !=nil data, it is stored against the name
//		so your handler can retieve it with:
//			checkData := ctx.Value(name).(expectedType)
//		or
//			checkData,ok := ctx.Value(name).(expectedType)
//			if !ok { ... }
//you can implement one starter that does everything and return a struct or
//implement one for your db, one for rate limit, one for ...
//the name must be snake-case, e.g. "this_is_my_starter_name"
func (s service) WithStarter(name string, starter IStarter) Service {
	if !string_utils.IsSnakeCase(name) {
		panic(errors.Errorf("invalid starter name=\"%s\", expecting snake_case names only", name))
	}
	if starter == nil {
		panic(errors.Errorf("starter(%s)==nil", name))
	}
	if _, ok := s.starters[name]; ok {
		panic(errors.Errorf("starter(%s) already defined", name))
	}
	s.starters[name] = starter
	return s
}

func (s service) WithProducer(producer Producer) Service {
	if producer != nil {
		s.Producer = producer
	}
	return s
}

func (s service) WithErrorReporter(reporter IErrorReporter) Service {
	if reporter == nil {
		panic(errors.Errorf("ErrorReporter==nil"))
	}
	s.IErrorReporter = reporter
	return s
}

func (s service) WithAuditor(auditor audit.Auditor) Service {
	if auditor != nil {
		s.Auditor = auditor
	}
	return s
}

type IErrorReporter interface {
	ReportError(fields map[string]interface{}, err error)
}

type DoNotReportErrors struct{}

func (DoNotReportErrors) ReportError(fields map[string]interface{}, err error) {}