//
// DISCLAIMER
//
// Copyright 2017 ArangoDB GmbH, Cologne, Germany
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Copyright holder is ArangoDB GmbH, Cologne, Germany
//
// Author Ewout Prangsma
// Author Tomasz Mielech <tomasz@arangodb.com>
//

package driver

import (
	"context"
	"fmt"
	"reflect"
	"strconv"
	"time"

	"github.com/arangodb/go-driver/util"
)

// ContextKey is an internal type used for holding values in a `context.Context`
// do not use!.
type ContextKey string

const (
	keyRevision                 ContextKey = "arangodb-revision"
	keyRevisions                ContextKey = "arangodb-revisions"
	keyReturnNew                ContextKey = "arangodb-returnNew"
	keyReturnOld                ContextKey = "arangodb-returnOld"
	keySilent                   ContextKey = "arangodb-silent"
	keyWaitForSync              ContextKey = "arangodb-waitForSync"
	keyDetails                  ContextKey = "arangodb-details"
	keyKeepNull                 ContextKey = "arangodb-keepNull"
	keyMergeObjects             ContextKey = "arangodb-mergeObjects"
	keyRawResponse              ContextKey = "arangodb-rawResponse"
	keyImportDetails            ContextKey = "arangodb-importDetails"
	keyResponse                 ContextKey = "arangodb-response"
	keyEndpoint                 ContextKey = "arangodb-endpoint"
	keyIsRestore                ContextKey = "arangodb-isRestore"
	keyIsSystem                 ContextKey = "arangodb-isSystem"
	keyIgnoreRevs               ContextKey = "arangodb-ignoreRevs"
	keyEnforceReplicationFactor ContextKey = "arangodb-enforceReplicationFactor"
	keyConfigured               ContextKey = "arangodb-configured"
	keyFollowLeaderRedirect     ContextKey = "arangodb-followLeaderRedirect"
	keyDBServerID               ContextKey = "arangodb-dbserverID"
	keyBatchID                  ContextKey = "arangodb-batchID"
	keyJobIDResponse            ContextKey = "arangodb-jobIDResponse"
	keyAllowDirtyReads          ContextKey = "arangodb-allowDirtyReads"
	keyTransactionID            ContextKey = "arangodb-transactionID"
	keyOverwriteMode            ContextKey = "arangodb-overwriteMode"
	keyOverwrite                ContextKey = "arangodb-overwrite"
	keyUseQueueTimeout          ContextKey = "arangodb-use-queue-timeout"
	keyMaxQueueTime             ContextKey = "arangodb-max-queue-time-seconds"
)

type OverwriteMode string

const (
	OverwriteModeIgnore   OverwriteMode = "ignore"
	OverwriteModeReplace  OverwriteMode = "replace"
	OverwriteModeUpdate   OverwriteMode = "update"
	OverwriteModeConflict OverwriteMode = "conflict"
)

// WithRevision is used to configure a context to make document
// functions specify an explicit revision of the document using an `If-Match` condition.
func WithRevision(parent context.Context, revision string) context.Context {
	return context.WithValue(contextOrBackground(parent), keyRevision, revision)
}

// WithRevisions is used to configure a context to make multi-document
// functions specify explicit revisions of the documents.
func WithRevisions(parent context.Context, revisions []string) context.Context {
	return context.WithValue(contextOrBackground(parent), keyRevisions, revisions)
}

// WithReturnNew is used to configure a context to make create, update & replace document
// functions return the new document into the given result.
func WithReturnNew(parent context.Context, result interface{}) context.Context {
	return context.WithValue(contextOrBackground(parent), keyReturnNew, result)
}

// WithReturnOld is used to configure a context to make update & replace document
// functions return the old document into the given result.
func WithReturnOld(parent context.Context, result interface{}) context.Context {
	return context.WithValue(contextOrBackground(parent), keyReturnOld, result)
}

// WithDetails is used to configure a context to make Client.Version return additional details.
// You can pass a single (optional) boolean. If that is set to false, you explicitly ask to not provide details.
func WithDetails(parent context.Context, value ...bool) context.Context {
	v := true
	if len(value) == 1 {
		v = value[0]
	}
	return context.WithValue(contextOrBackground(parent), keyDetails, v)
}

