package service

import (
	"context"
	"sync"

	"github.com/keys-pub/keys"
	"github.com/keys-pub/keys-ext/auth/fido2"
	"github.com/keys-pub/keys-ext/vault"
	"github.com/keys-pub/keys/dstore"
	"github.com/keys-pub/keys/encoding"
	"github.com/pkg/errors"
	"google.golang.org/grpc"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/metadata"
	"google.golang.org/grpc/status"
)

// TODO: Some clients could log grpc requests which for AuthSetup and AuthUnlock include a password.
//       We need to ensure client logging doesn't do this in the future accidentally.

type auth struct {
	sync.Mutex
	env       *Env
	tokens    map[string]string
	allowlist *dstore.StringSet

	fas fido2.FIDO2Server
}

func newAuth(env *Env) *auth {
	// We don't need auth for the following methods.
	allowlist := dstore.NewStringSet(
		"/service.RPC/AuthSetup",
		"/service.RPC/AuthUnlock",
		"/service.RPC/AuthLock",
		"/service.RPC/AuthVault",
		"/service.RPC/AuthReset",
		"/service.RPC/AuthRecover",
		"/service.RPC/Rand",
		"/service.RPC/RandPassword",
		"/service.RPC/RuntimeStatus",
	)

	return &auth{
		env:       env,
		tokens:    map[string]string{},
		allowlist: allowlist,
	}
}

func (a *auth) setup(ctx context.Context, vlt *vault.Vault, req *AuthSetupRequest) error {
	logger.Infof("Setup (%s)", req.Type)
	switch req.Type {
	case PasswordAuth:
		if err := setupPassword(vlt, req.Secret); err != nil {
			return authErr(err, req.Type, "failed to setup")
		}
		return nil
	case PaperKeyAuth:
		// TODO: Implement
		return errors.Errorf("setup with paper key not supported")
	case FIDO2HMACSecretAuth:
		_, err := generateHMACSecret(ctx, a.fas, vlt, req.Secret, req.Device, a.env.AppName())
		if err != nil {
			return authErr(err, req.Type, "failed to setup")
		}
		return nil
	default:
		return errors.Errorf("unsupported auth type")
	}
}

func (a *auth) unlock(ctx context.Context, vlt *vault.Vault, req *AuthUnlockRequest) (string, error) {
	logger.Infof("Unlock (%s)", req.Type)

	switch req.Type {
	case PasswordAuth:
		if _, err := unlockPassword(vlt, req.Secret); err != nil {
			return "", authErr(err, req.Type, "failed to unlock")
		}
	case PaperKeyAuth:
		if _, err := unlockPaperKey(vlt, req.Secret); err != nil {
			return "", authErr(err, req.Type, "failed to unlock")
		}
	case FIDO2HMACSecretAuth:
		if err := unlockHMACSecret(ctx, a.fas, vlt, req.Secret); err != nil {
			return "", authErr(err, req.Type, "failed to unlock")
		}
	default:
		return "", errors.Errorf("unsupported auth type")
	}

	logger.Infof("Unlocked (%s)", req.Type)
	token := a.registerToken(req.Client)
	return token, nil
}

func (a *auth) lock(vlt *vault.Vault) {
	a.tokens = map[string]string{}
	vlt.Lock()
}

func (a *auth) provision(ctx context.Context, vlt *vault.Vault, req *AuthProvisionRequest) (*vault.Provision, error) {
	logger.Infof("Provision (%s)", req.Type)
	switch req.Type {
	case PasswordAuth:
		return provisionPassword(vlt, req.Secret)
	case PaperKeyAuth:
		return provisionPaperKey(vlt, req.Secret)
	case FIDO2HMACSecretAuth:
		if req.Generate {
			logger.Infof("Generate FIDO2 HMAC-Secret with device ", req.Device)
			return generateHMACSecret(ctx, a.fas, vlt, req.Secret, req.Device, a.env.AppName())
		}
		logger.Infof("Provision FIDO2 HMAC-Secret...")
		return provisionHMACSecret(ctx, a.fas, vlt, req.Secret)
	default:
		return nil, errors.Errorf("unknown auth type")
	}
}

func (a *auth) registerToken(client string) string {
	token := generateToken()
	logger.Debugf("Auth register client: %s", client)
	a.tokens[client] = token
	return token
}

// ErrInvalidPassword if invalid password.
var ErrInvalidPassword = status.Error(codes.Unauthenticated, "invalid password")

// ErrInvalidAuth if invalid auth.
var ErrInvalidAuth = status.Error(codes.Unauthenticated, "invalid auth")

func authErr(err error, typ AuthType, wrap string) error {
	if errors.Cause(err) == vault.ErrInvalidAuth {
		switch typ {
		case PasswordAuth:
			return ErrInvalidPassword
		default:
			return ErrInvalidAuth
		}

	}
	return errors.Wrapf(err, wrap)
}

func generateToken() string {
	return encoding.MustEncode(keys.Rand32()[:], encoding.Base62)
}

func (a *auth) streamInterceptor(srv interface{}, stream grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
	if err := a.authorize(stream.Context(), info.FullMethod); err != nil {
		return err
	}
	return handler(srv, stream)
}

func (a *auth) unaryInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
	if err := a.authorize(ctx, info.FullMethod); err != nil {
		return nil, err
	}
	return handler(ctx, req)
}

func (a *auth) checkToken(token string) error {
	for _, t := range a.tokens {
		if t == token {
			return nil
		}
	}
	logger.Infof("Invalid auth token")
	return status.Error(codes.Unauthenticated, "invalid token")
}

func (a *auth) authorize(ctx context.Context, method string) error {
	// No authorization needed for allowed methods.
	if a.allowlist.Contains(method) {
		// TODO: Auth token could be set and invalid here
		logger.Infof("Authorization is not required for %s", method)
		return nil
	}

	logger.Infof("Authorize %s", method)
	if md, ok := metadata.FromIncomingContext(ctx); ok {
		if len(md["authorization"]) == 0 {
			logger.Warningf("Auth token missing from request")
			return status.Error(codes.Unauthenticated, "authorization missing")
		}
		token := md["authorization"][0]
		return a.checkToken(token)
	}
	return status.Error(codes.Unauthenticated, "no authorization in context")
}

type clientAuth struct {
	token string
}

func newClientAuth(token string) clientAuth {
	return clientAuth{token: token}
}

func (a clientAuth) GetRequestMetadata(context.Context, ...string) (map[string]string, error) {
	if a.token == "" {
		return nil, nil
	}
	return map[string]string{
		"authorization": a.token,
	}, nil
}

func (a clientAuth) RequireTransportSecurity() bool {
	return true
}
