// Copyright 2023 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package object

import (
	"errors"
	"time"

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

const (
	MfaTotpPeriodInSeconds = 30
)

type TotpMfa struct {
	*MfaProps
	period     uint
	secretSize uint
	digits     otp.Digits
}

func (mfa *TotpMfa) Initiate(userId string, issuer string) (*MfaProps, error) {
	// Use the provided issuer (application display name), or fall back to "Casdoor" if not provided
	if issuer == "" {
		issuer = "Casdoor"
	}

	key, err := totp.Generate(totp.GenerateOpts{
		Issuer:      issuer,
		AccountName: userId,
		Period:      mfa.period,
		SecretSize:  mfa.secretSize,
		Digits:      mfa.digits,
	})
	if err != nil {
		return nil, err
	}

	mfaProps := MfaProps{
		MfaType: mfa.MfaType,
		Secret:  key.Secret(),
		URL:     key.URL(),
	}
	return &mfaProps, nil
}

func (mfa *TotpMfa) SetupVerify(passcode string) error {
	result, err := totp.ValidateCustom(passcode, mfa.Secret, time.Now().UTC(), totp.ValidateOpts{
		Period:    MfaTotpPeriodInSeconds,
		Skew:      1,
		Digits:    otp.DigitsSix,
		Algorithm: otp.AlgorithmSHA1,
	})
	if err != nil {
		return err
	}

	if result {
		return nil
	} else {
		return errors.New("totp passcode error")
	}
}

func (mfa *TotpMfa) Enable(user *User) error {
	columns := []string{"recovery_codes", "preferred_mfa_type", "totp_secret"}

	user.RecoveryCodes = append(user.RecoveryCodes, mfa.RecoveryCodes...)
	user.TotpSecret = mfa.Secret
	if user.PreferredMfaType == "" {
		user.PreferredMfaType = mfa.MfaType
	}

	_, err := updateUser(user.GetId(), user, columns)
	if err != nil {
		return err
	}

	return nil
}

func (mfa *TotpMfa) Verify(passcode string) error {
	result, err := totp.ValidateCustom(passcode, mfa.Secret, time.Now().UTC(), totp.ValidateOpts{
		Period:    MfaTotpPeriodInSeconds,
		Skew:      1,
		Digits:    otp.DigitsSix,
		Algorithm: otp.AlgorithmSHA1,
	})
	if err != nil {
		return err
	}

	if result {
		return nil
	} else {
		return errors.New("totp passcode error")
	}
}

func NewTotpMfaUtil(config *MfaProps) *TotpMfa {
	if config == nil {
		config = &MfaProps{
			MfaType: TotpType,
		}
	}

	return &TotpMfa{
		MfaProps:   config,
		period:     MfaTotpPeriodInSeconds,
		secretSize: 20,
		digits:     otp.DigitsSix,
	}
}
