package service

import (
	"context"
	"reflect"
	"regexp"
	"time"

	"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"
)

// Ctx stores lambda-wide context e.g. claims, request ID etc.
var Ctx Context

type Context interface {
	context.Context
	logger.Logger
	Producer
	audit.Auditor

	RequestID() string
	MillisecondsSinceStart() int64
	StartTime() time.Time

	//set claim values - things that cannot change - typically UserID, AccountID, Username, ...
	//the fieldName must be public, i.e. start with uppercase A-Z, no underscores, its not a tag name :-)
	//once set, it will override any field in params/body struct recursively with this Golang field name (before validation)
	//Set will fail if the value is already set on this context, i.e. the value cannot change
	//Call this in your app Start(), before params/body is extracted
	//value may only be of type int64 or string for now...
	ClaimSet(fieldName string, value interface{}) error

	//Get can retrieve any claim value
	ClaimGet(fieldName string) (interface{}, bool)

	//Claim() return all values so you can iterate over them, but not change them...
	//note: a context.Context also support its own set of values, which you can use as you like
	//but the idea is that you use this instead, which we apply to params/body
	Claim() map[string]interface{}

	//context data (names must be snake_case)
	//unlike claim, any type of value may be stored
	//but like claims, value can never change
	WithValue(name string, value interface{}) Context
	Set(name string, value interface{}) error
	Get(name string) (interface{}, bool)
	//Value(name string) interface{} //return nil if not set, inherited from context.Context and overloaded to retrieve local first, just like Get()
	ValueOrDefault(name string, defaultValue interface{}) interface{}
	Data() map[string]interface{}

	//write an audit event
	AuditChange(eventType string, orgValue, newValue interface{})
}

//values: are added to context and logger
//these values are logged for every log event in this context
//values can be added later using with value, but won't be logged
//	they are just for retrieval between unrelated packages, e.g.
//	authentication may set the user_id etc... and other package may retrieve it but not change it
type valueKey string

func (s service) NewContext(base context.Context, requestID string, values map[string]interface{}) (Context, error) {
	if values == nil {
		values = map[string]interface{}{}
	}
	values["request-id"] = requestID

	for n, v := range values {
		base = context.WithValue(base, valueKey(n), v)
	}
	l := logger.New().WithFields(values)
	l.IFormatter = l.IFormatter.NextColor()

	Ctx = &serviceContext{
		Context:   base,
		Logger:    l,
		Producer:  s.Producer,
		Auditor:   s.Auditor,
		startTime: time.Now(),
		requestID: requestID,
		data:      map[string]interface{}{},
		claim:     map[string]interface{}{},
	}

	for starterName, starter := range s.starters {
		var starterData interface{}
		starterData, err := starter.Start(Ctx)
		if err != nil {
			Ctx.Errorf("Start(%s) failed: %+v ...", starterName, err)
			return nil, errors.Wrapf(err, "%s", starterName)
		}
		if err = Ctx.Set(starterName, starterData); err != nil {
			Ctx.Errorf("Start(%s) failed to set (%T)%+v: %+v ...", starterName, starterData, starterData, err)
			return nil, errors.Wrapf(err, "failed to set starter(%s) data=(%T)%+v", starterName, starterData, starterData)
		}
		Ctx.Debugf("Start(%s)=(%T)%+v", starterName, starterData, starterData)
	}


	return Ctx, nil
}

type serviceContext struct {
	context.Context
	logger.Logger
	Producer
	audit.Auditor
	startTime time.Time
	requestID string
	claim     map[string]interface{}
	data      map[string]interface{}
}

func (ctx serviceContext) RequestID() string {
	return ctx.requestID
}

const claimFieldNamePattern = `[A-Z][a-zA-Z0-9]*`

var claimFieldNameRegex = regexp.MustCompile("^" + claimFieldNamePattern + "$")

func (ctx *serviceContext) ClaimSet(fieldName string, value interface{}) error {
	if !claimFieldNameRegex.MatchString(fieldName) {
		return errors.Errorf("invalid claim field name \"%s\"", fieldName)
	}
	if oldValue, exists := ctx.claim[fieldName]; exists {
		return errors.Errorf("ClaimSet(%s=(%T)%v) failed because already set to (%T)%v", fieldName, value, value, oldValue, oldValue)
	}
	switch reflect.TypeOf(value).Kind() {
	case reflect.Int64:
	case reflect.String:
	default:
		panic(errors.Errorf("claim(%s)=(%T)%v is neither sting nor int64", fieldName, value, value))
	}
	ctx.claim[fieldName] = value
	return nil
}

func (ctx serviceContext) ClaimGet(fieldName string) (interface{}, bool) {
	if cv, ok := ctx.claim[fieldName]; ok {
		return cv, true
	}
	return nil, false
}

func (ctx serviceContext) Claim() map[string]interface{} {
	claim := map[string]interface{}{}
	for n, v := range ctx.claim {
		claim[n] = v
	}
	return claim
}

func (ctx *serviceContext) WithValue(name string, value interface{}) Context {
	ctx.Context = context.WithValue(ctx.Context, valueKey(name), value)
	return ctx
}

func (ctx *serviceContext) Set(name string, value interface{}) error {
	if !string_utils.IsSnakeCase(name) {
		return errors.Errorf("invalid context value name \"%s\"", name)
	}
	if oldValue, exists := ctx.data[name]; exists {
		return errors.Errorf("Set(%s=(%T)%v) failed because already set to (%T)%v", name, value, value, oldValue, oldValue)
	}
	ctx.data[name] = value
	return nil
}

func (ctx *serviceContext) Get(name string) (interface{}, bool) {
	if cv, ok := ctx.data[name]; ok {
		return cv, true
	}
	//alternative: try value from context.Context
	if cv := ctx.Context.Value(name); cv != nil {
		return cv, true
	}
	return nil, false
}

//Value override context.Context.Value to retrieve first from our own data
func (ctx *serviceContext) Value(key interface{}) interface{} {
	if name, ok := key.(string); ok {
		if cv, ok := ctx.data[name]; ok {
			return cv
		}
	}
	//alternative: try value from context.Context
	if cv := ctx.Context.Value(key); cv != nil {
		return cv
	}
	return nil
}

func (ctx *serviceContext) Data() map[string]interface{} {
	data := map[string]interface{}{}
	for n, v := range ctx.data {
		data[n] = v
	}
	return data
}

func (ctx *serviceContext) MillisecondsSinceStart() int64 {
	return time.Since(ctx.startTime).Milliseconds()
}

func (ctx *serviceContext) StartTime() time.Time {
	return ctx.startTime
}

func (ctx *serviceContext) ValueOrDefault(name string, defaultValue interface{}) interface{} {
	if value := ctx.Value(valueKey(name)); value != nil {
		return value
	}
	return defaultValue
}

func (ctx *serviceContext) AuditChange(eventType string, orgValue, newValue interface{}) {
	username, _ := ctx.Claim()["username"].(string)
	event, err := audit.NewEvent(
		username, //use username as source (will default to "SYSTEM" if undefined)
		eventType,
		orgValue,
		newValue,
	)
	if err != nil {
		ctx.Errorf("failed to define audit event: %+v", err)
		return
	}
	if err := ctx.Auditor.WriteEvent(ctx.requestID, event); err != nil {
		ctx.Errorf("failed to audit change: %+v", err)
	}
}