// WithEndpoint is used to configure a context that forces a request to be executed on a specific endpoint.
// If you specify an endpoint like this, failover is disabled.
// If you specify an unknown endpoint, an InvalidArgumentError is returned from requests.
func WithEndpoint(parent context.Context, endpoint string) context.Context {
	endpoint = util.FixupEndpointURLScheme(endpoint)
	return context.WithValue(contextOrBackground(parent), keyEndpoint, endpoint)
}

// WithKeepNull is used to configure a context to make update functions keep null fields (value==true)
// or remove fields with null values (value==false).
func WithKeepNull(parent context.Context, value bool) context.Context {
	return context.WithValue(contextOrBackground(parent), keyKeepNull, value)
}

// WithMergeObjects is used to configure a context to make update functions merge objects present in both
// the existing document and the patch document (value==true) or overwrite objects in the existing document
// with objects found in the patch document (value==false)
func WithMergeObjects(parent context.Context, value bool) context.Context {
	return context.WithValue(contextOrBackground(parent), keyMergeObjects, value)
}

// WithSilent is used to configure a context to make functions return an empty result (silent==true),
// instead of a metadata result (silent==false, default).
// You can pass a single (optional) boolean. If that is set to false, you explicitly ask to return metadata result.
func WithSilent(parent context.Context, value ...bool) context.Context {
	v := true
	if len(value) == 1 {
		v = value[0]
	}
	return context.WithValue(contextOrBackground(parent), keySilent, v)
}

// WithWaitForSync is used to configure a context to make modification
// functions wait until the data has been synced to disk (or not).
// You can pass a single (optional) boolean. If that is set to false, you explicitly do not wait for
// data to be synced to disk.
func WithWaitForSync(parent context.Context, value ...bool) context.Context {
	v := true
	if len(value) == 1 {
		v = value[0]
	}
	return context.WithValue(contextOrBackground(parent), keyWaitForSync, v)
}

// WithAllowDirtyReads is used in an active failover deployment to allow reads from the follower.
// You can pass a reference to a boolean that will set according to whether a potentially dirty read
// happened or not. nil is allowed.
// This is valid for document reads, aql queries, gharial vertex and edge reads.
// Since 3.10 This feature is available in the Enterprise Edition for cluster deployments as well
func WithAllowDirtyReads(parent context.Context, wasDirtyRead *bool) context.Context {
	return context.WithValue(contextOrBackground(parent), keyAllowDirtyReads, wasDirtyRead)
}

// WithArangoQueueTimeout is used to enable Queue timeout on the server side.
// If WithArangoQueueTime is used then its value takes precedence in other case value of ctx.Deadline will be taken
func WithArangoQueueTimeout(parent context.Context, useQueueTimeout bool) context.Context {
	return context.WithValue(contextOrBackground(parent), keyUseQueueTimeout, useQueueTimeout)
}

// WithArangoQueueTime defines max queue timeout on the server side.
func WithArangoQueueTime(parent context.Context, duration time.Duration) context.Context {
	return context.WithValue(contextOrBackground(parent), keyMaxQueueTime, duration)
}

// WithRawResponse is used to configure a context that will make all functions store the raw response into a
// buffer.
func WithRawResponse(parent context.Context, value *[]byte) context.Context {
	return context.WithValue(contextOrBackground(parent), keyRawResponse, value)
}

// WithResponse is used to configure a context that will make all functions store the response into the given value.
func WithResponse(parent context.Context, value *Response) context.Context {
	return context.WithValue(contextOrBackground(parent), keyResponse, value)
}

// WithImportDetails is used to configure a context that will make import document requests return
// details about documents that could not be imported.
func WithImportDetails(parent context.Context, value *[]string) context.Context {
	return context.WithValue(contextOrBackground(parent), keyImportDetails, value)
}

// WithIsRestore is used to configure a context to make insert functions use the "isRestore=<value>"
// setting.
// Note: This function is intended for internal (replication) use. It is NOT intended to
// be used by normal client. This CAN screw up your database.
func WithIsRestore(parent context.Context, value bool) context.Context {
	return context.WithValue(contextOrBackground(parent), keyIsRestore, value)
}

