// The MIT License
//
// Copyright (c) 2020 Temporal Technologies Inc.  All rights reserved.
//
// Copyright (c) 2020 Uber Technologies, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

package internal

// All code in this file is private to the package.

import (
	"context"
	"errors"
	"fmt"
	"reflect"
	"time"

	commonpb "go.temporal.io/api/common/v1"

	"go.temporal.io/sdk/converter"
	"go.temporal.io/sdk/internal/common/metrics"
	"go.temporal.io/sdk/log"
)

type (
	// activity is an interface of an activity implementation.
	activity interface {
		Execute(ctx context.Context, input *commonpb.Payloads) (*commonpb.Payloads, error)
		ActivityType() ActivityType
		GetFunction() interface{}
	}

	// ActivityID uniquely identifies an activity execution
	ActivityID struct {
		id string
	}

	// LocalActivityID uniquely identifies a local activity execution
	LocalActivityID struct {
		id string
	}

	// ExecuteActivityOptions option for executing an activity
	ExecuteActivityOptions struct {
		ActivityID             string // Users can choose IDs but our framework makes it optional to decrease the crust.
		TaskQueueName          string
		ScheduleToCloseTimeout time.Duration
		ScheduleToStartTimeout time.Duration
		StartToCloseTimeout    time.Duration
		HeartbeatTimeout       time.Duration
		WaitForCancellation    bool
		OriginalTaskQueueName  string
		RetryPolicy            *commonpb.RetryPolicy
	}

	// ExecuteLocalActivityOptions options for executing a local activity
	ExecuteLocalActivityOptions struct {
		ScheduleToCloseTimeout time.Duration
		StartToCloseTimeout    time.Duration
		RetryPolicy            *RetryPolicy
	}

	// ExecuteActivityParams parameters for executing an activity
	ExecuteActivityParams struct {
		ExecuteActivityOptions
		ActivityType  ActivityType
		Input         *commonpb.Payloads
		DataConverter converter.DataConverter
		Header        *commonpb.Header
	}

	// ExecuteLocalActivityParams parameters for executing a local activity
	ExecuteLocalActivityParams struct {
		ExecuteLocalActivityOptions
		ActivityFn    interface{} // local activity function pointer
		ActivityType  string      // local activity type
		InputArgs     []interface{}
		WorkflowInfo  *WorkflowInfo
		DataConverter converter.DataConverter
		Attempt       int32
		ScheduledTime time.Time
		Header        *commonpb.Header
	}

	// AsyncActivityClient for requesting activity execution
	AsyncActivityClient interface {
		// The ExecuteActivity schedules an activity with a callback handler.
		// If the activity failed to complete the callback error would indicate the failure
		// and it can be one of ActivityTaskFailedError, ActivityTaskTimeoutError, ActivityTaskCanceledError
		ExecuteActivity(parameters ExecuteActivityParams, callback ResultHandler) ActivityID

		// This only initiates cancel request for activity. if the activity is configured to not WaitForCancellation then
		// it would invoke the callback handler immediately with error code ActivityTaskCanceledError.
		// If the activity is not running(either scheduled or started) then it is a no-operation.
		RequestCancelActivity(activityID ActivityID)
	}

	// LocalActivityClient for requesting local activity execution
	LocalActivityClient interface {
		ExecuteLocalActivity(params ExecuteLocalActivityParams, callback LocalActivityResultHandler) LocalActivityID

		RequestCancelLocalActivity(activityID LocalActivityID)
	}

	activityEnvironment struct {
		taskToken          []byte
		workflowExecution  WorkflowExecution
		activityID         string
		activityType       ActivityType
		serviceInvoker     ServiceInvoker
		logger             log.Logger
		metricsHandler     metrics.Handler
		isLocalActivity    bool
		heartbeatTimeout   time.Duration
		deadline           time.Time
		scheduledTime      time.Time
		startedTime        time.Time
		taskQueue          string
		dataConverter      converter.DataConverter
		attempt            int32 // starts from 1.
		heartbeatDetails   *commonpb.Payloads
		workflowType       *WorkflowType
		workflowNamespace  string
		workerStopChannel  <-chan struct{}
		contextPropagators []ContextPropagator
	}

	// context.WithValue need this type instead of basic type string to avoid lint error
	contextKey string
)

const (
	activityEnvContextKey            contextKey = "activityEnv"
	activityOptionsContextKey        contextKey = "activityOptions"
	localActivityOptionsContextKey   contextKey = "localActivityOptions"
	activityInterceptorContextKey    contextKey = "activityInterceptor"
	activityEnvInterceptorContextKey contextKey = "activityEnvInterceptor"
)

func (i ActivityID) String() string {
	return i.id
}

