package handler

import (
	"context"
	"time"

	"github.com/micro/micro/v3/service/errors"
	"github.com/micro/micro/v3/service/logger"
	pb "github.com/micro/services/otp/proto"
	"github.com/micro/services/pkg/cache"

	"github.com/pquerna/otp"
	"github.com/pquerna/otp/totp"
)

type Otp struct{}

type otpKey struct {
	Secret string
	Expiry uint
}

func (e *Otp) Generate(ctx context.Context, req *pb.GenerateRequest, rsp *pb.GenerateResponse) error {
	if len(req.Id) == 0 {
		return errors.BadRequest("otp.generate", "missing id")
	}

	// check if a key exists for the user
	okey := new(otpKey)

	if req.Expiry <= 0 {
		req.Expiry = 60
	}

	if req.Size <= 0 {
		req.Size = 6
	}

	if err := cache.Context(ctx).Get("otp:"+req.Id, &okey); err != nil || okey == nil {
		// generate a key
		key, err := totp.Generate(totp.GenerateOpts{
			Issuer:      "Micro",
			AccountName: req.Id,
			Period:      300,
			Algorithm:   otp.AlgorithmSHA1,
		})
		if err != nil {
			logger.Error("Failed to generate secret: %v", err)
			return errors.InternalServerError("otp.generate", "failed to generate code")
		}

		okey = &otpKey{
			Secret: key.Secret(),
			Expiry: uint(req.Expiry),
		}

		if err := cache.Context(ctx).Set("otp:"+req.Id, okey, time.Now().Add(time.Minute*5)); err != nil {
			logger.Error("Failed to store secret: %v", err)
			return errors.InternalServerError("otp.generate", "failed to generate code")
		}
	}

	logger.Info("generating the code: ", okey.Secret, " ", okey.Expiry)

	// generate a new code
	code, err := totp.GenerateCodeCustom(okey.Secret, time.Now(), totp.ValidateOpts{
		Period:    uint(req.Expiry),
		Skew:      1,
		Digits:    otp.Digits(req.Size),
		Algorithm: otp.AlgorithmSHA1,
	})

	if err != nil {
		return errors.InternalServerError("otp.generate", "failed to generate code: %v", err)
	}

	// we have to replaced the cached value if the expiry is different
	if v := uint(req.Expiry); v != okey.Expiry {
		okey.Expiry = v

		if err := cache.Context(ctx).Set("otp:"+req.Id, okey, time.Now().Add(time.Minute*5)); err != nil {
			logger.Error("Failed to store secret: %v", err)
			return errors.InternalServerError("otp.generate", "failed to generate code")
		}
	}

	// return the code
	rsp.Code = code

	return nil
}

func (e *Otp) Validate(ctx context.Context, req *pb.ValidateRequest, rsp *pb.ValidateResponse) error {
	if len(req.Id) == 0 {
		return errors.BadRequest("otp.generate", "missing id")
	}
	if len(req.Code) == 0 {
		return errors.BadRequest("otp.generate", "missing code")
	}

	key := new(otpKey)

	if err := cache.Context(ctx).Get("otp:"+req.Id, &key); err != nil {
		logger.Error("Failed to get secret from store: %v", err)
		return errors.InternalServerError("otp.generate", "failed to validate code")
	}

	logger.Info("validating the code: ", key.Secret, " ", key.Expiry)
	ok, err := totp.ValidateCustom(req.Code, key.Secret, time.Now(), totp.ValidateOpts{
		Period:    key.Expiry,
		Skew:      1,
		Digits:    otp.Digits(len(req.Code)),
		Algorithm: otp.AlgorithmSHA1,
	})
	if err != nil {
		return errors.InternalServerError("otp.generate", "failed to validate code")
	}

	// set the response
	rsp.Success = ok

	return nil
}
