// Copyright 2015 Keybase, Inc. All rights reserved. Use of
// this source code is governed by the included BSD license.

package engine

import (
	"errors"
	"fmt"

	"github.com/keybase/client/go/libkb"
	keybase1 "github.com/keybase/client/go/protocol/keybase1"
)

// TrackToken is an engine.
type TrackToken struct {
	libkb.Contextified
	arg                 *TrackTokenArg
	them                *libkb.User
	trackStatementBytes []byte
	trackStatement      *libkb.ProofMetadataRes
}

type TrackTokenArg struct {
	Token   keybase1.TrackToken
	Me      *libkb.User
	Options keybase1.TrackOptions
	Outcome *libkb.IdentifyOutcome
}

// NewTrackToken creates a TrackToken engine.
func NewTrackToken(g *libkb.GlobalContext, arg *TrackTokenArg) *TrackToken {
	if arg.Options.SigVersion == nil || libkb.SigVersion(*arg.Options.SigVersion) == libkb.KeybaseNullSigVersion {
		tmp := keybase1.SigVersion(libkb.GetDefaultSigVersion(g))
		arg.Options.SigVersion = &tmp
	}

	return &TrackToken{
		arg:          arg,
		Contextified: libkb.NewContextified(g),
	}
}

// Name is the unique engine name.
func (e *TrackToken) Name() string {
	return "TrackToken"
}

// GetPrereqs returns the engine prereqs.
func (e *TrackToken) Prereqs() Prereqs {
	return Prereqs{
		Device: true,
	}
}

// RequiredUIs returns the required UIs.
func (e *TrackToken) RequiredUIs() []libkb.UIKind {
	return []libkb.UIKind{}
}

// SubConsumers returns the other UI consumers for this engine.
func (e *TrackToken) SubConsumers() []libkb.UIConsumer {
	return nil
}

// Run starts the engine.
func (e *TrackToken) Run(m libkb.MetaContext) (err error) {
	defer m.Trace("TrackToken#Run", func() error { return err })()

	if len(e.arg.Token) == 0 && e.arg.Outcome == nil {
		err = fmt.Errorf("missing TrackToken argument")
		return err
	}
	if err = e.loadMe(m); err != nil {
		m.Info("loadme err: %s", err)
		return err
	}

	// We can either pass this in directly, or look it up via TrackToken
	outcome := e.arg.Outcome
	if outcome == nil {
		outcome, err = m.G().TrackCache().Get(e.arg.Token)
		if err != nil {
			return err
		}
	}

	if outcome.TrackStatus() == keybase1.TrackStatus_UPDATE_OK && !e.arg.Options.ForceRetrack {
		m.Debug("tracking statement up-to-date.")
		return nil
	}

	if err = e.loadThem(m, outcome.Username); err != nil {
		return err
	}

	if e.arg.Me.Equal(e.them) {
		err = libkb.SelfTrackError{}
		return err
	}

	if err = e.isTrackTokenStale(m, outcome); err != nil {
		m.Debug("Track statement is stale")
		return err
	}

	// need public signing key for track statement
	signingKeyPub, err := e.arg.Me.SigningKeyPub()
	if err != nil {
		return err
	}

	e.trackStatement, err = e.arg.Me.TrackingProofFor(m, signingKeyPub, libkb.SigVersion(*e.arg.Options.SigVersion), e.them, outcome)
	if err != nil {
		m.Debug("tracking proof err: %s", err)
		return err
	}
	if e.trackStatementBytes, err = e.trackStatement.J.Marshal(); err != nil {
		return err
	}

	m.Debug("| Tracking statement: %s", string(e.trackStatementBytes))

	if e.arg.Options.LocalOnly || e.arg.Options.ExpiringLocal {
		m.Debug("| Local")
		err = e.storeLocalTrack(m)
	} else {
		err = e.storeRemoteTrack(m, signingKeyPub.GetKID())
		if err == nil {
			// if the remote track succeeded, remove local tracks
			// (this also removes any snoozes)
			err := e.removeLocalTracks(m)
			if err != nil {
				return err
			}
		}
	}
	if err != nil {
		return err
	}
	themUPAK, err := e.them.ExportToUPKV2AllIncarnations()
	if err != nil {
		return err
	}
	err = m.G().Pegboard.TrackUPAK(m, themUPAK.Current)
	if err != nil {
		return err
	}

	// Remove this after desktop notification change complete:
	m.G().UserChanged(m.Ctx(), e.them.GetUID())

	// Remove these after desktop notification change complete, but
	// add in: m.G().BustLocalUserCache(e.arg.Me.GetUID())
	m.G().UserChanged(m.Ctx(), e.arg.Me.GetUID())

	// Keep these:
	m.G().NotifyRouter.HandleTrackingChanged(e.arg.Me.GetUID(), e.arg.Me.GetNormalizedName(), false)
	m.G().NotifyRouter.HandleTrackingChanged(e.them.GetUID(), e.them.GetNormalizedName(), true)

	// Dismiss any associated gregor item.
	if outcome.ResponsibleGregorItem != nil {
		err = m.G().GregorState.DismissItem(m.Ctx(), nil,
			outcome.ResponsibleGregorItem.Metadata().MsgID())
	}

	return err
}

