// Package ctxutil provides a set of functions to manage context values
// in a gopass application. It allows to set and get values in the context.
package ctxutil

import (
	"context"
	"fmt"
	"time"

	"github.com/gopasspw/gopass/internal/store"
	"github.com/urfave/cli/v2"
)

type contextKey int

const (
	ctxKeyTerminal contextKey = iota
	ctxKeyInteractive
	ctxKeyStdin
	ctxKeyGitCommit
	ctxKeyAlwaysYes
	ctxKeyProgressCallback
	ctxKeyAlias
	ctxKeyGitInit
	ctxKeyForce
	ctxKeyCommitMessage
	ctxKeyNoNetwork
	ctxKeyUsername
	ctxKeyEmail
	ctxKeyImportFunc
	ctxKeyPasswordCallback
	ctxKeyPasswordPurgeCallback
	ctxKeyCommitTimestamp
	ctxKeyShowParsing
	ctxKeyHidden
	ctxFollowRef
)

// ErrNoCallback is returned when no callback is set in the context.
var ErrNoCallback = fmt.Errorf("no callback")

// WithGlobalFlags parses any global flags from the cli context and returns
// a regular context. It handles the --yes flag and sets the appropriate
// context value.
func WithGlobalFlags(c *cli.Context) context.Context {
	if c.Bool("yes") {
		return WithAlwaysYes(c.Context, true)
	}

	return c.Context
}

// ProgressCallback is a callback for updating progress.
type ProgressCallback func()

// WithTerminal returns a context with an explicit value for whether or not we are
// in a terminal.
func WithTerminal(ctx context.Context, isTerm bool) context.Context {
	return context.WithValue(ctx, ctxKeyTerminal, isTerm)
}

// HasTerminal returns true if a value for Terminal has been set in this context.
func HasTerminal(ctx context.Context) bool {
	_, ok := ctx.Value(ctxKeyTerminal).(bool)

	return ok
}

// IsTerminal returns the value of terminal or the default (true).
func IsTerminal(ctx context.Context) bool {
	bv, ok := ctx.Value(ctxKeyTerminal).(bool)
	if !ok {
		return true
	}

	return bv
}

// WithInteractive returns a context with an explicit value for whether or not we are
// in an interactive session.
func WithInteractive(ctx context.Context, isInteractive bool) context.Context {
	return context.WithValue(ctx, ctxKeyInteractive, isInteractive)
}

// HasInteractive returns true if a value for Interactive has been set in this context.
func HasInteractive(ctx context.Context) bool {
	_, ok := ctx.Value(ctxKeyInteractive).(bool)

	return ok
}

// IsInteractive returns the value of interactive or the default (true).
func IsInteractive(ctx context.Context) bool {
	bv, ok := ctx.Value(ctxKeyInteractive).(bool)
	if !ok {
		return true
	}

	return bv
}

// WithStdin returns a context with the value for Stdin set. If true some input
// is available on Stdin (e.g. something is being piped into it).
func WithStdin(ctx context.Context, isStdin bool) context.Context {
	return context.WithValue(ctx, ctxKeyStdin, isStdin)
}

// HasStdin returns true if a value for Stdin has been set in this context.
func HasStdin(ctx context.Context) bool {
	_, ok := ctx.Value(ctxKeyStdin).(bool)

	return ok
}

// IsStdin returns the value of stdin, i.e. if it's true some data is being
// piped to stdin. If not set it returns the default value (false).
func IsStdin(ctx context.Context) bool {
	bv, ok := ctx.Value(ctxKeyStdin).(bool)
	if !ok {
		return false
	}

	return bv
}

// WithShowParsing returns a context with the value for ShowParsing set.
// This is used to control whether to show parsing errors.
func WithShowParsing(ctx context.Context, bv bool) context.Context {
	return context.WithValue(ctx, ctxKeyShowParsing, bv)
}

// HasShowParsing returns true if a value for ShowParsing has been set in this context.
func HasShowParsing(ctx context.Context) bool {
	_, ok := ctx.Value(ctxKeyShowParsing).(bool)

	return ok
}

// IsShowParsing returns the value of ShowParsing or the default (true).
func IsShowParsing(ctx context.Context) bool {
	bv, ok := ctx.Value(ctxKeyShowParsing).(bool)
	if !ok {
		return true
	}

	return bv
}

