package service

import (
	"bytes"
	"context"
	"time"

	"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/encoding"
	"github.com/pkg/errors"
)

type authDevice struct {
	Device     *fido2.Device
	DeviceInfo *fido2.DeviceInfo
	Provision  *vault.Provision
}

func findDevice(ctx context.Context, auths fido2.FIDO2Server, query string) (*authDevice, error) {
	if auths == nil {
		return nil, errors.Errorf("fido2 plugin not available")
	}
	if query == "" {
		return nil, errors.Errorf("no device specified")
	}
	devicesResp, err := auths.Devices(ctx, &fido2.DevicesRequest{})
	if err != nil {
		return nil, err
	}
	for _, device := range devicesResp.Devices {
		if device.Path == query || device.Product == query {
			infoResp, err := auths.DeviceInfo(ctx, &fido2.DeviceInfoRequest{Device: device.Path})
			if err != nil {
				// TODO: Test not a FIDO2 device
				logger.Infof("Failed to get device info: %s", err)
				continue
			}
			return &authDevice{
				Device:     device,
				DeviceInfo: infoResp.Info,
			}, nil
		}
	}
	return nil, nil
}

// findDeviceProvision returns a device with matching the auth credentials (aaguid).
func findDeviceProvision(ctx context.Context, auths fido2.FIDO2Server, provisions []*vault.Provision) (*authDevice, error) {
	if auths == nil {
		return nil, errors.Errorf("fido2 plugin not available")
	}
	if len(provisions) == 0 {
		return nil, errors.Errorf("no provisions specified")
	}

	devicesResp, err := auths.Devices(ctx, &fido2.DevicesRequest{})
	if err != nil {
		return nil, err
	}
	if len(devicesResp.Devices) == 0 {
		return nil, errors.Errorf("no devices found")
	}

	for _, device := range devicesResp.Devices {
		infoResp, err := auths.DeviceInfo(ctx, &fido2.DeviceInfoRequest{Device: device.Path})
		if err != nil {
			// TODO: Test not a FIDO2 device
			logger.Infof("Failed to get device info: %s", err)
			continue
		}
		deviceInfo := infoResp.Info
		logger.Debugf("Checking device: %v", deviceInfo)
		if deviceInfo.HasExtension(fido2.HMACSecretExtension) {
			provision := matchAAGUID(provisions, deviceInfo.AAGUID)
			if provision != nil {
				logger.Debugf("Found device: %v", device.Path)
				return &authDevice{
					Device:     device,
					DeviceInfo: deviceInfo,
					Provision:  provision,
				}, nil
			}
		}
	}
	return nil, errors.Errorf("no devices found matching our credentials")
}

func generateHMACSecret(ctx context.Context, auths fido2.FIDO2Server, vlt *vault.Vault, pin string, device string, appName string) (*vault.Provision, error) {
	if auths == nil {
		return nil, errors.Errorf("fido2 plugin not available")
	}
	if device == "" {
		return nil, errors.Errorf("no device specified")
	}

	cdh := bytes.Repeat([]byte{0x00}, 32) // No client data
	rp := &fido2.RelyingParty{
		ID:   "keys.pub",
		Name: "keys.pub",
	}

	logger.Debugf("Auth setup hmac-secret...")
	dev, err := findDevice(ctx, auths, device)
	if err != nil {
		return nil, err
	}
	if dev == nil {
		return nil, errors.Errorf("device not found: %s", device)
	}

	userID := keys.Rand16()[:]

	// TODO: Default to using resident key?

	logger.Debugf("Generating hmac-secret...")
	resp, err := auths.GenerateHMACSecret(ctx, &fido2.GenerateHMACSecretRequest{
		Device:         dev.Device.Path,
		PIN:            pin,
		ClientDataHash: cdh[:],
		RP:             rp,
		User: &fido2.User{
			ID:   userID,
			Name: appName,
		},
		// RK: fido2.True,
	})
	if err != nil {
		return nil, err
	}

	noPin := false
	if pin == "" {
		noPin = true
	}

	id := encoding.MustEncode(resp.CredentialID, encoding.Base62)
	salt := keys.Rand32()
	provision := &vault.Provision{
		ID:        id,
		Type:      vault.FIDO2HMACSecretAuth,
		AAGUID:    dev.DeviceInfo.AAGUID,
		Salt:      salt[:],
		NoPin:     noPin,
		CreatedAt: time.Now(),
	}

	logger.Debugf("Saving provision: %v...", provision)
	if err := vlt.ProvisionSave(provision); err != nil {
		return nil, err
	}

	return provision, nil
}

