package service

import (
	"context"
	"encoding/json"
	"os"

	"github.com/keys-pub/keys"
	"github.com/keys-pub/keys-ext/vault"
	"github.com/keys-pub/keys/encoding"
	"github.com/keys-pub/keys/tsutil"
	"github.com/pkg/errors"
)

// AuthSetup (RPC) ...
func (s *service) AuthSetup(ctx context.Context, req *AuthSetupRequest) (*AuthSetupResponse, error) {
	logger.Infof("Auth setup...")
	status, err := s.vault.Status()
	if err != nil {
		return nil, err
	}
	if status != vault.SetupNeeded {
		return nil, errors.Errorf("auth already setup")
	}

	if err := s.auth.setup(ctx, s.vault, req); err != nil {
		return nil, err
	}

	// If setting up auth, and service database exists we should nuke it since the
	// pre-existing key is different. The database will be rebuilt on Open.
	path, err := s.env.AppPath(cdbPath, false)
	if err != nil {
		return nil, err
	}
	logger.Debugf("Checking for existing db...")
	exists, err := pathExists(path)
	if err != nil {
		return nil, err
	}
	if exists {
		logger.Debugf("Removing existing db: %s", path)
		if err := os.RemoveAll(path); err != nil {
			return nil, err
		}
	}

	return &AuthSetupResponse{}, nil
}

// AuthVault (RPC) ...
func (s *service) AuthVault(ctx context.Context, req *AuthVaultRequest) (*AuthVaultResponse, error) {
	logger.Infof("Auth vault...")
	status, err := s.vault.Status()
	if err != nil {
		return nil, err
	}
	if status != vault.SetupNeeded {
		return nil, errors.Errorf("auth already setup")
	}

	otkSeed, err := encoding.Decode(req.Phrase, encoding.BIP39)
	if err != nil {
		return nil, err
	}
	if len(otkSeed) != 32 {
		return nil, errors.Errorf("invalid byte length for otk")
	}
	otk := keys.NewEdX25519KeyFromSeed(keys.Bytes32(otkSeed))

	remoteBytes, err := s.client.ShareOpen(ctx, otk)
	if err != nil {
		return nil, err
	}
	if remoteBytes == nil {
		return nil, errors.Errorf("vault not found")
	}
	var remote vault.Remote
	if err := json.Unmarshal(remoteBytes, &remote); err != nil {
		return nil, errors.Wrapf(err, "invalid vault remote bytes")
	}
	if err := s.vault.Clone(ctx, &remote); err != nil {
		return nil, err
	}

	return &AuthVaultResponse{}, nil
}

// AuthUnlock (RPC) ...
func (s *service) AuthUnlock(ctx context.Context, req *AuthUnlockRequest) (*AuthUnlockResponse, error) {
	token, err := s.unlock(ctx, req)
	if err != nil {
		// if errors.Cause(err).Error() == "pin required" {
		// 	return nil, status.Error(codes.FailedPrecondition, "pin required")
		// }
		return nil, err
	}
	return &AuthUnlockResponse{
		AuthToken: token,
	}, nil
}

// AuthLock (RPC) ...
func (s *service) AuthLock(ctx context.Context, req *AuthLockRequest) (*AuthLockResponse, error) {
	s.lock()
	return &AuthLockResponse{}, nil
}

// AuthProvision (RPC) ...
func (s *service) AuthProvision(ctx context.Context, req *AuthProvisionRequest) (*AuthProvisionResponse, error) {
	provision, err := s.auth.provision(ctx, s.vault, req)
	if err != nil {
		return nil, err
	}
	return &AuthProvisionResponse{
		Provision: provisionToRPC(provision),
	}, nil
}

// AuthDeprovision (RPC) ...
func (s *service) AuthDeprovision(ctx context.Context, req *AuthDeprovisionRequest) (*AuthDeprovisionResponse, error) {
	ok, err := s.vault.Deprovision(req.ID, false)
	if err != nil {
		return nil, err
	}
	if !ok {
		return nil, keys.NewErrNotFound(req.ID)
	}

	// TODO: If FIDO2 resident key and supports credMgmt remove from the device also?

	return &AuthDeprovisionResponse{}, nil
}

// AuthProvisions (RPC) ...
func (s *service) AuthProvisions(ctx context.Context, req *AuthProvisionsRequest) (*AuthProvisionsResponse, error) {
	provisions, err := s.vault.Provisions()
	if err != nil {
		return nil, err
	}

	out := make([]*AuthProvision, 0, len(provisions))
	for _, provision := range provisions {
		out = append(out, provisionToRPC(provision))
	}

	return &AuthProvisionsResponse{
		Provisions: out,
	}, nil
}

type testClientAuth struct {
	token string
}

func newTestClientAuth(token string) testClientAuth {
	return testClientAuth{token: token}
}

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

func (a testClientAuth) RequireTransportSecurity() bool {
	// For test client
	return false
}

func provisionToRPC(p *vault.Provision) *AuthProvision {
	return &AuthProvision{
		ID:        p.ID,
		Type:      authTypeToRPC(p.Type),
		AAGUID:    p.AAGUID,
		NoPin:     p.NoPin,
		CreatedAt: tsutil.Millis(p.CreatedAt),
	}
}

func authTypeToRPC(t vault.AuthType) AuthType {
	switch t {
	case vault.PasswordAuth:
		return PasswordAuth
	case vault.PaperKeyAuth:
		return PaperKeyAuth
	case vault.FIDO2HMACSecretAuth:
		return FIDO2HMACSecretAuth
	default:
		return UnknownAuth
	}
}

func matchAAGUID(provisions []*vault.Provision, aaguid string) *vault.Provision {
	for _, provision := range provisions {
		if provision.AAGUID == aaguid {
			return provision
		}
	}
	return nil
}

func (s *service) AuthRecover(ctx context.Context, req *AuthRecoverRequest) (*AuthRecoverResponse, error) {
	if req.PaperKey == "" {
		return nil, errors.Errorf("no paper key specified")
	}
	if req.NewPassword == "" {
		return nil, errors.Errorf("no password specified")
	}
	unlockResp, err := s.AuthUnlock(ctx, &AuthUnlockRequest{
		Secret: req.PaperKey,
		Type:   PaperKeyAuth,
	})
	if err != nil {
		return nil, err
	}
	if _, err := s.AuthProvision(ctx, &AuthProvisionRequest{
		Secret: req.NewPassword,
		Type:   PasswordAuth,
	}); err != nil {
		return nil, err
	}

	return &AuthRecoverResponse{
		AuthToken: unlockResp.AuthToken,
	}, nil
}

func (s *service) AuthReset(ctx context.Context, req *AuthResetRequest) (*AuthResetResponse, error) {
	if s.unlocked {
		return nil, errors.Wrapf(errors.Errorf("auth is unlocked"), "failed to reset")
	}

	if req.AppName != s.env.AppName() {
		return nil, errors.Wrapf(errors.Errorf("invalid app name"), "failed to reset")
	}

	if err := s.vault.Reset(); err != nil {
		return nil, err
	}

	path, err := s.env.AppPath(cdbPath, false)
	if err != nil {
		return nil, err
	}
	if err := os.RemoveAll(path); err != nil {
		return nil, err
	}

	return &AuthResetResponse{}, nil
}