// WithGitCommit returns a context with the value of git commit set.
// If true, changes will be committed to git.
func WithGitCommit(ctx context.Context, bv bool) context.Context {
	return context.WithValue(ctx, ctxKeyGitCommit, bv)
}

// HasGitCommit returns true if a value for GitCommit has been set in this context.
func HasGitCommit(ctx context.Context) bool {
	_, ok := ctx.Value(ctxKeyGitCommit).(bool)

	return ok
}

// IsGitCommit returns the value of git commit or the default (true).
func IsGitCommit(ctx context.Context) bool {
	return is(ctx, ctxKeyGitCommit, true)
}

// IsFollowRef returns the value of follow-ref or the default (false).
// If true, symlinks will be followed.
func IsFollowRef(ctx context.Context) bool {
	return is(ctx, ctxFollowRef, false)
}

// HasFollowRef returns true if a value for follow-ref has been set in this context.
func HasFollowRef(ctx context.Context) bool {
	_, ok := ctx.Value(ctxFollowRef).(bool)

	return ok
}

// WithFollowRef returns a context with the value of follow-ref set.
func WithFollowRef(ctx context.Context, bv bool) context.Context {
	return context.WithValue(ctx, ctxFollowRef, bv)
}

// WithAlwaysYes returns a context with the value of always yes set.
// If true, any prompts will be answered with yes.
func WithAlwaysYes(ctx context.Context, bv bool) context.Context {
	return context.WithValue(ctx, ctxKeyAlwaysYes, bv)
}

// HasAlwaysYes returns true if a value for AlwaysYes has been set in this context.
func HasAlwaysYes(ctx context.Context) bool {
	_, ok := ctx.Value(ctxKeyAlwaysYes).(bool)

	return ok
}

// IsAlwaysYes returns the value of always yes or the default (false).
func IsAlwaysYes(ctx context.Context) bool {
	bv, ok := ctx.Value(ctxKeyAlwaysYes).(bool)
	if !ok {
		return false
	}

	return bv
}

// WithProgressCallback returns a context with the value of ProgressCallback set.
func WithProgressCallback(ctx context.Context, cb ProgressCallback) context.Context {
	return context.WithValue(ctx, ctxKeyProgressCallback, cb)
}

// HasProgressCallback returns true if a ProgressCallback has been set.
func HasProgressCallback(ctx context.Context) bool {
	_, ok := ctx.Value(ctxKeyProgressCallback).(ProgressCallback)

	return ok
}

// GetProgressCallback return the set progress callback or a default one.
// It never returns nil.
func GetProgressCallback(ctx context.Context) ProgressCallback {
	cb, ok := ctx.Value(ctxKeyProgressCallback).(ProgressCallback)
	if !ok || cb == nil {
		return func() {}
	}

	return cb
}

// WithAlias returns a context with the alias set.
func WithAlias(ctx context.Context, alias string) context.Context {
	return context.WithValue(ctx, ctxKeyAlias, alias)
}

// HasAlias returns true if a value for alias has been set.
func HasAlias(ctx context.Context) bool {
	return hasString(ctx, ctxKeyAlias)
}

// GetAlias returns an alias if it has been set or an empty string otherwise.
func GetAlias(ctx context.Context) string {
	a, ok := ctx.Value(ctxKeyAlias).(string)
	if !ok {
		return ""
	}

	return a
}

// WithGitInit returns a context with the value for the git init flag set.
// If true, a git repository will be initialized.
func WithGitInit(ctx context.Context, bv bool) context.Context {
	return context.WithValue(ctx, ctxKeyGitInit, bv)
}

// HasGitInit returns true if the git init flag was set.
func HasGitInit(ctx context.Context) bool {
	return hasBool(ctx, ctxKeyGitInit)
}

// IsGitInit returns the value of the git init flag or true if none was set.
func IsGitInit(ctx context.Context) bool {
	return is(ctx, ctxKeyGitInit, true)
}

// WithForce returns a context with the force flag set.
// If true, operations that would otherwise fail will be forced to succeed.
func WithForce(ctx context.Context, bv bool) context.Context {
	return context.WithValue(ctx, ctxKeyForce, bv)
}