// WithIsSystem is used to configure a context to make insert functions use the "isSystem=<value>"
// setting.
func WithIsSystem(parent context.Context, value bool) context.Context {
	return context.WithValue(contextOrBackground(parent), keyIsSystem, value)
}

// WithIgnoreRevisions is used to configure a context to make modification
// functions ignore revisions in the update.
// Do not use in combination with WithRevision or WithRevisions.
func WithIgnoreRevisions(parent context.Context, value ...bool) context.Context {
	v := true
	if len(value) == 1 {
		v = value[0]
	}
	return context.WithValue(contextOrBackground(parent), keyIgnoreRevs, v)
}

// WithEnforceReplicationFactor is used to configure a context to make adding collections
// fail if the replication factor is too high (default or true) or
// silently accept (false).
func WithEnforceReplicationFactor(parent context.Context, value bool) context.Context {
	return context.WithValue(contextOrBackground(parent), keyEnforceReplicationFactor, value)
}

// WithConfigured is used to configure a context to return the configured value of
// a user grant instead of the effective grant.
func WithConfigured(parent context.Context, value ...bool) context.Context {
	v := true
	if len(value) == 1 {
		v = value[0]
	}
	return context.WithValue(contextOrBackground(parent), keyConfigured, v)
}

// WithFollowLeaderRedirect is used to configure a context to return turn on/off
// following redirection responses from the server when the request is answered by a follower.
// Default behavior is "on".
func WithFollowLeaderRedirect(parent context.Context, value bool) context.Context {
	return context.WithValue(contextOrBackground(parent), keyFollowLeaderRedirect, value)
}

// WithDBServerID is used to configure a context that includes an ID of a specific DBServer.
func WithDBServerID(parent context.Context, id string) context.Context {
	return context.WithValue(contextOrBackground(parent), keyDBServerID, id)
}

// WithBatchID is used to configure a context that includes an ID of a Batch.
// This is used in replication functions.
func WithBatchID(parent context.Context, id string) context.Context {
	return context.WithValue(contextOrBackground(parent), keyBatchID, id)
}

// WithJobIDResponse is used to configure a context that includes a reference to a JobID
// that is filled on a error-free response.
// This is used in cluster functions.
func WithJobIDResponse(parent context.Context, jobID *string) context.Context {
	return context.WithValue(contextOrBackground(parent), keyJobIDResponse, jobID)
}

// WithTransactionID is used to bind a request to a specific transaction
func WithTransactionID(parent context.Context, tid TransactionID) context.Context {
	return context.WithValue(contextOrBackground(parent), keyTransactionID, tid)
}

// WithOverwriteMode is used to configure a context to instruct how a document should be overwritten.
func WithOverwriteMode(parent context.Context, mode OverwriteMode) context.Context {
	return context.WithValue(contextOrBackground(parent), keyOverwriteMode, mode)
}

// WithOverwrite is used to configure a context to instruct if a document should be overwritten.
func WithOverwrite(parent context.Context) context.Context {
	return context.WithValue(contextOrBackground(parent), keyOverwrite, true)
}

type contextSettings struct {
	Silent                   bool
	WaitForSync              bool
	ReturnOld                interface{}
	ReturnNew                interface{}
	Revision                 string
	Revisions                []string
	ImportDetails            *[]string
	IsRestore                bool
	IsSystem                 bool
	AllowDirtyReads          bool
	DirtyReadFlag            *bool
	IgnoreRevs               *bool
	EnforceReplicationFactor *bool
	Configured               *bool
	FollowLeaderRedirect     *bool
	DBServerID               string
	BatchID                  string
	JobIDResponse            *string
	OverwriteMode            OverwriteMode
	Overwrite                bool
	QueueTimeout             bool
	MaxQueueTime             time.Duration
}

// loadContextResponseValue loads generic values from the response and puts it into variables specified
// via context values.
func loadContextResponseValues(cs contextSettings, resp Response) {
	// Parse potential dirty read
	if cs.DirtyReadFlag != nil {
		if dirtyRead := resp.Header("X-Arango-Potential-Dirty-Read"); dirtyRead != "" {
			*cs.DirtyReadFlag = true // The documentation does not say anything about the actual value (dirtyRead == "true")
		} else {
			*cs.DirtyReadFlag = false
		}
	}
}

