// 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

import (
	"context"
	"fmt"
	"time"

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

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

type (
	// ActivityType identifies a activity type.
	ActivityType struct {
		Name string
	}

	// ActivityInfo contains information about currently executing activity.
	ActivityInfo struct {
		TaskToken         []byte
		WorkflowType      *WorkflowType
		WorkflowNamespace string
		WorkflowExecution WorkflowExecution
		ActivityID        string
		ActivityType      ActivityType
		TaskQueue         string
		HeartbeatTimeout  time.Duration // Maximum time between heartbeats. 0 means no heartbeat needed.
		ScheduledTime     time.Time     // Time of activity scheduled by a workflow
		StartedTime       time.Time     // Time of activity start
		Deadline          time.Time     // Time of activity timeout
		Attempt           int32         // Attempt starts from 1, and increased by 1 for every retry if retry policy is specified.
	}

	// RegisterActivityOptions consists of options for registering an activity
	RegisterActivityOptions struct {
		// When an activity is a function the name is an actual activity type name.
		// When an activity is part of a structure then each member of the structure becomes an activity with
		// this Name as a prefix + activity function name.
		Name                          string
		DisableAlreadyRegisteredCheck bool

		// When registering a struct with activities, skip functions that are not valid activities. If false,
		// registration panics.
		SkipInvalidStructFunctions bool
	}

	// ActivityOptions stores all activity-specific parameters that will be stored inside of a context.
	// The current timeout resolution implementation is in seconds and uses math.Ceil(d.Seconds()) as the duration. But is
	// subjected to change in the future.
	ActivityOptions struct {
		// TaskQueue that the activity needs to be scheduled on.
		// optional: The default task queue with the same name as the workflow task queue.
		TaskQueue string

		// ScheduleToCloseTimeout - Total time that a workflow is willing to wait for Activity to complete.
		// ScheduleToCloseTimeout limits the total time of an Activity's execution including retries
		// 		(use StartToCloseTimeout to limit the time of a single attempt).
		// The zero value of this uses default value.
		// Either this option or StartToClose is required: Defaults to unlimited.
		ScheduleToCloseTimeout time.Duration

		// ScheduleToStartTimeout - Time that the Activity Task can stay in the Task Queue before it is picked up by
		// a Worker. Do not specify this timeout unless using host specific Task Queues for Activity Tasks are being
		// used for routing. In almost all situations that don't involve routing activities to specific hosts it is
		// better to rely on the default value.
		// ScheduleToStartTimeout is always non-retryable. Retrying after this timeout doesn't make sense as it would
		// just put the Activity Task back into the same Task Queue.
		// If ScheduleToClose is not provided then this timeout is required.
		// Optional: Defaults to unlimited.
		ScheduleToStartTimeout time.Duration

		// StartToCloseTimeout - Maximum time of a single Activity execution attempt.
		// Note that the Temporal Server doesn't detect Worker process failures directly. It relies on this timeout
		// to detect that an Activity that didn't complete on time. So this timeout should be as short as the longest
		// possible execution of the Activity body. Potentially long running Activities must specify HeartbeatTimeout
		// and call Activity.RecordHeartbeat(ctx, "my-heartbeat") periodically for timely failure detection.
		// If ScheduleToClose is not provided then this timeout is required: Defaults to the ScheduleToCloseTimeout value.
		StartToCloseTimeout time.Duration

		// HeartbeatTimeout - Heartbeat interval. Activity must call Activity.RecordHeartbeat(ctx, "my-heartbeat")
		// before this interval passes after the last heartbeat or the Activity starts.
		HeartbeatTimeout time.Duration

		// WaitForCancellation - Whether to wait for canceled activity to be completed(
		// activity can be failed, completed, cancel accepted)
		// Optional: default false
		WaitForCancellation bool

		// ActivityID - Business level activity ID, this is not needed for most of the cases if you have
		// to specify this then talk to temporal team. This is something will be done in future.
		// Optional: default empty string
		ActivityID string

		// RetryPolicy specifies how to retry an Activity if an error occurs.
		// More details are available at docs.temporal.io.
		// RetryPolicy is optional. If one is not specified a default RetryPolicy is provided by the server.
		// The default RetryPolicy provided by the server specifies:
		// - InitialInterval of 1 second
		// - BackoffCoefficient of 2.0
		// - MaximumInterval of 100 x InitialInterval
		// - MaximumAttempts of 0 (unlimited)
		// To disable retries set MaximumAttempts to 1.
		// The default RetryPolicy provided by the server can be overridden by the dynamic config.
		RetryPolicy *RetryPolicy
	}

	// LocalActivityOptions stores local activity specific parameters that will be stored inside of a context.
	LocalActivityOptions struct {
		// ScheduleToCloseTimeout - The end to end timeout for the local activity including retries.
		// This field is required.
		ScheduleToCloseTimeout time.Duration

		// StartToCloseTimeout - The timeout for a single execution of the local activity.
		// Optional: defaults to ScheduleToClose
		StartToCloseTimeout time.Duration

		// RetryPolicy specify how to retry activity if error happens.
		// Optional: default is to retry according to the default retry policy up to ScheduleToCloseTimeout
		// with 1sec initial delay between retries and 2x backoff.
		RetryPolicy *RetryPolicy
	}
)

// GetActivityInfo returns information about currently executing activity.
func GetActivityInfo(ctx context.Context) ActivityInfo {
	return getActivityOutboundInterceptor(ctx).GetInfo(ctx)
}

// HasHeartbeatDetails checks if there is heartbeat details from last attempt.
func HasHeartbeatDetails(ctx context.Context) bool {
	return getActivityOutboundInterceptor(ctx).HasHeartbeatDetails(ctx)
}

// GetHeartbeatDetails extract heartbeat details from last failed attempt. This is used in combination with retry policy.
// An activity could be scheduled with an optional retry policy on ActivityOptions. If the activity failed then server
// would attempt to dispatch another activity task to retry according to the retry policy. If there was heartbeat
// details reported by activity from the failed attempt, the details would be delivered along with the activity task for
// retry attempt. Activity could extract the details by GetHeartbeatDetails() and resume from the progress.
func GetHeartbeatDetails(ctx context.Context, d ...interface{}) error {
	return getActivityOutboundInterceptor(ctx).GetHeartbeatDetails(ctx, d...)
}

// GetActivityLogger returns a logger that can be used in activity
func GetActivityLogger(ctx context.Context) log.Logger {
	return getActivityOutboundInterceptor(ctx).GetLogger(ctx)
}

// GetActivityMetricsHandler returns a metrics handler that can be used in activity
func GetActivityMetricsHandler(ctx context.Context) metrics.Handler {
	return getActivityOutboundInterceptor(ctx).GetMetricsHandler(ctx)
}

// GetWorkerStopChannel returns a read-only channel. The closure of this channel indicates the activity worker is stopping.
// When the worker is stopping, it will close this channel and wait until the worker stop timeout finishes. After the timeout
// hit, the worker will cancel the activity context and then exit. The timeout can be defined by worker option: WorkerStopTimeout.
// Use this channel to handle activity graceful exit when the activity worker stops.
func GetWorkerStopChannel(ctx context.Context) <-chan struct{} {
	return getActivityOutboundInterceptor(ctx).GetWorkerStopChannel(ctx)
}

// RecordActivityHeartbeat sends heartbeat for the currently executing activity
// If the activity is either canceled (or) workflow/activity doesn't exist then we would cancel
// the context with error context.Canceled.
//  TODO: we don't have a way to distinguish between the two cases when context is canceled because
//  context doesn't support overriding value of ctx.Error.
//  TODO: Implement automatic heartbeating with cancellation through ctx.
// details - the details that you provided here can be seen in the workflow when it receives TimeoutError, you
// can check error TimeoutType()/Details().
func RecordActivityHeartbeat(ctx context.Context, details ...interface{}) {
	getActivityOutboundInterceptor(ctx).RecordHeartbeat(ctx, details...)
}

// ServiceInvoker abstracts calls to the Temporal service from an activity implementation.
// Implement to unit test activities.
type ServiceInvoker interface {
	// Returns ActivityTaskCanceledError if activity is canceled
	Heartbeat(ctx context.Context, details *commonpb.Payloads, skipBatching bool) error
	Close(ctx context.Context, flushBufferedHeartbeat bool)
	GetClient(options ClientOptions) Client
}

// WithActivityTask adds activity specific information into context.
// Use this method to unit test activity implementations that use context extractor methodshared.
func WithActivityTask(
	ctx context.Context,
	task *workflowservice.PollActivityTaskQueueResponse,
	taskQueue string,
	invoker ServiceInvoker,
	logger log.Logger,
	metricsHandler metrics.Handler,
	dataConverter converter.DataConverter,
	workerStopChannel <-chan struct{},
	contextPropagators []ContextPropagator,
	interceptors []WorkerInterceptor,
) (context.Context, error) {
	var deadline time.Time
	scheduled := common.TimeValue(task.GetScheduledTime())
	started := common.TimeValue(task.GetStartedTime())
	scheduleToCloseTimeout := common.DurationValue(task.GetScheduleToCloseTimeout())
	startToCloseTimeout := common.DurationValue(task.GetStartToCloseTimeout())
	heartbeatTimeout := common.DurationValue(task.GetHeartbeatTimeout())

	startToCloseDeadline := started.Add(startToCloseTimeout)
	if scheduleToCloseTimeout > 0 {
		scheduleToCloseDeadline := scheduled.Add(scheduleToCloseTimeout)
		// Minimum of the two deadlines.
		if scheduleToCloseDeadline.Before(startToCloseDeadline) {
			deadline = scheduleToCloseDeadline
		} else {
			deadline = startToCloseDeadline
		}
	} else {
		deadline = startToCloseDeadline
	}

	logger = log.With(logger,
		tagActivityID, task.ActivityId,
		tagActivityType, task.ActivityType.GetName(),
		tagAttempt, task.Attempt,
		tagWorkflowType, task.WorkflowType.GetName(),
		tagWorkflowID, task.WorkflowExecution.WorkflowId,
		tagRunID, task.WorkflowExecution.RunId,
	)

	return newActivityContext(ctx, interceptors, &activityEnvironment{
		taskToken:      task.TaskToken,
		serviceInvoker: invoker,
		activityType:   ActivityType{Name: task.ActivityType.GetName()},
		activityID:     task.ActivityId,
		workflowExecution: WorkflowExecution{
			RunID: task.WorkflowExecution.RunId,
			ID:    task.WorkflowExecution.WorkflowId},
		logger:           logger,
		metricsHandler:   metricsHandler,
		deadline:         deadline,
		heartbeatTimeout: heartbeatTimeout,
		scheduledTime:    scheduled,
		startedTime:      started,
		taskQueue:        taskQueue,
		dataConverter:    dataConverter,
		attempt:          task.GetAttempt(),
		heartbeatDetails: task.HeartbeatDetails,
		workflowType: &WorkflowType{
			Name: task.WorkflowType.GetName(),
		},
		workflowNamespace:  task.WorkflowNamespace,
		workerStopChannel:  workerStopChannel,
		contextPropagators: contextPropagators,
	})
}

// WithLocalActivityTask adds local activity specific information into context.
func WithLocalActivityTask(
	ctx context.Context,
	task *localActivityTask,
	logger log.Logger,
	metricsHandler metrics.Handler,
	dataConverter converter.DataConverter,
	interceptors []WorkerInterceptor,
) (context.Context, error) {
	if ctx == nil {
		ctx = context.Background()
	}
	workflowTypeLocal := task.params.WorkflowInfo.WorkflowType
	workflowType := task.params.WorkflowInfo.WorkflowType.Name
	activityType := task.params.ActivityType
	logger = log.With(logger,
		tagActivityID, task.activityID,
		tagActivityType, activityType,
		tagAttempt, task.attempt,
		tagWorkflowType, workflowType,
		tagWorkflowID, task.params.WorkflowInfo.WorkflowExecution.ID,
		tagRunID, task.params.WorkflowInfo.WorkflowExecution.RunID,
	)
	return newActivityContext(ctx, interceptors, &activityEnvironment{
		workflowType:      &workflowTypeLocal,
		workflowNamespace: task.params.WorkflowInfo.Namespace,
		taskQueue:         task.params.WorkflowInfo.TaskQueueName,
		activityType:      ActivityType{Name: activityType},
		activityID:        fmt.Sprintf("%v", task.activityID),
		workflowExecution: task.params.WorkflowInfo.WorkflowExecution,
		logger:            logger,
		metricsHandler:    metricsHandler,
		isLocalActivity:   true,
		dataConverter:     dataConverter,
		attempt:           task.attempt,
	})
}

func newActivityContext(
	ctx context.Context,
	interceptors []WorkerInterceptor,
	env *activityEnvironment,
) (context.Context, error) {
	ctx = context.WithValue(ctx, activityEnvContextKey, env)

	// Create interceptor with default inbound and outbound values and put on
	// context
	envInterceptor := &activityEnvironmentInterceptor{env: env}
	envInterceptor.inboundInterceptor = envInterceptor
	envInterceptor.outboundInterceptor = envInterceptor
	ctx = context.WithValue(ctx, activityEnvInterceptorContextKey, envInterceptor)
	ctx = context.WithValue(ctx, activityInterceptorContextKey, envInterceptor.outboundInterceptor)

	// Intercept, run init, and put the new outbound interceptor on the context
	for i := len(interceptors) - 1; i >= 0; i-- {
		envInterceptor.inboundInterceptor = interceptors[i].InterceptActivity(ctx, envInterceptor.inboundInterceptor)
	}
	err := envInterceptor.inboundInterceptor.Init(envInterceptor)
	if err != nil {
		return nil, err
	}
	ctx = context.WithValue(ctx, activityInterceptorContextKey, envInterceptor.outboundInterceptor)

	return ctx, nil
}