// HasForce returns true if the context has the force flag set.
func HasForce(ctx context.Context) bool {
	return hasBool(ctx, ctxKeyForce)
}

// IsForce returns the force flag value of the default (false).
func IsForce(ctx context.Context) bool {
	return is(ctx, ctxKeyForce, false)
}

// AddToCommitMessageBody returns a context with something added to the commit's body.
func AddToCommitMessageBody(ctx context.Context, sv string) context.Context {
	ht, ok := ctx.Value(ctxKeyCommitMessage).(*HeadedText)
	if !ok {
		var headedText HeadedText
		ht = &headedText
		ctx = context.WithValue(ctx, ctxKeyCommitMessage, ht)
	}
	ht.AddToBody(sv)

	return ctx
}

// HasCommitMessageBody returns true if the commit message body is nonempty.
func HasCommitMessageBody(ctx context.Context) bool {
	ht, ok := ctx.Value(ctxKeyCommitMessage).(*HeadedText)
	if !ok {
		return false
	}

	return ht.HasBody()
}

// GetCommitMessageBody returns the set commit message body or an empty string.
func GetCommitMessageBody(ctx context.Context) string {
	ht, ok := ctx.Value(ctxKeyCommitMessage).(*HeadedText)
	if !ok {
		return ""
	}

	return ht.GetBody()
}

// WithCommitMessage returns a context with a commit message (head) set.
// (full commit message is the commit message's body is not defined, commit message head otherwise).
func WithCommitMessage(ctx context.Context, sv string) context.Context {
	ht, ok := ctx.Value(ctxKeyCommitMessage).(*HeadedText)
	if !ok {
		var headedText HeadedText
		ht = &headedText
		ctx = context.WithValue(ctx, ctxKeyCommitMessage, ht)
	}
	ht.SetHead(sv)

	return ctx
}

// HasCommitMessage returns true if the commit message (head) was set.
func HasCommitMessage(ctx context.Context) bool {
	ht, ok := ctx.Value(ctxKeyCommitMessage).(*HeadedText)

	return ok && ht.head != "" // not the most intuitive answer, but a backwards-compatible one. for now.
}

// GetCommitMessage returns the set commit message (head) or an empty string.
func GetCommitMessage(ctx context.Context) string {
	ht, ok := ctx.Value(ctxKeyCommitMessage).(*HeadedText)
	if !ok {
		return ""
	}

	return ht.head
}

// GetCommitMessageFull returns the set commit message (head+body, if either are defined) or an empty string.
func GetCommitMessageFull(ctx context.Context) string {
	ht, ok := ctx.Value(ctxKeyCommitMessage).(*HeadedText)
	if !ok {
		return ""
	}

	return ht.GetText()
}

// WithNoNetwork returns a context with the value of no network set.
// If true, no network operations will be performed.
func WithNoNetwork(ctx context.Context, bv bool) context.Context {
	return context.WithValue(ctx, ctxKeyNoNetwork, bv)
}

// HasNoNetwork returns true if no network was set.
func HasNoNetwork(ctx context.Context) bool {
	return hasBool(ctx, ctxKeyNoNetwork)
}

// IsNoNetwork returns the value of no network or false.
func IsNoNetwork(ctx context.Context) bool {
	return is(ctx, ctxKeyNoNetwork, false)
}

// WithUsername returns a context with the username set in the context.
func WithUsername(ctx context.Context, sv string) context.Context {
	return context.WithValue(ctx, ctxKeyUsername, sv)
}

// GetUsername returns the username from the context.
func GetUsername(ctx context.Context) string {
	sv, ok := ctx.Value(ctxKeyUsername).(string)
	if !ok {
		return ""
	}

	return sv
}

// WithEmail returns a context with the email set in the context.
func WithEmail(ctx context.Context, sv string) context.Context {
	return context.WithValue(ctx, ctxKeyEmail, sv)
}

// GetEmail returns the email from the context.
func GetEmail(ctx context.Context) string {
	sv, ok := ctx.Value(ctxKeyEmail).(string)
	if !ok {
		return ""
	}

	return sv
}

// WithImportFunc will return a context with the import callback set.
// The callback is used to ask the user for confirmation before importing a key.
func WithImportFunc(ctx context.Context, imf store.ImportCallback) context.Context {
	return context.WithValue(ctx, ctxKeyImportFunc, imf)
}