// setDirtyReadFlagIfRequired is a helper function that sets the bool reference for allowDirtyReads to the
// specified value, if required and reference is not nil.
func setDirtyReadFlagIfRequired(ctx context.Context, wasDirty bool) {
	if v := ctx.Value(keyAllowDirtyReads); v != nil {
		if ref, ok := v.(*bool); ok && ref != nil {
			*ref = wasDirty
		}
	}
}

// applyContextSettings returns the settings configured in the context in the given request.
// It then returns information about the applied settings that may be needed later in API implementation functions.
func applyContextSettings(ctx context.Context, req Request) contextSettings {
	result := contextSettings{}
	if ctx == nil {
		return result
	}
	// Details
	if v := ctx.Value(keyDetails); v != nil {
		if details, ok := v.(bool); ok {
			req.SetQuery("details", strconv.FormatBool(details))
		}
	}
	// KeepNull
	if v := ctx.Value(keyKeepNull); v != nil {
		if keepNull, ok := v.(bool); ok {
			req.SetQuery("keepNull", strconv.FormatBool(keepNull))
		}
	}
	// MergeObjects
	if v := ctx.Value(keyMergeObjects); v != nil {
		if mergeObjects, ok := v.(bool); ok {
			req.SetQuery("mergeObjects", strconv.FormatBool(mergeObjects))
		}
	}
	// Silent
	if v := ctx.Value(keySilent); v != nil {
		if silent, ok := v.(bool); ok {
			req.SetQuery("silent", strconv.FormatBool(silent))
			result.Silent = silent
		}
	}
	// WaitForSync
	if v := ctx.Value(keyWaitForSync); v != nil {
		if waitForSync, ok := v.(bool); ok {
			req.SetQuery("waitForSync", strconv.FormatBool(waitForSync))
			result.WaitForSync = waitForSync
		}
	}
	// AllowDirtyReads
	if v := ctx.Value(keyAllowDirtyReads); v != nil {
		req.SetHeader("x-arango-allow-dirty-read", "true")
		result.AllowDirtyReads = true
		if dirtyReadFlag, ok := v.(*bool); ok {
			result.DirtyReadFlag = dirtyReadFlag
		}
	}

	// Enable Queue timeout
	if v := ctx.Value(keyUseQueueTimeout); v != nil {
		if useQueueTimeout, ok := v.(bool); ok && useQueueTimeout {
			result.QueueTimeout = useQueueTimeout
			if v := ctx.Value(keyMaxQueueTime); v != nil {
				if timeout, ok := v.(time.Duration); ok {
					result.MaxQueueTime = timeout
					req.SetHeader("x-arango-queue-time-seconds", fmt.Sprint(timeout.Seconds()))
				}
			} else if deadline, ok := ctx.Deadline(); ok {
				timeout := deadline.Sub(time.Now())
				req.SetHeader("x-arango-queue-time-seconds", fmt.Sprint(timeout.Seconds()))
			}
		}
	}

	// TransactionID
	if v := ctx.Value(keyTransactionID); v != nil {
		req.SetHeader("x-arango-trx-id", string(v.(TransactionID)))
	}
	// ReturnOld
	if v := ctx.Value(keyReturnOld); v != nil {
		req.SetQuery("returnOld", "true")
		result.ReturnOld = v
	}
	// ReturnNew
	if v := ctx.Value(keyReturnNew); v != nil {
		req.SetQuery("returnNew", "true")
		result.ReturnNew = v
	}
	// If-Match
	if v := ctx.Value(keyRevision); v != nil {
		if rev, ok := v.(string); ok {
			req.SetHeader("If-Match", rev)
			result.Revision = rev
		}
	}
	// Revisions
	if v := ctx.Value(keyRevisions); v != nil {
		if revs, ok := v.([]string); ok {
			req.SetQuery("ignoreRevs", "false")
			result.Revisions = revs
		}
	}
	// ImportDetails
	if v := ctx.Value(keyImportDetails); v != nil {
		if details, ok := v.(*[]string); ok {
			req.SetQuery("details", "true")
			result.ImportDetails = details
		}
	}
	// IsRestore
	if v := ctx.Value(keyIsRestore); v != nil {
		if isRestore, ok := v.(bool); ok {
			req.SetQuery("isRestore", strconv.FormatBool(isRestore))
			result.IsRestore = isRestore
		}
	}
	// IsSystem
	if v := ctx.Value(keyIsSystem); v != nil {
		if isSystem, ok := v.(bool); ok {
			req.SetQuery("isSystem", strconv.FormatBool(isSystem))
			result.IsSystem = isSystem
		}
	}
	// IgnoreRevs
	if v := ctx.Value(keyIgnoreRevs); v != nil {
		if ignoreRevs, ok := v.(bool); ok {
			req.SetQuery("ignoreRevs", strconv.FormatBool(ignoreRevs))
			result.IgnoreRevs = &ignoreRevs
		}
	}
	// EnforeReplicationFactor
	if v := ctx.Value(keyEnforceReplicationFactor); v != nil {
		if enforceReplicationFactor, ok := v.(bool); ok {
			req.SetQuery("enforceReplicationFactor", strconv.FormatBool(enforceReplicationFactor))
			result.EnforceReplicationFactor = &enforceReplicationFactor
		}
	}
	// Configured
	if v := ctx.Value(keyConfigured); v != nil {
		if configured, ok := v.(bool); ok {
			req.SetQuery("configured", strconv.FormatBool(configured))
			result.Configured = &configured
		}
	}
	// FollowLeaderRedirect
	if v := ctx.Value(keyFollowLeaderRedirect); v != nil {
		if followLeaderRedirect, ok := v.(bool); ok {
			result.FollowLeaderRedirect = &followLeaderRedirect
		}
	}
	// DBServerID
	if v := ctx.Value(keyDBServerID); v != nil {
		if id, ok := v.(string); ok {
			req.SetQuery("DBserver", id)
			result.DBServerID = id
		}
	}
	// BatchID
	if v := ctx.Value(keyBatchID); v != nil {
		if id, ok := v.(string); ok {
			req.SetQuery("batchId", id)
			result.BatchID = id
		}
	}
	// JobIDResponse
	if v := ctx.Value(keyJobIDResponse); v != nil {
		if idRef, ok := v.(*string); ok {
			result.JobIDResponse = idRef
		}
	}
	// OverwriteMode
	if v := ctx.Value(keyOverwriteMode); v != nil {
		if mode, ok := v.(OverwriteMode); ok {
			req.SetQuery("overwriteMode", string(mode))
			result.OverwriteMode = mode
		}
	}

	if v := ctx.Value(keyOverwrite); v != nil {
		if overwrite, ok := v.(bool); ok && overwrite {
			req.SetQuery("overwrite", "true")
			result.Overwrite = true
		}
	}

	return result
}