// ParseActivityID returns ActivityID constructed from its string representation.
// The string representation should be obtained through ActivityID.String()
func ParseActivityID(id string) (ActivityID, error) {
	return ActivityID{id: id}, nil
}

func (i LocalActivityID) String() string {
	return i.id
}

// ParseLocalActivityID returns LocalActivityID constructed from its string representation.
// The string representation should be obtained through LocalActivityID.String()
func ParseLocalActivityID(v string) (LocalActivityID, error) {
	return LocalActivityID{id: v}, nil
}

func getActivityEnv(ctx context.Context) *activityEnvironment {
	env := ctx.Value(activityEnvContextKey)
	if env == nil {
		panic("getActivityEnv: Not an activity context")
	}
	return env.(*activityEnvironment)
}

func getActivityOptions(ctx Context) *ExecuteActivityOptions {
	eap := ctx.Value(activityOptionsContextKey)
	if eap == nil {
		return nil
	}
	return eap.(*ExecuteActivityOptions)
}

func getLocalActivityOptions(ctx Context) *ExecuteLocalActivityOptions {
	opts := ctx.Value(localActivityOptionsContextKey)
	if opts == nil {
		return nil
	}
	return opts.(*ExecuteLocalActivityOptions)
}

func getValidatedLocalActivityOptions(ctx Context) (*ExecuteLocalActivityOptions, error) {
	p := getLocalActivityOptions(ctx)
	if p == nil {
		return nil, errLocalActivityParamsBadRequest
	}
	if p.ScheduleToCloseTimeout < 0 {
		return nil, errors.New("negative ScheduleToCloseTimeout")
	}
	if p.StartToCloseTimeout < 0 {
		return nil, errors.New("negative StartToCloseTimeout")
	}
	if p.ScheduleToCloseTimeout == 0 && p.StartToCloseTimeout == 0 {
		return nil, errors.New("at least one of ScheduleToCloseTimeout and StartToCloseTimeout is required")
	}
	if p.ScheduleToCloseTimeout == 0 {
		p.ScheduleToCloseTimeout = p.StartToCloseTimeout
	} else {
		p.StartToCloseTimeout = p.ScheduleToCloseTimeout
	}
	return p, nil
}

func validateFunctionArgs(workflowFunc interface{}, args []interface{}, isWorkflow bool) error {
	fType := reflect.TypeOf(workflowFunc)
	switch getKind(fType) {
	case reflect.String:
		// We can't validate function passed as string.
		return nil
	case reflect.Func:
	default:
		return fmt.Errorf(
			"invalid type 'workflowFunc' parameter provided, it can be either worker function or function name: %v",
			workflowFunc)
	}

	fnName, _ := getFunctionName(workflowFunc)
	fnArgIndex := 0
	// Skip Context function argument.
	if fType.NumIn() > 0 {
		if isWorkflow && isWorkflowContext(fType.In(0)) {
			fnArgIndex++
		}
		if !isWorkflow && isActivityContext(fType.In(0)) {
			fnArgIndex++
		}
	}

	// Validate provided args match with function order match.
	if fType.NumIn()-fnArgIndex != len(args) {
		return fmt.Errorf(
			"expected %d args for function: %v but found %v",
			fType.NumIn()-fnArgIndex, fnName, len(args))
	}

	for i := 0; fnArgIndex < fType.NumIn(); fnArgIndex, i = fnArgIndex+1, i+1 {
		fnArgType := fType.In(fnArgIndex)
		argType := reflect.TypeOf(args[i])
		if argType != nil && !argType.AssignableTo(fnArgType) {
			return fmt.Errorf(
				"cannot assign function argument: %d from type: %s to type: %s",
				fnArgIndex+1, argType, fnArgType,
			)
		}
	}

	return nil
}

func getValidatedActivityFunction(f interface{}, args []interface{}, registry *registry) (*ActivityType, error) {
	fnName := ""
	fType := reflect.TypeOf(f)
	switch getKind(fType) {
	case reflect.String:
		fnName = reflect.ValueOf(f).String()
	case reflect.Func:
		if err := validateFunctionArgs(f, args, false); err != nil {
			return nil, err
		}
		fnName, _ = getFunctionName(f)
		if alias, ok := registry.getActivityAlias(fnName); ok {
			fnName = alias
		}

	default:
		return nil, fmt.Errorf(
			"invalid type 'f' parameter provided, it can be either activity function or name of the activity: %v", f)
	}

	return &ActivityType{Name: fnName}, nil
}

func getKind(fType reflect.Type) reflect.Kind {
	if fType == nil {
		return reflect.Invalid
	}
	return fType.Kind()
}

func isActivityContext(inType reflect.Type) bool {
	contextElem := reflect.TypeOf((*context.Context)(nil)).Elem()
	return inType != nil && inType.Implements(contextElem)
}