// HasImportFunc returns true if a value for import func has been set in this
// context.
func HasImportFunc(ctx context.Context) bool {
	imf, ok := ctx.Value(ctxKeyImportFunc).(store.ImportCallback)

	return ok && imf != nil
}

// GetImportFunc will return the import callback or a default one returning true
// Note: will never return nil.
func GetImportFunc(ctx context.Context) store.ImportCallback {
	imf, ok := ctx.Value(ctxKeyImportFunc).(store.ImportCallback)
	if !ok || imf == nil {
		return func(context.Context, string, []string) bool {
			return true
		}
	}

	return imf
}

// PasswordCallback is a password prompt callback used in our age crypto backend.
// The arguments are typically the filename and whether or not to ask for a confirmation
// of the provided password, we use it notably together with askPass.
// It is linked to the GOPASS_AGE_PASSWORD env variable.
type PasswordCallback func(string, bool) ([]byte, error)

// WithPasswordCallback returns a context with the password callback set.
func WithPasswordCallback(ctx context.Context, cb PasswordCallback) context.Context {
	return context.WithValue(ctx, ctxKeyPasswordCallback, cb)
}

// HasPasswordCallback returns true if a password callback was set in the context.
func HasPasswordCallback(ctx context.Context) bool {
	_, ok := ctx.Value(ctxKeyPasswordCallback).(PasswordCallback)

	return ok
}

// GetPasswordCallback returns the password callback or a default (which always fails).
// The default callback returns ErrNoCallback.
func GetPasswordCallback(ctx context.Context) PasswordCallback {
	pwcb, ok := ctx.Value(ctxKeyPasswordCallback).(PasswordCallback)
	if !ok || pwcb == nil {
		return func(string, bool) ([]byte, error) {
			return nil, ErrNoCallback
		}
	}

	return pwcb
}

// PasswordPurgeCallback is a callback to purge a password cached by PasswordCallback.
type PasswordPurgeCallback func(string)

// WithPasswordPurgeCallback returns a context with the password purge callback set.
func WithPasswordPurgeCallback(ctx context.Context, cb PasswordPurgeCallback) context.Context {
	return context.WithValue(ctx, ctxKeyPasswordPurgeCallback, cb)
}

// HasPasswordPurgeCallback returns true if a password purge callback was set in the context.
func HasPasswordPurgeCallback(ctx context.Context) bool {
	_, ok := ctx.Value(ctxKeyPasswordPurgeCallback).(PasswordPurgeCallback)

	return ok
}

// GetPasswordPurgeCallback returns the password purge callback or a default (which is a no-op).
// The default callback does nothing.
func GetPasswordPurgeCallback(ctx context.Context) PasswordPurgeCallback {
	ppcb, ok := ctx.Value(ctxKeyPasswordPurgeCallback).(PasswordPurgeCallback)
	if !ok || ppcb == nil {
		return func(string) {}
	}

	return ppcb
}

// WithCommitTimestamp returns a context with the value for the commit
// timestamp set.
// This is used to allow for reproducible builds.
func WithCommitTimestamp(ctx context.Context, ts time.Time) context.Context {
	return context.WithValue(ctx, ctxKeyCommitTimestamp, ts)
}

// HasCommitTimestamp returns true if the value for the commit timestamp
// was set in the context.
func HasCommitTimestamp(ctx context.Context) bool {
	_, ok := ctx.Value(ctxKeyCommitTimestamp).(time.Time)

	return ok
}

// GetCommitTimestamp returns the commit timestamp from the context if
// set or the default (now) otherwise.
func GetCommitTimestamp(ctx context.Context) time.Time {
	if ts, ok := ctx.Value(ctxKeyCommitTimestamp).(time.Time); ok {
		return ts
	}

	return time.Now()
}

// WithHidden returns a context with the flag value for hidden set.
// This is used to hide secrets from the output.
func WithHidden(ctx context.Context, hidden bool) context.Context {
	return context.WithValue(ctx, ctxKeyHidden, hidden)
}

// IsHidden returns true if any output should be hidden in this context.
func IsHidden(ctx context.Context) bool {
	bv, ok := ctx.Value(ctxKeyHidden).(bool)
	if !ok {
		return false
	}

	return bv
}