// contextOrBackground returns the given context if it is not nil.
// Returns context.Background() otherwise.
func contextOrBackground(ctx context.Context) context.Context {
	if ctx != nil {
		return ctx
	}
	return context.Background()
}

// withDocumentAt returns a context derived from the given parent context to be used in multi-document options
// that needs a client side "loop" implementation.
// It handle:
// - WithRevisions
// - WithReturnNew
// - WithReturnOld
func withDocumentAt(ctx context.Context, index int) (context.Context, error) {
	if ctx == nil {
		return nil, nil
	}
	// Revisions
	if v := ctx.Value(keyRevisions); v != nil {
		if revs, ok := v.([]string); ok {
			if index >= len(revs) {
				return nil, WithStack(InvalidArgumentError{Message: "Index out of range: revisions"})
			}
			ctx = WithRevision(ctx, revs[index])
		}
	}
	// ReturnOld
	if v := ctx.Value(keyReturnOld); v != nil {
		val := reflect.ValueOf(v)
		ctx = WithReturnOld(ctx, val.Index(index).Addr().Interface())
	}
	// ReturnNew
	if v := ctx.Value(keyReturnNew); v != nil {
		val := reflect.ValueOf(v)
		ctx = WithReturnNew(ctx, val.Index(index).Addr().Interface())
	}

	return ctx, nil
}