func setActivityParametersIfNotExist(ctx Context) Context {
	params := getActivityOptions(ctx)
	var newParams ExecuteActivityOptions
	if params != nil {
		newParams = *params
		if params.RetryPolicy != nil {
			newRetryPolicy := *newParams.RetryPolicy
			newParams.RetryPolicy = &newRetryPolicy
		}
	}
	return WithValue(ctx, activityOptionsContextKey, &newParams)
}

func setLocalActivityParametersIfNotExist(ctx Context) Context {
	params := getLocalActivityOptions(ctx)
	var newParams ExecuteLocalActivityOptions
	if params != nil {
		newParams = *params
	}
	return WithValue(ctx, localActivityOptionsContextKey, &newParams)
}

type activityEnvironmentInterceptor struct {
	env                 *activityEnvironment
	inboundInterceptor  ActivityInboundInterceptor
	outboundInterceptor ActivityOutboundInterceptor
	fn                  interface{}
}

func getActivityEnvironmentInterceptor(ctx context.Context) *activityEnvironmentInterceptor {
	a := ctx.Value(activityEnvInterceptorContextKey)
	if a == nil {
		panic("getActivityEnvironmentInterceptor: Not an activity context")
	}
	return a.(*activityEnvironmentInterceptor)
}

func getActivityOutboundInterceptor(ctx context.Context) ActivityOutboundInterceptor {
	a := ctx.Value(activityInterceptorContextKey)
	if a == nil {
		panic("getActivityOutboundInterceptor: Not an activity context")
	}
	return a.(ActivityOutboundInterceptor)
}

func (a *activityEnvironmentInterceptor) Init(outbound ActivityOutboundInterceptor) error {
	a.outboundInterceptor = outbound
	return nil
}

func (a *activityEnvironmentInterceptor) ExecuteActivity(
	ctx context.Context,
	in *ExecuteActivityInput,
) (interface{}, error) {
	// Remove header from context
	ctx = contextWithoutHeader(ctx)

	return executeFunctionWithContext(ctx, a.fn, in.Args)
}

func (a *activityEnvironmentInterceptor) GetInfo(ctx context.Context) ActivityInfo {
	return ActivityInfo{
		ActivityID:        a.env.activityID,
		ActivityType:      a.env.activityType,
		TaskToken:         a.env.taskToken,
		WorkflowExecution: a.env.workflowExecution,
		HeartbeatTimeout:  a.env.heartbeatTimeout,
		Deadline:          a.env.deadline,
		ScheduledTime:     a.env.scheduledTime,
		StartedTime:       a.env.startedTime,
		TaskQueue:         a.env.taskQueue,
		Attempt:           a.env.attempt,
		WorkflowType:      a.env.workflowType,
		WorkflowNamespace: a.env.workflowNamespace,
	}
}

func (a *activityEnvironmentInterceptor) GetLogger(ctx context.Context) log.Logger {
	return a.env.logger
}

func (a *activityEnvironmentInterceptor) GetMetricsHandler(ctx context.Context) metrics.Handler {
	return a.env.metricsHandler
}

func (a *activityEnvironmentInterceptor) RecordHeartbeat(ctx context.Context, details ...interface{}) {
	if a.env.isLocalActivity {
		// no-op for local activity
		return
	}
	var data *commonpb.Payloads
	var err error
	// We would like to be able to pass in "nil" as part of details(that is no progress to report to)
	if len(details) > 1 || (len(details) == 1 && details[0] != nil) {
		data, err = encodeArgs(getDataConverterFromActivityCtx(ctx), details)
		if err != nil {
			panic(err)
		}
	}

	// Heartbeat error is logged inside ServiceInvoker.internalHeartBeat
	_ = a.env.serviceInvoker.Heartbeat(ctx, data, false)
}

func (a *activityEnvironmentInterceptor) HasHeartbeatDetails(ctx context.Context) bool {
	return a.env.heartbeatDetails != nil
}

func (a *activityEnvironmentInterceptor) GetHeartbeatDetails(ctx context.Context, d ...interface{}) error {
	if a.env.heartbeatDetails == nil {
		return ErrNoData
	}
	encoded := newEncodedValues(a.env.heartbeatDetails, a.env.dataConverter)
	return encoded.Get(d...)
}

func (a *activityEnvironmentInterceptor) GetWorkerStopChannel(ctx context.Context) <-chan struct{} {
	return a.env.workerStopChannel
}

// Needed so this can properly be considered an inbound interceptor
func (a *activityEnvironmentInterceptor) mustEmbedActivityInboundInterceptorBase() {}

// Needed so this can properly be considered an outbound interceptor
func (a *activityEnvironmentInterceptor) mustEmbedActivityOutboundInterceptorBase() {}