func hmacSecret(ctx context.Context, auths fido2.FIDO2Server, vlt *vault.Vault, pin string) ([]byte, *vault.Provision, error) {
	if auths == nil {
		return nil, nil, errors.Errorf("fido2 plugin not available")
	}
	cdh := bytes.Repeat([]byte{0x00}, 32) // No client data
	rp := &fido2.RelyingParty{
		ID:   "keys.pub",
		Name: "keys.pub",
	}

	provisions, err := vlt.Provisions()
	if err != nil {
		return nil, nil, err
	}
	if len(provisions) == 0 {
		return nil, nil, errors.Errorf("no provisions found for hmac-secret")
	}

	logger.Debugf("Looking for device with a matching credential...")
	authDevice, err := findDeviceProvision(ctx, auths, provisions)
	if err != nil {
		return nil, nil, err
	}
	if authDevice.Provision == nil {
		return nil, nil, errors.Errorf("device has no matching provision")
	}

	credID, err := encoding.Decode(authDevice.Provision.ID, encoding.Base62)
	if err != nil {
		return nil, nil, errors.Wrapf(err, "credential (provision) id was invalid")
	}

	logger.Debugf("Getting HMAC-Secret...")
	secretResp, err := auths.HMACSecret(ctx, &fido2.HMACSecretRequest{
		Device:         authDevice.Device.Path,
		PIN:            pin,
		ClientDataHash: cdh[:],
		RPID:           rp.ID,
		CredentialID:   credID,
		Salt:           authDevice.Provision.Salt,
	})
	if err != nil {
		return nil, nil, err
	}

	return secretResp.HMACSecret, authDevice.Provision, nil
}

func unlockHMACSecret(ctx context.Context, auths fido2.FIDO2Server, vlt *vault.Vault, pin string) error {
	secret, provision, err := hmacSecret(ctx, auths, vlt, pin)
	if err != nil {
		return err
	}
	if len(secret) != 32 {
		return errors.Errorf("invalid key length from hmac-secret")
	}
	key := keys.Bytes32(secret)

	// If we have setup hmac-secret provision but have not finished setup, we do
	// that on the first unlock. When we setup the hmac-secret, we use MakeCredential
	// which usually requires user presence (touching the device). Unlock also
	// usually requires user presence so we split up these blocking calls into
	// two requests. The first request doesn't give us the auth, so we do the
	// setup of first unlock.
	status, err := vlt.Status()
	if err != nil {
		return err
	}
	if status == vault.SetupNeeded {
		if err := vlt.Setup(key, provision); err != nil {
			return err
		}
		if _, err := vlt.Unlock(key); err != nil {
			return err
		}
	} else {
		if _, err := vlt.Unlock(key); err != nil {
			return err
		}
	}
	return nil
}

func provisionHMACSecret(ctx context.Context, auths fido2.FIDO2Server, vlt *vault.Vault, pin string) (*vault.Provision, error) {
	secret, provision, err := hmacSecret(ctx, auths, vlt, pin)
	if err != nil {
		return nil, err
	}
	if len(secret) != 32 {
		return nil, errors.Errorf("invalid key length for hmac secret")
	}
	key := keys.Bytes32(secret)

	if err := vlt.Provision(key, provision); err != nil {
		return nil, err
	}
	logger.Infof("Provision (hmac-secret): %s", provision.ID)
	return provision, nil
}
