package cursor

import (
	"context"
	"encoding/base64"
	"errors"
	"fmt"

	v1 "github.com/authzed/authzed-go/proto/authzed/api/v1"

	"github.com/authzed/spicedb/pkg/datastore"
	dispatch "github.com/authzed/spicedb/pkg/proto/dispatch/v1"
	impl "github.com/authzed/spicedb/pkg/proto/impl/v1"
	"github.com/authzed/spicedb/pkg/spiceerrors"
	"github.com/authzed/spicedb/pkg/zedtoken"
)

// Encode converts a decoded cursor to its opaque version.
func Encode(decoded *impl.DecodedCursor) (*v1.Cursor, error) {
	marshalled, err := decoded.MarshalVT()
	if err != nil {
		return nil, NewInvalidCursorErr(fmt.Errorf(errEncodeError, err))
	}

	return &v1.Cursor{
		Token: base64.StdEncoding.EncodeToString(marshalled),
	}, nil
}

// Decode converts an encoded cursor to its decoded version.
func Decode(encoded *v1.Cursor) (*impl.DecodedCursor, error) {
	if encoded == nil {
		return nil, NewInvalidCursorErr(errors.New("cursor pointer was nil"))
	}

	decodedBytes, err := base64.StdEncoding.DecodeString(encoded.Token)
	if err != nil {
		return nil, NewInvalidCursorErr(fmt.Errorf(errDecodeError, err))
	}

	decoded := &impl.DecodedCursor{}
	if err := decoded.UnmarshalVT(decodedBytes); err != nil {
		return nil, NewInvalidCursorErr(fmt.Errorf(errDecodeError, err))
	}

	return decoded, nil
}

// EncodeFromDispatchCursor encodes an internal dispatching cursor into a V1 cursor for external
// consumption, including the provided call context to ensure the API cursor reflects the calling
// API method. The call hash should contain all the parameters of the calling API function,
// as well as its revision and name.
func EncodeFromDispatchCursor(dispatchCursor *dispatch.Cursor, callAndParameterHash string, revision datastore.Revision, flags map[string]string) (*v1.Cursor, error) {
	if dispatchCursor == nil {
		return nil, spiceerrors.MustBugf("got nil dispatch cursor")
	}

	return Encode(&impl.DecodedCursor{
		VersionOneof: &impl.DecodedCursor_V1{
			V1: &impl.V1Cursor{
				Revision:              revision.String(),
				DispatchVersion:       dispatchCursor.DispatchVersion,
				Sections:              dispatchCursor.Sections,
				CallAndParametersHash: callAndParameterHash,
				Flags:                 flags,
			},
		},
	})
}

func EncodeFromDispatchCursorSections(dispatchCursorSections []string, callAndParameterHash string, revision datastore.Revision, flags map[string]string) (*v1.Cursor, error) {
	return Encode(&impl.DecodedCursor{
		VersionOneof: &impl.DecodedCursor_V1{
			V1: &impl.V1Cursor{
				Revision:              revision.String(),
				DispatchVersion:       1,
				Sections:              dispatchCursorSections,
				CallAndParametersHash: callAndParameterHash,
				Flags:                 flags,
			},
		},
	})
}

// GetCursorFlag retrieves a flag from an encoded API cursor, if any.
func GetCursorFlag(encoded *v1.Cursor, flagName string) (string, bool, error) {
	decoded, err := Decode(encoded)
	if err != nil {
		return "", false, err
	}

	v1decoded := decoded.GetV1()
	if v1decoded == nil {
		return "", false, NewInvalidCursorErr(ErrNilCursor)
	}

	value, ok := v1decoded.Flags[flagName]
	return value, ok, nil
}

// DecodeToDispatchCursor decodes an encoded API cursor into an internal dispatching cursor,
// ensuring that the provided call context matches that encoded into the API cursor. The call
// hash should contain all the parameters of the calling API function, as well as its revision
// and name.
func DecodeToDispatchCursor(encoded *v1.Cursor, callAndParameterHash string) (*dispatch.Cursor, map[string]string, error) {
	decoded, err := Decode(encoded)
	if err != nil {
		return nil, nil, err
	}

	v1decoded := decoded.GetV1()
	if v1decoded == nil {
		return nil, nil, NewInvalidCursorErr(ErrNilCursor)
	}

	if v1decoded.CallAndParametersHash != callAndParameterHash {
		return nil, nil, NewInvalidCursorErr(ErrHashMismatch)
	}

	return &dispatch.Cursor{
		DispatchVersion: v1decoded.DispatchVersion,
		Sections:        v1decoded.Sections,
	}, v1decoded.Flags, nil
}

// DecodeToDispatchRevision decodes an encoded API cursor into an internal dispatch revision.
// NOTE: this method does *not* verify the caller's method signature.
func DecodeToDispatchRevision(ctx context.Context, encoded *v1.Cursor, ds revisionDecoder) (datastore.Revision, zedtoken.TokenStatus, error) {
	decoded, err := Decode(encoded)
	if err != nil {
		return nil, zedtoken.StatusUnknown, err
	}

	v1decoded := decoded.GetV1()
	if v1decoded == nil {
		return nil, zedtoken.StatusUnknown, ErrNilCursor
	}

	datastoreUniqueID, err := ds.UniqueID(ctx)
	if err != nil {
		return nil, zedtoken.StatusUnknown, fmt.Errorf(errEncodeError, err)
	}

	parsed, err := ds.RevisionFromString(v1decoded.Revision)
	if err != nil {
		return datastore.NoRevision, zedtoken.StatusUnknown, fmt.Errorf(errDecodeError, err)
	}

	if v1decoded.DatastoreUniqueId == "" {
		return parsed, zedtoken.StatusLegacyEmptyDatastoreID, nil
	}

	if v1decoded.DatastoreUniqueId != datastoreUniqueID {
		return parsed, zedtoken.StatusMismatchedDatastoreID, nil
	}

	return parsed, zedtoken.StatusValid, nil
}

type revisionDecoder interface {
	UniqueID(_ context.Context) (string, error)
	RevisionFromString(string) (datastore.Revision, error)
}