func (e *TrackToken) isTrackTokenStale(m libkb.MetaContext, o *libkb.IdentifyOutcome) (err error) {
	if idt := e.arg.Me.IDTable(); idt == nil {
		return nil
	} else if tm := idt.GetTrackMap(); tm == nil {
		return nil
	} else if v := tm[o.Username]; len(v) == 0 {
		return nil
	} else if lastTrack := v[len(v)-1]; lastTrack != nil && !lastTrack.IsRevoked() && o.TrackUsed == nil {
		// If we had a valid track that we didn't use in the identification, then
		// someone must have slipped in before us. Distinguish this case from the
		// other case below for the purposes of testing, to make sure we hit
		// both error cases in our tests.
		return libkb.TrackStaleError{FirstTrack: true}
	} else if o.TrackUsed == nil || lastTrack == nil {
		return nil
	} else if o.TrackUsed.GetTrackerSeqno() < lastTrack.GetSeqno() {
		// Similarly, if there was a last track for this user that wasn't the
		// one we were expecting, someone also must have intervened.
		m.Debug("Stale track! We were at seqno %d, but %d is already in chain", o.TrackUsed.GetTrackerSeqno(), lastTrack.GetSeqno())
		return libkb.TrackStaleError{FirstTrack: false}
	}
	return nil
}

func (e *TrackToken) loadMe(m libkb.MetaContext) error {
	if e.arg.Me != nil {
		return nil
	}

	me, err := libkb.LoadMe(libkb.NewLoadUserArgWithMetaContext(m))
	if err != nil {
		return err
	}
	e.arg.Me = me
	return nil
}

func (e *TrackToken) loadThem(m libkb.MetaContext, username libkb.NormalizedUsername) error {

	arg := libkb.NewLoadUserArgWithMetaContext(m).WithName(username.String()).WithPublicKeyOptional()
	them, err := libkb.LoadUser(arg)
	if err != nil {
		return err
	}
	e.them = them
	return nil
}

func (e *TrackToken) storeLocalTrack(m libkb.MetaContext) error {
	return libkb.StoreLocalTrack(m, e.arg.Me.GetUID(), e.them.GetUID(), e.arg.Options.ExpiringLocal, e.trackStatement.J)
}

func (e *TrackToken) storeRemoteTrack(m libkb.MetaContext, pubKID keybase1.KID) (err error) {
	defer m.Trace("TrackToken#StoreRemoteTrack", func() error { return err })()

	// need unlocked signing key
	me := e.arg.Me
	ska := libkb.SecretKeyArg{
		Me:      me,
		KeyType: libkb.DeviceSigningKeyType,
	}
	arg := m.SecretKeyPromptArg(ska, "tracking signature")
	signingKey, err := m.G().Keyrings.GetSecretKeyWithPrompt(m, arg)
	if err != nil {
		return err
	}
	if signingKey == nil {
		return libkb.NoSecretKeyError{}
	}
	// double-check that the KID of the unlocked key matches
	if signingKey.GetKID().NotEqual(pubKID) {
		return errors.New("unexpeceted KID mismatch between locked and unlocked signing key")
	}

	sigVersion := libkb.SigVersion(*e.arg.Options.SigVersion)
	sig, sigID, linkID, err := libkb.MakeSig(
		m,
		signingKey,
		libkb.LinkTypeTrack,
		e.trackStatementBytes,
		libkb.SigHasRevokes(false),
		keybase1.SeqType_PUBLIC,
		libkb.SigIgnoreIfUnsupported(false),
		me,
		sigVersion,
	)

	if err != nil {
		return err
	}

	httpsArgs := libkb.HTTPArgs{
		"sig_id_base":  libkb.S{Val: sigID.ToString(false)},
		"sig_id_short": libkb.S{Val: sigID.ToShortID()},
		"sig":          libkb.S{Val: sig},
		"uid":          libkb.UIDArg(e.them.GetUID()),
		"type":         libkb.S{Val: "track"},
		"signing_kid":  signingKey.GetKID(),
	}

	if sigVersion == libkb.KeybaseSignatureV2 {
		httpsArgs["sig_inner"] = libkb.S{Val: string(e.trackStatementBytes)}
	}
	_, err = m.G().API.Post(m, libkb.APIArg{
		Endpoint:    "follow",
		SessionType: libkb.APISessionTypeREQUIRED,
		Args:        httpsArgs,
	})
	if err != nil {
		m.Warning("api error: %s", err)
		return err
	}
	if err = libkb.MerkleCheckPostedUserSig(m, me.GetUID(), e.trackStatement.Seqno, linkID); err != nil {
		return err
	}

	me.SigChainBump(linkID, sigID, false)
	m.G().IdentifyDispatch.NotifyTrackingSuccess(m, e.them.GetUID())

	return err
}

func (e *TrackToken) removeLocalTracks(m libkb.MetaContext) (err error) {
	defer m.Trace("removeLocalTracks", func() error { return err })()
	err = libkb.RemoveLocalTracks(m, e.arg.Me.GetUID(), e.them.GetUID())
	return err
}
